mirror of https://github.com/astral-sh/uv
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:
parent
f2309bfd1e
commit
29f53c3c63
|
|
@ -31,7 +31,7 @@ use pypi_types::{
|
||||||
redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement,
|
redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement,
|
||||||
RequirementSource, ResolverMarkerEnvironment,
|
RequirementSource, ResolverMarkerEnvironment,
|
||||||
};
|
};
|
||||||
use uv_configuration::ExtrasSpecification;
|
use uv_configuration::{BuildOptions, ExtrasSpecification};
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::DistributionDatabase;
|
||||||
use uv_fs::{relative_to, PortablePath, PortablePathBuf};
|
use uv_fs::{relative_to, PortablePath, PortablePathBuf};
|
||||||
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
|
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
|
||||||
|
|
@ -561,6 +561,7 @@ impl Lock {
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
extras: &ExtrasSpecification,
|
extras: &ExtrasSpecification,
|
||||||
dev: &[GroupName],
|
dev: &[GroupName],
|
||||||
|
build_options: &BuildOptions,
|
||||||
) -> Result<Resolution, LockError> {
|
) -> Result<Resolution, LockError> {
|
||||||
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
|
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
|
||||||
let mut seen = FxHashSet::default();
|
let mut seen = FxHashSet::default();
|
||||||
|
|
@ -649,7 +650,11 @@ impl Lock {
|
||||||
}
|
}
|
||||||
map.insert(
|
map.insert(
|
||||||
dist.id.name.clone(),
|
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());
|
hashes.insert(dist.id.name.clone(), dist.hashes());
|
||||||
}
|
}
|
||||||
|
|
@ -876,6 +881,7 @@ impl Lock {
|
||||||
constraints: &[Requirement],
|
constraints: &[Requirement],
|
||||||
overrides: &[Requirement],
|
overrides: &[Requirement],
|
||||||
indexes: Option<&IndexLocations>,
|
indexes: Option<&IndexLocations>,
|
||||||
|
build_options: &BuildOptions,
|
||||||
tags: &Tags,
|
tags: &Tags,
|
||||||
database: &DistributionDatabase<'_, Context>,
|
database: &DistributionDatabase<'_, Context>,
|
||||||
) -> Result<SatisfiesResult<'_>, LockError> {
|
) -> Result<SatisfiesResult<'_>, LockError> {
|
||||||
|
|
@ -1066,7 +1072,7 @@ impl Lock {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the metadata for the distribution.
|
// 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
|
let Ok(archive) = database
|
||||||
.get_or_build_wheel_metadata(&dist, HashPolicy::None)
|
.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.
|
/// 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(
|
||||||
if let Some(best_wheel_index) = self.find_best_wheel(tags) {
|
&self,
|
||||||
return match &self.id.source {
|
workspace_root: &Path,
|
||||||
Source::Registry(source) => {
|
tags: &Tags,
|
||||||
let wheels = self
|
build_options: &BuildOptions,
|
||||||
.wheels
|
) -> Result<Dist, LockError> {
|
||||||
.iter()
|
let no_binary = build_options.no_binary_package(&self.id.name);
|
||||||
.map(|wheel| wheel.to_registry_dist(source, workspace_root))
|
let no_build = build_options.no_build_package(&self.id.name);
|
||||||
.collect::<Result<_, LockError>>()?;
|
|
||||||
let reg_built_dist = RegistryBuiltDist {
|
if !no_binary {
|
||||||
wheels,
|
if let Some(best_wheel_index) = self.find_best_wheel(tags) {
|
||||||
best_wheel_index,
|
return match &self.id.source {
|
||||||
sdist: None,
|
Source::Registry(source) => {
|
||||||
};
|
let wheels = self
|
||||||
Ok(Dist::Built(BuiltDist::Registry(reg_built_dist)))
|
.wheels
|
||||||
}
|
.iter()
|
||||||
Source::Path(path) => {
|
.map(|wheel| wheel.to_registry_dist(source, workspace_root))
|
||||||
let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone();
|
.collect::<Result<_, LockError>>()?;
|
||||||
let path_dist = PathBuiltDist {
|
let reg_built_dist = RegistryBuiltDist {
|
||||||
filename,
|
wheels,
|
||||||
url: verbatim_url(workspace_root.join(path), &self.id)?,
|
best_wheel_index,
|
||||||
install_path: workspace_root.join(path),
|
sdist: None,
|
||||||
};
|
};
|
||||||
let built_dist = BuiltDist::Path(path_dist);
|
Ok(Dist::Built(BuiltDist::Registry(reg_built_dist)))
|
||||||
Ok(Dist::Built(built_dist))
|
}
|
||||||
}
|
Source::Path(path) => {
|
||||||
Source::Direct(url, direct) => {
|
let filename: WheelFilename =
|
||||||
let filename: WheelFilename = self.wheels[best_wheel_index].filename.clone();
|
self.wheels[best_wheel_index].filename.clone();
|
||||||
let url = Url::from(ParsedArchiveUrl {
|
let path_dist = PathBuiltDist {
|
||||||
url: url.to_url(),
|
filename,
|
||||||
subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
|
url: verbatim_url(workspace_root.join(path), &self.id)?,
|
||||||
ext: DistExtension::Wheel,
|
install_path: workspace_root.join(path),
|
||||||
});
|
};
|
||||||
let direct_dist = DirectUrlBuiltDist {
|
let built_dist = BuiltDist::Path(path_dist);
|
||||||
filename,
|
Ok(Dist::Built(built_dist))
|
||||||
location: url.clone(),
|
}
|
||||||
url: VerbatimUrl::from_url(url),
|
Source::Direct(url, direct) => {
|
||||||
};
|
let filename: WheelFilename =
|
||||||
let built_dist = BuiltDist::DirectUrl(direct_dist);
|
self.wheels[best_wheel_index].filename.clone();
|
||||||
Ok(Dist::Built(built_dist))
|
let url = Url::from(ParsedArchiveUrl {
|
||||||
}
|
url: url.to_url(),
|
||||||
Source::Git(_, _) => Err(LockErrorKind::InvalidWheelSource {
|
subdirectory: direct.subdirectory.as_ref().map(PathBuf::from),
|
||||||
id: self.id.clone(),
|
ext: DistExtension::Wheel,
|
||||||
source_type: "Git",
|
});
|
||||||
}
|
let direct_dist = DirectUrlBuiltDist {
|
||||||
.into()),
|
filename,
|
||||||
Source::Directory(_) => Err(LockErrorKind::InvalidWheelSource {
|
location: url.clone(),
|
||||||
id: self.id.clone(),
|
url: VerbatimUrl::from_url(url),
|
||||||
source_type: "directory",
|
};
|
||||||
}
|
let built_dist = BuiltDist::DirectUrl(direct_dist);
|
||||||
.into()),
|
Ok(Dist::Built(built_dist))
|
||||||
Source::Editable(_) => Err(LockErrorKind::InvalidWheelSource {
|
}
|
||||||
id: self.id.clone(),
|
Source::Git(_, _) => Err(LockErrorKind::InvalidWheelSource {
|
||||||
source_type: "editable",
|
id: self.id.clone(),
|
||||||
}
|
source_type: "Git",
|
||||||
.into()),
|
}
|
||||||
Source::Virtual(_) => Err(LockErrorKind::InvalidWheelSource {
|
.into()),
|
||||||
id: self.id.clone(),
|
Source::Directory(_) => Err(LockErrorKind::InvalidWheelSource {
|
||||||
source_type: "virtual",
|
id: self.id.clone(),
|
||||||
}
|
source_type: "directory",
|
||||||
.into()),
|
}
|
||||||
|
.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 {
|
if !no_build {
|
||||||
id: self.id.clone(),
|
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.
|
/// 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.
|
/// The ID of the distribution that has a missing base.
|
||||||
id: PackageId,
|
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.
|
/// An error that occurs when converting between URLs and paths.
|
||||||
#[error("found dependency `{id}` with no locked distribution")]
|
#[error("found dependency `{id}` with no locked distribution")]
|
||||||
VerbatimUrl {
|
VerbatimUrl {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,9 @@ use pypi_types::{Requirement, SupportedEnvironments};
|
||||||
use uv_auth::store_credentials_from_url;
|
use uv_auth::store_credentials_from_url;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder};
|
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_dispatch::BuildDispatch;
|
||||||
use uv_distribution::DistributionDatabase;
|
use uv_distribution::DistributionDatabase;
|
||||||
use uv_fs::CWD;
|
use uv_fs::CWD;
|
||||||
|
|
@ -436,6 +438,7 @@ async fn do_lock(
|
||||||
interpreter,
|
interpreter,
|
||||||
&requires_python,
|
&requires_python,
|
||||||
index_locations,
|
index_locations,
|
||||||
|
build_options,
|
||||||
upgrade,
|
upgrade,
|
||||||
&options,
|
&options,
|
||||||
&database,
|
&database,
|
||||||
|
|
@ -590,6 +593,7 @@ impl ValidatedLock {
|
||||||
interpreter: &Interpreter,
|
interpreter: &Interpreter,
|
||||||
requires_python: &RequiresPython,
|
requires_python: &RequiresPython,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
|
build_options: &BuildOptions,
|
||||||
upgrade: &Upgrade,
|
upgrade: &Upgrade,
|
||||||
options: &Options,
|
options: &Options,
|
||||||
database: &DistributionDatabase<'_, Context>,
|
database: &DistributionDatabase<'_, Context>,
|
||||||
|
|
@ -706,6 +710,7 @@ impl ValidatedLock {
|
||||||
constraints,
|
constraints,
|
||||||
overrides,
|
overrides,
|
||||||
indexes,
|
indexes,
|
||||||
|
build_options,
|
||||||
interpreter.tags()?,
|
interpreter.tags()?,
|
||||||
database,
|
database,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,7 @@ pub(super) async fn do_sync(
|
||||||
let tags = venv.interpreter().tags()?;
|
let tags = venv.interpreter().tags()?;
|
||||||
|
|
||||||
// Read the lockfile.
|
// 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.
|
// Always skip virtual projects, which shouldn't be built or installed.
|
||||||
let resolution = apply_no_virtual_project(resolution);
|
let resolution = apply_no_virtual_project(resolution);
|
||||||
|
|
|
||||||
|
|
@ -1930,3 +1930,113 @@ fn sync_environment_prompt() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue