mirror of https://github.com/astral-sh/uv
Merge 63a06880ff into 4f6f56b070
This commit is contained in:
commit
472017bbb2
|
|
@ -5194,7 +5194,7 @@ pub enum ToolCommand {
|
|||
/// The tool executable directory is determined according to the XDG standard and can be
|
||||
/// retrieved with `uv tool dir --bin`.
|
||||
#[command(alias = "ensurepath")]
|
||||
UpdateShell,
|
||||
UpdateShell(PythonUpdateShellArgs),
|
||||
/// Show the path to the uv tools directory.
|
||||
///
|
||||
/// The tools directory is used to store environments and metadata for installed tools.
|
||||
|
|
@ -6010,7 +6010,16 @@ pub enum PythonCommand {
|
|||
/// The Python executable directory is determined according to the XDG standard and can be
|
||||
/// retrieved with `uv python dir --bin`.
|
||||
#[command(alias = "ensurepath")]
|
||||
UpdateShell,
|
||||
UpdateShell(PythonUpdateShellArgs),
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
pub struct PythonUpdateShellArgs {
|
||||
/// Force prepending the executable directory to PATH even if it's already present.
|
||||
///
|
||||
/// This is useful when the directory is in PATH but binaries are shadowed by earlier entries.
|
||||
#[arg(long)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ pub fn prepend_path(path: &Path) -> anyhow::Result<bool> {
|
|||
}
|
||||
|
||||
/// Set the windows `PATH` variable in the registry.
|
||||
fn apply_windows_path_var(path: &HSTRING) -> anyhow::Result<()> {
|
||||
pub fn apply_windows_path_var(path: &HSTRING) -> anyhow::Result<()> {
|
||||
let environment = CURRENT_USER.create("Environment")?;
|
||||
|
||||
if path.is_empty() {
|
||||
|
|
@ -52,7 +52,7 @@ fn apply_windows_path_var(path: &HSTRING) -> anyhow::Result<()> {
|
|||
/// Retrieve the windows `PATH` variable from the registry.
|
||||
///
|
||||
/// Returns `Ok(None)` if the `PATH` variable is not a string.
|
||||
fn get_windows_path_var() -> anyhow::Result<Option<HSTRING>> {
|
||||
pub fn get_windows_path_var() -> anyhow::Result<Option<HSTRING>> {
|
||||
let environment = CURRENT_USER
|
||||
.create("Environment")
|
||||
.context("Failed to open `Environment` key")?;
|
||||
|
|
@ -72,7 +72,7 @@ fn get_windows_path_var() -> anyhow::Result<Option<HSTRING>> {
|
|||
/// Prepend a path to the `PATH` variable in the Windows registry.
|
||||
///
|
||||
/// Returns `Ok(None)` if the given path is already in `PATH`.
|
||||
fn prepend_to_path(existing_path: &HSTRING, path: HSTRING) -> Option<HSTRING> {
|
||||
pub fn prepend_to_path(existing_path: &HSTRING, path: HSTRING) -> Option<HSTRING> {
|
||||
if existing_path.is_empty() {
|
||||
Some(path)
|
||||
} else if existing_path.windows(path.len()).any(|p| *p == *path) {
|
||||
|
|
@ -84,3 +84,60 @@ fn prepend_to_path(existing_path: &HSTRING, path: HSTRING) -> Option<HSTRING> {
|
|||
Some(HSTRING::from(new_path))
|
||||
}
|
||||
}
|
||||
|
||||
/// Force prepend a path to the `PATH` variable, removing any existing entry first.
|
||||
///
|
||||
/// Used with `--force` to ensure the path is at the beginning of PATH.
|
||||
pub fn force_prepend_path(path: &Path) -> anyhow::Result<()> {
|
||||
let windows_path = get_windows_path_var()?.unwrap_or_default();
|
||||
|
||||
// Remove the path if it exists
|
||||
let new_path = remove_from_path(&windows_path, path);
|
||||
|
||||
// Prepend the path
|
||||
let final_path =
|
||||
prepend_to_path(&new_path, HSTRING::from(path)).unwrap_or_else(|| HSTRING::from(path));
|
||||
|
||||
// Set the `PATH` variable in the registry.
|
||||
apply_windows_path_var(&final_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a path is already in the Windows PATH variable (read-only).
|
||||
pub fn contains_path(path: &Path) -> anyhow::Result<bool> {
|
||||
let windows_path = get_windows_path_var()?.unwrap_or_default();
|
||||
if windows_path.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let path_str = path.to_string_lossy();
|
||||
let path_string = windows_path.to_string_lossy();
|
||||
let is_in_path = path_string
|
||||
.split(';')
|
||||
.any(|p| p.trim() == path_str.as_ref());
|
||||
|
||||
Ok(is_in_path)
|
||||
}
|
||||
|
||||
/// Remove a path from the `PATH` variable string.
|
||||
///
|
||||
/// Used with `--force` to remove an existing entry before prepending it again.
|
||||
pub fn remove_from_path(existing_path: &HSTRING, path_to_remove: &Path) -> HSTRING {
|
||||
if existing_path.is_empty() {
|
||||
return existing_path.clone();
|
||||
}
|
||||
|
||||
let path_str = path_to_remove.to_string_lossy();
|
||||
let path_string = existing_path.to_string_lossy();
|
||||
let paths: Vec<&str> = path_string
|
||||
.split(';')
|
||||
.filter(|p| p.trim() != path_str.as_ref())
|
||||
.collect();
|
||||
|
||||
if paths.is_empty() {
|
||||
HSTRING::new()
|
||||
} else {
|
||||
HSTRING::from(paths.join(";"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use owo_colors::OwoColorize;
|
|||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::debug;
|
||||
|
||||
use uv_cli::PythonUpdateShellArgs;
|
||||
use uv_fs::Simplified;
|
||||
use uv_python::managed::python_executable_dir;
|
||||
use uv_shell::Shell;
|
||||
|
|
@ -15,7 +16,10 @@ use crate::commands::ExitStatus;
|
|||
use crate::printer::Printer;
|
||||
|
||||
/// Ensure that the executable directory is in PATH.
|
||||
pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
||||
pub(crate) async fn update_shell(
|
||||
args: PythonUpdateShellArgs,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
let executable_directory = python_executable_dir()?;
|
||||
debug!(
|
||||
"Ensuring that the executable directory is in PATH: {}",
|
||||
|
|
@ -24,25 +28,44 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
|||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if uv_shell::windows::prepend_path(&executable_directory)? {
|
||||
// Check if path is in PATH (read-only, doesn't mutate)
|
||||
let is_in_path = uv_shell::windows::contains_path(&executable_directory)?;
|
||||
|
||||
if is_in_path && !args.force {
|
||||
// Already in PATH and not forcing - exit early
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Executable directory {} is already in PATH",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if is_in_path && args.force {
|
||||
// Already in PATH but forcing - remove it first, then prepend
|
||||
uv_shell::windows::force_prepend_path(&executable_directory)?;
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Force updated PATH to prioritize executable directory {}",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
|
||||
} else if !is_in_path {
|
||||
// Not in PATH - normal prepend
|
||||
uv_shell::windows::prepend_path(&executable_directory)?;
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Updated PATH to include executable directory {}",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Executable directory {} is already in PATH",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
}
|
||||
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if Shell::contains_path(&executable_directory) {
|
||||
if Shell::contains_path(&executable_directory) && !args.force {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Executable directory {} is already in PATH",
|
||||
|
|
@ -82,20 +105,54 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
|||
// Search for the command in the file, to avoid redundant updates.
|
||||
match fs_err::tokio::read_to_string(&file).await {
|
||||
Ok(contents) => {
|
||||
if contents
|
||||
// Check if command already exists
|
||||
let command_exists = contents
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.starts_with('#'))
|
||||
.any(|line| line.contains(&command))
|
||||
{
|
||||
.any(|line| line.contains(&command));
|
||||
|
||||
if command_exists {
|
||||
if args.force {
|
||||
// With --force: Remove old entry and add it again at the end (ensures first priority)
|
||||
let new_contents: String = contents
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
// Remove old # uv comments and lines containing the command
|
||||
!(trimmed == "# uv"
|
||||
|| (!trimmed.starts_with('#') && trimmed.contains(&command)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
// Add the command at the end to ensure it's executed last (first in PATH)
|
||||
let final_contents = format!("{new_contents}\n# uv\n{command}\n");
|
||||
|
||||
fs_err::tokio::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&file)
|
||||
.await?
|
||||
.write_all(final_contents.as_bytes())
|
||||
.await?;
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Force updated configuration file: {}",
|
||||
file.simplified_display().cyan()
|
||||
)?;
|
||||
updated = true;
|
||||
} else {
|
||||
// Without --force: Skip if already exists
|
||||
debug!(
|
||||
"Skipping already-updated configuration file: {}",
|
||||
file.simplified_display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append the command to the file.
|
||||
} else {
|
||||
// Command doesn't exist: Add it normally
|
||||
fs_err::tokio::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
|
|
@ -112,6 +169,7 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
|||
)?;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Ensure that the directory containing the file exists.
|
||||
if let Some(parent) = file.parent() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use owo_colors::OwoColorize;
|
|||
use tokio::io::AsyncWriteExt;
|
||||
use tracing::debug;
|
||||
|
||||
use uv_cli::PythonUpdateShellArgs;
|
||||
use uv_fs::Simplified;
|
||||
use uv_shell::Shell;
|
||||
use uv_tool::tool_executable_dir;
|
||||
|
|
@ -15,7 +16,10 @@ use crate::commands::ExitStatus;
|
|||
use crate::printer::Printer;
|
||||
|
||||
/// Ensure that the executable directory is in PATH.
|
||||
pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
||||
pub(crate) async fn update_shell(
|
||||
args: PythonUpdateShellArgs,
|
||||
printer: Printer,
|
||||
) -> Result<ExitStatus> {
|
||||
let executable_directory = tool_executable_dir()?;
|
||||
debug!(
|
||||
"Ensuring that the executable directory is in PATH: {}",
|
||||
|
|
@ -24,25 +28,44 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
|||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if uv_shell::windows::prepend_path(&executable_directory)? {
|
||||
// Check if path is in PATH (read-only, doesn't mutate)
|
||||
let is_in_path = uv_shell::windows::contains_path(&executable_directory)?;
|
||||
|
||||
if is_in_path && !args.force {
|
||||
// Already in PATH and not forcing - exit early
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Executable directory {} is already in PATH",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if is_in_path && args.force {
|
||||
// Already in PATH but forcing - remove it first, then prepend
|
||||
uv_shell::windows::force_prepend_path(&executable_directory)?;
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Force updated PATH to prioritize executable directory {}",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
|
||||
} else if !is_in_path {
|
||||
// Not in PATH - normal prepend
|
||||
uv_shell::windows::prepend_path(&executable_directory)?;
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Updated PATH to include executable directory {}",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
writeln!(printer.stderr(), "Restart your shell to apply changes")?;
|
||||
} else {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Executable directory {} is already in PATH",
|
||||
executable_directory.simplified_display().cyan()
|
||||
)?;
|
||||
}
|
||||
|
||||
return Ok(ExitStatus::Success);
|
||||
}
|
||||
|
||||
if Shell::contains_path(&executable_directory) {
|
||||
if Shell::contains_path(&executable_directory) && !args.force {
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Executable directory {} is already in PATH",
|
||||
|
|
@ -82,20 +105,53 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
|||
// Search for the command in the file, to avoid redundant updates.
|
||||
match fs_err::tokio::read_to_string(&file).await {
|
||||
Ok(contents) => {
|
||||
if contents
|
||||
// Check if command already exists
|
||||
let command_exists = contents
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.starts_with('#'))
|
||||
.any(|line| line.contains(&command))
|
||||
{
|
||||
.any(|line| line.contains(&command));
|
||||
|
||||
if command_exists {
|
||||
if args.force {
|
||||
// With --force: Remove old entry and add it again at the end
|
||||
let new_contents: String = contents
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let trimmed = line.trim();
|
||||
// Remove old # uv comments and lines containing the command
|
||||
!(trimmed == "# uv"
|
||||
|| (!trimmed.starts_with('#') && trimmed.contains(&command)))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
let final_contents = format!("{new_contents}\n# uv\n{command}\n");
|
||||
|
||||
fs_err::tokio::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(&file)
|
||||
.await?
|
||||
.write_all(final_contents.as_bytes())
|
||||
.await?;
|
||||
|
||||
writeln!(
|
||||
printer.stderr(),
|
||||
"Force updated configuration file: {}",
|
||||
file.simplified_display().cyan()
|
||||
)?;
|
||||
updated = true;
|
||||
} else {
|
||||
// Without --force: Skip if already exists
|
||||
debug!(
|
||||
"Skipping already-updated configuration file: {}",
|
||||
file.simplified_display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Append the command to the file.
|
||||
} else {
|
||||
// Command doesn't exist: Add it normally
|
||||
fs_err::tokio::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
|
|
@ -112,6 +168,7 @@ pub(crate) async fn update_shell(printer: Printer) -> Result<ExitStatus> {
|
|||
)?;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||
// Ensure that the directory containing the file exists.
|
||||
if let Some(parent) = file.parent() {
|
||||
|
|
|
|||
|
|
@ -1551,9 +1551,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
commands::tool_uninstall(args.name, printer).await
|
||||
}
|
||||
Commands::Tool(ToolNamespace {
|
||||
command: ToolCommand::UpdateShell,
|
||||
command: ToolCommand::UpdateShell(args),
|
||||
}) => {
|
||||
commands::tool_update_shell(printer).await?;
|
||||
commands::tool_update_shell(args, printer).await?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Commands::Tool(ToolNamespace {
|
||||
|
|
@ -1745,9 +1745,9 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
|||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Commands::Python(PythonNamespace {
|
||||
command: PythonCommand::UpdateShell,
|
||||
command: PythonCommand::UpdateShell(args),
|
||||
}) => {
|
||||
commands::python_update_shell(printer).await?;
|
||||
commands::python_update_shell(args, printer).await?;
|
||||
Ok(ExitStatus::Success)
|
||||
}
|
||||
Commands::Publish(args) => {
|
||||
|
|
|
|||
|
|
@ -1289,6 +1289,14 @@ impl TestContext {
|
|||
command
|
||||
}
|
||||
|
||||
/// Create a `uv python update-shell` command with options shared across scenarios.
|
||||
pub fn python_update_shell(&self) -> Command {
|
||||
let mut command = Self::new_command();
|
||||
command.arg("python").arg("update-shell");
|
||||
self.add_shared_options(&mut command, true);
|
||||
command
|
||||
}
|
||||
|
||||
/// Create a `uv run` command with options shared across scenarios.
|
||||
pub fn run(&self) -> Command {
|
||||
let mut command = Self::new_command();
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ mod python_pin;
|
|||
#[cfg(feature = "python-managed")]
|
||||
mod python_upgrade;
|
||||
|
||||
#[cfg(feature = "python")]
|
||||
mod python_update_shell;
|
||||
|
||||
#[cfg(all(feature = "python", feature = "pypi"))]
|
||||
mod run;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::fixture::PathChild;
|
||||
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::common::{TestContext, uv_snapshot};
|
||||
|
||||
#[test]
|
||||
fn python_update_shell_not_in_path() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
// Zsh uses .zshenv, not .zshrc
|
||||
let shell_config = context.home_dir.child(".zshenv");
|
||||
|
||||
uv_snapshot!(context.filters(), context
|
||||
.python_update_shell()
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str())
|
||||
.env("SHELL", "/bin/zsh"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Created configuration file: [HOME]/.zshenv
|
||||
Restart your shell to apply changes
|
||||
"###);
|
||||
|
||||
// Verify the file was created with the correct content
|
||||
let contents = fs_err::read_to_string(shell_config.path()).unwrap();
|
||||
assert!(contents.contains("export PATH="));
|
||||
assert!(contents.contains("# uv"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, PATH is updated in the registry, not a config file
|
||||
uv_snapshot!(context.filters(), context
|
||||
.python_update_shell()
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Updated PATH to include executable directory [USER_CONFIG_DIR]/data/../bin
|
||||
Restart your shell to apply changes
|
||||
"###);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_update_shell_already_in_path() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
// Set a specific bin directory using UV_PYTHON_BIN_DIR
|
||||
let bin_dir = context.home_dir.child("bin");
|
||||
fs_err::create_dir_all(bin_dir.path()).unwrap();
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
// Set PATH to include the bin directory so it's "already in PATH"
|
||||
let path_with_bin =
|
||||
std::env::join_paths(std::iter::once(bin_dir.path().to_path_buf()).chain(
|
||||
std::env::split_paths(&std::env::var(EnvVars::PATH).unwrap_or_default()),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// Run without --force - should skip because it's already in PATH
|
||||
uv_snapshot!(context.filters(), context
|
||||
.python_update_shell()
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str())
|
||||
.env(EnvVars::UV_PYTHON_BIN_DIR, bin_dir.path())
|
||||
.env(EnvVars::PATH, path_with_bin)
|
||||
.env("SHELL", "/bin/zsh"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Executable directory [HOME]/bin is already in PATH
|
||||
"###);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, contains_path checks the registry, not env vars
|
||||
// Since we can't easily set up the registry in tests, we'll test
|
||||
// that the command succeeds. The "already in PATH" check will
|
||||
// depend on the actual registry state, which we can't control.
|
||||
// This test verifies the command works even if the path is already there.
|
||||
context
|
||||
.python_update_shell()
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str())
|
||||
.env(EnvVars::UV_PYTHON_BIN_DIR, bin_dir.path())
|
||||
.assert()
|
||||
.success();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_update_shell_force() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
// Zsh uses .zshenv, not .zshrc
|
||||
let shell_config = context.home_dir.child(".zshenv");
|
||||
|
||||
// First run - add to PATH
|
||||
context
|
||||
.python_update_shell()
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str())
|
||||
.env("SHELL", "/bin/zsh")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let first_contents = fs_err::read_to_string(shell_config.path()).unwrap();
|
||||
let first_count = first_contents.matches("export PATH=").count();
|
||||
|
||||
// Second run with --force - should update
|
||||
uv_snapshot!(context.filters(), context
|
||||
.python_update_shell()
|
||||
.arg("--force")
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str())
|
||||
.env("SHELL", "/bin/zsh"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Force updated configuration file: [HOME]/.zshenv
|
||||
Restart your shell to apply changes
|
||||
"###);
|
||||
|
||||
// Verify only one PATH export exists (old one removed, new one added)
|
||||
let second_contents = fs_err::read_to_string(shell_config.path()).unwrap();
|
||||
let second_count = second_contents.matches("export PATH=").count();
|
||||
assert_eq!(
|
||||
first_count, second_count,
|
||||
"Should have same number of PATH exports"
|
||||
);
|
||||
|
||||
// Verify the command is at the end
|
||||
assert!(
|
||||
second_contents.trim_end().ends_with("export PATH=")
|
||||
|| second_contents.trim_end().ends_with('"')
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, --force updates the registry
|
||||
// First run - add to PATH
|
||||
context
|
||||
.python_update_shell()
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Second run with --force - should update registry
|
||||
uv_snapshot!(context.filters(), context
|
||||
.python_update_shell()
|
||||
.arg("--force")
|
||||
.env(EnvVars::HOME, context.home_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Force updated PATH to prioritize executable directory [USER_CONFIG_DIR]/data/../bin
|
||||
Restart your shell to apply changes
|
||||
"###);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue