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).
This commit is contained in:
Charlie Marsh 2024-08-09 14:21:49 -04:00 committed by GitHub
parent 44f94524f3
commit f89403f4f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 604 additions and 131 deletions

1
Cargo.lock generated
View file

@ -5179,6 +5179,7 @@ dependencies = [
"uv-fs",
"uv-installer",
"uv-python",
"uv-settings",
"uv-state",
"uv-virtualenv",
]

View file

@ -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 }

View file

@ -290,7 +290,8 @@ impl From<VerbatimUrl> 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<IndexUrl>,
extra_index: Vec<IndexUrl>,

View file

@ -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))]

View file

@ -197,6 +197,27 @@ impl From<Url> for VerbatimUrl {
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for VerbatimUrl {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.url.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for VerbatimUrl {
fn deserialize<D>(deserializer: D) -> Result<VerbatimUrl, D::Error>
where
D: serde::Deserializer<'de>,
{
let url = Url::deserialize(deserializer)?;
Ok(VerbatimUrl::from_url(url))
}
}
impl Pep508Url for VerbatimUrl {
type Err = VerbatimUrlError;

View file

@ -2759,9 +2759,6 @@ pub struct ToolUpgradeArgs {
#[command(flatten)]
pub build: BuildArgs,
#[command(flatten)]
pub refresh: RefreshArgs,
}
#[derive(Args)]

View file

@ -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::<ConfigSettings>()),
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 },
}
}

View file

@ -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))]

View file

@ -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))]

View file

@ -80,7 +80,7 @@ impl<'de> serde::Deserialize<'de> for ConfigSettingValue {
/// list of strings.
///
/// See: <https://peps.python.org/pep-0517/#config-settings>
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConfigSettings(BTreeMap<String, ConfigSettingValue>);

View file

@ -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<Reinstall> 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]

View file

@ -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]

View file

@ -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))]

View file

@ -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<ResolverInstallerOptions> 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<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<FlatIndexLocation>>,
pub index_strategy: Option<IndexStrategy>,
pub keyring_provider: Option<KeyringProviderType>,
pub resolution: Option<ResolutionMode>,
pub prerelease: Option<PrereleaseMode>,
pub config_settings: Option<ConfigSettings>,
pub no_build_isolation: Option<bool>,
pub no_build_isolation_package: Option<Vec<PackageName>>,
pub exclude_newer: Option<ExcludeNewer>,
pub link_mode: Option<LinkMode>,
pub compile_bytecode: Option<bool>,
pub no_sources: Option<bool>,
pub no_build: Option<bool>,
pub no_build_package: Option<Vec<PackageName>>,
pub no_binary: Option<bool>,
pub no_binary_package: Option<Vec<PackageName>>,
}
impl From<ResolverInstallerOptions> 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<ToolOptions> 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,
}
}
}

View file

@ -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-python = { workspace = true }
uv-virtualenv = { workspace = true }
uv-installer = { workspace = true }
uv-python = { workspace = true }
uv-settings = { workspace = true }
uv-state = { workspace = true }
uv-virtualenv = { workspace = true }
dirs-sys = { workspace = true }
fs-err = { workspace = true }

View file

@ -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<Tool> for ToolReceipt {
fn from(tool: Tool) -> Self {
ToolReceipt {

View file

@ -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<String>,
/// A mapping of entry point names to their metadata.
entrypoints: Vec<ToolEntrypoint>,
/// The [`ToolOptions`] used to install this tool.
options: ToolOptions,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ToolWire {
pub requirements: Vec<RequirementWire>,
pub python: Option<String>,
pub entrypoints: Vec<ToolEntrypoint>,
struct ToolWire {
requirements: Vec<RequirementWire>,
python: Option<String>,
entrypoints: Vec<ToolEntrypoint>,
#[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<Tool> for ToolWire {
.collect(),
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
}
}
}
@ -68,6 +74,7 @@ impl TryFrom<ToolWire> for Tool {
.collect(),
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options,
})
}
}
@ -112,6 +119,7 @@ impl Tool {
requirements: Vec<Requirement>,
python: Option<String>,
entrypoints: impl Iterator<Item = ToolEntrypoint>,
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<Table, toml_edit::ser::Error> {
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<String> {
&self.python
}
pub fn options(&self) -> &ToolOptions {
&self.options
}
}
impl ToolEntrypoint {

View file

@ -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<String>,
requirements: Vec<Requirement>,
action: InstallAction,
printer: Printer,
) -> anyhow::Result<ExitStatus> {
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)?;

View file

@ -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<String>,
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,
)
}

View file

@ -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<PackageName>,
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,
)?;
}

View file

@ -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<ExitStatus> {
&requirements,
args.python,
args.force,
args.options,
args.settings,
globals.preview,
globals.python_preference,
@ -812,12 +813,13 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
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,

View file

@ -320,6 +320,7 @@ pub(crate) struct ToolInstallSettings {
pub(crate) with_requirements: Vec<PathBuf>,
pub(crate) python: Option<String>,
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,10 +364,50 @@ 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 upgrade` invocation.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct ToolUpgradeSettings {
pub(crate) name: Option<PackageName>,
pub(crate) args: ResolverInstallerOptions,
pub(crate) filesystem: ResolverInstallerOptions,
}
impl ToolUpgradeSettings {
/// Resolve the [`ToolUpgradeSettings`] from the CLI and filesystem configuration.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: ToolUpgradeArgs, filesystem: Option<FilesystemOptions>) -> Self {
let ToolUpgradeArgs {
name,
all,
mut installer,
build,
} = args;
if installer.upgrade {
// If `--upgrade` was passed explicitly, warn.
warn_user_once!("`--upgrade` is enabled by default on `uv tool upgrade`");
} else if installer.upgrade_package.is_empty() {
// If neither `--upgrade` nor `--upgrade-package` were passed in, assume `--upgrade`.
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),
args,
filesystem,
}
}
}
@ -379,46 +429,6 @@ impl ToolListSettings {
}
}
/// 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<PackageName>,
pub(crate) settings: ResolverInstallerSettings,
pub(crate) refresh: Refresh,
}
impl ToolUpgradeSettings {
/// Resolve the [`ToolUpgradeSettings`] from the CLI and filesystem configuration.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: ToolUpgradeArgs, filesystem: Option<FilesystemOptions>) -> Self {
let ToolUpgradeArgs {
name,
all,
mut installer,
build,
refresh,
} = args;
if installer.upgrade {
// If `--upgrade` was passed explicitly, warn.
warn_user_once!("`--upgrade` is enabled by default on `uv tool upgrade`");
} else if installer.upgrade_package.is_empty() {
// If neither `--upgrade` nor `--upgrade-package` were passed in, assume `--upgrade`.
installer.upgrade = true;
}
Self {
name: name.filter(|_| !all),
settings: ResolverInstallerSettings::combine(
resolver_installer_options(installer, build),
filesystem,
),
refresh: Refresh::from(refresh),
}
}
}
/// The resolved settings to use for a `tool uninstall` invocation.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
@ -1668,31 +1678,6 @@ impl From<ResolverOptions> for ResolverSettings {
}
}
/// 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)]
pub(crate) struct ResolverInstallerSettings {
pub(crate) index_locations: IndexLocations,
pub(crate) index_strategy: IndexStrategy,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) config_setting: ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>,
pub(crate) exclude_newer: Option<ExcludeNewer>,
pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool,
pub(crate) sources: SourceStrategy,
pub(crate) upgrade: Upgrade,
pub(crate) reinstall: Reinstall,
pub(crate) build_options: BuildOptions,
}
#[derive(Debug, Clone)]
pub(crate) struct ResolverInstallerSettingsRef<'a> {
pub(crate) index_locations: &'a IndexLocations,
@ -1712,6 +1697,32 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> {
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, 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,
pub(crate) keyring_provider: KeyringProviderType,
pub(crate) resolution: ResolutionMode,
pub(crate) prerelease: PrereleaseMode,
pub(crate) config_setting: ConfigSettings,
pub(crate) no_build_isolation: bool,
pub(crate) no_build_isolation_package: Vec<PackageName>,
pub(crate) exclude_newer: Option<ExcludeNewer>,
pub(crate) link_mode: LinkMode,
pub(crate) compile_bytecode: bool,
pub(crate) sources: SourceStrategy,
pub(crate) upgrade: Upgrade,
pub(crate) reinstall: Reinstall,
pub(crate) build_options: BuildOptions,
}
impl ResolverInstallerSettings {
/// Reconcile the [`ResolverInstallerSettings`] from the CLI and filesystem configuration.
pub(crate) fn combine(

View file

@ -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,

View file

@ -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"
"###);
});
}

View file

@ -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"
"###);
});

View file

@ -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]

View file

@ -2426,10 +2426,6 @@ uv tool upgrade [OPTIONS] <NAME>
</ul>
</dd><dt><code>--quiet</code>, <code>-q</code></dt><dd><p>Do not print any output</p>
</dd><dt><code>--refresh</code></dt><dd><p>Refresh all cached data</p>
</dd><dt><code>--refresh-package</code> <i>refresh-package</i></dt><dd><p>Refresh cached data for a specific package</p>
</dd><dt><code>--reinstall</code></dt><dd><p>Reinstall all packages, regardless of whether they&#8217;re already installed. Implies <code>--refresh</code></p>
</dd><dt><code>--reinstall-package</code> <i>reinstall-package</i></dt><dd><p>Reinstall a specific package, regardless of whether it&#8217;s already installed. Implies <code>--refresh-package</code></p>