mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Support legacy tool receipts with PEP 508 requirements (#5679)
## Summary In #5494, I made breaking changes to the tool receipt format. This would break existing tools for all users. This PR makes the change backwards-compatible by supporting deserialization for the deprecated format. Closes https://github.com/astral-sh/uv/issues/5680. ## Test Plan Beyond the automated tests, you can run `cargo run tool list` on your existing machine. Before: ``` warning: `uv tool list` is experimental and may change without warning warning: Ignoring malformed tool `black` (run `uv tool uninstall black` to remove) warning: Ignoring malformed tool `poetry` (run `uv tool uninstall poetry` to remove) warning: Ignoring malformed tool `ruff` (run `uv tool uninstall ruff` to remove) ``` After: ``` warning: `uv tool list` is experimental and may change without warning black v0.1.0 - black poetry v1.8.3 - poetry ruff v0.0.60 - ruff ```
This commit is contained in:
parent
b9b41d4a38
commit
ff2e1fcec0
2 changed files with 140 additions and 4 deletions
|
@ -6,13 +6,13 @@ use toml_edit::Array;
|
|||
use toml_edit::Table;
|
||||
use toml_edit::Value;
|
||||
|
||||
use pypi_types::Requirement;
|
||||
use pypi_types::{Requirement, VerbatimParsedUrl};
|
||||
use uv_fs::PortablePath;
|
||||
|
||||
/// A tool entry.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[serde(try_from = "ToolWire", into = "ToolWire")]
|
||||
pub struct Tool {
|
||||
/// The requirements requested by the user during installation.
|
||||
requirements: Vec<Requirement>,
|
||||
|
@ -22,6 +22,56 @@ pub struct Tool {
|
|||
entrypoints: Vec<ToolEntrypoint>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ToolWire {
|
||||
pub requirements: Vec<RequirementWire>,
|
||||
pub python: Option<String>,
|
||||
pub entrypoints: Vec<ToolEntrypoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum RequirementWire {
|
||||
/// A [`Requirement`] following our uv-specific schema.
|
||||
Requirement(Requirement),
|
||||
/// A PEP 508-compatible requirement. We no longer write these, but there might be receipts out
|
||||
/// there that still use them.
|
||||
Deprecated(pep508_rs::Requirement<VerbatimParsedUrl>),
|
||||
}
|
||||
|
||||
impl From<Tool> for ToolWire {
|
||||
fn from(tool: Tool) -> Self {
|
||||
Self {
|
||||
requirements: tool
|
||||
.requirements
|
||||
.into_iter()
|
||||
.map(RequirementWire::Requirement)
|
||||
.collect(),
|
||||
python: tool.python,
|
||||
entrypoints: tool.entrypoints,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ToolWire> for Tool {
|
||||
type Error = serde::de::value::Error;
|
||||
|
||||
fn try_from(tool: ToolWire) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
requirements: tool
|
||||
.requirements
|
||||
.into_iter()
|
||||
.map(|req| match req {
|
||||
RequirementWire::Requirement(requirements) => requirements,
|
||||
RequirementWire::Deprecated(requirement) => Requirement::from(requirement),
|
||||
})
|
||||
.collect(),
|
||||
python: tool.python,
|
||||
entrypoints: tool.entrypoints,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct ToolEntrypoint {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
use anyhow::Result;
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::fixture::PathChild;
|
||||
use fs_err as fs;
|
||||
|
||||
use common::{uv_snapshot, TestContext};
|
||||
use fs_err as fs;
|
||||
use insta::assert_snapshot;
|
||||
|
||||
mod common;
|
||||
|
||||
|
@ -170,3 +170,89 @@ fn tool_list_bad_environment() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_list_deprecated() -> Result<()> {
|
||||
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||
let tool_dir = context.temp_dir.child("tools");
|
||||
let bin_dir = context.temp_dir.child("bin");
|
||||
|
||||
// Install `black`
|
||||
context
|
||||
.tool_install()
|
||||
.arg("black==24.2.0")
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str())
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Ensure that we have a modern tool receipt.
|
||||
insta::with_settings!({
|
||||
filters => context.filters(),
|
||||
}, {
|
||||
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 = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||
]
|
||||
"###);
|
||||
});
|
||||
|
||||
// Replace with a legacy receipt.
|
||||
fs::write(
|
||||
tool_dir.join("black").join("uv-receipt.toml"),
|
||||
r#"
|
||||
[tool]
|
||||
requirements = ["black==24.2.0"]
|
||||
entrypoints = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Ensure that we can still list the tool.
|
||||
uv_snapshot!(context.filters(), context.tool_list()
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
black v24.2.0
|
||||
- black
|
||||
- blackd
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool list` is experimental and may change without warning
|
||||
"###);
|
||||
|
||||
// Replace with an invalid receipt.
|
||||
fs::write(
|
||||
tool_dir.join("black").join("uv-receipt.toml"),
|
||||
r#"
|
||||
[tool]
|
||||
requirements = ["black<>24.2.0"]
|
||||
entrypoints = [
|
||||
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Ensure that listing fails.
|
||||
uv_snapshot!(context.filters(), context.tool_list()
|
||||
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: `uv tool list` is experimental and may change without warning
|
||||
warning: Ignoring malformed tool `black` (run `uv tool uninstall black` to remove)
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue