mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Track tool entry points in receipts (#4634)
We need this to power uninstallations! The latter two commits were reviewed in: - #4637 - #4638 Note this is a breaking change for existing tool installations, but it's in preview and very new. In the future, we'll need a clear upgrade path for tool receipt changes.
This commit is contained in:
parent
72438ef5bb
commit
3a627f3799
7 changed files with 206 additions and 50 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -5019,12 +5019,14 @@ dependencies = [
|
||||||
"dirs-sys",
|
"dirs-sys",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"install-wheel-rs",
|
"install-wheel-rs",
|
||||||
|
"path-slash",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"pypi-types",
|
"pypi-types",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
"toml_edit",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uv-cache",
|
"uv-cache",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
|
|
|
@ -13,20 +13,22 @@ license = { workspace = true }
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uv-fs = { workspace = true }
|
|
||||||
uv-state = { workspace = true }
|
|
||||||
pep508_rs = { workspace = true }
|
|
||||||
pypi-types = { workspace = true }
|
|
||||||
uv-virtualenv = { workspace = true }
|
|
||||||
uv-toolchain = { workspace = true }
|
|
||||||
install-wheel-rs = { workspace = true }
|
install-wheel-rs = { workspace = true }
|
||||||
pep440_rs = { workspace = true }
|
pep440_rs = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
|
pypi-types = { workspace = true }
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
|
uv-fs = { workspace = true }
|
||||||
|
uv-state = { workspace = true }
|
||||||
|
uv-toolchain = { workspace = true }
|
||||||
|
uv-virtualenv = { workspace = true }
|
||||||
|
uv-warnings = { workspace = true }
|
||||||
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
tracing = { workspace = true }
|
|
||||||
fs-err = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
toml = { workspace = true }
|
|
||||||
dirs-sys = { workspace = true }
|
dirs-sys = { workspace = true }
|
||||||
|
fs-err = { workspace = true }
|
||||||
|
path-slash = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
toml = { workspace = true }
|
||||||
|
toml_edit = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
|
@ -14,7 +14,7 @@ use uv_toolchain::{Interpreter, PythonEnvironment};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
pub use receipt::ToolReceipt;
|
pub use receipt::ToolReceipt;
|
||||||
pub use tool::Tool;
|
pub use tool::{Tool, ToolEntrypoint};
|
||||||
|
|
||||||
use uv_state::{StateBucket, StateStore};
|
use uv_state::{StateBucket, StateStore};
|
||||||
mod receipt;
|
mod receipt;
|
||||||
|
@ -135,10 +135,9 @@ impl InstalledTools {
|
||||||
path.user_display()
|
path.user_display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let doc = toml::to_string(&tool_receipt)
|
let doc = tool_receipt.to_toml();
|
||||||
.map_err(|err| Error::ReceiptWrite(path.clone(), Box::new(err)))?;
|
|
||||||
|
|
||||||
// Save the modified `tools.toml`.
|
// Save the modified `uv-receipt.toml`.
|
||||||
fs_err::write(&path, doc)?;
|
fs_err::write(&path, doc)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::Tool;
|
use crate::Tool;
|
||||||
|
|
||||||
/// A `uv-receipt.toml` file tracking the installation of a tool.
|
/// A `uv-receipt.toml` file tracking the installation of a tool.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
pub struct ToolReceipt {
|
pub struct ToolReceipt {
|
||||||
pub(crate) tool: Tool,
|
pub(crate) tool: Tool,
|
||||||
|
|
||||||
|
@ -30,6 +30,16 @@ impl ToolReceipt {
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the TOML representation of this receipt.
|
||||||
|
pub(crate) fn to_toml(&self) -> String {
|
||||||
|
// 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();
|
||||||
|
doc.insert("tool", toml_edit::Item::Table(self.tool.to_toml()));
|
||||||
|
|
||||||
|
doc.to_string()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore raw document in comparison.
|
// Ignore raw document in comparison.
|
||||||
|
|
|
@ -1,14 +1,59 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use path_slash::PathBufExt;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
use pypi_types::VerbatimParsedUrl;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
|
use toml_edit::value;
|
||||||
|
use toml_edit::Array;
|
||||||
|
use toml_edit::Table;
|
||||||
|
use toml_edit::Value;
|
||||||
|
|
||||||
/// A tool entry.
|
/// A tool entry.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Tool {
|
pub struct Tool {
|
||||||
|
// The requirements requested by the user during installation.
|
||||||
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
||||||
|
/// The Python requested by the user during installation.
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
|
// A mapping of entry point names to their metadata.
|
||||||
|
entrypoints: Vec<ToolEntrypoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct ToolEntrypoint {
|
||||||
|
name: String,
|
||||||
|
install_path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format an array so that each element is on its own line and has a trailing comma.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
///
|
||||||
|
/// ```toml
|
||||||
|
/// requirements = [
|
||||||
|
/// "foo",
|
||||||
|
/// "bar",
|
||||||
|
/// ]
|
||||||
|
/// ```
|
||||||
|
fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value>>) -> Array {
|
||||||
|
let mut array = elements
|
||||||
|
.map(Into::into)
|
||||||
|
.map(|mut value| {
|
||||||
|
// Each dependency is on its own line and indented.
|
||||||
|
value.decor_mut().set_prefix("\n ");
|
||||||
|
value
|
||||||
|
})
|
||||||
|
.collect::<Array>();
|
||||||
|
// With a trailing comma, inserting another entry doesn't change the preceding line,
|
||||||
|
// reducing the diff noise.
|
||||||
|
array.set_trailing_comma(true);
|
||||||
|
// The line break between the last element's comma and the closing square bracket.
|
||||||
|
array.set_trailing("\n");
|
||||||
|
array
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tool {
|
impl Tool {
|
||||||
|
@ -16,10 +61,67 @@ impl Tool {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
|
entrypoints: impl Iterator<Item = ToolEntrypoint>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
let mut entrypoints: Vec<_> = entrypoints.collect();
|
||||||
|
entrypoints.sort();
|
||||||
Self {
|
Self {
|
||||||
requirements,
|
requirements,
|
||||||
python,
|
python,
|
||||||
|
entrypoints,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the TOML table for this tool.
|
||||||
|
pub(crate) fn to_toml(&self) -> Table {
|
||||||
|
let mut table = Table::new();
|
||||||
|
|
||||||
|
table.insert("requirements", {
|
||||||
|
let requirements = match self.requirements.as_slice() {
|
||||||
|
[] => Array::new(),
|
||||||
|
[requirement] => Array::from_iter([Value::from(requirement.to_string())]),
|
||||||
|
requirements => each_element_on_its_line_array(
|
||||||
|
requirements
|
||||||
|
.iter()
|
||||||
|
.map(|requirement| Value::from(requirement.to_string())),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
value(requirements)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ref python) = self.python {
|
||||||
|
table.insert("python", value(python));
|
||||||
|
}
|
||||||
|
|
||||||
|
table.insert("entrypoints", {
|
||||||
|
let entrypoints = each_element_on_its_line_array(
|
||||||
|
self.entrypoints
|
||||||
|
.iter()
|
||||||
|
.map(ToolEntrypoint::to_toml)
|
||||||
|
.map(toml_edit::Table::into_inline_table),
|
||||||
|
);
|
||||||
|
value(entrypoints)
|
||||||
|
});
|
||||||
|
|
||||||
|
table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToolEntrypoint {
|
||||||
|
/// Create a new [`ToolEntrypoint`].
|
||||||
|
pub fn new(name: String, install_path: PathBuf) -> Self {
|
||||||
|
Self { name, install_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the TOML table for this entrypoint.
|
||||||
|
pub(crate) fn to_toml(&self) -> Table {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.insert("name", value(&self.name));
|
||||||
|
table.insert(
|
||||||
|
"install-path",
|
||||||
|
// Use cross-platform slashes so the toml string type does not change
|
||||||
|
value(self.install_path.to_slash_lossy().to_string()),
|
||||||
|
);
|
||||||
|
table
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ use uv_fs::replace_symlink;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
use uv_requirements::RequirementsSource;
|
use uv_requirements::RequirementsSource;
|
||||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool};
|
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||||
use uv_toolchain::{EnvironmentPreference, Toolchain, ToolchainPreference, ToolchainRequest};
|
use uv_toolchain::{EnvironmentPreference, Toolchain, ToolchainPreference, ToolchainRequest};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
|
@ -115,7 +115,6 @@ pub(crate) async fn install(
|
||||||
let Some(from) = requirements.first().cloned() else {
|
let Some(from) = requirements.first().cloned() else {
|
||||||
bail!("Expected at least one requirement")
|
bail!("Expected at least one requirement")
|
||||||
};
|
};
|
||||||
let tool = Tool::new(requirements, python.clone());
|
|
||||||
|
|
||||||
let interpreter = Toolchain::find(
|
let interpreter = Toolchain::find(
|
||||||
&python
|
&python
|
||||||
|
@ -176,7 +175,7 @@ pub(crate) async fn install(
|
||||||
executable_directory.user_display()
|
executable_directory.user_display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let entrypoints = entrypoint_paths(
|
let entry_points = entrypoint_paths(
|
||||||
&environment,
|
&environment,
|
||||||
installed_dist.name(),
|
installed_dist.name(),
|
||||||
installed_dist.version(),
|
installed_dist.version(),
|
||||||
|
@ -184,68 +183,79 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
// Determine the entry points targets
|
// Determine the entry points targets
|
||||||
// Use a sorted collection for deterministic output
|
// Use a sorted collection for deterministic output
|
||||||
let targets = entrypoints
|
let target_entry_points = entry_points
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(name, path)| {
|
.map(|(name, source_path)| {
|
||||||
let target = executable_directory.join(
|
let target_path = executable_directory.join(
|
||||||
path.file_name()
|
source_path
|
||||||
|
.file_name()
|
||||||
.map(std::borrow::ToOwned::to_owned)
|
.map(std::borrow::ToOwned::to_owned)
|
||||||
.unwrap_or_else(|| OsString::from(name.clone())),
|
.unwrap_or_else(|| OsString::from(name.clone())),
|
||||||
);
|
);
|
||||||
(name, path, target)
|
(name, source_path, target_path)
|
||||||
})
|
})
|
||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
// Check if they exist, before installing
|
// Check if they exist, before installing
|
||||||
let mut existing_targets = targets
|
let mut existing_entry_points = target_entry_points
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, _, target)| target.exists())
|
.filter(|(_, _, target_path)| target_path.exists())
|
||||||
.peekable();
|
.peekable();
|
||||||
|
|
||||||
// Note we use `reinstall_entry_points` here instead of `reinstall`; requesting reinstall
|
// Note we use `reinstall_entry_points` here instead of `reinstall`; requesting reinstall
|
||||||
// will _not_ remove existing entry points when they are not managed by uv.
|
// will _not_ remove existing entry points when they are not managed by uv.
|
||||||
if force || reinstall_entry_points {
|
if force || reinstall_entry_points {
|
||||||
for (name, _, target) in existing_targets {
|
for (name, _, target) in existing_entry_points {
|
||||||
debug!("Removing existing entry point `{name}`");
|
debug!("Removing existing entry point `{name}`");
|
||||||
fs_err::remove_file(target)?;
|
fs_err::remove_file(target)?;
|
||||||
}
|
}
|
||||||
} else if existing_targets.peek().is_some() {
|
} else if existing_entry_points.peek().is_some() {
|
||||||
// Clean up the environment we just created
|
// Clean up the environment we just created
|
||||||
installed_tools.remove_environment(&name)?;
|
installed_tools.remove_environment(&name)?;
|
||||||
|
|
||||||
let existing_targets = existing_targets
|
let existing_entry_points = existing_entry_points
|
||||||
// SAFETY: We know the target has a filename because we just constructed it above
|
// SAFETY: We know the target has a filename because we just constructed it above
|
||||||
.map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
|
.map(|(_, _, target)| target.file_name().unwrap().to_string_lossy())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let (s, exists) = if existing_targets.len() == 1 {
|
let (s, exists) = if existing_entry_points.len() == 1 {
|
||||||
("", "exists")
|
("", "exists")
|
||||||
} else {
|
} else {
|
||||||
("s", "exist")
|
("s", "exist")
|
||||||
};
|
};
|
||||||
bail!(
|
bail!(
|
||||||
"Entry point{s} for tool already {exists}: {} (use `--force` to overwrite)",
|
"Entry point{s} for tool already {exists}: {} (use `--force` to overwrite)",
|
||||||
existing_targets.iter().join(", ")
|
existing_entry_points.iter().join(", ")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(zanieb): Handle the case where there are no entrypoints
|
// TODO(zanieb): Handle the case where there are no entrypoints
|
||||||
for (name, path, target) in &targets {
|
for (name, source_path, target_path) in &target_entry_points {
|
||||||
debug!("Installing `{name}`");
|
debug!("Installing `{name}`");
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
replace_symlink(path, target).context("Failed to install entrypoint")?;
|
replace_symlink(source_path, target_path).context("Failed to install entrypoint")?;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
fs_err::copy(path, target).context("Failed to install entrypoint")?;
|
fs_err::copy(source_path, target_path).context("Failed to install entrypoint")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Adding receipt for tool `{name}`",);
|
|
||||||
let installed_tools = installed_tools.init()?;
|
|
||||||
installed_tools.add_tool_receipt(&name, tool)?;
|
|
||||||
|
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
"Installed: {}",
|
"Installed: {}",
|
||||||
targets.iter().map(|(name, _, _)| name).join(", ")
|
target_entry_points
|
||||||
|
.iter()
|
||||||
|
.map(|(name, _, _)| name)
|
||||||
|
.join(", ")
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
debug!("Adding receipt for tool `{name}`",);
|
||||||
|
let installed_tools = installed_tools.init()?;
|
||||||
|
let tool = Tool::new(
|
||||||
|
requirements,
|
||||||
|
python,
|
||||||
|
target_entry_points
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)),
|
||||||
|
);
|
||||||
|
installed_tools.add_tool_receipt(&name, tool)?;
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,9 @@ mod common;
|
||||||
/// Test installing a tool with `uv tool install`
|
/// Test installing a tool with `uv tool install`
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install() {
|
fn tool_install() {
|
||||||
let context = TestContext::new("3.12").with_filtered_counts();
|
let context = TestContext::new("3.12")
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
@ -77,6 +79,10 @@ fn tool_install() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["black"]
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -154,6 +160,9 @@ fn tool_install() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["flask"]
|
requirements = ["flask"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "flask", install-path = "[TEMP_DIR]/bin/flask" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -161,7 +170,7 @@ fn tool_install() {
|
||||||
/// Test installing a tool at a version
|
/// Test installing a tool at a version
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_version() {
|
fn tool_install_version() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
@ -223,6 +232,10 @@ fn tool_install_version() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["black==24.2.0"]
|
requirements = ["black==24.2.0"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -240,7 +253,7 @@ fn tool_install_version() {
|
||||||
/// Test installing a tool with `uv tool install --from`
|
/// Test installing a tool with `uv tool install --from`
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_from() {
|
fn tool_install_from() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
@ -305,7 +318,9 @@ fn tool_install_from() {
|
||||||
/// Test installing and reinstalling an already installed tool
|
/// Test installing and reinstalling an already installed tool
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_already_installed() {
|
fn tool_install_already_installed() {
|
||||||
let context = TestContext::new("3.12").with_filtered_counts();
|
let context = TestContext::new("3.12")
|
||||||
|
.with_filtered_counts()
|
||||||
|
.with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
@ -367,6 +382,10 @@ fn tool_install_already_installed() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["black"]
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -396,6 +415,10 @@ fn tool_install_already_installed() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["black"]
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -613,6 +636,10 @@ fn tool_install_entry_point_exists() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["black"]
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -642,6 +669,10 @@ fn tool_install_entry_point_exists() {
|
||||||
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-receipt.toml")).unwrap(), @r###"
|
||||||
[tool]
|
[tool]
|
||||||
requirements = ["black"]
|
requirements = ["black"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
"###);
|
"###);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -662,7 +693,7 @@ fn tool_install_entry_point_exists() {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_home() {
|
fn tool_install_home() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
|
||||||
// Install `black`
|
// Install `black`
|
||||||
|
@ -694,7 +725,7 @@ fn tool_install_home() {
|
||||||
/// Test `uv tool install` when the bin directory is inferred from `$XDG_DATA_HOME`
|
/// Test `uv tool install` when the bin directory is inferred from `$XDG_DATA_HOME`
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_xdg_data_home() {
|
fn tool_install_xdg_data_home() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let data_home = context.temp_dir.child("data/home");
|
let data_home = context.temp_dir.child("data/home");
|
||||||
|
|
||||||
|
@ -730,7 +761,7 @@ fn tool_install_xdg_data_home() {
|
||||||
/// Test `uv tool install` when the bin directory is set by `$XDG_BIN_HOME`
|
/// Test `uv tool install` when the bin directory is set by `$XDG_BIN_HOME`
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_install_xdg_bin_home() {
|
fn tool_install_xdg_bin_home() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
let tool_dir = context.temp_dir.child("tools");
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
let bin_dir = context.temp_dir.child("bin");
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue