Support --no-build and --no-binary in uv sync et al (#7100)

## Summary

This option already existed, but `--no-binary` always errored.

Closes https://github.com/astral-sh/uv/issues/7099.
This commit is contained in:
Charlie Marsh 2024-09-05 17:45:42 -04:00 committed by GitHub
parent f2309bfd1e
commit 29f53c3c63
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 242 additions and 73 deletions

View file

@ -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<Resolution, LockError> {
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<SatisfiesResult<'_>, 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,7 +1571,16 @@ impl Package {
}
/// Convert the [`Package`] to a [`Dist`] that can be used in installation.
fn to_dist(&self, workspace_root: &Path, tags: &Tags) -> Result<Dist, LockError> {
fn to_dist(
&self,
workspace_root: &Path,
tags: &Tags,
build_options: &BuildOptions,
) -> Result<Dist, LockError> {
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) => {
@ -1582,7 +1597,8 @@ impl Package {
Ok(Dist::Built(BuiltDist::Registry(reg_built_dist)))
}
Source::Path(path) => {
let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone();
let filename: WheelFilename =
self.wheels[best_wheel_index].filename.clone();
let path_dist = PathBuiltDist {
filename,
url: verbatim_url(workspace_root.join(path), &self.id)?,
@ -1592,7 +1608,8 @@ impl Package {
Ok(Dist::Built(built_dist))
}
Source::Direct(url, direct) => {
let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone();
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),
@ -1628,15 +1645,32 @@ impl Package {
.into()),
};
};
}
if !no_build {
if let Some(sdist) = self.to_source_dist(workspace_root)? {
return Ok(Dist::Source(sdist));
}
}
Err(LockErrorKind::NeitherSourceDistNorWheel {
match (no_binary, no_build) {
(true, true) => Err(LockErrorKind::NoBinaryNoBuild {
id: self.id.clone(),
}
.into())
.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()),
}
}
/// 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 {

View file

@ -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,
)

View file

@ -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);

View file

@ -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(())
}