diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 93897abc1..53b2d6070 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -31,7 +31,7 @@ use pypi_types::{ redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, RequirementSource, ResolverMarkerEnvironment, }; -use uv_configuration::ExtrasSpecification; +use uv_configuration::{BuildOptions, ExtrasSpecification}; use uv_distribution::DistributionDatabase; use uv_fs::{relative_to, PortablePath, PortablePathBuf}; use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference}; @@ -561,6 +561,7 @@ impl Lock { tags: &Tags, extras: &ExtrasSpecification, dev: &[GroupName], + build_options: &BuildOptions, ) -> Result { let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new(); let mut seen = FxHashSet::default(); @@ -649,7 +650,11 @@ impl Lock { } map.insert( dist.id.name.clone(), - ResolvedDist::Installable(dist.to_dist(project.workspace().install_path(), tags)?), + ResolvedDist::Installable(dist.to_dist( + project.workspace().install_path(), + tags, + build_options, + )?), ); hashes.insert(dist.id.name.clone(), dist.hashes()); } @@ -876,6 +881,7 @@ impl Lock { constraints: &[Requirement], overrides: &[Requirement], indexes: Option<&IndexLocations>, + build_options: &BuildOptions, tags: &Tags, database: &DistributionDatabase<'_, Context>, ) -> Result, LockError> { @@ -1066,7 +1072,7 @@ impl Lock { } // Get the metadata for the distribution. - let dist = package.to_dist(workspace.install_path(), tags)?; + let dist = package.to_dist(workspace.install_path(), tags, build_options)?; let Ok(archive) = database .get_or_build_wheel_metadata(&dist, HashPolicy::None) @@ -1565,78 +1571,106 @@ impl Package { } /// Convert the [`Package`] to a [`Dist`] that can be used in installation. - fn to_dist(&self, workspace_root: &Path, tags: &Tags) -> Result { - if let Some(best_wheel_index) = self.find_best_wheel(tags) { - return match &self.id.source { - Source::Registry(source) => { - let wheels = self - .wheels - .iter() - .map(|wheel| wheel.to_registry_dist(source, workspace_root)) - .collect::>()?; - let reg_built_dist = RegistryBuiltDist { - wheels, - best_wheel_index, - sdist: None, - }; - Ok(Dist::Built(BuiltDist::Registry(reg_built_dist))) - } - Source::Path(path) => { - let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone(); - let path_dist = PathBuiltDist { - filename, - url: verbatim_url(workspace_root.join(path), &self.id)?, - install_path: workspace_root.join(path), - }; - let built_dist = BuiltDist::Path(path_dist); - Ok(Dist::Built(built_dist)) - } - Source::Direct(url, direct) => { - let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone(); - let url = Url::from(ParsedArchiveUrl { - url: url.to_url(), - subdirectory: direct.subdirectory.as_ref().map(PathBuf::from), - ext: DistExtension::Wheel, - }); - let direct_dist = DirectUrlBuiltDist { - filename, - location: url.clone(), - url: VerbatimUrl::from_url(url), - }; - let built_dist = BuiltDist::DirectUrl(direct_dist); - Ok(Dist::Built(built_dist)) - } - Source::Git(_, _) => Err(LockErrorKind::InvalidWheelSource { - id: self.id.clone(), - source_type: "Git", - } - .into()), - Source::Directory(_) => Err(LockErrorKind::InvalidWheelSource { - id: self.id.clone(), - source_type: "directory", - } - .into()), - Source::Editable(_) => Err(LockErrorKind::InvalidWheelSource { - id: self.id.clone(), - source_type: "editable", - } - .into()), - Source::Virtual(_) => Err(LockErrorKind::InvalidWheelSource { - id: self.id.clone(), - source_type: "virtual", - } - .into()), + fn to_dist( + &self, + workspace_root: &Path, + tags: &Tags, + build_options: &BuildOptions, + ) -> Result { + let no_binary = build_options.no_binary_package(&self.id.name); + let no_build = build_options.no_build_package(&self.id.name); + + if !no_binary { + if let Some(best_wheel_index) = self.find_best_wheel(tags) { + return match &self.id.source { + Source::Registry(source) => { + let wheels = self + .wheels + .iter() + .map(|wheel| wheel.to_registry_dist(source, workspace_root)) + .collect::>()?; + let reg_built_dist = RegistryBuiltDist { + wheels, + best_wheel_index, + sdist: None, + }; + Ok(Dist::Built(BuiltDist::Registry(reg_built_dist))) + } + Source::Path(path) => { + let filename: WheelFilename = + self.wheels[best_wheel_index].filename.clone(); + let path_dist = PathBuiltDist { + filename, + url: verbatim_url(workspace_root.join(path), &self.id)?, + install_path: workspace_root.join(path), + }; + let built_dist = BuiltDist::Path(path_dist); + Ok(Dist::Built(built_dist)) + } + Source::Direct(url, direct) => { + let filename: WheelFilename = + self.wheels[best_wheel_index].filename.clone(); + let url = Url::from(ParsedArchiveUrl { + url: url.to_url(), + subdirectory: direct.subdirectory.as_ref().map(PathBuf::from), + ext: DistExtension::Wheel, + }); + let direct_dist = DirectUrlBuiltDist { + filename, + location: url.clone(), + url: VerbatimUrl::from_url(url), + }; + let built_dist = BuiltDist::DirectUrl(direct_dist); + Ok(Dist::Built(built_dist)) + } + Source::Git(_, _) => Err(LockErrorKind::InvalidWheelSource { + id: self.id.clone(), + source_type: "Git", + } + .into()), + Source::Directory(_) => Err(LockErrorKind::InvalidWheelSource { + id: self.id.clone(), + source_type: "directory", + } + .into()), + Source::Editable(_) => Err(LockErrorKind::InvalidWheelSource { + id: self.id.clone(), + source_type: "editable", + } + .into()), + Source::Virtual(_) => Err(LockErrorKind::InvalidWheelSource { + id: self.id.clone(), + source_type: "virtual", + } + .into()), + }; }; - }; - - if let Some(sdist) = self.to_source_dist(workspace_root)? { - return Ok(Dist::Source(sdist)); } - Err(LockErrorKind::NeitherSourceDistNorWheel { - id: self.id.clone(), + if !no_build { + if let Some(sdist) = self.to_source_dist(workspace_root)? { + return Ok(Dist::Source(sdist)); + } + } + + match (no_binary, no_build) { + (true, true) => Err(LockErrorKind::NoBinaryNoBuild { + id: self.id.clone(), + } + .into()), + (true, false) => Err(LockErrorKind::NoBinary { + id: self.id.clone(), + } + .into()), + (false, true) => Err(LockErrorKind::NoBuild { + id: self.id.clone(), + } + .into()), + (false, false) => Err(LockErrorKind::NeitherSourceDistNorWheel { + id: self.id.clone(), + } + .into()), } - .into()) } /// Convert the source of this [`Package`] to a [`SourceDist`] that can be used in installation. @@ -3758,6 +3792,26 @@ enum LockErrorKind { /// The ID of the distribution that has a missing base. id: PackageId, }, + /// An error that occurs when a distribution is marked as both `--no-binary` and `--no-build`. + #[error("distribution {id} can't be installed because it is marked as both `--no-binary` and `--no-build`")] + NoBinaryNoBuild { + /// The ID of the distribution. + id: PackageId, + }, + /// An error that occurs when a distribution is marked as both `--no-binary`, but no source + /// distribution is available. + #[error("distribution {id} can't be installed because it is marked as `--no-binary` but has no source distribution")] + NoBinary { + /// The ID of the distribution. + id: PackageId, + }, + /// An error that occurs when a distribution is marked as both `--no-build`, but no binary + /// distribution is available. + #[error("distribution {id} can't be installed because it is marked as `--no-build` but has no binary distribution")] + NoBuild { + /// The ID of the distribution. + id: PackageId, + }, /// An error that occurs when converting between URLs and paths. #[error("found dependency `{id}` with no locked distribution")] VerbatimUrl { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index d5003a855..f9af474c4 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -16,7 +16,9 @@ use pypi_types::{Requirement, SupportedEnvironments}; use uv_auth::store_credentials_from_url; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; -use uv_configuration::{Concurrency, Constraints, ExtrasSpecification, Reinstall, Upgrade}; +use uv_configuration::{ + BuildOptions, Concurrency, Constraints, ExtrasSpecification, Reinstall, Upgrade, +}; use uv_dispatch::BuildDispatch; use uv_distribution::DistributionDatabase; use uv_fs::CWD; @@ -436,6 +438,7 @@ async fn do_lock( interpreter, &requires_python, index_locations, + build_options, upgrade, &options, &database, @@ -590,6 +593,7 @@ impl ValidatedLock { interpreter: &Interpreter, requires_python: &RequiresPython, index_locations: &IndexLocations, + build_options: &BuildOptions, upgrade: &Upgrade, options: &Options, database: &DistributionDatabase<'_, Context>, @@ -706,6 +710,7 @@ impl ValidatedLock { constraints, overrides, indexes, + build_options, interpreter.tags()?, database, ) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index a46797882..bedd90733 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -219,7 +219,7 @@ pub(super) async fn do_sync( let tags = venv.interpreter().tags()?; // Read the lockfile. - let resolution = lock.to_resolution(target, &markers, tags, extras, &dev)?; + let resolution = lock.to_resolution(target, &markers, tags, extras, &dev, build_options)?; // Always skip virtual projects, which shouldn't be built or installed. let resolution = apply_no_virtual_project(resolution); diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index 2c1c0491e..8b558b672 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -1930,3 +1930,113 @@ fn sync_environment_prompt() -> Result<()> { Ok(()) } + +#[test] +fn no_binary() -> 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" + dependencies = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.sync().arg("--no-binary-package").arg("iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +} + +#[test] +fn no_binary_error() -> 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" + dependencies = ["django_allauth==0.51.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("django-allauth"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 19 packages in [TIME] + error: distribution django-allauth==0.51.0 @ registry+https://pypi.org/simple can't be installed because it is marked as `--no-build` but has no binary distribution + "###); + + assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +} + +#[test] +fn no_build() -> 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" + dependencies = ["iniconfig"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + uv_snapshot!(context.filters(), context.sync().arg("--no-build-package").arg("iniconfig"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 2 packages in [TIME] + Installed 2 packages in [TIME] + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + assert!(context.temp_dir.child("uv.lock").exists()); + + Ok(()) +}