Skip to content

Commit f80ddf1

Browse files
Avoid trailing slash when deserializing from lockfile (#9848)
## Summary Very tricky problem whereby `workspace_root.join(path)` returns the workspace root with a trailing slash if `path` is empty... This caused us to accidentally _include_ excluded members during workspace discovery, since (e.g.) `packages/seeds` doesn't match `packages/seeds/`. Closes #9832 (comment).
1 parent a13e3f5 commit f80ddf1

File tree

2 files changed

+135
-12
lines changed

2 files changed

+135
-12
lines changed

crates/uv-resolver/src/lock/mod.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,10 +1682,11 @@ impl Package {
16821682
Source::Path(path) => {
16831683
let filename: WheelFilename =
16841684
self.wheels[best_wheel_index].filename.clone();
1685+
let install_path = absolute_path(workspace_root, path)?;
16851686
let path_dist = PathBuiltDist {
16861687
filename,
1687-
url: verbatim_url(workspace_root.join(path), &self.id)?,
1688-
install_path: workspace_root.join(path),
1688+
url: verbatim_url(&install_path, &self.id)?,
1689+
install_path: absolute_path(workspace_root, path)?,
16891690
};
16901691
let built_dist = BuiltDist::Path(path_dist);
16911692
Ok(Dist::Built(built_dist))
@@ -1780,40 +1781,44 @@ impl Package {
17801781
let DistExtension::Source(ext) = DistExtension::from_path(path)? else {
17811782
return Ok(None);
17821783
};
1784+
let install_path = absolute_path(workspace_root, path)?;
17831785
let path_dist = PathSourceDist {
17841786
name: self.id.name.clone(),
17851787
version: Some(self.id.version.clone()),
1786-
url: verbatim_url(workspace_root.join(path), &self.id)?,
1787-
install_path: workspace_root.join(path),
1788+
url: verbatim_url(&install_path, &self.id)?,
1789+
install_path,
17881790
ext,
17891791
};
17901792
uv_distribution_types::SourceDist::Path(path_dist)
17911793
}
17921794
Source::Directory(path) => {
1795+
let install_path = absolute_path(workspace_root, path)?;
17931796
let dir_dist = DirectorySourceDist {
17941797
name: self.id.name.clone(),
1795-
url: verbatim_url(workspace_root.join(path), &self.id)?,
1796-
install_path: workspace_root.join(path),
1798+
url: verbatim_url(&install_path, &self.id)?,
1799+
install_path,
17971800
editable: false,
17981801
r#virtual: false,
17991802
};
18001803
uv_distribution_types::SourceDist::Directory(dir_dist)
18011804
}
18021805
Source::Editable(path) => {
1806+
let install_path = absolute_path(workspace_root, path)?;
18031807
let dir_dist = DirectorySourceDist {
18041808
name: self.id.name.clone(),
1805-
url: verbatim_url(workspace_root.join(path), &self.id)?,
1806-
install_path: workspace_root.join(path),
1809+
url: verbatim_url(&install_path, &self.id)?,
1810+
install_path,
18071811
editable: true,
18081812
r#virtual: false,
18091813
};
18101814
uv_distribution_types::SourceDist::Directory(dir_dist)
18111815
}
18121816
Source::Virtual(path) => {
1817+
let install_path = absolute_path(workspace_root, path)?;
18131818
let dir_dist = DirectorySourceDist {
18141819
name: self.id.name.clone(),
1815-
url: verbatim_url(workspace_root.join(path), &self.id)?,
1816-
install_path: workspace_root.join(path),
1820+
url: verbatim_url(&install_path, &self.id)?,
1821+
install_path,
18171822
editable: false,
18181823
r#virtual: true,
18191824
};
@@ -2181,15 +2186,21 @@ impl Package {
21812186
}
21822187

21832188
/// Attempts to construct a `VerbatimUrl` from the given `Path`.
2184-
fn verbatim_url(path: PathBuf, id: &PackageId) -> Result<VerbatimUrl, LockError> {
2189+
fn verbatim_url(path: &Path, id: &PackageId) -> Result<VerbatimUrl, LockError> {
21852190
let url = VerbatimUrl::from_absolute_path(path).map_err(|err| LockErrorKind::VerbatimUrl {
21862191
id: id.clone(),
21872192
err,
21882193
})?;
2189-
21902194
Ok(url)
21912195
}
21922196

2197+
/// Attempts to construct an absolute path from the given `Path`.
2198+
fn absolute_path(workspace_root: &Path, path: &Path) -> Result<PathBuf, LockError> {
2199+
let path = uv_fs::normalize_absolute_path(&workspace_root.join(path))
2200+
.map_err(LockErrorKind::AbsolutePath)?;
2201+
Ok(path)
2202+
}
2203+
21932204
#[derive(Clone, Debug, serde::Deserialize)]
21942205
#[serde(rename_all = "kebab-case")]
21952206
struct PackageWire {
@@ -4059,6 +4070,13 @@ enum LockErrorKind {
40594070
#[source]
40604071
std::io::Error,
40614072
),
4073+
/// An error that occurs when converting a lockfile path from relative to absolute.
4074+
#[error("Could not compute absolute path from workspace root and lockfile path")]
4075+
AbsolutePath(
4076+
/// The inner error we forward.
4077+
#[source]
4078+
std::io::Error,
4079+
),
40624080
/// An error that occurs when an ambiguous `package.dependency` is
40634081
/// missing a `version` field.
40644082
#[error(

crates/uv/tests/it/lock.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6242,6 +6242,111 @@ fn lock_exclusion() -> Result<()> {
62426242
Ok(())
62436243
}
62446244

6245+
/// See: <https://github.com/astral-sh/uv/issues/9832#issuecomment-2539121761>
6246+
#[test]
6247+
fn lock_relative_lock_deserialization() -> Result<()> {
6248+
let context = TestContext::new("3.12");
6249+
6250+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
6251+
pyproject_toml.write_str(
6252+
r#"
6253+
[project]
6254+
name = "project"
6255+
requires-python = ">=3.12"
6256+
dependencies = ["member"]
6257+
dynamic = ["version"]
6258+
6259+
[tool.uv.sources]
6260+
member = { workspace = true }
6261+
6262+
[tool.uv.workspace]
6263+
members = ["packages/*"]
6264+
exclude = ["packages/child"]
6265+
6266+
[build-system]
6267+
requires = ["setuptools>=42"]
6268+
build-backend = "setuptools.build_meta"
6269+
"#,
6270+
)?;
6271+
6272+
let packages = context.temp_dir.child("packages");
6273+
6274+
let member = packages.child("member");
6275+
member.child("pyproject.toml").write_str(
6276+
r#"
6277+
[project]
6278+
name = "member"
6279+
requires-python = ">=3.12"
6280+
dependencies = []
6281+
dynamic = ["version"]
6282+
6283+
[build-system]
6284+
requires = ["setuptools>=42"]
6285+
build-backend = "setuptools.build_meta"
6286+
"#,
6287+
)?;
6288+
6289+
let child = packages.child("child");
6290+
child.child("pyproject.toml").write_str(
6291+
r#"
6292+
[project]
6293+
name = "child"
6294+
version = "0.1.0"
6295+
requires-python = ">=3.12"
6296+
dependencies = ["member"]
6297+
6298+
[build-system]
6299+
requires = ["setuptools>=42"]
6300+
build-backend = "setuptools.build_meta"
6301+
6302+
[tool.uv.sources]
6303+
member = { workspace = true }
6304+
"#,
6305+
)?;
6306+
6307+
// Add an arbitrary lockfile, to ensure that we attempt to validate it, which is necessary to
6308+
// trigger the bug.
6309+
child.child("uv.lock").write_str(
6310+
r#"
6311+
version = 1
6312+
requires-python = ">=3.12"
6313+
6314+
[options]
6315+
exclude-newer = "2024-03-25T00:00:00Z"
6316+
6317+
[[package]]
6318+
name = "child"
6319+
version = "0.1.0"
6320+
source = { editable = "." }
6321+
dependencies = [
6322+
{ name = "project" },
6323+
]
6324+
6325+
[package.metadata]
6326+
requires-dist = [{ name = "project", directory = "../" }]
6327+
6328+
[[package]]
6329+
name = "project"
6330+
version = "0.1.0"
6331+
source = { directory = "../" }
6332+
"#,
6333+
)?;
6334+
6335+
uv_snapshot!(context.filters(), context.lock().current_dir(&child), @r###"
6336+
success: false
6337+
exit_code: 2
6338+
----- stdout -----
6339+
6340+
----- stderr -----
6341+
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
6342+
error: Failed to generate package metadata for `child==0.1.0 @ editable+.`
6343+
Caused by: Failed to parse entry: `member`
6344+
Caused by: `member` references a workspace in `tool.uv.sources` (e.g., `member = { workspace = true }`), but is not a workspace member
6345+
"###);
6346+
6347+
Ok(())
6348+
}
6349+
62456350
/// Lock a workspace member with a non-workspace source.
62466351
#[test]
62476352
fn lock_non_workspace_source() -> Result<()> {

0 commit comments

Comments
 (0)