Skip to content

Commit

Permalink
Add uv python install --default
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Oct 31, 2024
1 parent 8d3408f commit 4f242a2
Show file tree
Hide file tree
Showing 10 changed files with 350 additions and 79 deletions.
15 changes: 15 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3907,6 +3907,21 @@ pub struct PythonInstallArgs {
/// installed.
#[arg(long, short, alias = "force")]
pub reinstall: 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)]
Expand Down
23 changes: 21 additions & 2 deletions crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,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,
Expand All @@ -316,6 +316,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 {
Expand Down
190 changes: 118 additions & 72 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use uv_python::{PythonDownloads, PythonInstallationKey, PythonRequest, PythonVer
use uv_shell::Shell;
use uv_warnings::warn_user;

use crate::commands::python::{ChangeEvent, ChangeEventKind};
use crate::commands::python::{ChangeEvent, ChangeEventKind, ExecutableChangeEvent};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::{elapsed, ExitStatus};
use crate::printer::Printer;
Expand Down Expand Up @@ -72,7 +72,7 @@ struct Changelog {
existing: FxHashSet<PythonInstallationKey>,
installed: FxHashSet<PythonInstallationKey>,
uninstalled: FxHashSet<PythonInstallationKey>,
installed_executables: FxHashMap<PythonInstallationKey, Vec<PathBuf>>,
installed_executables: FxHashMap<PythonInstallationKey, FxHashSet<PathBuf>>,
uninstalled_executables: FxHashSet<PathBuf>,
}

Expand Down Expand Up @@ -104,10 +104,12 @@ impl Changelog {
}

/// Download and install Python versions.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn install(
project_dir: &Path,
targets: Vec<String>,
reinstall: bool,
default: bool,
python_downloads: PythonDownloads,
native_tls: bool,
connectivity: Connectivity,
Expand All @@ -117,6 +119,11 @@ pub(crate) async fn install(
) -> Result<ExitStatus> {
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() {
Expand All @@ -139,6 +146,10 @@ pub(crate) async fn install(
.collect::<Result<Vec<_>>>()?
};

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();
Expand Down Expand Up @@ -275,31 +286,24 @@ 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.user_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 =>
{
// TODO(zanieb): Add `--force`
if reinstall {
fs_err::remove_file(&to)?;
installation.create_bin_link(&target)?;
for target in targets {
let target = bin.join(target);
match installation.create_bin_link(&target) {
Ok(()) => {
debug!(
"Updated executable at {} to {}",
"Installed executable at {} for {}",
target.user_display(),
installation.key(),
);
Expand All @@ -308,21 +312,41 @@ pub(crate) async fn install(
.installed_executables
.entry(installation.key().clone())
.or_default()
.push(target.clone());
changelog.uninstalled_executables.insert(target);
} else {
if !is_same_file(&to, &from).unwrap_or_default() {
errors.push((
.insert(target.clone());
}
Err(uv_python::managed::Error::LinkExecutable { from, to, err })
if err.kind() == ErrorKind::AlreadyExists =>
{
// TODO(zanieb): Add `--force`
if reinstall {
fs_err::remove_file(&to)?;
installation.create_bin_link(&target)?;
debug!(
"Updated executable at {} to {}",
target.user_display(),
installation.key(),
anyhow::anyhow!(
"Executable already exists at `{}`. Use `--reinstall` to force replacement.",
to.user_display()
),
));
);
changelog.installed.insert(installation.key().clone());
changelog
.installed_executables
.entry(installation.key().clone())
.or_default()
.insert(target.clone());
changelog.uninstalled_executables.insert(target);
} else {
if !is_same_file(&to, &from).unwrap_or_default() {
errors.push((
installation.key(),
anyhow::anyhow!(
"Executable already exists at `{}`. Use `--reinstall` to force replacement.",
to.user_display()
),
));
}
}
}
Err(err) => return Err(err.into()),
}
Err(err) => return Err(err.into()),
}
}

Expand Down Expand Up @@ -369,33 +393,16 @@ pub(crate) async fn install(
for event in changelog.events() {
match event.kind {
ChangeEventKind::Added => {
writeln!(
printer.stderr(),
" {} {}{}",
"+".green(),
event.key.bold(),
format_installed_executables(&event.key, &changelog.installed_executables)
)?;
writeln!(printer.stderr(), " {} {}", "+".green(), event.key.bold())?;
}
ChangeEventKind::Removed => {
writeln!(
printer.stderr(),
" {} {}{}",
"-".red(),
event.key.bold(),
format_installed_executables(&event.key, &changelog.installed_executables)
)?;
writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?;
}
ChangeEventKind::Reinstalled => {
writeln!(
printer.stderr(),
" {} {}{}",
"~".yellow(),
event.key.bold(),
format_installed_executables(&event.key, &changelog.installed_executables)
)?;
writeln!(printer.stderr(), " {} {}", "~".yellow(), event.key.bold(),)?;
}
}
print_executable_changes(printer, &event, &changelog)?;
}

if preview.is_enabled() {
Expand Down Expand Up @@ -433,22 +440,61 @@ 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<PythonInstallationKey, Vec<PathBuf>>,
) -> 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 print_executable_changes(
printer: Printer,
event: &ChangeEvent,
changelog: &Changelog,
) -> Result<(), anyhow::Error> {
let installed = changelog
.installed_executables
.get(&event.key)
.into_iter()
.flatten()
.filter_map(|path| path.file_name())
.map(|name| name.to_string_lossy())
.collect::<FxHashSet<_>>();

let uninstalled = changelog
.uninstalled_executables
.iter()
.filter_map(|path| path.file_name())
.map(|name| name.to_string_lossy())
.collect::<FxHashSet<_>>();

let reinstalled = uninstalled
.intersection(&installed)
.cloned()
.collect::<FxHashSet<_>>();
let installed = installed.difference(&reinstalled).cloned();

for event in installed
.map(|name| ExecutableChangeEvent {
name: name.to_string(),
kind: ChangeEventKind::Added,
})
.chain(reinstalled.iter().map(|name| ExecutableChangeEvent {
name: name.to_string(),
kind: ChangeEventKind::Reinstalled,
}))
.sorted_unstable_by(|a, b| a.name.cmp(&b.name).then_with(|| a.kind.cmp(&b.kind)))
{
match event.kind {
ChangeEventKind::Added => {
writeln!(printer.stderr(), " {} {}", "+".green(), event.name.bold())?;
}
ChangeEventKind::Reinstalled => {
writeln!(
printer.stderr(),
" {} {}",
"~".yellow(),
event.name.bold(),
)?;
}
ChangeEventKind::Removed => unreachable!(),
}
}

Ok(())
}

fn warn_if_not_on_path(bin: &Path) {
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/commands/python/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ pub(super) struct ChangeEvent {
key: uv_python::PythonInstallationKey,
kind: ChangeEventKind,
}

#[derive(Debug)]
pub(super) struct ExecutableChangeEvent {
name: String,
kind: ChangeEventKind,
}
8 changes: 5 additions & 3 deletions crates/uv/src/commands/python/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -224,7 +226,7 @@ async fn do_uninstall(
" {} {} ({})",
"-".red(),
event.key.bold(),
event.key.versioned_executable_name()
event.key.executable_name_minor()
)?;
}
_ => unreachable!(),
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1053,6 +1053,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
&project_dir,
args.targets,
args.reinstall,
args.default,
globals.python_downloads,
globals.native_tls,
globals.connectivity,
Expand Down
13 changes: 11 additions & 2 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,15 +620,24 @@ impl PythonDirSettings {
pub(crate) struct PythonInstallSettings {
pub(crate) targets: Vec<String>,
pub(crate) reinstall: bool,
pub(crate) default: bool,
}

impl PythonInstallSettings {
/// Resolve the [`PythonInstallSettings`] from the CLI and filesystem configuration.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: PythonInstallArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let PythonInstallArgs { targets, reinstall } = args;
let PythonInstallArgs {
targets,
reinstall,
default,
} = args;

Self { targets, reinstall }
Self {
targets,
reinstall,
default,
}
}
}

Expand Down
Loading

0 comments on commit 4f242a2

Please sign in to comment.