mirror of https://github.com/astral-sh/uv
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
|
|
@ -6,13 +6,13 @@ use toml_edit::Array;
|
||||||
use toml_edit::Table;
|
use toml_edit::Table;
|
||||||
use toml_edit::Value;
|
use toml_edit::Value;
|
||||||
|
|
||||||
use pypi_types::Requirement;
|
use pypi_types::{Requirement, VerbatimParsedUrl};
|
||||||
use uv_fs::PortablePath;
|
use uv_fs::PortablePath;
|
||||||
|
|
||||||
/// A tool entry.
|
/// A tool entry.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(try_from = "ToolWire", into = "ToolWire")]
|
||||||
pub struct Tool {
|
pub struct Tool {
|
||||||
/// The requirements requested by the user during installation.
|
/// The requirements requested by the user during installation.
|
||||||
requirements: Vec<Requirement>,
|
requirements: Vec<Requirement>,
|
||||||
|
|
@ -22,6 +22,56 @@ pub struct Tool {
|
||||||
entrypoints: Vec<ToolEntrypoint>,
|
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)]
|
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub struct ToolEntrypoint {
|
pub struct ToolEntrypoint {
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assert_cmd::assert::OutputAssertExt;
|
use assert_cmd::assert::OutputAssertExt;
|
||||||
use assert_fs::fixture::PathChild;
|
use assert_fs::fixture::PathChild;
|
||||||
use fs_err as fs;
|
|
||||||
|
|
||||||
use common::{uv_snapshot, TestContext};
|
use common::{uv_snapshot, TestContext};
|
||||||
|
use fs_err as fs;
|
||||||
|
use insta::assert_snapshot;
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
|
@ -170,3 +170,89 @@ fn tool_list_bad_environment() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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…
Reference in New Issue