From f89403f4f6aca11a9461358d099a81523610b7e5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 9 Aug 2024 14:21:49 -0400 Subject: [PATCH] Retain and respect settings in tool upgrades (#5937) ## Summary We now persist the `ResolverInstallerOptions` when writing out a tool receipt. When upgrading, we grab the saved options, and merge with the command-line arguments and user-level filesystem settings (CLI > receipt > filesystem). --- Cargo.lock | 1 + crates/distribution-types/Cargo.toml | 2 +- crates/distribution-types/src/index_url.rs | 3 +- crates/install-wheel-rs/src/linker.rs | 2 +- crates/pep508-rs/src/verbatim_url.rs | 21 ++ crates/uv-cli/src/lib.rs | 3 - crates/uv-cli/src/options.rs | 30 ++- crates/uv-configuration/src/authentication.rs | 2 +- crates/uv-configuration/src/build_options.rs | 11 +- .../uv-configuration/src/config_settings.rs | 2 +- .../uv-configuration/src/package_options.rs | 6 +- crates/uv-configuration/src/sources.rs | 3 +- crates/uv-resolver/src/prerelease.rs | 2 +- crates/uv-settings/src/settings.rs | 93 ++++++- crates/uv-tool/Cargo.toml | 5 +- crates/uv-tool/src/receipt.rs | 9 - crates/uv-tool/src/tool.rs | 44 +++- crates/uv/src/commands/tool/common.rs | 5 +- crates/uv/src/commands/tool/install.rs | 20 +- crates/uv/src/commands/tool/upgrade.rs | 20 +- crates/uv/src/lib.rs | 8 +- crates/uv/src/settings.rs | 109 +++++---- crates/uv/tests/show_settings.rs | 33 +++ crates/uv/tests/tool_install.rs | 231 +++++++++++++++++- crates/uv/tests/tool_list.rs | 3 + crates/uv/tests/tool_upgrade.rs | 21 +- docs/reference/cli.md | 4 - 27 files changed, 583 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec85647affb7..9423dbd38261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5179,6 +5179,7 @@ dependencies = [ "uv-fs", "uv-installer", "uv-python", + "uv-settings", "uv-state", "uv-virtualenv", ] diff --git a/crates/distribution-types/Cargo.toml b/crates/distribution-types/Cargo.toml index 0b8990028dde..3f25d1438d72 100644 --- a/crates/distribution-types/Cargo.toml +++ b/crates/distribution-types/Cargo.toml @@ -16,7 +16,7 @@ workspace = true cache-key = { workspace = true } distribution-filename = { workspace = true } pep440_rs = { workspace = true } -pep508_rs = { workspace = true } +pep508_rs = { workspace = true, features = ["serde"] } platform-tags = { workspace = true } pypi-types = { workspace = true } uv-fs = { workspace = true } diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index e88ec80ba6af..22b76c2d25f0 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -290,7 +290,8 @@ impl From for FlatIndexLocation { /// The index locations to use for fetching packages. By default, uses the PyPI index. /// /// From a pip perspective, this type merges `--index-url`, `--extra-index-url`, and `--find-links`. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct IndexLocations { index: Option, extra_index: Vec, diff --git a/crates/install-wheel-rs/src/linker.rs b/crates/install-wheel-rs/src/linker.rs index ff43ba4f3967..1c2af8520d85 100644 --- a/crates/install-wheel-rs/src/linker.rs +++ b/crates/install-wheel-rs/src/linker.rs @@ -228,7 +228,7 @@ fn parse_scripts( scripts_from_ini(extras, python_minor, ini) } -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index b444b677d5c3..5975194c3fdb 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/src/verbatim_url.rs @@ -197,6 +197,27 @@ impl From for VerbatimUrl { } } +#[cfg(feature = "serde")] +impl serde::Serialize for VerbatimUrl { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.url.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for VerbatimUrl { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let url = Url::deserialize(deserializer)?; + Ok(VerbatimUrl::from_url(url)) + } +} + impl Pep508Url for VerbatimUrl { type Err = VerbatimUrlError; diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index eec316b059a5..63a27ff98d9f 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -2759,9 +2759,6 @@ pub struct ToolUpgradeArgs { #[command(flatten)] pub build: BuildArgs, - - #[command(flatten)] - pub refresh: RefreshArgs, } #[derive(Args)] diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index 87ec4c5f1867..bbf7133e9626 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -306,9 +306,17 @@ pub fn resolver_installer_options( }, find_links: index_args.find_links, upgrade: flag(upgrade, no_upgrade), - upgrade_package: Some(upgrade_package), + upgrade_package: if upgrade_package.is_empty() { + None + } else { + Some(upgrade_package) + }, reinstall: flag(reinstall, no_reinstall), - reinstall_package: Some(reinstall_package), + reinstall_package: if reinstall_package.is_empty() { + None + } else { + Some(reinstall_package) + }, index_strategy, keyring_provider, resolution, @@ -320,14 +328,26 @@ pub fn resolver_installer_options( config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), no_build_isolation: flag(no_build_isolation, build_isolation), - no_build_isolation_package: Some(no_build_isolation_package), + no_build_isolation_package: if no_build_isolation_package.is_empty() { + None + } else { + Some(no_build_isolation_package) + }, exclude_newer, link_mode, compile_bytecode: flag(compile_bytecode, no_compile_bytecode), no_build: flag(no_build, build), - no_build_package: Some(no_build_package), + no_build_package: if no_build_package.is_empty() { + None + } else { + Some(no_build_package) + }, no_binary: flag(no_binary, binary), - no_binary_package: Some(no_binary_package), + no_binary_package: if no_binary_package.is_empty() { + None + } else { + Some(no_binary_package) + }, no_sources: if no_sources { Some(true) } else { None }, } } diff --git a/crates/uv-configuration/src/authentication.rs b/crates/uv-configuration/src/authentication.rs index 14634f3ffe79..ce22e5749d32 100644 --- a/crates/uv-configuration/src/authentication.rs +++ b/crates/uv-configuration/src/authentication.rs @@ -1,7 +1,7 @@ use uv_auth::{self, KeyringProvider}; /// Keyring provider type to use for credential lookup. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/uv-configuration/src/build_options.rs b/crates/uv-configuration/src/build_options.rs index 9ae5274ffc71..6a75f03f2138 100644 --- a/crates/uv-configuration/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -32,7 +32,8 @@ impl Display for BuildKind { } } -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct BuildOptions { no_binary: NoBinary, no_build: NoBuild, @@ -111,7 +112,8 @@ impl BuildOptions { } } -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum NoBinary { /// Allow installation of any wheel. #[default] @@ -206,7 +208,8 @@ impl NoBinary { } } -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum NoBuild { /// Allow building wheels from any source distribution. #[default] @@ -305,7 +308,7 @@ impl NoBuild { } } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/uv-configuration/src/config_settings.rs b/crates/uv-configuration/src/config_settings.rs index d69bb19c71e1..f8f33e39ec0e 100644 --- a/crates/uv-configuration/src/config_settings.rs +++ b/crates/uv-configuration/src/config_settings.rs @@ -80,7 +80,7 @@ impl<'de> serde::Deserialize<'de> for ConfigSettingValue { /// list of strings. /// /// See: -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ConfigSettings(BTreeMap); diff --git a/crates/uv-configuration/src/package_options.rs b/crates/uv-configuration/src/package_options.rs index 24388c23a5d6..0a58d9987312 100644 --- a/crates/uv-configuration/src/package_options.rs +++ b/crates/uv-configuration/src/package_options.rs @@ -6,7 +6,8 @@ use rustc_hash::FxHashMap; use uv_cache::{Refresh, Timestamp}; /// Whether to reinstall packages. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum Reinstall { /// Don't reinstall any packages; respect the existing installation. #[default] @@ -58,7 +59,8 @@ impl From for Refresh { } /// Whether to allow package upgrades. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum Upgrade { /// Prefer pinned versions from the existing lockfile, if possible. #[default] diff --git a/crates/uv-configuration/src/sources.rs b/crates/uv-configuration/src/sources.rs index 268e983dd298..c60d69ef449d 100644 --- a/crates/uv-configuration/src/sources.rs +++ b/crates/uv-configuration/src/sources.rs @@ -1,4 +1,5 @@ -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub enum SourceStrategy { /// Use `tool.uv.sources` when resolving dependencies. #[default] diff --git a/crates/uv-resolver/src/prerelease.rs b/crates/uv-resolver/src/prerelease.rs index c5b59d539002..dc1c216fef16 100644 --- a/crates/uv-resolver/src/prerelease.rs +++ b/crates/uv-resolver/src/prerelease.rs @@ -6,7 +6,7 @@ use uv_normalize::PackageName; use crate::resolver::ForkSet; use crate::{DependencyMode, Manifest, ResolverMarkers}; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 8332ff3c975f..d593fa767d62 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use distribution_types::{FlatIndexLocation, IndexUrl}; use install_wheel_rs::linker::LinkMode; @@ -212,7 +212,9 @@ pub struct ResolverOptions { /// Shared settings, relevant to all operations that must resolve and install dependencies. The /// union of [`InstallerOptions`] and [`ResolverOptions`]. #[allow(dead_code)] -#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)] +#[derive( + Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, CombineOptions, OptionsMetadata, +)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ResolverInstallerOptions { @@ -1243,3 +1245,90 @@ impl From for InstallerOptions { } } } + +/// The options persisted alongside an installed tool. +/// +/// A mirror of [`ResolverInstallerOptions`], without upgrades and reinstalls, which shouldn't be +/// persisted in a tool receipt. +#[derive( + Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, CombineOptions, OptionsMetadata, +)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +pub struct ToolOptions { + pub index_url: Option, + pub extra_index_url: Option>, + pub no_index: Option, + pub find_links: Option>, + pub index_strategy: Option, + pub keyring_provider: Option, + pub resolution: Option, + pub prerelease: Option, + pub config_settings: Option, + pub no_build_isolation: Option, + pub no_build_isolation_package: Option>, + pub exclude_newer: Option, + pub link_mode: Option, + pub compile_bytecode: Option, + pub no_sources: Option, + pub no_build: Option, + pub no_build_package: Option>, + pub no_binary: Option, + pub no_binary_package: Option>, +} + +impl From for ToolOptions { + fn from(value: ResolverInstallerOptions) -> Self { + Self { + index_url: value.index_url, + extra_index_url: value.extra_index_url, + no_index: value.no_index, + find_links: value.find_links, + index_strategy: value.index_strategy, + keyring_provider: value.keyring_provider, + resolution: value.resolution, + prerelease: value.prerelease, + config_settings: value.config_settings, + no_build_isolation: value.no_build_isolation, + no_build_isolation_package: value.no_build_isolation_package, + exclude_newer: value.exclude_newer, + link_mode: value.link_mode, + compile_bytecode: value.compile_bytecode, + no_sources: value.no_sources, + no_build: value.no_build, + no_build_package: value.no_build_package, + no_binary: value.no_binary, + no_binary_package: value.no_binary_package, + } + } +} + +impl From for ResolverInstallerOptions { + fn from(value: ToolOptions) -> Self { + Self { + index_url: value.index_url, + extra_index_url: value.extra_index_url, + no_index: value.no_index, + find_links: value.find_links, + index_strategy: value.index_strategy, + keyring_provider: value.keyring_provider, + resolution: value.resolution, + prerelease: value.prerelease, + config_settings: value.config_settings, + no_build_isolation: value.no_build_isolation, + no_build_isolation_package: value.no_build_isolation_package, + exclude_newer: value.exclude_newer, + link_mode: value.link_mode, + compile_bytecode: value.compile_bytecode, + no_sources: value.no_sources, + upgrade: None, + upgrade_package: None, + reinstall: None, + reinstall_package: None, + no_build: value.no_build, + no_build_package: value.no_build_package, + no_binary: value.no_binary, + no_binary_package: value.no_binary_package, + } + } +} diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index 270514e36543..94507fd8fb6b 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -19,10 +19,11 @@ pep508_rs = { workspace = true } pypi-types = { workspace = true } uv-cache = { workspace = true } uv-fs = { workspace = true } -uv-state = { workspace = true } +uv-installer = { workspace = true } uv-python = { workspace = true } +uv-settings = { workspace = true } +uv-state = { workspace = true } uv-virtualenv = { workspace = true } -uv-installer = { workspace = true } dirs-sys = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-tool/src/receipt.rs b/crates/uv-tool/src/receipt.rs index 537ea482fa7b..84b57ca0067e 100644 --- a/crates/uv-tool/src/receipt.rs +++ b/crates/uv-tool/src/receipt.rs @@ -42,15 +42,6 @@ impl ToolReceipt { } } -// Ignore raw document in comparison. -impl PartialEq for ToolReceipt { - fn eq(&self, other: &Self) -> bool { - self.tool.eq(&other.tool) - } -} - -impl Eq for ToolReceipt {} - impl From for ToolReceipt { fn from(tool: Tool) -> Self { ToolReceipt { diff --git a/crates/uv-tool/src/tool.rs b/crates/uv-tool/src/tool.rs index 39f0667a92e5..0eb47b763010 100644 --- a/crates/uv-tool/src/tool.rs +++ b/crates/uv-tool/src/tool.rs @@ -2,16 +2,17 @@ use std::path::PathBuf; use serde::Deserialize; use toml_edit::value; -use toml_edit::Array; use toml_edit::Table; use toml_edit::Value; +use toml_edit::{Array, Item}; use pypi_types::{Requirement, VerbatimParsedUrl}; use uv_fs::PortablePath; +use uv_settings::ToolOptions; /// A tool entry. #[allow(dead_code)] -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(try_from = "ToolWire", into = "ToolWire")] pub struct Tool { /// The requirements requested by the user during installation. @@ -20,18 +21,22 @@ pub struct Tool { python: Option, /// A mapping of entry point names to their metadata. entrypoints: Vec, + /// The [`ToolOptions`] used to install this tool. + options: ToolOptions, } #[derive(Clone, Debug, Deserialize)] -pub struct ToolWire { - pub requirements: Vec, - pub python: Option, - pub entrypoints: Vec, +struct ToolWire { + requirements: Vec, + python: Option, + entrypoints: Vec, + #[serde(default)] + options: ToolOptions, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[serde(untagged)] -pub enum RequirementWire { +enum RequirementWire { /// A [`Requirement`] following our uv-specific schema. Requirement(Requirement), /// A PEP 508-compatible requirement. We no longer write these, but there might be receipts out @@ -49,6 +54,7 @@ impl From for ToolWire { .collect(), python: tool.python, entrypoints: tool.entrypoints, + options: tool.options, } } } @@ -68,6 +74,7 @@ impl TryFrom for Tool { .collect(), python: tool.python, entrypoints: tool.entrypoints, + options: tool.options, }) } } @@ -112,6 +119,7 @@ impl Tool { requirements: Vec, python: Option, entrypoints: impl Iterator, + options: ToolOptions, ) -> Self { let mut entrypoints: Vec<_> = entrypoints.collect(); entrypoints.sort(); @@ -119,9 +127,16 @@ impl Tool { requirements, python, entrypoints, + options, } } + /// Create a new [`Tool`] with the given [`ToolOptions`]. + #[must_use] + pub fn with_options(self, options: ToolOptions) -> Self { + Self { options, ..self } + } + /// Returns the TOML table for this tool. pub(crate) fn to_toml(&self) -> Result { let mut table = Table::new(); @@ -160,6 +175,17 @@ impl Tool { value(entrypoints) }); + if self.options != ToolOptions::default() { + let serialized = + serde::Serialize::serialize(&self.options, toml_edit::ser::ValueSerializer::new())?; + let Value::InlineTable(serialized) = serialized else { + return Err(toml_edit::ser::Error::Custom( + "Expected an inline table".to_string(), + )); + }; + table.insert("options", Item::Table(serialized.into_table())); + } + Ok(table) } @@ -174,6 +200,10 @@ impl Tool { pub fn python(&self) -> &Option { &self.python } + + pub fn options(&self) -> &ToolOptions { + &self.options + } } impl ToolEntrypoint { diff --git a/crates/uv/src/commands/tool/common.rs b/crates/uv/src/commands/tool/common.rs index a56496d9a40e..5acaf44349b0 100644 --- a/crates/uv/src/commands/tool/common.rs +++ b/crates/uv/src/commands/tool/common.rs @@ -14,6 +14,7 @@ use uv_fs::replace_symlink; use uv_fs::Simplified; use uv_installer::SitePackages; use uv_python::PythonEnvironment; +use uv_settings::ToolOptions; use uv_shell::Shell; use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint}; use uv_warnings::warn_user; @@ -72,11 +73,12 @@ pub(crate) fn install_executables( environment: &PythonEnvironment, name: &PackageName, installed_tools: &InstalledTools, - printer: Printer, + options: ToolOptions, force: bool, python: Option, requirements: Vec, action: InstallAction, + printer: Printer, ) -> anyhow::Result { let site_packages = SitePackages::from_environment(environment)?; let installed = site_packages.get_packages(name); @@ -199,6 +201,7 @@ pub(crate) fn install_executables( target_entry_points .into_iter() .map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)), + options, ); installed_tools.add_tool_receipt(name, tool)?; diff --git a/crates/uv/src/commands/tool/install.rs b/crates/uv/src/commands/tool/install.rs index d389e291a08c..32486fe7a43d 100644 --- a/crates/uv/src/commands/tool/install.rs +++ b/crates/uv/src/commands/tool/install.rs @@ -14,6 +14,7 @@ use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, }; use uv_requirements::{RequirementsSource, RequirementsSpecification}; +use uv_settings::{ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use uv_warnings::{warn_user, warn_user_once}; @@ -37,6 +38,7 @@ pub(crate) async fn install( with: &[RequirementsSource], python: Option, force: bool, + options: ResolverInstallerOptions, settings: ResolverInstallerSettings, preview: PreviewMode, python_preference: PythonPreference, @@ -75,6 +77,7 @@ pub(crate) async fn install( // Initialize any shared state. let state = SharedState::default(); + let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls); @@ -177,6 +180,9 @@ pub(crate) async fn install( requirements }; + // Convert to tool options. + let options = ToolOptions::from(options); + let installed_tools = InstalledTools::from_settings()?.init()?; let _lock = installed_tools.acquire_lock()?; @@ -236,12 +242,21 @@ pub(crate) async fn install( if requirements == receipt { // And the user didn't request a reinstall or upgrade... if !force && settings.reinstall.is_none() && settings.upgrade.is_none() { - // We're done. + if *tool_receipt.options() != options { + // ...but the options differ, we need to update the receipt. + installed_tools.add_tool_receipt( + &from.name, + tool_receipt.clone().with_options(options), + )?; + } + + // We're done, though we might need to update the receipt. writeln!( printer.stderr(), "`{from}` is already installed", from = from.cyan() )?; + return Ok(ExitStatus::Success); } } @@ -333,10 +348,11 @@ pub(crate) async fn install( &environment, &from.name, &installed_tools, - printer, + options, force || invalid_tool_receipt, python, requirements, InstallAction::Install, + printer, ) } diff --git a/crates/uv/src/commands/tool/upgrade.rs b/crates/uv/src/commands/tool/upgrade.rs index 665588e7046a..8e3b9da1db9e 100644 --- a/crates/uv/src/commands/tool/upgrade.rs +++ b/crates/uv/src/commands/tool/upgrade.rs @@ -15,6 +15,7 @@ use uv_client::Connectivity; use uv_configuration::{Concurrency, PreviewMode}; use uv_normalize::PackageName; use uv_requirements::RequirementsSpecification; +use uv_settings::{Combine, ResolverInstallerOptions, ToolOptions}; use uv_tool::InstalledTools; use uv_warnings::warn_user_once; @@ -22,7 +23,8 @@ use uv_warnings::warn_user_once; pub(crate) async fn upgrade( name: Option, connectivity: Connectivity, - settings: ResolverInstallerSettings, + args: ResolverInstallerOptions, + filesystem: ResolverInstallerOptions, concurrency: Concurrency, native_tls: bool, cache: &Cache, @@ -107,14 +109,19 @@ pub(crate) async fn upgrade( } }; + // Resolve the appropriate settings, preferring: CLI > receipt > user. + let options = args.clone().combine( + ResolverInstallerOptions::from(existing_tool_receipt.options().clone()) + .combine(filesystem.clone()), + ); + let settings = ResolverInstallerSettings::from(options.clone()); + // Resolve the requirements. let requirements = existing_tool_receipt.requirements(); let spec = RequirementsSpecification::from_requirements(requirements.to_vec()); - // TODO(zanieb): Build the environment in the cache directory then copy into the tool directory. - // This lets us confirm the environment is valid before removing an existing install. However, - // entrypoints always contain an absolute path to the relevant Python interpreter, which would - // be invalidated by moving the environment. + // TODO(zanieb): Build the environment in the cache directory then copy into the tool + // directory. let environment = update_environment( existing_environment, spec, @@ -139,11 +146,12 @@ pub(crate) async fn upgrade( &environment, &name, &installed_tools, - printer, + ToolOptions::from(options), true, existing_tool_receipt.python().to_owned(), requirements.to_vec(), InstallAction::Update, + printer, )?; } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 8d849265e7f4..87c01a5d0743 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -12,7 +12,7 @@ use owo_colors::OwoColorize; use tracing::{debug, instrument}; use settings::PipTreeSettings; -use uv_cache::{Cache, Refresh}; +use uv_cache::{Cache, Refresh, Timestamp}; use uv_cli::{ compat::CompatArgs, CacheCommand, CacheNamespace, Cli, Commands, PipCommand, PipNamespace, ProjectCommand, @@ -780,6 +780,7 @@ async fn run(cli: Cli) -> Result { &requirements, args.python, args.force, + args.options, args.settings, globals.preview, globals.python_preference, @@ -812,12 +813,13 @@ async fn run(cli: Cli) -> Result { show_settings!(args); // Initialize the cache. - let cache = cache.init()?.with_refresh(args.refresh); + let cache = cache.init()?.with_refresh(Refresh::All(Timestamp::now())); commands::tool_upgrade( args.name, globals.connectivity, - args.settings, + args.args, + args.filesystem, Concurrency::default(), globals.native_tls, &cache, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 2e80206a3aae..be54b1de8edd 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -320,6 +320,7 @@ pub(crate) struct ToolInstallSettings { pub(crate) with_requirements: Vec, pub(crate) python: Option, pub(crate) refresh: Refresh, + pub(crate) options: ResolverInstallerOptions, pub(crate) settings: ResolverInstallerSettings, pub(crate) force: bool, pub(crate) editable: bool, @@ -342,6 +343,15 @@ impl ToolInstallSettings { python, } = args; + let options = resolver_installer_options(installer, build).combine( + filesystem + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) + .unwrap_or_default(), + ); + + let settings = ResolverInstallerSettings::from(options.clone()); + Self { package, from, @@ -354,38 +364,19 @@ impl ToolInstallSettings { force, editable, refresh: Refresh::from(refresh), - settings: ResolverInstallerSettings::combine( - resolver_installer_options(installer, build), - filesystem, - ), + options, + settings, } } } -/// The resolved settings to use for a `tool list` invocation. -#[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Clone)] -pub(crate) struct ToolListSettings { - pub(crate) show_paths: bool, -} - -impl ToolListSettings { - /// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration. - #[allow(clippy::needless_pass_by_value)] - pub(crate) fn resolve(args: ToolListArgs, _filesystem: Option) -> Self { - let ToolListArgs { show_paths } = args; - - Self { show_paths } - } -} - /// The resolved settings to use for a `tool upgrade` invocation. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] pub(crate) struct ToolUpgradeSettings { pub(crate) name: Option, - pub(crate) settings: ResolverInstallerSettings, - pub(crate) refresh: Refresh, + pub(crate) args: ResolverInstallerOptions, + pub(crate) filesystem: ResolverInstallerOptions, } impl ToolUpgradeSettings { @@ -397,7 +388,6 @@ impl ToolUpgradeSettings { all, mut installer, build, - refresh, } = args; if installer.upgrade { @@ -408,17 +398,37 @@ impl ToolUpgradeSettings { installer.upgrade = true; } + let args = resolver_installer_options(installer, build); + let filesystem = filesystem + .map(FilesystemOptions::into_options) + .map(|options| options.top_level) + .unwrap_or_default(); + Self { name: name.filter(|_| !all), - settings: ResolverInstallerSettings::combine( - resolver_installer_options(installer, build), - filesystem, - ), - refresh: Refresh::from(refresh), + args, + filesystem, } } } +/// The resolved settings to use for a `tool list` invocation. +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Clone)] +pub(crate) struct ToolListSettings { + pub(crate) show_paths: bool, +} + +impl ToolListSettings { + /// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration. + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn resolve(args: ToolListArgs, _filesystem: Option) -> Self { + let ToolListArgs { show_paths } = args; + + Self { show_paths } + } +} + /// The resolved settings to use for a `tool uninstall` invocation. #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Clone)] @@ -1668,13 +1678,33 @@ impl From for ResolverSettings { } } +#[derive(Debug, Clone)] +pub(crate) struct ResolverInstallerSettingsRef<'a> { + pub(crate) index_locations: &'a IndexLocations, + pub(crate) index_strategy: IndexStrategy, + pub(crate) keyring_provider: KeyringProviderType, + pub(crate) resolution: ResolutionMode, + pub(crate) prerelease: PrereleaseMode, + pub(crate) config_setting: &'a ConfigSettings, + pub(crate) no_build_isolation: bool, + pub(crate) no_build_isolation_package: &'a [PackageName], + pub(crate) exclude_newer: Option, + pub(crate) link_mode: LinkMode, + pub(crate) compile_bytecode: bool, + pub(crate) sources: SourceStrategy, + pub(crate) upgrade: &'a Upgrade, + pub(crate) reinstall: &'a Reinstall, + pub(crate) build_options: &'a BuildOptions, +} + /// The resolved settings to use for an invocation of the uv CLI with both resolver and installer /// capabilities. /// /// Represents the shared settings that are used across all uv commands outside the `pip` API. /// Analogous to the settings contained in the `[tool.uv]` table, combined with [`ResolverInstallerArgs`]. #[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] pub(crate) struct ResolverInstallerSettings { pub(crate) index_locations: IndexLocations, pub(crate) index_strategy: IndexStrategy, @@ -1693,25 +1723,6 @@ pub(crate) struct ResolverInstallerSettings { pub(crate) build_options: BuildOptions, } -#[derive(Debug, Clone)] -pub(crate) struct ResolverInstallerSettingsRef<'a> { - pub(crate) index_locations: &'a IndexLocations, - pub(crate) index_strategy: IndexStrategy, - pub(crate) keyring_provider: KeyringProviderType, - pub(crate) resolution: ResolutionMode, - pub(crate) prerelease: PrereleaseMode, - pub(crate) config_setting: &'a ConfigSettings, - pub(crate) no_build_isolation: bool, - pub(crate) no_build_isolation_package: &'a [PackageName], - pub(crate) exclude_newer: Option, - pub(crate) link_mode: LinkMode, - pub(crate) compile_bytecode: bool, - pub(crate) sources: SourceStrategy, - pub(crate) upgrade: &'a Upgrade, - pub(crate) reinstall: &'a Reinstall, - pub(crate) build_options: &'a BuildOptions, -} - impl ResolverInstallerSettings { /// Reconcile the [`ResolverInstallerSettings`] from the CLI and filesystem configuration. pub(crate) fn combine( diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index e65c01d36d8b..9326b55894da 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -2444,6 +2444,39 @@ fn resolve_tool() -> anyhow::Result<()> { }, ), ), + options: ResolverInstallerOptions { + index_url: None, + extra_index_url: None, + no_index: None, + find_links: None, + index_strategy: None, + keyring_provider: None, + resolution: Some( + LowestDirect, + ), + prerelease: None, + config_settings: None, + no_build_isolation: None, + no_build_isolation_package: None, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + link_mode: Some( + Clone, + ), + compile_bytecode: None, + no_sources: None, + upgrade: None, + upgrade_package: None, + reinstall: None, + reinstall_package: None, + no_build: None, + no_build_package: None, + no_binary: None, + no_binary_package: None, + }, settings: ResolverInstallerSettings { index_locations: IndexLocations { index: None, diff --git a/crates/uv/tests/tool_install.rs b/crates/uv/tests/tool_install.rs index cb403fca7ef3..29158b6a65df 100644 --- a/crates/uv/tests/tool_install.rs +++ b/crates/uv/tests/tool_install.rs @@ -87,6 +87,9 @@ fn tool_install() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -166,6 +169,9 @@ fn tool_install() { entrypoints = [ { name = "flask", install-path = "[TEMP_DIR]/bin/flask" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); } @@ -304,6 +310,9 @@ fn tool_install_version() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -383,6 +392,9 @@ fn tool_install_editable() { entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -420,6 +432,9 @@ fn tool_install_editable() { entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -462,6 +477,9 @@ fn tool_install_editable() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); } @@ -508,6 +526,9 @@ fn tool_install_remove_on_empty() -> Result<()> { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -597,6 +618,9 @@ fn tool_install_remove_on_empty() -> Result<()> { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -670,6 +694,9 @@ fn tool_install_editable_from() { entrypoints = [ { name = "black", install-path = "[TEMP_DIR]/bin/black" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -822,6 +849,9 @@ fn tool_install_already_installed() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -856,6 +886,9 @@ fn tool_install_already_installed() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1164,6 +1197,9 @@ fn tool_install_entry_point_exists() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1197,6 +1233,9 @@ fn tool_install_entry_point_exists() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1427,6 +1466,9 @@ fn tool_install_unnamed_package() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1539,6 +1581,9 @@ fn tool_install_unnamed_from() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1629,6 +1674,9 @@ fn tool_install_unnamed_with() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1696,6 +1744,9 @@ fn tool_install_requirements_txt() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1739,6 +1790,9 @@ fn tool_install_requirements_txt() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); } @@ -1801,6 +1855,9 @@ fn tool_install_requirements_txt_arguments() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1915,6 +1972,9 @@ fn tool_install_upgrade() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1945,6 +2005,9 @@ fn tool_install_upgrade() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -1983,6 +2046,9 @@ fn tool_install_upgrade() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); @@ -2021,6 +2087,9 @@ fn tool_install_upgrade() { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); } @@ -2282,14 +2351,14 @@ fn tool_install_bad_receipt() -> Result<()> { /// Test installing a tool with a malformed `.dist-info` directory (i.e., a `.dist-info` directory /// that isn't properly normalized). #[test] -fn tool_install_malformed() { +fn tool_install_malformed_dist_info() { let context = TestContext::new("3.12") .with_filtered_counts() .with_filtered_exe_suffix(); let tool_dir = context.temp_dir.child("tools"); let bin_dir = context.temp_dir.child("bin"); - // Install `black` + // Install `babel` uv_snapshot!(context.filters(), context.tool_install() .arg("babel") .env("UV_TOOL_DIR", tool_dir.as_os_str()) @@ -2346,6 +2415,164 @@ fn tool_install_malformed() { entrypoints = [ { name = "pybabel", install-path = "[TEMP_DIR]/bin/pybabel" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" + "###); + }); +} + +/// Test installing, then re-installing with different settings. +#[test] +fn tool_install_settings() { + let context = TestContext::new("3.12") + .with_filtered_counts() + .with_filtered_exe_suffix(); + let tool_dir = context.temp_dir.child("tools"); + let bin_dir = context.temp_dir.child("bin"); + + // Install `black` + uv_snapshot!(context.filters(), context.tool_install() + .arg("flask>=3") + .arg("--resolution=lowest-direct") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv tool install` is experimental and may change without warning + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Installed [N] packages in [TIME] + + blinker==1.7.0 + + click==8.1.7 + + flask==3.0.0 + + itsdangerous==2.1.2 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + werkzeug==3.0.1 + Installed 1 executable: flask + "###); + + tool_dir.child("flask").assert(predicate::path::is_dir()); + tool_dir + .child("flask") + .child("uv-receipt.toml") + .assert(predicate::path::exists()); + + let executable = bin_dir.child(format!("flask{}", std::env::consts::EXE_SUFFIX)); + assert!(executable.exists()); + + // On Windows, we can't snapshot an executable file. + #[cfg(not(windows))] + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###" + #![TEMP_DIR]/tools/flask/bin/python + # -*- coding: utf-8 -*- + import re + import sys + from flask.cli import main + if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(main()) + "###); + + }); + + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r###" + [tool] + requirements = [{ name = "flask", specifier = ">=3" }] + entrypoints = [ + { name = "flask", install-path = "[TEMP_DIR]/bin/flask" }, + ] + + [tool.options] + resolution = "lowest-direct" + exclude-newer = "2024-03-25T00:00:00Z" + "###); + }); + + // Reinstall with `highest`. This is a no-op, since we _do_ have a compatible version installed. + uv_snapshot!(context.filters(), context.tool_install() + .arg("flask>=3") + .arg("--resolution=highest") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv tool install` is experimental and may change without warning + `flask>=3` is already installed + "###); + + // It should update the receipt though. + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r###" + [tool] + requirements = [{ name = "flask", specifier = ">=3" }] + entrypoints = [ + { name = "flask", install-path = "[TEMP_DIR]/bin/flask" }, + ] + + [tool.options] + resolution = "highest" + exclude-newer = "2024-03-25T00:00:00Z" + "###); + }); + + // Reinstall with `highest` and `--upgrade`. This should change the setting and install a higher + // version. + uv_snapshot!(context.filters(), context.tool_install() + .arg("flask>=3") + .arg("--resolution=highest") + .arg("--upgrade") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv tool install` is experimental and may change without warning + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Uninstalled [N] packages in [TIME] + Installed [N] packages in [TIME] + - flask==3.0.0 + + flask==3.0.2 + Installed 1 executable: flask + "###); + + insta::with_settings!({ + filters => context.filters(), + }, { + // We should have a tool receipt + assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r###" + [tool] + requirements = [{ name = "flask", specifier = ">=3" }] + entrypoints = [ + { name = "flask", install-path = "[TEMP_DIR]/bin/flask" }, + ] + + [tool.options] + resolution = "highest" + exclude-newer = "2024-03-25T00:00:00Z" "###); }); } diff --git a/crates/uv/tests/tool_list.rs b/crates/uv/tests/tool_list.rs index a00a2a0fb122..a7d16911e944 100644 --- a/crates/uv/tests/tool_list.rs +++ b/crates/uv/tests/tool_list.rs @@ -197,6 +197,9 @@ fn tool_list_deprecated() -> Result<()> { { name = "black", install-path = "[TEMP_DIR]/bin/black" }, { name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" }, ] + + [tool.options] + exclude-newer = "2024-03-25T00:00:00Z" "###); }); diff --git a/crates/uv/tests/tool_upgrade.rs b/crates/uv/tests/tool_upgrade.rs index 35f422842589..9a6404b885f7 100644 --- a/crates/uv/tests/tool_upgrade.rs +++ b/crates/uv/tests/tool_upgrade.rs @@ -203,8 +203,7 @@ fn test_tool_upgrade_settings() { Installed 2 executables: black, blackd "###); - // Upgrade `black`. It should respect `lowest-direct`, but doesn't right now, so it's - // unintentionally upgraded. + // Upgrade `black`. This should be a no-op, since the resolution is set to `lowest-direct`. uv_snapshot!(context.filters(), context.tool_upgrade() .arg("black") .env("UV_TOOL_DIR", tool_dir.as_os_str()) @@ -214,6 +213,24 @@ fn test_tool_upgrade_settings() { exit_code: 0 ----- stdout ----- + ----- stderr ----- + warning: `uv tool upgrade` is experimental and may change without warning + Resolved [N] packages in [TIME] + Audited [N] packages in [TIME] + Updated 2 executables: black, blackd + "###); + + // Upgrade `black`, but override the resolution. + uv_snapshot!(context.filters(), context.tool_upgrade() + .arg("black") + .arg("--resolution=highest") + .env("UV_TOOL_DIR", tool_dir.as_os_str()) + .env("XDG_BIN_HOME", bin_dir.as_os_str()) + .env("PATH", bin_dir.as_os_str()), @r###" + success: true + exit_code: 0 + ----- stdout ----- + ----- stderr ----- warning: `uv tool upgrade` is experimental and may change without warning Resolved [N] packages in [TIME] diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 8d099b2902b1..ca3a20d1de40 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -2426,10 +2426,6 @@ uv tool upgrade [OPTIONS]
--quiet, -q

Do not print any output

-
--refresh

Refresh all cached data

- -
--refresh-package refresh-package

Refresh cached data for a specific package

-
--reinstall

Reinstall all packages, regardless of whether they’re already installed. Implies --refresh

--reinstall-package reinstall-package

Reinstall a specific package, regardless of whether it’s already installed. Implies --refresh-package