mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Add a lockfile to tools
This commit is contained in:
Generated
+1
@@ -7315,6 +7315,7 @@ dependencies = [
|
||||
"uv-pep508",
|
||||
"uv-pypi-types",
|
||||
"uv-python",
|
||||
"uv-resolver",
|
||||
"uv-settings",
|
||||
"uv-state",
|
||||
"uv-static",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
@@ -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,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -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
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user