mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
Store tool locks as standalone files
This commit is contained in:
@@ -663,6 +663,11 @@ impl Lock {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return whether this lock was generated with the given [`ResolverManifest`].
|
||||
pub fn matches_manifest(&self, manifest: &ResolverManifest) -> bool {
|
||||
self.manifest == *manifest
|
||||
}
|
||||
|
||||
/// Record the conflicting groups that were used to generate this lock.
|
||||
#[must_use]
|
||||
pub fn with_conflicts(mut self, conflicts: Conflicts) -> Self {
|
||||
|
||||
@@ -16,6 +16,7 @@ use uv_installer::SitePackages;
|
||||
use uv_normalize::{InvalidNameError, PackageName};
|
||||
use uv_pep440::Version;
|
||||
use uv_python::{BrokenLink, Interpreter, PythonEnvironment};
|
||||
use uv_resolver::Lock;
|
||||
use uv_state::{StateBucket, StateStore};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
@@ -70,6 +71,8 @@ pub enum Error {
|
||||
ReceiptWrite(PathBuf, #[source] Box<toml_edit::ser::Error>),
|
||||
#[error("Failed to read `uv-receipt.toml` at {0}")]
|
||||
ReceiptRead(PathBuf, #[source] Box<toml::de::Error>),
|
||||
#[error("Failed to update `uv.lock` at {0}")]
|
||||
LockWrite(PathBuf, #[source] Box<toml_edit::ser::Error>),
|
||||
#[error(transparent)]
|
||||
VirtualEnvError(#[from] uv_virtualenv::Error),
|
||||
#[error("Failed to read package entry points {0}")]
|
||||
@@ -98,6 +101,7 @@ impl Error {
|
||||
Self::VirtualEnvError(uv_virtualenv::Error::Io(err)) => Some(err),
|
||||
Self::ReceiptWrite(_, _)
|
||||
| Self::ReceiptRead(_, _)
|
||||
| Self::LockWrite(_, _)
|
||||
| Self::VirtualEnvError(_)
|
||||
| Self::EntrypointRead(_)
|
||||
| Self::NoExecutableDirectory
|
||||
@@ -146,6 +150,32 @@ impl InstalledTools {
|
||||
self.root.join(name.to_string())
|
||||
}
|
||||
|
||||
/// Read the lock for a tool, if it has been generated and still matches its receipt.
|
||||
fn read_lock(directory: &Path, tool: &Tool) -> Result<Option<Lock>, Error> {
|
||||
let path = directory.join("uv.lock");
|
||||
match fs_err::read_to_string(&path) {
|
||||
Ok(contents) => match toml::from_str::<Lock>(&contents) {
|
||||
Ok(lock) if tool.matches_lock(&lock, directory) => Ok(Some(lock)),
|
||||
Ok(_) => {
|
||||
debug!(
|
||||
"Ignoring tool lock at `{}` because it does not match its receipt",
|
||||
path.user_display()
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
Err(err) => {
|
||||
debug!(
|
||||
"Ignoring invalid tool lock at `{}`: {err}",
|
||||
path.user_display()
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
},
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the metadata for all installed tools.
|
||||
///
|
||||
/// If a tool is present, but is missing a receipt or the receipt is invalid, the tool will be
|
||||
@@ -174,7 +204,13 @@ impl InstalledTools {
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
match ToolReceipt::from_string(contents) {
|
||||
Ok(tool_receipt) => tools.push((name, Ok(tool_receipt.tool))),
|
||||
Ok(tool_receipt) => {
|
||||
let tool = tool_receipt.tool;
|
||||
tools.push((
|
||||
name,
|
||||
Self::read_lock(&directory, &tool).map(|lock| tool.with_lock(lock)),
|
||||
));
|
||||
}
|
||||
Err(err) => {
|
||||
let err = Error::ReceiptRead(path, Box::new(err));
|
||||
tools.push((name, Err(err)));
|
||||
@@ -191,9 +227,14 @@ impl InstalledTools {
|
||||
///
|
||||
/// Note it is generally incorrect to use this without [`Self::acquire_lock`].
|
||||
pub fn get_tool_receipt(&self, name: &PackageName) -> Result<Option<Tool>, Error> {
|
||||
let path = self.tool_dir(name).join("uv-receipt.toml");
|
||||
let directory = self.tool_dir(name);
|
||||
let path = directory.join("uv-receipt.toml");
|
||||
match ToolReceipt::from_path(&path) {
|
||||
Ok(tool_receipt) => Ok(Some(tool_receipt.tool)),
|
||||
Ok(tool_receipt) => {
|
||||
let tool = tool_receipt.tool;
|
||||
let lock = Self::read_lock(&directory, &tool)?;
|
||||
Ok(Some(tool.with_lock(lock)))
|
||||
}
|
||||
Err(Error::Io(err)) if err.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
@@ -215,6 +256,12 @@ impl InstalledTools {
|
||||
///
|
||||
/// Note it is generally incorrect to use this without [`Self::acquire_lock`].
|
||||
pub fn add_tool_receipt(&self, name: &PackageName, tool: Tool) -> Result<(), Error> {
|
||||
let lock_path = self.tool_dir(name).join("uv.lock");
|
||||
let lock = tool
|
||||
.lock()
|
||||
.map(Lock::to_toml)
|
||||
.transpose()
|
||||
.map_err(|err| Error::LockWrite(lock_path.clone(), Box::new(err)))?;
|
||||
let tool_receipt = ToolReceipt::from(tool);
|
||||
let path = self.tool_dir(name).join("uv-receipt.toml");
|
||||
|
||||
@@ -227,8 +274,17 @@ impl InstalledTools {
|
||||
.to_toml()
|
||||
.map_err(|err| Error::ReceiptWrite(path.clone(), Box::new(err)))?;
|
||||
|
||||
// Save the modified `uv-receipt.toml`.
|
||||
fs_err::write(&path, doc)?;
|
||||
// Store the derived lock before making its authoritative receipt visible.
|
||||
match lock {
|
||||
Some(lock) => uv_fs::write_atomic_sync(&lock_path, lock)?,
|
||||
None => match fs_err::remove_file(&lock_path) {
|
||||
Ok(()) => (),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => (),
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
}
|
||||
|
||||
uv_fs::write_atomic_sync(&path, doc)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Deserialize;
|
||||
use uv_resolver::Lock;
|
||||
|
||||
use crate::Tool;
|
||||
|
||||
@@ -19,14 +18,8 @@ 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 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)
|
||||
let tool = toml::from_str(&raw)?;
|
||||
Ok(Self { raw, ..tool })
|
||||
}
|
||||
|
||||
/// Read a [`ToolReceipt`] from the given path.
|
||||
@@ -42,17 +35,7 @@ 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 = 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()
|
||||
};
|
||||
let mut doc = toml_edit::DocumentMut::new();
|
||||
doc.insert("tool", toml_edit::Item::Table(self.tool.to_toml()?));
|
||||
|
||||
Ok(doc.to_string())
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::Deserialize;
|
||||
use toml_edit::{Array, Item, Table, Value, value};
|
||||
|
||||
use uv_distribution_types::Requirement;
|
||||
use uv_distribution_types::{Requirement, StaticMetadata};
|
||||
use uv_fs::{PortablePath, Simplified};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_normalize::{GroupName, PackageName};
|
||||
use uv_pypi_types::VerbatimParsedUrl;
|
||||
use uv_python::PythonRequest;
|
||||
use uv_resolver::Lock;
|
||||
use uv_resolver::{Lock, ResolverManifest};
|
||||
use uv_settings::{ToolOptions, ToolOptionsWire};
|
||||
|
||||
/// A tool entry.
|
||||
@@ -213,6 +213,22 @@ impl Tool {
|
||||
Self { lock, ..self }
|
||||
}
|
||||
|
||||
/// Return whether a standalone [`Lock`] was generated for this receipt.
|
||||
pub(crate) fn matches_lock(&self, lock: &Lock, directory: &Path) -> bool {
|
||||
ResolverManifest::new(
|
||||
std::iter::empty::<PackageName>(),
|
||||
self.requirements.iter().cloned(),
|
||||
self.constraints.iter().cloned(),
|
||||
self.overrides.iter().cloned(),
|
||||
self.excludes.iter().cloned(),
|
||||
self.build_constraints.iter().cloned(),
|
||||
std::iter::empty::<(GroupName, Vec<Requirement>)>(),
|
||||
std::iter::empty::<StaticMetadata>(),
|
||||
)
|
||||
.relative_to(directory)
|
||||
.is_ok_and(|manifest| lock.matches_manifest(&manifest))
|
||||
}
|
||||
|
||||
/// Returns the TOML table for this tool.
|
||||
pub(crate) fn to_toml(&self) -> Result<Table, toml_edit::ser::Error> {
|
||||
let mut table = Table::new();
|
||||
|
||||
@@ -290,8 +290,8 @@ pub(crate) fn normalize_tool_local_requirements(
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build the lock manifest for a tool receipt.
|
||||
pub(crate) fn tool_receipt_manifest(
|
||||
/// Build the lock manifest for a tool environment.
|
||||
pub(crate) fn tool_lock_manifest(
|
||||
requirements: &[Requirement],
|
||||
constraints: &[Requirement],
|
||||
overrides: &[Requirement],
|
||||
@@ -310,8 +310,8 @@ pub(crate) fn tool_receipt_manifest(
|
||||
)
|
||||
}
|
||||
|
||||
/// Build the receipt lock for a tool environment.
|
||||
pub(crate) fn tool_receipt_lock(
|
||||
/// Build the lock for a tool environment.
|
||||
pub(crate) fn tool_lock(
|
||||
root: &Path,
|
||||
resolution: &ResolverOutput,
|
||||
manifest: &ResolverManifest,
|
||||
@@ -320,19 +320,19 @@ pub(crate) fn tool_receipt_lock(
|
||||
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}");
|
||||
debug!("Failed to relativize tool lock manifest: {err}");
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
debug!("Failed to build tool receipt lock: {err}");
|
||||
debug!("Failed to build tool 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.
|
||||
/// Build an environment specification for a tool, preferring versions from its existing lock when
|
||||
/// available, then falling back to the installed environment.
|
||||
pub(crate) fn tool_environment_spec<'lock>(
|
||||
requirements: RequirementsSpecification,
|
||||
tool: Option<&'lock Tool>,
|
||||
|
||||
@@ -17,7 +17,6 @@ use uv_distribution_types::{
|
||||
ExtraBuildRequires, IndexCapabilities, NameRequirementSpecification, Requirement,
|
||||
RequirementSource, Resolution, UnresolvedRequirementSpecification,
|
||||
};
|
||||
use uv_fs::CWD;
|
||||
use uv_installer::{InstallationStrategy, Planner, SitePackages};
|
||||
use uv_normalize::PackageName;
|
||||
use uv_pep440::{VersionSpecifier, VersionSpecifiers};
|
||||
@@ -48,19 +47,18 @@ use crate::commands::project::{
|
||||
};
|
||||
use crate::commands::tool::common::{
|
||||
ToolPython, finalize_tool_install, normalize_tool_local_requirements, refine_interpreter,
|
||||
remove_entrypoints, tool_environment_spec, tool_receipt_lock, tool_receipt_manifest,
|
||||
remove_entrypoints, tool_environment_spec, tool_lock, tool_lock_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`].
|
||||
/// An [`Installable`] adapter for a tool [`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.
|
||||
/// Tools store a `uv.lock`, 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 lock when checking whether an existing environment is already up-to-date.
|
||||
struct ToolLockInstallTarget<'lock> {
|
||||
install_path: &'lock Path,
|
||||
lock: &'lock Lock,
|
||||
@@ -68,7 +66,7 @@ struct ToolLockInstallTarget<'lock> {
|
||||
}
|
||||
|
||||
impl<'lock> ToolLockInstallTarget<'lock> {
|
||||
/// Create a [`ToolLockInstallTarget`] for a tool environment and its embedded [`Lock`].
|
||||
/// Create a [`ToolLockInstallTarget`] for a tool environment and its [`Lock`].
|
||||
fn new(
|
||||
install_path: &'lock Path,
|
||||
lock: &'lock Lock,
|
||||
@@ -507,7 +505,7 @@ pub(crate) async fn install(
|
||||
|
||||
// Convert to tool options.
|
||||
let options = ToolOptions::from(options);
|
||||
let receipt_manifest = tool_receipt_manifest(
|
||||
let lock_manifest = tool_lock_manifest(
|
||||
&requirements,
|
||||
&constraints,
|
||||
&overrides,
|
||||
@@ -643,8 +641,7 @@ pub(crate) async fn install(
|
||||
)?
|
||||
.is_empty()
|
||||
} else {
|
||||
// Force legacy receipts through the update path so they get rewritten with an
|
||||
// embedded lock.
|
||||
// Force tools without locks through the update path so a lock gets generated.
|
||||
false
|
||||
};
|
||||
if already_installed {
|
||||
@@ -693,7 +690,7 @@ 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, receipt_lock) = if let Some(environment) = existing_environment {
|
||||
let (environment, tool_lock) = if let Some(environment) = existing_environment {
|
||||
let update = match update_environment(
|
||||
environment.into_environment(),
|
||||
spec.clone(),
|
||||
@@ -734,13 +731,13 @@ pub(crate) async fn install(
|
||||
remove_entrypoints(existing_receipt);
|
||||
}
|
||||
|
||||
let receipt_lock = if let Some(tool_receipt) = existing_tool_receipt.as_ref() {
|
||||
let tool_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}"
|
||||
"Failed to read tool environment site-packages while rebuilding lock after update: {err}"
|
||||
);
|
||||
None
|
||||
}
|
||||
@@ -772,13 +769,13 @@ pub(crate) async fn install(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(resolution) => tool_receipt_lock(
|
||||
Ok(resolution) => tool_lock(
|
||||
&installed_tools.tool_dir(package_name),
|
||||
&resolution,
|
||||
&receipt_manifest,
|
||||
&lock_manifest,
|
||||
),
|
||||
Err(err) => {
|
||||
debug!("Failed to rebuild tool receipt lock after update: {err}");
|
||||
debug!("Failed to rebuild tool lock after update: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -786,7 +783,7 @@ pub(crate) async fn install(
|
||||
None
|
||||
};
|
||||
|
||||
(update.environment, receipt_lock)
|
||||
(update.environment, tool_lock)
|
||||
} else {
|
||||
let spec = EnvironmentSpecification::from(spec);
|
||||
|
||||
@@ -882,10 +879,10 @@ pub(crate) async fn install(
|
||||
};
|
||||
|
||||
let environment = installed_tools.create_environment(package_name, interpreter)?;
|
||||
let receipt_lock = tool_receipt_lock(
|
||||
let tool_lock = tool_lock(
|
||||
&installed_tools.tool_dir(package_name),
|
||||
&resolution,
|
||||
&receipt_manifest,
|
||||
&lock_manifest,
|
||||
);
|
||||
let resolution: Resolution = resolution.into();
|
||||
|
||||
@@ -917,7 +914,7 @@ pub(crate) async fn install(
|
||||
debug!("Failed to sync environment; removing `{}`", package_name);
|
||||
let _ = installed_tools.remove_environment(package_name);
|
||||
}) {
|
||||
Ok(environment) => (environment, receipt_lock),
|
||||
Ok(environment) => (environment, tool_lock),
|
||||
Err(ProjectError::Operation(err)) => {
|
||||
return diagnostics::OperationDiagnostic::with_system_certs(
|
||||
client_builder.system_certs(),
|
||||
@@ -947,7 +944,7 @@ pub(crate) async fn install(
|
||||
overrides,
|
||||
excludes,
|
||||
build_constraints,
|
||||
receipt_lock,
|
||||
tool_lock,
|
||||
printer,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ 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::{
|
||||
normalize_tool_local_requirements, remove_entrypoints, tool_environment_spec,
|
||||
tool_receipt_lock, tool_receipt_manifest,
|
||||
normalize_tool_local_requirements, remove_entrypoints, tool_environment_spec, tool_lock,
|
||||
tool_lock_manifest,
|
||||
};
|
||||
use crate::commands::{ExitStatus, conjunction, tool::common::finalize_tool_install};
|
||||
use crate::printer::Printer;
|
||||
@@ -329,7 +329,7 @@ async fn upgrade_tool(
|
||||
)
|
||||
.into_iter(),
|
||||
);
|
||||
let receipt_manifest = tool_receipt_manifest(
|
||||
let lock_manifest = tool_lock_manifest(
|
||||
existing_tool_receipt.requirements(),
|
||||
existing_tool_receipt.constraints(),
|
||||
existing_tool_receipt.overrides(),
|
||||
@@ -358,7 +358,7 @@ async fn upgrade_tool(
|
||||
|
||||
// Check if we need to create a new environment — if so, resolve it first, then
|
||||
// install the requested tool
|
||||
let (environment, outcome, receipt_lock) = if let Some(interpreter) =
|
||||
let (environment, outcome, tool_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.
|
||||
@@ -386,11 +386,7 @@ 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 tool_lock = tool_lock(&installed_tools.tool_dir(name), &resolution, &lock_manifest);
|
||||
|
||||
let environment = sync_environment(
|
||||
environment,
|
||||
@@ -409,11 +405,7 @@ async fn upgrade_tool(
|
||||
)
|
||||
.await?;
|
||||
|
||||
(
|
||||
environment,
|
||||
UpgradeOutcome::UpgradeEnvironment,
|
||||
receipt_lock,
|
||||
)
|
||||
(environment, UpgradeOutcome::UpgradeEnvironment, tool_lock)
|
||||
} else {
|
||||
let resolution = resolve_environment(
|
||||
tool_environment_spec(
|
||||
@@ -438,11 +430,7 @@ async fn upgrade_tool(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let receipt_lock = tool_receipt_lock(
|
||||
&installed_tools.tool_dir(name),
|
||||
&resolution,
|
||||
&receipt_manifest,
|
||||
);
|
||||
let tool_lock = tool_lock(&installed_tools.tool_dir(name), &resolution, &lock_manifest);
|
||||
let resolution = Resolution::from(resolution);
|
||||
|
||||
let ResolverInstallerSettings {
|
||||
@@ -514,7 +502,7 @@ async fn upgrade_tool(
|
||||
.await?
|
||||
};
|
||||
|
||||
(environment, outcome, receipt_lock)
|
||||
(environment, outcome, tool_lock)
|
||||
};
|
||||
|
||||
if matches!(
|
||||
@@ -545,7 +533,7 @@ 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,
|
||||
tool_lock,
|
||||
printer,
|
||||
)?;
|
||||
} else {
|
||||
@@ -554,7 +542,7 @@ async fn upgrade_tool(
|
||||
existing_tool_receipt
|
||||
.clone()
|
||||
.with_options(ToolOptions::from(options))
|
||||
.with_lock(receipt_lock),
|
||||
.with_lock(tool_lock),
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::collections::BTreeSet;
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
#[cfg(feature = "test-git")]
|
||||
use assert_fs::fixture::ChildPath;
|
||||
@@ -85,6 +85,10 @@ fn tool_install() {
|
||||
.child("black")
|
||||
.child("uv-receipt.toml")
|
||||
.assert(predicate::path::exists());
|
||||
tool_dir
|
||||
.child("black")
|
||||
.child("uv.lock")
|
||||
.assert(predicate::path::exists());
|
||||
|
||||
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
|
||||
assert!(executable.exists());
|
||||
@@ -113,8 +117,8 @@ fn tool_install() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -189,7 +193,8 @@ fn tool_install() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -277,7 +282,7 @@ fn tool_install() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -372,7 +377,8 @@ fn tool_install() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/fc/254c3e9b5feb89ff5b9076a23218dafbc99c96ac5941e900b71206e6313b/werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10", size = 226669, upload-time = "2023-10-24T20:57:47.326Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "flask" }]
|
||||
entrypoints = [
|
||||
@@ -409,7 +415,7 @@ fn tool_install_relative_exclude_newer_receipt_preserves_span() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -485,7 +491,8 @@ fn tool_install_relative_exclude_newer_receipt_preserves_span() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.2.0" }]
|
||||
entrypoints = [
|
||||
@@ -2018,8 +2025,8 @@ fn tool_install_with_compatible_build_constraints() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.9.[X]"
|
||||
@@ -2136,7 +2143,8 @@ fn tool_install_with_compatible_build_constraints() -> Result<()> {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/01/f3/936e209267d6ef7510322191003885de524fc48d1b43269810cd589ceaf5/typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a", size = 34698, upload-time = "2024-04-05T12:35:44.388Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "black" },
|
||||
@@ -2327,8 +2335,8 @@ fn tool_install_version() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -2403,7 +2411,8 @@ fn tool_install_version() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.2.0" }]
|
||||
entrypoints = [
|
||||
@@ -2488,8 +2497,8 @@ fn tool_install_editable() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -2517,7 +2526,8 @@ fn tool_install_editable() {
|
||||
{ name = "uvloop", marker = "extra == 'uvloop'", specifier = ">=0.15.2" },
|
||||
]
|
||||
provides-extras = ["colorama", "uvloop", "d", "jupyter", "dev"]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", editable = "[WORKSPACE]/test/packages/black_editable" }]
|
||||
entrypoints = [
|
||||
@@ -2559,8 +2569,8 @@ fn tool_install_editable() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -2635,7 +2645,8 @@ fn tool_install_editable() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -2679,8 +2690,8 @@ fn tool_install_editable() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -2755,7 +2766,8 @@ fn tool_install_editable() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.2.0" }]
|
||||
entrypoints = [
|
||||
@@ -3162,8 +3174,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -3182,7 +3194,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "pyflakes" }]
|
||||
entrypoints = [
|
||||
@@ -3261,8 +3274,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -3281,7 +3294,8 @@ fn tool_install_remove_on_empty() -> Result<()> {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "pyflakes" }]
|
||||
entrypoints = [
|
||||
@@ -3357,8 +3371,8 @@ fn tool_install_editable_from() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -3386,7 +3400,8 @@ fn tool_install_editable_from() {
|
||||
{ name = "uvloop", marker = "extra == 'uvloop'", specifier = ">=0.15.2" },
|
||||
]
|
||||
provides-extras = ["colorama", "uvloop", "d", "jupyter", "dev"]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", editable = "[WORKSPACE]/test/packages/black_editable" }]
|
||||
entrypoints = [
|
||||
@@ -3539,8 +3554,8 @@ fn tool_install_already_installed() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -3615,7 +3630,8 @@ fn tool_install_already_installed() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -3652,8 +3668,8 @@ fn tool_install_already_installed() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should not have an additional tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should not have an additional tool lock or receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -3728,7 +3744,8 @@ fn tool_install_already_installed() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -3742,9 +3759,9 @@ fn tool_install_already_installed() {
|
||||
});
|
||||
}
|
||||
|
||||
/// Test installing a tool with a valid legacy receipt that lacks a lock.
|
||||
/// Test installing over a valid tool installation with an invalid lock.
|
||||
#[test]
|
||||
fn tool_install_migrates_lockless_receipt() -> Result<()> {
|
||||
fn tool_install_migrates_invalid_lock() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12")
|
||||
.with_filtered_counts()
|
||||
.with_filtered_exe_suffix();
|
||||
@@ -3763,17 +3780,13 @@ fn tool_install_migrates_lockless_receipt() -> Result<()> {
|
||||
.success();
|
||||
|
||||
let receipt_path = tool_dir.child("black").child("uv-receipt.toml");
|
||||
fs_err::write(tool_dir.child("black").child("uv.lock"), "[invalid")?;
|
||||
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}"))?;
|
||||
let lockless_receipt = fs_err::read_to_string(&receipt_path)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(lockless_receipt, @r#"
|
||||
assert_snapshot!(receipt, @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -3786,6 +3799,19 @@ fn tool_install_migrates_lockless_receipt() -> Result<()> {
|
||||
"#);
|
||||
});
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_list()
|
||||
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
|
||||
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str()), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
black v24.3.0
|
||||
- black
|
||||
- blackd
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("--python-platform")
|
||||
.arg("linux")
|
||||
@@ -3803,11 +3829,10 @@ fn tool_install_migrates_lockless_receipt() -> Result<()> {
|
||||
Installed 2 executables: black, blackd
|
||||
");
|
||||
|
||||
let receipt = fs_err::read_to_string(&receipt_path)?;
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(receipt, @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -3882,7 +3907,8 @@ fn tool_install_migrates_lockless_receipt() -> Result<()> {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -3898,9 +3924,74 @@ fn tool_install_migrates_lockless_receipt() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test migrating a legacy receipt without upgrading dependencies beyond the installed environment.
|
||||
/// Test installing over a tool whose lock belongs to a different receipt.
|
||||
#[test]
|
||||
fn tool_install_migrates_lockless_receipt_with_installed_preferences() -> Result<()> {
|
||||
fn tool_install_regenerates_mismatched_lock() -> 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("--python-platform")
|
||||
.arg("linux")
|
||||
.arg("black")
|
||||
.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 lock_path = tool_dir.child("black").child("uv.lock");
|
||||
let lock = fs_err::read_to_string(&lock_path)?;
|
||||
let mismatched_lock = lock.replacen(
|
||||
r#"requirements = [{ name = "black" }]"#,
|
||||
r#"requirements = [{ name = "flask" }]"#,
|
||||
1,
|
||||
);
|
||||
assert_ne!(lock, mismatched_lock);
|
||||
fs_err::write(&lock_path, mismatched_lock)?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("--python-platform")
|
||||
.arg("linux")
|
||||
.arg("black")
|
||||
.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 -----
|
||||
Resolved [N] packages in [TIME]
|
||||
Checked [N] packages in [TIME]
|
||||
Installed 2 executables: black, blackd
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_install()
|
||||
.arg("--python-platform")
|
||||
.arg("linux")
|
||||
.arg("black")
|
||||
.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 -----
|
||||
`black` is already installed
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test migrating a legacy tool installation without upgrading installed dependencies.
|
||||
#[test]
|
||||
fn tool_install_migrates_missing_lock_with_installed_preferences() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12")
|
||||
.with_filtered_counts()
|
||||
.with_filtered_exe_suffix();
|
||||
@@ -3920,12 +4011,7 @@ fn tool_install_migrates_lockless_receipt_with_installed_preferences() -> Result
|
||||
.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}"))?;
|
||||
fs_err::remove_file(tool_dir.child("black").child("uv.lock"))?;
|
||||
|
||||
context
|
||||
.tool_install()
|
||||
@@ -4247,8 +4333,8 @@ fn tool_install_force() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -4267,7 +4353,8 @@ fn tool_install_force() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("pyflakes").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "pyflakes" }]
|
||||
entrypoints = [
|
||||
@@ -4881,8 +4968,8 @@ fn tool_install_git_lfs() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("test-lfs-repo").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("test-lfs-repo").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13.[X]"
|
||||
@@ -4897,7 +4984,8 @@ fn tool_install_git_lfs() {
|
||||
name = "test-lfs-repo"
|
||||
version = "0.1.0"
|
||||
source = { git = "https://github.com/astral-sh/test-lfs-repo?lfs=true&rev=e282f5be233e3f1d44934164895a043fc534b8aa#e282f5be233e3f1d44934164895a043fc534b8aa" }
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("test-lfs-repo").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "test-lfs-repo", git = "https://github.com/astral-sh/test-lfs-repo?lfs=true&rev=e282f5be233e3f1d44934164895a043fc534b8aa" }]
|
||||
entrypoints = [
|
||||
@@ -5281,8 +5369,8 @@ fn tool_install_with_dependencies_from_script() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -5391,7 +5479,8 @@ fn tool_install_with_dependencies_from_script() -> Result<()> {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "black" },
|
||||
@@ -5445,8 +5534,8 @@ fn tool_install_with_dependencies_from_script() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -5565,7 +5654,8 @@ fn tool_install_with_dependencies_from_script() -> Result<()> {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "black" },
|
||||
@@ -5628,8 +5718,8 @@ fn tool_install_requirements_txt() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -5716,7 +5806,8 @@ fn tool_install_requirements_txt() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "black" },
|
||||
@@ -5762,8 +5853,8 @@ fn tool_install_requirements_txt() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -5850,7 +5941,8 @@ fn tool_install_requirements_txt() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "black" },
|
||||
@@ -5915,8 +6007,8 @@ fn tool_install_requirements_txt_arguments() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -6003,7 +6095,8 @@ fn tool_install_requirements_txt_arguments() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "black" },
|
||||
@@ -6125,8 +6218,8 @@ fn tool_install_upgrade() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -6201,7 +6294,8 @@ fn tool_install_upgrade() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.1.1" }]
|
||||
entrypoints = [
|
||||
@@ -6236,8 +6330,8 @@ fn tool_install_upgrade() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -6312,7 +6406,8 @@ fn tool_install_upgrade() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -6395,8 +6490,8 @@ fn tool_install_upgrade() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -6471,7 +6566,8 @@ fn tool_install_upgrade() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -6939,8 +7035,8 @@ fn tool_install_malformed_dist_info() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -6959,7 +7055,8 @@ fn tool_install_malformed_dist_info() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application" }]
|
||||
entrypoints = [
|
||||
@@ -7040,8 +7137,8 @@ fn tool_install_settings() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7137,7 +7234,8 @@ fn tool_install_settings() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/fc/254c3e9b5feb89ff5b9076a23218dafbc99c96ac5941e900b71206e6313b/werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10", size = 226669, upload-time = "2023-10-24T20:57:47.326Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "flask", specifier = ">=3" }]
|
||||
entrypoints = [
|
||||
@@ -7171,8 +7269,8 @@ fn tool_install_settings() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7268,7 +7366,8 @@ fn tool_install_settings() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/fc/254c3e9b5feb89ff5b9076a23218dafbc99c96ac5941e900b71206e6313b/werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10", size = 226669, upload-time = "2023-10-24T20:57:47.326Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "flask", specifier = ">=3" }]
|
||||
entrypoints = [
|
||||
@@ -7309,8 +7408,8 @@ fn tool_install_settings() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7405,7 +7504,8 @@ fn tool_install_settings() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/fc/254c3e9b5feb89ff5b9076a23218dafbc99c96ac5941e900b71206e6313b/werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10", size = 226669, upload-time = "2023-10-24T20:57:47.326Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("flask").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "flask", specifier = ">=3" }]
|
||||
entrypoints = [
|
||||
@@ -7456,7 +7556,7 @@ fn tool_install_at_version() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7531,7 +7631,8 @@ fn tool_install_at_version() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.1.0" }]
|
||||
entrypoints = [
|
||||
@@ -7601,7 +7702,7 @@ fn tool_install_at_latest() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7676,7 +7777,8 @@ fn tool_install_at_latest() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -7722,7 +7824,7 @@ fn tool_install_from_at_latest() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7741,7 +7843,8 @@ fn tool_install_from_at_latest() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application" }]
|
||||
entrypoints = [
|
||||
@@ -7786,7 +7889,7 @@ fn tool_install_from_at_version() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7805,7 +7908,8 @@ fn tool_install_from_at_version() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/d6/d9e5e20e5fd52a2ff02c4dcd351a2a2fb1e1a159c5de355aded16bccadef/executable_application-0.2.0-py3-none-any.whl", hash = "sha256:2b26f00eb59ebe606697535aee0bfcf1f7ae0dfa7223703cd40cf1c31959d149", size = 1718, upload-time = "2025-01-17T23:21:08.354Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application", specifier = "==0.2.0" }]
|
||||
entrypoints = [
|
||||
@@ -7855,8 +7959,8 @@ fn tool_install_at_latest_upgrade() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -7931,7 +8035,8 @@ fn tool_install_at_latest_upgrade() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.1.1" }]
|
||||
entrypoints = [
|
||||
@@ -7966,8 +8071,8 @@ fn tool_install_at_latest_upgrade() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -8042,7 +8147,8 @@ fn tool_install_at_latest_upgrade() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -8080,8 +8186,8 @@ fn tool_install_at_latest_upgrade() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -8156,7 +8262,8 @@ fn tool_install_at_latest_upgrade() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -8215,8 +8322,8 @@ fn tool_install_constraints() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -8292,7 +8399,8 @@ fn tool_install_constraints() -> Result<()> {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
constraints = [
|
||||
@@ -8402,8 +8510,8 @@ fn tool_install_overrides() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -8482,7 +8590,8 @@ fn tool_install_overrides() -> Result<()> {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
overrides = [
|
||||
@@ -8660,8 +8769,8 @@ async fn tool_install_credentials() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -8680,7 +8789,8 @@ async fn tool_install_credentials() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application" }]
|
||||
entrypoints = [
|
||||
@@ -8771,8 +8881,8 @@ async fn tool_install_default_credentials() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
// We should have a tool receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
// We should have a tool lock and receipt
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -8791,7 +8901,8 @@ async fn tool_install_default_credentials() -> Result<()> {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application" }]
|
||||
entrypoints = [
|
||||
@@ -8888,7 +8999,7 @@ fn tool_install_with_executables_from() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("ansible").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("ansible").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -9113,7 +9224,8 @@ fn tool_install_with_executables_from() {
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/fc/e9ccf0521607bcd244aa0b3fbd574f71b65e9ce6a112c83af988bbbe2e23/resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf", size = 17194, upload-time = "2023-03-09T05:10:36.214Z" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("ansible").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [
|
||||
{ name = "ansible", specifier = "==9.3.0" },
|
||||
|
||||
@@ -362,11 +362,11 @@ fn tool_list_deprecated() -> Result<()> {
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Ensure that we have a modern tool receipt.
|
||||
// Ensure that we have modern tool metadata.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -441,7 +441,8 @@ fn tool_list_deprecated() -> Result<()> {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black", specifier = "==24.2.0" }]
|
||||
entrypoints = [
|
||||
@@ -466,6 +467,7 @@ fn tool_list_deprecated() -> Result<()> {
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
fs::remove_file(tool_dir.join("black").join("uv.lock"))?;
|
||||
|
||||
// Ensure that we can still list the tool.
|
||||
uv_snapshot!(context.filters(), context.tool_list()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::prelude::*;
|
||||
use indoc::indoc;
|
||||
@@ -610,7 +610,7 @@ fn tool_upgrade_recomputes_relative_exclude_newer() {
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -686,7 +686,8 @@ fn tool_upgrade_recomputes_relative_exclude_newer() {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "black" }]
|
||||
entrypoints = [
|
||||
@@ -702,7 +703,7 @@ fn tool_upgrade_recomputes_relative_exclude_newer() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_upgrade_migrates_lockless_receipt_with_installed_preferences() -> Result<()> {
|
||||
fn tool_upgrade_migrates_missing_lock_with_installed_preferences() -> Result<()> {
|
||||
let context = uv_test::test_context!("3.12")
|
||||
.with_filtered_counts()
|
||||
.with_filtered_exe_suffix();
|
||||
@@ -722,12 +723,7 @@ fn tool_upgrade_migrates_lockless_receipt_with_installed_preferences() -> Result
|
||||
.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}"))?;
|
||||
fs_err::remove_file(tool_dir.child("black").child("uv.lock"))?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.tool_upgrade()
|
||||
.arg("black")
|
||||
@@ -1776,7 +1772,7 @@ 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#"
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv.lock")).unwrap(), @r#"
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12.[X]"
|
||||
@@ -1795,7 +1791,8 @@ async fn tool_upgrade_invalid_auth() -> Result<()> {
|
||||
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" },
|
||||
]
|
||||
|
||||
"#);
|
||||
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
|
||||
[tool]
|
||||
requirements = [{ name = "executable-application" }]
|
||||
entrypoints = [
|
||||
|
||||
Reference in New Issue
Block a user