Add a lockfile to tools

This commit is contained in:
Charlie Marsh
2026-04-08 14:28:50 -04:00
parent a032056d2d
commit 20957a81ea
13 changed files with 4069 additions and 126 deletions
Generated
+1
View File
@@ -7315,6 +7315,7 @@ dependencies = [
"uv-pep508",
"uv-pypi-types",
"uv-python",
"uv-resolver",
"uv-settings",
"uv-state",
"uv-static",
+7
View File
@@ -858,6 +858,13 @@ impl Plan {
&& self.extraneous.is_empty()
}
/// Returns `true` if the plan installs or reinstalls the named package.
pub fn installs(&self, name: &PackageName) -> bool {
self.cached.iter().any(|dist| dist.name() == name)
|| self.remote.iter().any(|dist| dist.name() == name)
|| self.reinstalls.iter().any(|dist| dist.name() == name)
}
/// Partition the remote distributions based on a predicate function.
///
/// Returns a tuple of plans, where the first plan contains the remote distributions that match
+1
View File
@@ -28,6 +28,7 @@ uv-pep440 = { workspace = true }
uv-pep508 = { workspace = true }
uv-pypi-types = { workspace = true }
uv-python = { workspace = true }
uv-resolver = { workspace = true }
uv-settings = { workspace = true }
uv-state = { workspace = true }
uv-static = { workspace = true }
+20 -3
View File
@@ -1,6 +1,7 @@
use std::path::Path;
use serde::Deserialize;
use uv_resolver::Lock;
use crate::Tool;
@@ -18,8 +19,14 @@ pub struct ToolReceipt {
impl ToolReceipt {
/// Parse a [`ToolReceipt`] from a raw TOML string.
pub(crate) fn from_string(raw: String) -> Result<Self, toml::de::Error> {
let tool = toml::from_str(&raw)?;
Ok(Self { raw, ..tool })
let mut receipt: Self = toml::from_str(&raw)?;
let document: toml::Table = toml::from_str(&raw)?;
if document.contains_key("version") {
let lock: Lock = toml::from_str(&raw)?;
receipt.tool = receipt.tool.with_lock(Some(lock));
}
receipt.raw = raw;
Ok(receipt)
}
/// Read a [`ToolReceipt`] from the given path.
@@ -35,7 +42,17 @@ impl ToolReceipt {
pub(crate) fn to_toml(&self) -> Result<String, toml_edit::ser::Error> {
// We construct a TOML document manually instead of going through Serde to enable
// the use of inline tables.
let mut doc = toml_edit::DocumentMut::new();
let mut doc = if let Some(lock) = self.tool.lock() {
lock.to_toml()?
.parse::<toml_edit::DocumentMut>()
.map_err(|err| {
toml_edit::ser::Error::Custom(format!(
"Failed to parse embedded tool lock: {err}"
))
})?
} else {
toml_edit::DocumentMut::new()
};
doc.insert("tool", toml_edit::Item::Table(self.tool.to_toml()?));
Ok(doc.to_string())
+16
View File
@@ -9,6 +9,7 @@ use uv_fs::{PortablePath, Simplified};
use uv_normalize::PackageName;
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::PythonRequest;
use uv_resolver::Lock;
use uv_settings::{ToolOptions, ToolOptionsWire};
/// A tool entry.
@@ -34,6 +35,8 @@ pub struct Tool {
entrypoints: Vec<ToolEntrypoint>,
/// The [`ToolOptions`] used to install this tool.
options: ToolOptions,
/// The resolved lock for the installed environment, if available.
lock: Option<Lock>,
}
#[derive(Debug, Clone, Deserialize)]
@@ -104,6 +107,7 @@ impl TryFrom<ToolWire> for Tool {
python: tool.python,
entrypoints: tool.entrypoints,
options: tool.options.into(),
lock: None,
})
}
}
@@ -180,6 +184,7 @@ impl Tool {
python: Option<PythonRequest>,
entrypoints: impl IntoIterator<Item = ToolEntrypoint>,
options: ToolOptions,
lock: Option<Lock>,
) -> Self {
let mut entrypoints: Vec<_> = entrypoints.into_iter().collect();
entrypoints.sort();
@@ -192,6 +197,7 @@ impl Tool {
python,
entrypoints,
options,
lock,
}
}
@@ -201,6 +207,12 @@ impl Tool {
Self { options, ..self }
}
/// Create a new [`Tool`] with the given lock.
#[must_use]
pub fn with_lock(self, lock: Option<Lock>) -> Self {
Self { lock, ..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();
@@ -382,6 +394,10 @@ impl Tool {
pub fn options(&self) -> &ToolOptions {
&self.options
}
pub fn lock(&self) -> Option<&Lock> {
self.lock.as_ref()
}
}
impl ToolEntrypoint {
-12
View File
@@ -533,18 +533,6 @@ impl Changelog {
pub(crate) fn from_installed(installed: Vec<CachedDist>) -> Self {
Self::from_local(installed, Vec::new())
}
/// Returns `true` if the changelog includes a distribution with the given name, either via
/// an installation or uninstallation.
pub(crate) fn includes(&self, name: &PackageName) -> bool {
self.installed.iter().any(|dist| dist.name() == name)
|| self.uninstalled.iter().any(|dist| dist.name() == name)
}
/// Returns `true` if the changelog is empty.
pub(crate) fn is_empty(&self) -> bool {
self.installed.is_empty() && self.uninstalled.is_empty()
}
}
/// Install a set of requirements into the current environment.
+5 -6
View File
@@ -14,7 +14,7 @@ use uv_cache_key::{cache_digest, cache_name};
use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification,
GitLfsSetting, Reinstall, TargetTriple, Upgrade,
GitLfsSetting, Reinstall, TargetTriple,
};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::{DistributionDatabase, LoweredExtraBuildDependencies, LoweredRequirement};
@@ -2068,7 +2068,7 @@ pub(crate) async fn resolve_environment(
extra_build_variables,
exclude_newer,
link_mode,
upgrade: _,
upgrade,
build_options,
sources,
torch_backend,
@@ -2149,7 +2149,6 @@ pub(crate) async fn resolve_environment(
.index_strategy(*index_strategy)
.build_options(build_options.clone())
.build();
// TODO(charlie): These are all default values. We should consider whether we want to make them
// optional on the downstream APIs.
let extras = ExtrasSpecification::default();
@@ -2157,10 +2156,10 @@ pub(crate) async fn resolve_environment(
let hasher = HashStrategy::default();
let build_hasher = HashStrategy::default();
// When resolving from an interpreter, we assume an empty environment, so reinstalls and
// upgrades aren't relevant.
// When resolving from an interpreter, we assume an empty environment, so reinstalls aren't
// relevant.
let reinstall = Reinstall::default();
let upgrade = Upgrade::default();
let upgrade = upgrade.clone();
// If an existing lockfile exists, build up a set of preferences.
let preferences = match spec.preferences {
+105 -3
View File
@@ -15,7 +15,8 @@ use uv_client::BaseClientBuilder;
use uv_configuration::GitLfsSetting;
use uv_distribution::StaticMetadataDatabase;
use uv_distribution_types::{
InstalledDist, Name, Requirement, RequiresPython, UnresolvedRequirement,
InstalledDist, Name, Requirement, RequirementSource, RequiresPython, StaticMetadata,
UnresolvedRequirement,
};
use uv_errors::{ErrorWithHints, Hint, Hints};
#[cfg(unix)]
@@ -23,13 +24,15 @@ use uv_fs::replace_symlink;
use uv_fs::{CWD, Simplified};
use uv_git::GitResolver;
use uv_installer::SitePackages;
use uv_normalize::PackageName;
use uv_normalize::{GroupName, PackageName};
use uv_pep440::{Version, VersionSpecifier, VersionSpecifiers};
use uv_python::{
EnvironmentPreference, Interpreter, PythonDownloads, PythonEnvironment, PythonInstallation,
PythonPreference, PythonRequest, PythonVariant, PythonVersionFile, VersionFileDiscoveryOptions,
VersionRequest,
};
use uv_requirements::RequirementsSpecification;
use uv_resolver::{Lock, Preference, ResolverManifest, ResolverOutput};
use uv_settings::{PythonInstallMirrors, ToolOptions};
use uv_shell::Shell;
use uv_tool::{InstalledTools, Tool, ToolEntrypoint, entrypoint_paths};
@@ -95,7 +98,9 @@ impl Hint for NoExecutablesError {
hints
}
}
use crate::commands::project::{ProjectError, PythonRequestSource};
use crate::commands::project::{
EnvironmentSpecification, PreferenceLocation, ProjectError, PythonRequestSource,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::printer::Printer;
@@ -254,6 +259,101 @@ async fn infer_requires_python_from_requirement(
}
}
/// Normalize tool-local directory requirements so non-editable inputs are represented explicitly.
///
/// The CLI parser distinguishes editable local directories with `editable = Some(true)`, but
/// leaves non-editable directories as `editable = None`. Tools need to preserve each local
/// requirement's chosen mode while lowering implicit workspace members, so convert the implicit
/// non-editable case into `Some(false)` before resolution.
pub(crate) fn normalize_tool_local_requirements(
requirements: impl IntoIterator<Item = Requirement>,
) -> Vec<Requirement> {
requirements
.into_iter()
.map(|requirement| Requirement {
source: match requirement.source {
RequirementSource::Directory {
install_path,
editable: None,
r#virtual,
url,
} => RequirementSource::Directory {
install_path,
editable: Some(false),
r#virtual,
url,
},
source => source,
},
..requirement
})
.collect()
}
/// Build the lock manifest for a tool receipt.
pub(crate) fn tool_receipt_manifest(
requirements: &[Requirement],
constraints: &[Requirement],
overrides: &[Requirement],
excludes: &[PackageName],
build_constraints: &[Requirement],
) -> ResolverManifest {
ResolverManifest::new(
std::iter::empty::<PackageName>(),
requirements.iter().cloned(),
constraints.iter().cloned(),
overrides.iter().cloned(),
excludes.iter().cloned(),
build_constraints.iter().cloned(),
std::iter::empty::<(GroupName, Vec<Requirement>)>(),
std::iter::empty::<StaticMetadata>(),
)
}
/// Build the receipt lock for a tool environment.
pub(crate) fn tool_receipt_lock(
root: &Path,
resolution: &ResolverOutput,
manifest: &ResolverManifest,
) -> Option<Lock> {
match Lock::from_resolution(resolution, root, vec![]) {
Ok(lock) => match manifest.clone().relative_to(root) {
Ok(manifest) => Some(lock.with_manifest(manifest)),
Err(err) => {
debug!("Failed to relativize tool receipt lock manifest: {err}");
None
}
},
Err(err) => {
debug!("Failed to build tool receipt lock: {err}");
None
}
}
}
/// Build an environment specification for a tool, preferring versions from the existing receipt
/// lock when available, then falling back to the installed environment.
pub(crate) fn tool_environment_spec<'lock>(
requirements: RequirementsSpecification,
tool: Option<&'lock Tool>,
install_path: &'lock Path,
site_packages: Option<&SitePackages>,
) -> EnvironmentSpecification<'lock> {
let specification = EnvironmentSpecification::from(requirements);
if let Some(lock) = tool.and_then(Tool::lock) {
return specification.with_preferences(PreferenceLocation::Lock { lock, install_path });
}
let preferences = site_packages
.into_iter()
.flat_map(|site_packages| site_packages.iter().filter_map(Preference::from_installed))
.collect::<Vec<_>>();
if preferences.is_empty() {
return specification;
}
specification.with_preferences(PreferenceLocation::Entries(preferences))
}
/// Given a no-solution error and the [`Interpreter`] that was used during the solve, attempt to
/// discover an alternate [`Interpreter`] that satisfies the `requires-python` constraint.
pub(crate) async fn refine_interpreter(
@@ -362,6 +462,7 @@ pub(crate) fn finalize_tool_install(
overrides: Vec<Requirement>,
excludes: Vec<PackageName>,
build_constraints: Vec<Requirement>,
lock: Option<Lock>,
printer: Printer,
) -> anyhow::Result<()> {
let executable_directory = uv_tool::tool_executable_dir()?;
@@ -528,6 +629,7 @@ pub(crate) fn finalize_tool_install(
python,
installed_entrypoints,
options.clone(),
lock,
);
installed_tools.add_tool_receipt(name, tool)?;
+200 -57
View File
@@ -1,4 +1,5 @@
use std::fmt::Write;
use std::path::Path;
use std::str::FromStr;
use anyhow::{Result, bail};
@@ -9,14 +10,17 @@ use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
use uv_configuration::{
Concurrency, Constraints, DryRun, GitLfsSetting, Reinstall, TargetTriple, Upgrade,
Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun,
ExtrasSpecificationWithDefaults, GitLfsSetting, InstallOptions, Reinstall, TargetTriple,
Upgrade,
};
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{
ExtraBuildRequires, IndexCapabilities, NameRequirementSpecification, Requirement,
RequirementSource, UnresolvedRequirementSpecification,
RequirementSource, Resolution, UnresolvedRequirementSpecification,
};
use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages};
use uv_fs::CWD;
use uv_installer::{InstallationStrategy, Planner, SitePackages};
use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
use uv_pep508::MarkerTree;
@@ -26,15 +30,18 @@ use uv_python::{
PythonPreference, PythonRequest,
};
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{Installable, Lock};
use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::InstalledTools;
use uv_types::SourceTreeEditablePolicy;
use uv_types::{HashStrategy, SourceTreeEditablePolicy};
use uv_warnings::{warn_user, warn_user_once};
use uv_workspace::WorkspaceCache;
use crate::commands::ExitStatus;
use crate::commands::pip::latest::LatestClient;
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryResolveLogger,
};
use crate::commands::pip::operations::{self, Modifications};
use crate::commands::pip::{resolution_markers, resolution_tags};
use crate::commands::project::{
@@ -42,13 +49,59 @@ use crate::commands::project::{
sync_environment, update_environment,
};
use crate::commands::tool::common::{
ToolPython, finalize_tool_install, refine_interpreter, remove_entrypoints,
ToolPython, finalize_tool_install, normalize_tool_local_requirements, refine_interpreter,
remove_entrypoints, tool_environment_spec, tool_receipt_lock, tool_receipt_manifest,
};
use crate::commands::tool::{Target, ToolRequest};
use crate::commands::{diagnostics, reporters::PythonDownloadReporter};
use crate::printer::Printer;
use crate::settings::{ResolverInstallerSettings, ResolverSettings};
/// An [`Installable`] adapter for a tool receipt [`Lock`].
///
/// Tools embed a lock in `uv-receipt.toml`, but they are not modeled as workspace or project
/// install targets. This adapter lets tool installation reuse [`Installable::to_resolution`] to
/// derive a [`Resolution`] from the embedded lock when checking whether an existing environment is
/// already up-to-date.
struct ToolLockInstallTarget<'lock> {
install_path: &'lock Path,
lock: &'lock Lock,
project_name: Option<&'lock PackageName>,
}
impl<'lock> ToolLockInstallTarget<'lock> {
/// Create a [`ToolLockInstallTarget`] for a tool environment and its embedded [`Lock`].
fn new(
install_path: &'lock Path,
lock: &'lock Lock,
project_name: Option<&'lock PackageName>,
) -> Self {
Self {
install_path,
lock,
project_name,
}
}
}
impl<'lock> Installable<'lock> for ToolLockInstallTarget<'lock> {
fn install_path(&self) -> &'lock Path {
self.install_path
}
fn lock(&self) -> &'lock Lock {
self.lock
}
fn roots(&self) -> impl Iterator<Item = &PackageName> {
std::iter::empty()
}
fn project_name(&self) -> Option<&PackageName> {
self.project_name
}
}
/// Install a tool.
#[expect(clippy::fn_params_excessive_bools)]
pub(crate) async fn install(
@@ -383,7 +436,7 @@ pub(crate) async fn install(
)
.await?,
);
requirements
normalize_tool_local_requirements(requirements)
};
// Explicit local directory requirements should always be rebuilt and reinstalled, matching
@@ -417,41 +470,52 @@ pub(crate) async fn install(
};
// Resolve the constraints.
let constraints: Vec<_> = spec
.constraints
.into_iter()
.map(|constraint| constraint.requirement)
.collect();
let constraints = normalize_tool_local_requirements(
spec.constraints
.into_iter()
.map(|constraint| constraint.requirement)
.collect::<Vec<_>>(),
);
// Resolve the overrides.
let overrides = resolve_names(
spec.overrides,
&interpreter,
&settings,
&client_builder,
&state,
&concurrency,
&cache,
workspace_cache,
printer,
preview,
lfs,
)
.await?;
let overrides = normalize_tool_local_requirements(
resolve_names(
spec.overrides,
&interpreter,
&settings,
&client_builder,
&state,
&concurrency,
&cache,
workspace_cache,
printer,
preview,
lfs,
)
.await?,
);
// Resolve the excludes.
let excludes = spec.excludes.clone();
// Resolve the build constraints.
let build_constraints: Vec<Requirement> =
let build_constraints = normalize_tool_local_requirements(
operations::read_constraints(build_constraints, &client_builder)
.await?
.into_iter()
.map(|constraint| constraint.requirement)
.collect();
.collect::<Vec<_>>(),
);
// Convert to tool options.
let options = ToolOptions::from(options);
let receipt_manifest = tool_receipt_manifest(
&requirements,
&constraints,
&overrides,
&excludes,
&build_constraints,
);
let installed_tools = InstalledTools::from_settings()?.init()?;
let _lock = installed_tools.lock().await?;
@@ -550,25 +614,45 @@ pub(crate) async fn install(
environment.environment().interpreter(),
)?;
// Check if the installed packages meet the requirements.
let site_packages = SitePackages::from_environment(environment.environment())?;
// TODO(charlie): This fast path only validates the explicit requested
// requirements. It can miss editable-mode drift for implicit workspace members.
if matches!(
site_packages.satisfies_requirements(
requirements.iter(),
constraints.iter().chain(latest.iter()),
overrides.iter(),
InstallationStrategy::Permissive,
let already_installed = if let Some(lock) = tool_receipt.lock() {
let resolution = ToolLockInstallTarget::new(
environment.environment().root(),
lock,
tool_receipt
.target_requirement()
.map(|requirement| &requirement.name),
)
.to_resolution(
&markers,
&tags,
config_setting,
config_settings_package,
&extra_build_requires,
extra_build_variables,
),
Ok(SatisfiesResult::Fresh { .. })
) {
&ExtrasSpecificationWithDefaults::none(),
&DependencyGroupsWithDefaults::none(),
&settings.resolver.build_options,
&InstallOptions::default(),
)?;
Planner::new(&resolution)
.build(
SitePackages::from_environment(environment.environment())?,
InstallationStrategy::Permissive,
&Reinstall::default(),
&settings.resolver.build_options,
&HashStrategy::default(),
&settings.resolver.index_locations,
config_setting,
config_settings_package,
&extra_build_requires,
extra_build_variables,
&cache,
environment.environment(),
&tags,
)?
.is_empty()
} else {
// Force legacy receipts through the update path so they get rewritten with an
// embedded lock.
false
};
if already_installed {
// Then we're done! Though we might need to update the receipt.
if *tool_receipt.options() != options {
installed_tools.add_tool_receipt(
@@ -614,13 +698,13 @@ pub(crate) async fn install(
// 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.
let environment = if let Some(environment) = existing_environment {
let environment = match update_environment(
let (environment, receipt_lock) = if let Some(environment) = existing_environment {
let update = match update_environment(
environment.into_environment(),
spec,
spec.clone(),
Modifications::Exact,
python_platform.as_ref(),
SourceTreeEditablePolicy::Tool,
SourceTreeEditablePolicy::Respect,
Constraints::from_requirements(build_constraints.iter().cloned()),
ExtraBuildRequires::default(),
&settings,
@@ -638,7 +722,7 @@ pub(crate) async fn install(
)
.await
{
Ok(update) => update.into_environment(),
Ok(update) => update,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
@@ -651,11 +735,63 @@ pub(crate) async fn install(
// At this point, we updated the existing environment, so we should remove any of its
// existing executables.
if let Some(existing_receipt) = existing_tool_receipt {
remove_entrypoints(&existing_receipt);
if let Some(existing_receipt) = existing_tool_receipt.as_ref() {
remove_entrypoints(existing_receipt);
}
environment
let receipt_lock = if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
let site_packages = if tool_receipt.lock().is_none() {
match SitePackages::from_environment(&update.environment) {
Ok(site_packages) => Some(site_packages),
Err(err) => {
debug!(
"Failed to read tool environment site-packages while rebuilding receipt lock after update: {err}"
);
None
}
}
} else {
None
};
match resolve_environment(
tool_environment_spec(
spec,
Some(tool_receipt),
&installed_tools.tool_dir(package_name),
site_packages.as_ref(),
),
update.environment.interpreter(),
python_platform.as_ref(),
SourceTreeEditablePolicy::Respect,
Constraints::from_requirements(build_constraints.iter().cloned()),
&settings.resolver,
&client_builder,
&state,
Box::new(SummaryResolveLogger),
&concurrency,
&cache,
workspace_cache,
printer,
preview,
)
.await
{
Ok(resolution) => tool_receipt_lock(
&installed_tools.tool_dir(package_name),
&resolution,
&receipt_manifest,
),
Err(err) => {
debug!("Failed to rebuild tool receipt lock after update: {err}");
None
}
}
} else {
None
};
(update.environment, receipt_lock)
} else {
let spec = EnvironmentSpecification::from(spec);
@@ -665,7 +801,7 @@ pub(crate) async fn install(
spec.clone(),
&interpreter,
python_platform.as_ref(),
SourceTreeEditablePolicy::Tool,
SourceTreeEditablePolicy::Respect,
Constraints::from_requirements(build_constraints.iter().cloned()),
&settings.resolver,
&client_builder,
@@ -721,7 +857,7 @@ pub(crate) async fn install(
spec,
&interpreter,
python_platform.as_ref(),
SourceTreeEditablePolicy::Tool,
SourceTreeEditablePolicy::Respect,
Constraints::from_requirements(build_constraints.iter().cloned()),
&settings.resolver,
&client_builder,
@@ -751,6 +887,12 @@ pub(crate) async fn install(
};
let environment = installed_tools.create_environment(package_name, interpreter)?;
let receipt_lock = tool_receipt_lock(
&installed_tools.tool_dir(package_name),
&resolution,
&receipt_manifest,
);
let resolution: Resolution = resolution.into();
// At this point, we removed any existing environment, so we should remove any of its
// executables.
@@ -761,7 +903,7 @@ pub(crate) async fn install(
// Sync the environment with the resolved requirements.
match sync_environment(
environment,
&resolution.into(),
&resolution,
Modifications::Exact,
Constraints::from_requirements(build_constraints.iter().cloned()),
(&settings).into(),
@@ -780,7 +922,7 @@ pub(crate) async fn install(
debug!("Failed to sync environment; removing `{}`", package_name);
let _ = installed_tools.remove_environment(package_name);
}) {
Ok(environment) => environment,
Ok(environment) => (environment, receipt_lock),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::with_system_certs(
client_builder.system_certs(),
@@ -810,6 +952,7 @@ pub(crate) async fn install(
overrides,
excludes,
build_constraints,
receipt_lock,
printer,
)?;
+144 -40
View File
@@ -8,10 +8,12 @@ use tracing::{debug, trace};
use uv_cache::Cache;
use uv_client::BaseClientBuilder;
use uv_configuration::{Concurrency, Constraints, DryRun, TargetTriple};
use uv_distribution_types::{ExtraBuildRequires, Requirement, RequirementSource};
use uv_configuration::{Concurrency, Constraints, TargetTriple};
use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{Requirement, RequirementSource, Resolution};
use uv_errors::{ErrorOptions, write_error_chain_with_options};
use uv_fs::CWD;
use uv_installer::{InstallationStrategy, Planner, SitePackages};
use uv_normalize::PackageName;
use uv_pep440::{Operator, Version};
use uv_preview::Preview;
@@ -22,18 +24,19 @@ use uv_python::{
use uv_requirements::RequirementsSpecification;
use uv_settings::{Combine, PythonInstallMirrors, ResolverInstallerOptions, ToolOptions};
use uv_tool::{InstalledTools, Tool};
use uv_types::SourceTreeEditablePolicy;
use uv_types::{HashStrategy, SourceTreeEditablePolicy};
use uv_workspace::WorkspaceCache;
use crate::commands::pip::loggers::{
DefaultInstallLogger, SummaryResolveLogger, UpgradeInstallLogger,
};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::{
EnvironmentUpdate, PlatformState, resolve_environment, sync_environment, update_environment,
};
use crate::commands::pip::{operations::Modifications, resolution_tags};
use crate::commands::project::{PlatformState, resolve_environment, sync_environment};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::tool::common::remove_entrypoints;
use crate::commands::tool::common::{
normalize_tool_local_requirements, remove_entrypoints, tool_environment_spec,
tool_receipt_lock, tool_receipt_manifest,
};
use crate::commands::{ExitStatus, conjunction, tool::common::finalize_tool_install};
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;
@@ -320,35 +323,55 @@ async fn upgrade_tool(
);
let settings = ResolverInstallerSettings::from(options.clone());
let build_constraints =
Constraints::from_requirements(existing_tool_receipt.build_constraints().iter().cloned());
let build_constraints = Constraints::from_requirements(
normalize_tool_local_requirements(
existing_tool_receipt.build_constraints().iter().cloned(),
)
.into_iter(),
);
let receipt_manifest = tool_receipt_manifest(
existing_tool_receipt.requirements(),
existing_tool_receipt.constraints(),
existing_tool_receipt.overrides(),
existing_tool_receipt.excludes(),
existing_tool_receipt.build_constraints(),
);
// Resolve the requirements.
let spec = RequirementsSpecification::from_excludes(
existing_tool_receipt.requirements().to_vec(),
let constraints = normalize_tool_local_requirements(
existing_tool_receipt
.constraints()
.iter()
.chain(constraints)
.cloned()
.collect(),
existing_tool_receipt.overrides().to_vec(),
.collect::<Vec<_>>(),
);
let spec = RequirementsSpecification::from_excludes(
normalize_tool_local_requirements(existing_tool_receipt.requirements().to_vec()),
constraints,
normalize_tool_local_requirements(existing_tool_receipt.overrides().to_vec()),
existing_tool_receipt.excludes().to_vec(),
);
// Initialize any shared state.
let state = PlatformState::default();
let site_packages = SitePackages::from_environment(environment.environment())?;
// Check if we need to create a new environment — if so, resolve it first, then
// install the requested tool
let (environment, outcome) = if let Some(interpreter) =
let (environment, outcome, receipt_lock) = if let Some(interpreter) =
interpreter.filter(|interpreter| !environment.environment().uses(interpreter))
{
// If we're using a new interpreter, re-create the environment for each tool.
let resolution = resolve_environment(
spec.into(),
tool_environment_spec(
spec,
Some(&existing_tool_receipt),
&installed_tools.tool_dir(name),
Some(&site_packages),
),
interpreter,
python_platform,
SourceTreeEditablePolicy::Tool,
SourceTreeEditablePolicy::Respect,
build_constraints.clone(),
&settings.resolver,
client_builder,
@@ -363,10 +386,16 @@ async fn upgrade_tool(
.await?;
let environment = installed_tools.create_environment(name, interpreter.clone())?;
let receipt_lock = tool_receipt_lock(
&installed_tools.tool_dir(name),
&resolution,
&receipt_manifest,
);
let resolution = resolution.into();
let environment = sync_environment(
environment,
&resolution.into(),
&resolution,
Modifications::Exact,
build_constraints,
(&settings).into(),
@@ -381,46 +410,112 @@ async fn upgrade_tool(
)
.await?;
(environment, UpgradeOutcome::UpgradeEnvironment)
} else {
// Otherwise, upgrade the existing environment.
// TODO(zanieb): Build the environment in the cache directory then copy into the tool
// directory.
let EnvironmentUpdate {
(
environment,
changelog,
} = update_environment(
environment.into_environment(),
spec,
Modifications::Exact,
UpgradeOutcome::UpgradeEnvironment,
receipt_lock,
)
} else {
let resolution = resolve_environment(
tool_environment_spec(
spec,
Some(&existing_tool_receipt),
&installed_tools.tool_dir(name),
Some(&site_packages),
),
environment.environment().interpreter(),
python_platform,
SourceTreeEditablePolicy::Tool,
build_constraints,
ExtraBuildRequires::default(),
&settings,
SourceTreeEditablePolicy::Respect,
build_constraints.clone(),
&settings.resolver,
client_builder,
&state,
Box::new(SummaryResolveLogger),
Box::new(UpgradeInstallLogger::new(name.clone())),
installer_metadata,
concurrency,
cache,
workspace_cache,
DryRun::Disabled,
printer,
preview,
)
.await?;
let outcome = if changelog.includes(name) {
UpgradeOutcome::UpgradeTool
} else if changelog.is_empty() {
let receipt_lock = tool_receipt_lock(
&installed_tools.tool_dir(name),
&resolution,
&receipt_manifest,
);
let resolution = Resolution::from(resolution);
let ResolverInstallerSettings {
resolver:
crate::settings::ResolverSettings {
config_setting,
config_settings_package,
extra_build_dependencies,
extra_build_variables,
..
},
..
} = &settings;
let extra_build_requires =
LoweredExtraBuildDependencies::from_non_lowered(extra_build_dependencies.clone())
.into_inner();
let tags = resolution_tags(
None,
python_platform,
environment.environment().interpreter(),
)?;
let plan = Planner::new(&resolution).build(
SitePackages::from_environment(environment.environment())?,
InstallationStrategy::Permissive,
&settings.reinstall,
&settings.resolver.build_options,
&HashStrategy::default(),
&settings.resolver.index_locations,
config_setting,
config_settings_package,
&extra_build_requires,
extra_build_variables,
cache,
environment.environment(),
&tags,
)?;
let outcome = if plan.is_empty() {
UpgradeOutcome::NoOp
} else if plan.installs(name) {
UpgradeOutcome::UpgradeTool
} else {
UpgradeOutcome::UpgradeDependencies
};
(environment, outcome)
// 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.
let environment = if matches!(outcome, UpgradeOutcome::NoOp) {
environment.into_environment()
} else {
sync_environment(
environment.into_environment(),
&resolution,
Modifications::Exact,
build_constraints,
(&settings).into(),
client_builder,
&state,
Box::new(UpgradeInstallLogger::new(name.clone())),
installer_metadata,
concurrency,
cache,
printer,
preview,
)
.await?
};
(environment, outcome, receipt_lock)
};
if matches!(
@@ -451,8 +546,17 @@ async fn upgrade_tool(
existing_tool_receipt.overrides().to_vec(),
existing_tool_receipt.excludes().to_vec(),
existing_tool_receipt.build_constraints().to_vec(),
receipt_lock,
printer,
)?;
} else {
installed_tools.add_tool_receipt(
name,
existing_tool_receipt
.clone()
.with_options(ToolOptions::from(options))
.with_lock(receipt_lock),
)?;
}
let constraint = match &outcome {
File diff suppressed because it is too large Load Diff
+75
View File
@@ -365,6 +365,81 @@ fn tool_list_deprecated() -> Result<()> {
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
version = 1
revision = 3
requires-python = ">=3.12.[X]"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[manifest]
requirements = [{ name = "black", specifier = "==24.2.0" }]
[[package]]
name = "black"
version = "24.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/69/f3ab49cdb938b3eecb048fa64f86bdadb1fac26e92c435d287181d543b0a/black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894", size = 631598, upload-time = "2024-02-12T20:21:26.969Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/1e/67c87a1fb39592aa944f35cc26892946ebe0a10aa324b87f9380b8753862/black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b", size = 1585288, upload-time = "2024-02-12T20:37:13.8Z" },
{ url = "https://files.pythonhosted.org/packages/5e/62/6437212cf40e40b74dbc7e134700a21cb21a9ac7e46ade940b5d4826456f/black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9", size = 1417360, upload-time = "2024-02-12T20:34:56.41Z" },
{ url = "https://files.pythonhosted.org/packages/36/8f/de0d339ae683422a8e15d6f74b8022d4947009c347d8c2178c303c68cc4d/black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693", size = 1739406, upload-time = "2024-02-12T20:23:59.596Z" },
{ url = "https://files.pythonhosted.org/packages/3e/58/89e5f5a1c4c5b66dc74eabe6337623d53b4d1c27fbbbe16defee53397f60/black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982", size = 1373310, upload-time = "2024-02-12T20:25:27.243Z" },
{ url = "https://files.pythonhosted.org/packages/47/15/b3770bc3328685a53bc9c041136240146c5cd866a1f020c2cf47f2ff9683/black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6", size = 200610, upload-time = "2024-02-12T20:21:17.657Z" },
]
[[package]]
name = "click"
version = "8.1.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" },
]
[[package]]
name = "packaging"
version = "24.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/dc/c1d911bf5bb0fdc58cc05010e9f3efe3b67970cef779ba7fbc3183b987a8/platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768", size = 20055, upload-time = "2024-01-31T01:00:36.02Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", size = 17717, upload-time = "2024-01-31T01:00:34.019Z" },
]
[tool]
requirements = [{ name = "black", specifier = "==24.2.0" }]
entrypoints = [
+144 -1
View File
@@ -1,6 +1,6 @@
use std::process::Command;
use anyhow::Result;
use anyhow::{Context, Result};
use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::*;
use indoc::indoc;
@@ -607,6 +607,82 @@ fn tool_upgrade_recomputes_relative_exclude_newer() {
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
version = 1
revision = 3
requires-python = ">=3.12.[X]"
[options]
exclude-newer = "2024-03-25T00:00:00Z"
exclude-newer-span = "P3W"
[manifest]
requirements = [{ name = "black" }]
[[package]]
name = "black"
version = "24.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "mypy-extensions" },
{ name = "packaging" },
{ name = "pathspec" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8f/5f/bac24a952668c7482cfdb4ebf91ba57a796c9da8829363a772040c1a3312/black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", size = 634292, upload-time = "2024-03-15T19:35:43.699Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/c6/1d174efa9ff02b22d0124c73fc5f4d4fb006d0d9a081aadc354d05754a13/black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", size = 1600822, upload-time = "2024-03-15T19:45:20.337Z" },
{ url = "https://files.pythonhosted.org/packages/d9/ed/704731afffe460b8ff0672623b40fce9fe569f2ee617c15857e4d4440a3a/black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", size = 1429987, upload-time = "2024-03-15T19:45:00.637Z" },
{ url = "https://files.pythonhosted.org/packages/a8/05/8dd038e30caadab7120176d4bc109b7ca2f4457f12eef746b0560a583458/black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", size = 1755319, upload-time = "2024-03-15T19:38:24.009Z" },
{ url = "https://files.pythonhosted.org/packages/71/9d/e5fa1ff4ef1940be15a64883c0bb8d2fcf626efec996eab4ae5a8c691d2c/black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", size = 1385180, upload-time = "2024-03-15T19:39:37.014Z" },
{ url = "https://files.pythonhosted.org/packages/4d/ea/31770a7e49f3eedfd8cd7b35e78b3a3aaad860400f8673994bc988318135/black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", size = 201493, upload-time = "2024-03-15T19:35:41.572Z" },
]
[[package]]
name = "click"
version = "8.1.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" },
]
[[package]]
name = "packaging"
version = "24.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" },
]
[[package]]
name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
[[package]]
name = "platformdirs"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/dc/c1d911bf5bb0fdc58cc05010e9f3efe3b67970cef779ba7fbc3183b987a8/platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768", size = 20055, upload-time = "2024-01-31T01:00:36.02Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/72/4898c44ee9ea6f43396fbc23d9bfaf3d06e01b83698bdf2e4c919deceb7c/platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068", size = 17717, upload-time = "2024-01-31T01:00:34.019Z" },
]
[tool]
requirements = [{ name = "black" }]
entrypoints = [
@@ -621,6 +697,54 @@ fn tool_upgrade_recomputes_relative_exclude_newer() {
});
}
#[test]
fn tool_upgrade_migrates_lockless_receipt_with_installed_preferences() -> Result<()> {
let context = uv_test::test_context!("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");
context
.tool_install()
.arg("black==24.2.0")
.arg("--exclude-newer")
.arg("3 weeks")
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-03-22T00:00:00Z")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.assert()
.success();
let receipt_path = tool_dir.child("black").child("uv-receipt.toml");
let receipt = fs_err::read_to_string(&receipt_path)?;
let (_, tool_receipt) = receipt
.split_once("\n[tool]\n")
.context("expected the tool receipt to contain a tool section")?;
receipt_path.write_str(&format!("[tool]\n{tool_receipt}"))?;
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("black")
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-04-15T00:00:00Z")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
hint: `black` is pinned to `24.2.0` (installed with an exact version pin); reinstall with `uv tool install black@latest` to upgrade to a new version.
");
Ok(())
}
#[test]
fn tool_upgrade_multiple_names() {
let context = uv_test::test_context!("3.12")
@@ -1649,6 +1773,25 @@ async fn tool_upgrade_invalid_auth() -> Result<()> {
// Verify the receipt has `authenticate = "always"` (promoted from "auto" because the
// original URL had embedded credentials).
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
version = 1
revision = 3
requires-python = ">=3.12.[X]"
[options]
exclude-newer = "2025-01-18T00:00:00Z"
[manifest]
requirements = [{ name = "executable-application" }]
[[package]]
name = "executable-application"
version = "0.3.0"
source = { registry = "http://[LOCALHOST]/basic-auth/simple" }
sdist = { url = "http://[LOCALHOST]/basic-auth/files/packages/9a/36/e803315469274d62f2dab543e3916c0b5b65730074d295f7d48711aa9e36/executable_application-0.3.0.tar.gz", hash = "sha256:0ef8c5ddd28649503c6e4a9f55be17e5b3bd0685df7b83ff7c260b481025f261", size = 914, upload-time = "2025-01-17T23:21:24.559Z" }
wheels = [
{ url = "http://[LOCALHOST]/basic-auth/files/packages/32/97/8ab6fa1bbcb0a888f460c0a19c301f4cc4180573564ad7dd98b5ceca2ab6/executable_application-0.3.0-py3-none-any.whl", hash = "sha256:ca272aee7332e9d266663bc70037cd3ef1d74ffae40030eaf9ca46462dc8dcc6", size = 1719, upload-time = "2025-01-17T23:21:22.716Z" },
]
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [