diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 7c96ac7384d5..8a0a9f1f9659 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -4214,6 +4214,21 @@ pub struct PythonInstallArgs { /// Implies `--reinstall`. #[arg(long, short)] pub force: bool, + + /// Use as the default Python version. + /// + /// By default, only a `python{major}.{minor}` executable is installed, e.g., `python3.10`. When + /// the `--default` flag is used, `python{major}`, e.g., `python3`, and `python` executables are + /// also installed. + /// + /// Alternative Python variants will still include their tag. For example, installing + /// 3.13+freethreaded with `--default` will include in `python3t` and `pythont`, not `python3` + /// and `python`. + /// + /// If multiple Python versions are requested during the installation, the first request will be + /// the default. + #[arg(long)] + pub default: bool, } #[derive(Args)] diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index b1da7c252242..e1a7203c3709 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -327,8 +327,8 @@ impl PythonInstallationKey { &self.libc } - /// Return a canonical name for a versioned executable. - pub fn versioned_executable_name(&self) -> String { + /// Return a canonical name for a minor versioned executable. + pub fn executable_name_minor(&self) -> String { format!( "python{maj}.{min}{var}{exe}", maj = self.major, @@ -337,6 +337,25 @@ impl PythonInstallationKey { exe = std::env::consts::EXE_SUFFIX ) } + + /// Return a canonical name for a major versioned executable. + pub fn executable_name_major(&self) -> String { + format!( + "python{maj}{var}{exe}", + maj = self.major, + var = self.variant.suffix(), + exe = std::env::consts::EXE_SUFFIX + ) + } + + /// Return a canonical name for an un-versioned executable. + pub fn executable_name(&self) -> String { + format!( + "python{var}{exe}", + var = self.variant.suffix(), + exe = std::env::consts::EXE_SUFFIX + ) + } } impl fmt::Display for PythonInstallationKey { diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 27b292e84c07..9fdeba0db666 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -87,7 +87,7 @@ struct Changelog { existing: FxHashSet, installed: FxHashSet, uninstalled: FxHashSet, - installed_executables: FxHashMap>, + installed_executables: FxHashMap>, } impl Changelog { @@ -126,6 +126,7 @@ pub(crate) async fn install( force: bool, python_install_mirror: Option, pypy_install_mirror: Option, + default: bool, python_downloads: PythonDownloads, native_tls: bool, connectivity: Connectivity, @@ -136,6 +137,11 @@ pub(crate) async fn install( ) -> Result { let start = std::time::Instant::now(); + if default && !preview.is_enabled() { + writeln!(printer.stderr(), "The `--default` flag is only available in preview mode; add the `--preview` flag to use `--default.")?; + return Ok(ExitStatus::Failure); + } + // Resolve the requests let mut is_default_install = false; let requests: Vec<_> = if targets.is_empty() { @@ -163,6 +169,10 @@ pub(crate) async fn install( .collect::>>()? }; + let Some(first_request) = requests.first() else { + return Ok(ExitStatus::Success); + }; + // Read the existing installations, lock the directory for the duration let installations = ManagedPythonInstallations::from_settings()?.init()?; let installations_dir = installations.root(); @@ -302,114 +312,136 @@ pub(crate) async fn install( .expect("We should have a bin directory with preview enabled") .as_path(); - let target = bin.join(installation.key().versioned_executable_name()); + let targets = if (default || is_default_install) + && first_request.matches_installation(installation) + { + vec![ + installation.key().executable_name_minor(), + installation.key().executable_name_major(), + installation.key().executable_name(), + ] + } else { + vec![installation.key().executable_name_minor()] + }; - match installation.create_bin_link(&target) { - Ok(()) => { - debug!( - "Installed executable at {} for {}", - target.simplified_display(), - installation.key(), - ); - changelog.installed.insert(installation.key().clone()); - changelog - .installed_executables - .entry(installation.key().clone()) - .or_default() - .push(target.clone()); - } - Err(uv_python::managed::Error::LinkExecutable { from: _, to, err }) - if err.kind() == ErrorKind::AlreadyExists => - { - debug!( - "Inspecting existing executable at {}", - target.simplified_display() - ); + for target in targets { + let target = bin.join(target); + match installation.create_bin_link(&target) { + Ok(()) => { + debug!( + "Installed executable at `{}` for {}", + target.simplified_display(), + installation.key(), + ); + changelog.installed.insert(installation.key().clone()); + changelog + .installed_executables + .entry(installation.key().clone()) + .or_default() + .insert(target.clone()); + } + Err(uv_python::managed::Error::LinkExecutable { from: _, to, err }) + if err.kind() == ErrorKind::AlreadyExists => + { + debug!( + "Inspecting existing executable at `{}`", + target.simplified_display() + ); - // Figure out what installation it references, if any - let existing = find_matching_bin_link(&existing_installations, &target); + // Figure out what installation it references, if any + let existing = find_matching_bin_link(&existing_installations, &target); - match existing { - None => { - // There's an existing executable we don't manage, require `--force` - if !force { - errors.push(( - installation.key(), - anyhow::anyhow!( - "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it", - to.simplified_display() - ), - )); - continue; - } - debug!( - "Replacing existing executable at `{}` due to `--force`", - target.simplified_display() - ); - } - Some(existing) if existing == installation => { - // The existing link points to the same installation, so we're done unless - // they requested we reinstall - if !(reinstall || force) { + match existing { + None => { + // There's an existing executable we don't manage, require `--force` + if !force { + errors.push(( + installation.key(), + anyhow::anyhow!( + "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it", + to.simplified_display() + ), + )); + continue; + } debug!( - "Executable at `{}` is already for `{}`", - target.simplified_display(), - installation.key(), + "Replacing existing executable at `{}` due to `--force`", + target.simplified_display() ); - continue; } - debug!( - "Replacing existing executable for `{}` at `{}`", - installation.key(), - target.simplified_display(), - ); - } - Some(existing) => { - // The existing link points to a different installation, check if it - // is reasonable to replace - if force { + Some(existing) if existing == installation => { + // The existing link points to the same installation, so we're done unless + // they requested we reinstall + if !(reinstall || force) { + debug!( + "Executable at `{}` is already for `{}`", + target.simplified_display(), + installation.key(), + ); + continue; + } debug!( - "Replacing existing executable for `{}` at `{}` with executable for `{}` due to `--force` flag", - existing.key(), - target.simplified_display(), + "Replacing existing executable for `{}` at `{}`", installation.key(), + target.simplified_display(), ); - } else { - if installation.is_upgrade_of(existing) { + } + Some(existing) => { + // The existing link points to a different installation, check if it + // is reasonable to replace + if force { debug!( - "Replacing existing executable for `{}` at `{}` with executable for `{}` since it is an upgrade", + "Replacing existing executable for `{}` at `{}` with executable for `{}` due to `--force` flag", existing.key(), target.simplified_display(), installation.key(), ); } else { - debug!( - "Executable already exists at `{}` for `{}`. Use `--force` to replace it.", - existing.key(), - to.simplified_display() - ); - continue; + if installation.is_upgrade_of(existing) { + debug!( + "Replacing existing executable for `{}` at `{}` with executable for `{}` since it is an upgrade", + existing.key(), + target.simplified_display(), + installation.key(), + ); + } else if default { + debug!( + "Replacing existing executable for `{}` at `{}` with executable for `{}` since `--default` was requested`", + existing.key(), + target.simplified_display(), + installation.key(), + ); + } else { + debug!( + "Executable already exists for `{}` at `{}`. Use `--force` to replace it", + existing.key(), + to.simplified_display() + ); + continue; + } } } } - } - // Replace the existing link - fs_err::remove_file(&to)?; - installation.create_bin_link(&target)?; - debug!( - "Updated executable at `{}` to `{}`", - target.simplified_display(), - installation.key(), - ); - changelog.installed.insert(installation.key().clone()); - changelog - .installed_executables - .entry(installation.key().clone()) - .or_default() - .push(target.clone()); + // Replace the existing link + fs_err::remove_file(&to)?; + installation.create_bin_link(&target)?; + debug!( + "Updated executable at `{}` to {}", + target.simplified_display(), + installation.key(), + ); + changelog.installed.insert(installation.key().clone()); + changelog + .installed_executables + .entry(installation.key().clone()) + .or_default() + .insert(target.clone()); + } + Err(err) => { + errors.push((installation.key(), anyhow::Error::new(err))); + } } - Err(err) => return Err(err.into()), } } @@ -454,35 +486,33 @@ pub(crate) async fn install( } for event in changelog.events() { + let executables = format_executables(&event, &changelog); match event.kind { ChangeEventKind::Added => { writeln!( printer.stderr(), - " {} {}{}", + " {} {}{executables}", "+".green(), - event.key.bold(), - format_installed_executables(&event.key, &changelog.installed_executables) + event.key.bold() )?; } ChangeEventKind::Removed => { writeln!( printer.stderr(), - " {} {}{}", + " {} {}{executables}", "-".red(), - event.key.bold(), - format_installed_executables(&event.key, &changelog.installed_executables) + event.key.bold() )?; } ChangeEventKind::Reinstalled => { writeln!( printer.stderr(), - " {} {}{}", + " {} {}{executables}", "~".yellow(), event.key.bold(), - format_installed_executables(&event.key, &changelog.installed_executables) )?; } - } + }; } if preview.is_enabled() { @@ -520,22 +550,20 @@ pub(crate) async fn install( Ok(ExitStatus::Success) } -// TODO(zanieb): Change the formatting of this to something nicer, probably integrate with -// `Changelog` and `ChangeEventKind`. -fn format_installed_executables( - key: &PythonInstallationKey, - installed_executables: &FxHashMap>, -) -> String { - if let Some(executables) = installed_executables.get(key) { - let executables = executables - .iter() - .filter_map(|path| path.file_name()) - .map(|name| name.to_string_lossy()) - .join(", "); - format!(" ({executables})") - } else { - String::new() - } +fn format_executables(event: &ChangeEvent, changelog: &Changelog) -> String { + let Some(installed) = changelog.installed_executables.get(&event.key) else { + return String::new(); + }; + + let names = installed + .iter() + .filter_map(|path| path.file_name()) + .map(|name| name.to_string_lossy()) + // Do not include the `.exe` during comparisons, it can change the ordering + .sorted_unstable_by(|a, b| a.trim_end_matches(".exe").cmp(b.trim_end_matches(".exe"))) + .join(", "); + + format!(" ({names})") } fn warn_if_not_on_path(bin: &Path) { diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index d0dc7528444f..cfcca6fc9bfe 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -142,8 +142,10 @@ async fn do_uninstall( // leave broken links behind, i.e., if the user created them. .filter(|path| { matching_installations.iter().any(|installation| { - path.file_name().and_then(|name| name.to_str()) - == Some(&installation.key().versioned_executable_name()) + let name = path.file_name().and_then(|name| name.to_str()); + name == Some(&installation.key().executable_name_minor()) + || name == Some(&installation.key().executable_name_major()) + || name == Some(&installation.key().executable_name()) }) }) // Only include Python executables that match the installations @@ -224,7 +226,7 @@ async fn do_uninstall( " {} {} ({})", "-".red(), event.key.bold(), - event.key.versioned_executable_name() + event.key.executable_name_minor() )?; } _ => unreachable!(), diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 642efa2e65ec..183c72bb2cb7 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1077,6 +1077,7 @@ async fn run(mut cli: Cli) -> Result { args.force, args.python_install_mirror, args.pypy_install_mirror, + args.default, globals.python_downloads, globals.native_tls, globals.connectivity, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 6429b9f1c90a..2a19ca0b1917 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -751,6 +751,7 @@ pub(crate) struct PythonInstallSettings { pub(crate) force: bool, pub(crate) python_install_mirror: Option, pub(crate) pypy_install_mirror: Option, + pub(crate) default: bool, } impl PythonInstallSettings { @@ -774,6 +775,7 @@ impl PythonInstallSettings { force, mirror: _, pypy_mirror: _, + default, } = args; Self { @@ -782,6 +784,7 @@ impl PythonInstallSettings { force, python_install_mirror: python_mirror, pypy_install_mirror: pypy_mirror, + default, } } } diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 0eb48500adf6..db67192ad3af 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -516,6 +516,20 @@ fn help_subsubcommand() { Implies `--reinstall`. + --default + Use as the default Python version. + + By default, only a `python{major}.{minor}` executable is installed, e.g., `python3.10`. + When the `--default` flag is used, `python{major}`, e.g., `python3`, and `python` + executables are also installed. + + Alternative Python variants will still include their tag. For example, installing + 3.13+freethreaded with `--default` will include in `python3t` and `pythont`, not `python3` + and `python`. + + If multiple Python versions are requested during the installation, the first request will + be the default. + Cache options: -n, --no-cache Avoid reading from or writing to the cache, instead using a temporary directory for the @@ -751,6 +765,7 @@ fn help_flag_subsubcommand() { installations [env: UV_PYPY_INSTALL_MIRROR=] -r, --reinstall Reinstall the requested Python version, if it's already installed -f, --force Replace existing Python executables during installation + --default Use as the default Python version Cache options: -n, --no-cache Avoid reading from or writing to the cache, instead using a temporary diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index 47823cd934e5..d92b8862e201 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -105,7 +105,7 @@ fn python_install_preview() { ----- stderr ----- Installed Python 3.13.0 in [TIME] - + cpython-3.13.0-[PLATFORM] (python3.13) + + cpython-3.13.0-[PLATFORM] (python, python3, python3.13) "###); let bin_python = context @@ -149,7 +149,7 @@ fn python_install_preview() { ----- stderr ----- Installed Python 3.13.0 in [TIME] - ~ cpython-3.13.0-[PLATFORM] (python3.13) + ~ cpython-3.13.0-[PLATFORM] (python, python3, python3.13) "###); // The executable should still be present in the bin directory @@ -163,7 +163,7 @@ fn python_install_preview() { ----- stderr ----- Installed Python 3.13.0 in [TIME] - + cpython-3.13.0-[PLATFORM] (python3.13) + + cpython-3.13.0-[PLATFORM] (python, python3, python3.13) "###); // The executable should still be present in the bin directory @@ -479,6 +479,294 @@ fn python_install_invalid_request() { "###); } +#[test] +fn python_install_default() { + let context: TestContext = TestContext::new_with_versions(&[]) + .with_filtered_python_keys() + .with_filtered_exe_suffix(); + + let bin_python_minor_13 = context + .temp_dir + .child("bin") + .child(format!("python3.13{}", std::env::consts::EXE_SUFFIX)); + + let bin_python_major = context + .temp_dir + .child("bin") + .child(format!("python3{}", std::env::consts::EXE_SUFFIX)); + + let bin_python_default = context + .temp_dir + .child("bin") + .child(format!("python{}", std::env::consts::EXE_SUFFIX)); + + // `--preview` is required for `--default` + uv_snapshot!(context.filters(), context.python_install().arg("--default"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + The `--default` flag is only available in preview mode; add the `--preview` flag to use `--default. + "###); + + // Install a specific version + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] (python3.13) + "###); + + // Only the minor versioned executable should be installed + bin_python_minor_13.assert(predicate::path::exists()); + bin_python_major.assert(predicate::path::missing()); + bin_python_default.assert(predicate::path::missing()); + + // Install again, with `--default` + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--default").arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] (python, python3) + "###); + + // Now all the executables should be installed + bin_python_minor_13.assert(predicate::path::exists()); + bin_python_major.assert(predicate::path::exists()); + bin_python_default.assert(predicate::path::exists()); + + // Uninstall + uv_snapshot!(context.filters(), context.python_uninstall().arg("--all"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python installations + Uninstalled Python 3.13.0 in [TIME] + - cpython-3.13.0-[PLATFORM] (python3.13) + "###); + + // The executables should be removed + bin_python_minor_13.assert(predicate::path::missing()); + bin_python_major.assert(predicate::path::missing()); + bin_python_default.assert(predicate::path::missing()); + + // Install the latest version, i.e., a "default install" + uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] (python, python3, python3.13) + "###); + + // Since it's a default install, we should include all of the executables + bin_python_minor_13.assert(predicate::path::exists()); + bin_python_major.assert(predicate::path::exists()); + bin_python_default.assert(predicate::path::exists()); + + // Uninstall again + uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Searching for Python versions matching: Python 3.13 + Uninstalled Python 3.13.0 in [TIME] + - cpython-3.13.0-[PLATFORM] (python3.13) + "###); + + // We should remove all the executables + bin_python_minor_13.assert(predicate::path::missing()); + bin_python_major.assert(predicate::path::missing()); + bin_python_default.assert(predicate::path::missing()); + + // Install multiple versions, with the `--default` flag + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.12").arg("3.13").arg("--default"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 2 versions in [TIME] + + cpython-3.12.7-[PLATFORM] (python, python3, python3.12) + + cpython-3.13.0-[PLATFORM] (python3.13) + "###); + + let bin_python_minor_12 = context + .temp_dir + .child("bin") + .child(format!("python3.12{}", std::env::consts::EXE_SUFFIX)); + + // All the executables should exist + bin_python_minor_13.assert(predicate::path::exists()); + bin_python_minor_12.assert(predicate::path::exists()); + bin_python_major.assert(predicate::path::exists()); + bin_python_default.assert(predicate::path::exists()); + + // And 3.12 should be the default + if cfg!(unix) { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/bin/python3.12" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/bin/python3.13" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/bin/python3.12" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/bin/python3.12" + ); + }); + } else { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/python" + ); + }); + } + + // Change the default to 3.13 + uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13").arg("--default"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed Python 3.13.0 in [TIME] + + cpython-3.13.0-[PLATFORM] (python, python3) + "###); + + // All the executables should exist + bin_python_minor_13.assert(predicate::path::exists()); + bin_python_minor_12.assert(predicate::path::exists()); + bin_python_major.assert(predicate::path::exists()); + bin_python_default.assert(predicate::path::exists()); + + // And 3.13 should be the default now + if cfg!(unix) { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/bin/python3.13" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/bin/python3.13" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/bin/python3.12" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/bin/python3.13" + ); + }); + } else { + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_major), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_13), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_minor_12), @"[TEMP_DIR]/managed/cpython-3.12.7-[PLATFORM]/python" + ); + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + insta::assert_snapshot!( + read_link_path(&bin_python_default), @"[TEMP_DIR]/managed/cpython-3.13.0-[PLATFORM]/python" + ); + }); + } +} + fn read_link_path(path: &Path) -> String { if cfg!(unix) { path.read_link() diff --git a/docs/reference/cli.md b/docs/reference/cli.md index d2d31ef151c7..968b62f30af5 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -4513,6 +4513,14 @@ uv python install [OPTIONS] [TARGETS]...

While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.

May also be set with the UV_CONFIG_FILE environment variable.

+
--default

Use as the default Python version.

+ +

By default, only a python{major}.{minor} executable is installed, e.g., python3.10. When the --default flag is used, python{major}, e.g., python3, and python executables are also installed.

+ +

Alternative Python variants will still include their tag. For example, installing 3.13+freethreaded with --default will include in python3t and pythont, not python3 and python.

+ +

If multiple Python versions are requested during the installation, the first request will be the default.

+
--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.