Skip to content

Commit f75418e

Browse files
committed
Respect build options in pylock.toml
1 parent 17b4ebe commit f75418e

File tree

4 files changed

+210
-15
lines changed

4 files changed

+210
-15
lines changed

crates/uv-resolver/src/lock/export/pylock_toml.rs

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ use toml_edit::{value, Array, ArrayOfTables, Item, Table};
1212
use url::Url;
1313

1414
use uv_cache_key::RepositoryUrl;
15-
use uv_configuration::{DependencyGroupsWithDefaults, ExtrasSpecification, InstallOptions};
15+
use uv_configuration::{
16+
BuildOptions, DependencyGroupsWithDefaults, ExtrasSpecification, InstallOptions,
17+
};
1618
use uv_distribution_filename::{
1719
BuildTag, DistExtension, ExtensionError, SourceDistExtension, SourceDistFilename,
1820
SourceDistFilenameError, WheelFilename, WheelFilenameError,
@@ -80,6 +82,18 @@ pub enum PylockTomlError {
8082
PathToUrl,
8183
#[error("Failed to convert URL to path")]
8284
UrlToPath,
85+
#[error("Package `{0}` can't be installed because it doesn't have a source distribution or wheel for the current platform")]
86+
NeitherSourceDistNorWheel(PackageName),
87+
#[error("Package `{0}` can't be installed because it is marked as both `--no-binary` and `--no-build`")]
88+
NoBinaryNoBuild(PackageName),
89+
#[error("Package `{0}` can't be installed because it is marked as `--no-binary` but has no source distribution")]
90+
NoBinary(PackageName),
91+
#[error("Package `{0}` can't be installed because it is marked as `--no-build` but has no binary distribution")]
92+
NoBuild(PackageName),
93+
#[error("Package `{0}` can't be installed because the binary distribution is incompatible with the current platform")]
94+
IncompatibleWheelOnly(PackageName),
95+
#[error("Package `{0}` can't be installed because it is marked as `--no-binary` but is itself a binary distribution")]
96+
NoBinaryWheelOnly(PackageName),
8397
#[error(transparent)]
8498
WheelFilename(#[from] WheelFilenameError),
8599
#[error(transparent)]
@@ -857,6 +871,7 @@ impl<'lock> PylockToml {
857871
install_path: &Path,
858872
markers: &MarkerEnvironment,
859873
tags: &Tags,
874+
build_options: &BuildOptions,
860875
) -> Result<Resolution, PylockTomlError> {
861876
let mut graph =
862877
petgraph::graph::DiGraph::with_capacity(self.packages.len(), self.packages.len());
@@ -914,8 +929,19 @@ impl<'lock> PylockToml {
914929
_ => {}
915930
}
916931

932+
let no_binary = build_options.no_binary_package(&package.name);
933+
let no_build = build_options.no_build_package(&package.name);
934+
let is_wheel = package
935+
.archive
936+
.as_ref()
937+
.map(|archive| archive.is_wheel(&package.name))
938+
.transpose()?
939+
.unwrap_or_default();
940+
917941
// Search for a matching wheel.
918-
let dist = if let Some(best_wheel) = package.find_best_wheel(tags) {
942+
let dist = if let Some(best_wheel) =
943+
package.find_best_wheel(tags).filter(|_| !no_binary)
944+
{
919945
let hashes = HashDigests::from(best_wheel.hashes.clone());
920946
let built_dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist {
921947
wheels: vec![best_wheel.to_registry_wheel(
@@ -935,7 +961,7 @@ impl<'lock> PylockToml {
935961
hashes,
936962
install: true,
937963
}
938-
} else if let Some(sdist) = package.sdist.as_ref() {
964+
} else if let Some(sdist) = package.sdist.as_ref().filter(|_| !no_build) {
939965
let hashes = HashDigests::from(sdist.hashes.clone());
940966
let sdist = Dist::Source(SourceDist::Registry(sdist.to_sdist(
941967
install_path,
@@ -952,7 +978,7 @@ impl<'lock> PylockToml {
952978
hashes,
953979
install: true,
954980
}
955-
} else if let Some(sdist) = package.directory.as_ref() {
981+
} else if let Some(sdist) = package.directory.as_ref().filter(|_| !no_build) {
956982
let hashes = HashDigests::empty();
957983
let sdist = Dist::Source(SourceDist::Directory(
958984
sdist.to_sdist(install_path, &package.name)?,
@@ -966,7 +992,7 @@ impl<'lock> PylockToml {
966992
hashes,
967993
install: true,
968994
}
969-
} else if let Some(sdist) = package.vcs.as_ref() {
995+
} else if let Some(sdist) = package.vcs.as_ref().filter(|_| !no_build) {
970996
let hashes = HashDigests::empty();
971997
let sdist = Dist::Source(SourceDist::Git(
972998
sdist.to_sdist(install_path, &package.name)?,
@@ -980,7 +1006,12 @@ impl<'lock> PylockToml {
9801006
hashes,
9811007
install: true,
9821008
}
983-
} else if let Some(dist) = package.archive.as_ref() {
1009+
} else if let Some(dist) =
1010+
package
1011+
.archive
1012+
.as_ref()
1013+
.filter(|_| if is_wheel { !no_binary } else { !no_build })
1014+
{
9841015
let hashes = HashDigests::from(dist.hashes.clone());
9851016
let dist = dist.to_dist(install_path, &package.name, package.version.as_ref())?;
9861017
let dist = ResolvedDist::Installable {
@@ -993,13 +1024,20 @@ impl<'lock> PylockToml {
9931024
install: true,
9941025
}
9951026
} else {
996-
// This is only reachable if the package contains a `wheels` entry (and nothing
997-
// else), but there are no wheels available for the current environment. (If the
998-
// package doesn't contain _any_ of `wheels`, `sdist`, etc., then we error in the
999-
// match above.)
1000-
//
1001-
// TODO(charlie): Include a hint, like in `uv.lock`.
1002-
return Err(PylockTomlError::MissingWheel(package.name.clone()));
1027+
return match (no_binary, no_build) {
1028+
(true, true) => Err(PylockTomlError::NoBinaryNoBuild(package.name.clone())),
1029+
(true, false) if is_wheel => {
1030+
Err(PylockTomlError::NoBinaryWheelOnly(package.name.clone()))
1031+
}
1032+
(true, false) => Err(PylockTomlError::NoBinary(package.name.clone())),
1033+
(false, true) => Err(PylockTomlError::NoBuild(package.name.clone())),
1034+
(false, false) if is_wheel => {
1035+
Err(PylockTomlError::IncompatibleWheelOnly(package.name.clone()))
1036+
}
1037+
(false, false) => Err(PylockTomlError::NeitherSourceDistNorWheel(
1038+
package.name.clone(),
1039+
)),
1040+
};
10031041
};
10041042

10051043
let index = graph.add_node(dist);
@@ -1441,6 +1479,31 @@ impl PylockTomlArchive {
14411479
return Err(PylockTomlError::ArchiveMissingPathUrl(name.clone()));
14421480
}
14431481
}
1482+
1483+
/// Returns `true` if the [`PylockTomlArchive`] is a wheel.
1484+
fn is_wheel(&self, name: &PackageName) -> Result<bool, PylockTomlError> {
1485+
if let Some(url) = self.url.as_ref() {
1486+
let filename = url
1487+
.filename()
1488+
.map_err(|_| PylockTomlError::UrlMissingFilename(url.clone()))?;
1489+
1490+
let ext = DistExtension::from_path(filename.as_ref())?;
1491+
Ok(matches!(ext, DistExtension::Wheel))
1492+
} else if let Some(path) = self.path.as_ref() {
1493+
let filename = path
1494+
.as_ref()
1495+
.file_name()
1496+
.and_then(OsStr::to_str)
1497+
.ok_or_else(|| {
1498+
PylockTomlError::PathMissingFilename(Box::<Path>::from(path.clone()))
1499+
})?;
1500+
1501+
let ext = DistExtension::from_path(filename)?;
1502+
Ok(matches!(ext, DistExtension::Wheel))
1503+
} else {
1504+
return Err(PylockTomlError::ArchiveMissingPathUrl(name.clone()));
1505+
}
1506+
}
14441507
}
14451508

14461509
/// Convert a Jiff timestamp to a TOML datetime.

crates/uv/src/commands/pip/install.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,8 @@ pub(crate) async fn pip_install(
441441
let content = fs_err::tokio::read_to_string(&pylock).await?;
442442
let lock = toml::from_str::<PylockToml>(&content)?;
443443

444-
let resolution = lock.to_resolution(install_path, marker_env.markers(), &tags)?;
444+
let resolution =
445+
lock.to_resolution(install_path, marker_env.markers(), &tags, &build_options)?;
445446
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
446447

447448
(resolution, hasher)

crates/uv/src/commands/pip/sync.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,8 @@ pub(crate) async fn pip_sync(
376376
let content = fs_err::tokio::read_to_string(&pylock).await?;
377377
let lock = toml::from_str::<PylockToml>(&content)?;
378378

379-
let resolution = lock.to_resolution(install_path, marker_env.markers(), &tags)?;
379+
let resolution =
380+
lock.to_resolution(install_path, marker_env.markers(), &tags, &build_options)?;
380381
let hasher = HashStrategy::from_resolution(&resolution, HashCheckingMode::Verify)?;
381382

382383
(resolution, hasher)

crates/uv/tests/it/pip_sync.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5897,3 +5897,133 @@ fn pep_751_wheel_only() -> Result<()> {
58975897

58985898
Ok(())
58995899
}
5900+
5901+
/// Respect `--no-binary` et al when installing from a `pylock.toml`.
5902+
#[test]
5903+
fn pep_751_build_options() -> Result<()> {
5904+
let context = TestContext::new("3.12").with_exclude_newer("2025-01-29T00:00:00Z");
5905+
5906+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
5907+
pyproject_toml.write_str(
5908+
r#"
5909+
[project]
5910+
name = "project"
5911+
version = "0.1.0"
5912+
requires-python = ">=3.12"
5913+
dependencies = ["anyio"]
5914+
"#,
5915+
)?;
5916+
5917+
context
5918+
.export()
5919+
.arg("-o")
5920+
.arg("pylock.toml")
5921+
.assert()
5922+
.success();
5923+
5924+
uv_snapshot!(context.filters(), context.pip_sync()
5925+
.arg("--preview")
5926+
.arg("pylock.toml")
5927+
.arg("--no-binary")
5928+
.arg("anyio"), @r"
5929+
success: true
5930+
exit_code: 0
5931+
----- stdout -----
5932+
5933+
----- stderr -----
5934+
Prepared 4 packages in [TIME]
5935+
Installed 4 packages in [TIME]
5936+
+ anyio==4.8.0
5937+
+ idna==3.10
5938+
+ sniffio==1.3.1
5939+
+ typing-extensions==4.12.2
5940+
"
5941+
);
5942+
5943+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
5944+
pyproject_toml.write_str(
5945+
r#"
5946+
[project]
5947+
name = "project"
5948+
version = "0.1.0"
5949+
requires-python = ">=3.12"
5950+
dependencies = ["odrive"]
5951+
"#,
5952+
)?;
5953+
5954+
context
5955+
.export()
5956+
.arg("-o")
5957+
.arg("pylock.toml")
5958+
.assert()
5959+
.success();
5960+
5961+
uv_snapshot!(context.filters(), context.pip_sync()
5962+
.arg("--preview")
5963+
.arg("pylock.toml")
5964+
.arg("--no-binary")
5965+
.arg("odrive"), @r"
5966+
success: false
5967+
exit_code: 2
5968+
----- stdout -----
5969+
5970+
----- stderr -----
5971+
error: Package `odrive` can't be installed because it is marked as `--no-binary` but has no source distribution
5972+
"
5973+
);
5974+
5975+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
5976+
pyproject_toml.write_str(
5977+
r#"
5978+
[project]
5979+
name = "project"
5980+
version = "0.1.0"
5981+
requires-python = ">=3.12"
5982+
dependencies = ["source-distribution"]
5983+
"#,
5984+
)?;
5985+
5986+
context
5987+
.export()
5988+
.arg("-o")
5989+
.arg("pylock.toml")
5990+
.assert()
5991+
.success();
5992+
5993+
uv_snapshot!(context.filters(), context.pip_sync()
5994+
.arg("--preview")
5995+
.arg("pylock.toml")
5996+
.arg("--only-binary")
5997+
.arg("source-distribution"), @r"
5998+
success: false
5999+
exit_code: 2
6000+
----- stdout -----
6001+
6002+
----- stderr -----
6003+
error: Package `source-distribution` can't be installed because it is marked as `--no-build` but has no binary distribution
6004+
"
6005+
);
6006+
6007+
uv_snapshot!(context.filters(), context.pip_sync()
6008+
.arg("--preview")
6009+
.arg("pylock.toml")
6010+
.arg("--no-binary")
6011+
.arg("source-distribution"), @r"
6012+
success: true
6013+
exit_code: 0
6014+
----- stdout -----
6015+
6016+
----- stderr -----
6017+
Prepared 1 package in [TIME]
6018+
Uninstalled 4 packages in [TIME]
6019+
Installed 1 package in [TIME]
6020+
- anyio==4.8.0
6021+
- idna==3.10
6022+
- sniffio==1.3.1
6023+
+ source-distribution==0.0.3
6024+
- typing-extensions==4.12.2
6025+
"
6026+
);
6027+
6028+
Ok(())
6029+
}

0 commit comments

Comments
 (0)