From 17b4ebed8e359e63c640532576b09ed24c0d9441 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 27 Apr 2025 11:58:20 -0400 Subject: [PATCH] Avoid erroring on omitted wheel-only packages in `pylock.toml` (#13132) ## Summary Closes https://github.com/astral-sh/uv/issues/13127. --- .../src/lock/export/pylock_toml.rs | 31 +++++--- crates/uv/tests/it/pip_sync.rs | 75 +++++++++++++++++++ 2 files changed, 97 insertions(+), 9 deletions(-) diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index ef186dac8..4837dee8c 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -62,6 +62,8 @@ pub enum PylockTomlError { "Package `{0}` must include one of: `wheels`, `directory`, `archive`, `sdist`, or `vcs`" )] MissingSource(PackageName), + #[error("Package `{0}` does not include a compatible wheel for the current platform")] + MissingWheel(PackageName), #[error("`packages.wheel` entry for `{0}` must have a `path` or `url`")] WheelMissingPathUrl(PackageName), #[error("`packages.sdist` entry for `{0}` must have a `path` or `url`")] @@ -863,6 +865,11 @@ impl<'lock> PylockToml { let root = graph.add_node(Node::Root); for package in self.packages { + // Omit packages that aren't relevant to the current environment. + if !package.marker.evaluate(markers, &[]) { + continue; + } + match ( package.wheels.is_some(), package.sdist.is_some(), @@ -901,12 +908,12 @@ impl<'lock> PylockToml { (_, _, _, true, true) => { return Err(PylockTomlError::VcsWithArchive(package.name.clone())); } + (false, false, false, false, false) => { + return Err(PylockTomlError::MissingSource(package.name.clone())); + } _ => {} } - // Omit packages that aren't relevant to the current environment. - let install = package.marker.evaluate(markers, &[]); - // Search for a matching wheel. let dist = if let Some(best_wheel) = package.find_best_wheel(tags) { let hashes = HashDigests::from(best_wheel.hashes.clone()); @@ -926,7 +933,7 @@ impl<'lock> PylockToml { Node::Dist { dist, hashes, - install, + install: true, } } else if let Some(sdist) = package.sdist.as_ref() { let hashes = HashDigests::from(sdist.hashes.clone()); @@ -943,7 +950,7 @@ impl<'lock> PylockToml { Node::Dist { dist, hashes, - install, + install: true, } } else if let Some(sdist) = package.directory.as_ref() { let hashes = HashDigests::empty(); @@ -957,7 +964,7 @@ impl<'lock> PylockToml { Node::Dist { dist, hashes, - install, + install: true, } } else if let Some(sdist) = package.vcs.as_ref() { let hashes = HashDigests::empty(); @@ -971,7 +978,7 @@ impl<'lock> PylockToml { Node::Dist { dist, hashes, - install, + install: true, } } else if let Some(dist) = package.archive.as_ref() { let hashes = HashDigests::from(dist.hashes.clone()); @@ -983,10 +990,16 @@ impl<'lock> PylockToml { Node::Dist { dist, hashes, - install, + install: true, } } else { - return Err(PylockTomlError::MissingSource(package.name.clone())); + // This is only reachable if the package contains a `wheels` entry (and nothing + // else), but there are no wheels available for the current environment. (If the + // package doesn't contain _any_ of `wheels`, `sdist`, etc., then we error in the + // match above.) + // + // TODO(charlie): Include a hint, like in `uv.lock`. + return Err(PylockTomlError::MissingWheel(package.name.clone())); }; let index = graph.add_node(dist); diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 905ad4ce8..2d7893d46 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -5822,3 +5822,78 @@ fn pep_751() -> Result<()> { Ok(()) } + +/// Avoid erroring for packages that only include wheels, and _don't_ include a wheel for the +/// current platform, but are omitted by markers anyway. +/// +/// See: +#[test] +fn pep_751_wheel_only() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12.0" + dependencies = ["torch"] + "#, + )?; + + context + .export() + .arg("-o") + .arg("pylock.toml") + .assert() + .success(); + + // If there's no compatible wheel for a package we _don't_ need to install (e.g., anything + // CUDA-related), succeed. + uv_snapshot!(context.filters(), context.pip_sync() + .arg("--preview") + .arg("pylock.toml") + .arg("--dry-run") + .arg("--python-platform") + .arg("macos"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Would download 9 packages + Would install 9 packages + + filelock==3.13.1 + + fsspec==2024.3.1 + + jinja2==3.1.3 + + markupsafe==2.1.5 + + mpmath==1.3.0 + + networkx==3.2.1 + + sympy==1.12 + + torch==2.2.1 + + typing-extensions==4.10.0 + " + ); + + // However, if there's no compatible wheel for a package that we _do_ need to install, we should + // error + uv_snapshot!(context.filters(), context.pip_sync() + .arg("--preview") + .arg("pylock.toml") + .arg("--dry-run") + .arg("--python-platform") + .arg("macos") + .arg("--python-version") + .arg("3.8"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Package `torch` does not include a compatible wheel for the current platform + " + ); + + Ok(()) +}