From ff9f3dede1f9051310ff540e06d3f6f505f56c06 Mon Sep 17 00:00:00 2001 From: Ahmed Ilyas Date: Fri, 2 Aug 2024 04:15:58 +0200 Subject: [PATCH] Support build constraints (#5639) ## Summary Partially resolves #5561. Haven't added overrides support yet but I can add it tomorrow if the current approach for constraints is ok. ## Test Plan `cargo test` Manually checked trace logs after changing the constraints. --- PIP_COMPATIBILITY.md | 13 +++++ README.md | 2 + crates/bench/benches/uv.rs | 2 + crates/uv-cli/src/lib.rs | 27 +++++++++++ crates/uv-dev/src/build.rs | 2 + crates/uv-dispatch/src/lib.rs | 8 +++- crates/uv-resolver/src/manifest.rs | 6 +++ crates/uv/src/commands/pip/compile.rs | 6 +++ crates/uv/src/commands/pip/install.rs | 6 +++ crates/uv/src/commands/pip/operations.rs | 12 +++++ crates/uv/src/commands/pip/sync.rs | 6 +++ crates/uv/src/commands/project/add.rs | 4 ++ crates/uv/src/commands/project/lock.rs | 6 +++ crates/uv/src/commands/project/mod.rs | 13 +++++ crates/uv/src/commands/project/sync.rs | 3 ++ crates/uv/src/commands/venv.rs | 3 ++ crates/uv/src/lib.rs | 19 ++++++++ crates/uv/src/settings.rs | 18 +++++++ crates/uv/tests/common/mod.rs | 2 +- crates/uv/tests/help.rs | 2 +- crates/uv/tests/pip_compile.rs | 60 ++++++++++++++++++++++++ crates/uv/tests/pip_install.rs | 55 ++++++++++++++++++++++ crates/uv/tests/pip_sync.rs | 59 +++++++++++++++++++++++ crates/uv/tests/show_settings.rs | 18 +++++++ docs/reference/cli.md | 12 +++++ 25 files changed, 360 insertions(+), 4 deletions(-) diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index 4998a288f..cab319327 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -384,6 +384,19 @@ Specifically, uv does not support installing new `.egg-info`- or `.egg-link`-sty but will respect any such existing distributions during resolution, list them with `uv pip list` and `uv pip freeze`, and uninstall them with `uv pip uninstall`. +## Build constraints + +When constraints are provided via `--constraint` (or `UV_CONSTRAINT`), uv will _not_ apply the +constraints when resolving build dependencies (i.e., to build a source distribution). Instead, +build constraints should be provided via the dedicated `--build-constraint` (or `UV_BUILD_CONSTRAINT`) +setting. + +pip, meanwhile, applies constraints to build dependencies when specified via `PIP_CONSTRAINT`, but +not when provided via `--constraint` on the command line. + +For example, to ensure that `setuptools 60.0.0` is used to build any packages with a build +dependency on `setuptools`, use `--build-constraint`, rather than `--constraint`. + ## `pip compile` defaults There are a few small but notable differences in the default behaviors of `pip compile` and diff --git a/README.md b/README.md index 4be50cd4c..57c3b65ce 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,8 @@ uv accepts the following command-line arguments as environment variables: uv will require that all dependencies have a hash specified in the requirements file. - `UV_CONSTRAINT`: Equivalent to the `--constraint` command-line argument. If set, uv will use this file as the constraints file. Uses space-separated list of files. +- `UV_BUILD_CONSTRAINT`: Equivalent to the `--build-constraint` command-line argument. If set, uv + will use this file as constraints for any source distribution builds. Uses space-separated list of files. - `UV_OVERRIDE`: Equivalent to the `--override` command-line argument. If set, uv will use this file as the overrides file. Uses space-separated list of files. - `UV_LINK_MODE`: Equivalent to the `--link-mode` command-line argument. If set, uv will use this diff --git a/crates/bench/benches/uv.rs b/crates/bench/benches/uv.rs index 42c9d2fb8..9e262cf2b 100644 --- a/crates/bench/benches/uv.rs +++ b/crates/bench/benches/uv.rs @@ -151,10 +151,12 @@ mod resolver { let python_requirement = PythonRequirement::from_interpreter(interpreter); let options = OptionsBuilder::new().exclude_newer(exclude_newer).build(); + let build_constraints = []; let build_context = BuildDispatch::new( client, &cache, + &build_constraints, interpreter, &index_locations, &flat_index, diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 96a9605b6..5eed10878 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -541,6 +541,15 @@ pub struct PipCompileArgs { #[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub r#override: Vec>, + /// Constrain build dependencies using the given requirements files when building source + /// distributions. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + #[arg(long, short, env = "UV_BUILD_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub build_constraint: Vec>, + /// Include optional dependencies from the extra group name; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. @@ -838,6 +847,15 @@ pub struct PipSyncArgs { #[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub constraint: Vec>, + /// Constrain build dependencies using the given requirements files when building source + /// distributions. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + #[arg(long, short, env = "UV_BUILD_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub build_constraint: Vec>, + #[command(flatten)] pub installer: InstallerArgs, @@ -1111,6 +1129,15 @@ pub struct PipInstallArgs { #[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)] pub r#override: Vec>, + /// Constrain build dependencies using the given requirements files when building source + /// distributions. + /// + /// Constraints files are `requirements.txt`-like files that only control the _version_ of a + /// requirement that's installed. However, including a package in a constraints file will _not_ + /// trigger the installation of that package. + #[arg(long, short, env = "UV_BUILD_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)] + pub build_constraint: Vec>, + /// Include optional dependencies from the extra group name; may be provided more than once. /// /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources. diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index 5d4b1e5fb..51c318650 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -74,10 +74,12 @@ pub(crate) async fn build(args: BuildArgs) -> Result { &cache, )?; let build_options = BuildOptions::default(); + let build_constraints = []; let build_dispatch = BuildDispatch::new( &client, &cache, + &build_constraints, python.interpreter(), &index_urls, &flat_index, diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 77c703920..06c86d00a 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -17,7 +17,7 @@ use uv_build::{SourceBuild, SourceBuildContext}; use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{ - BuildKind, BuildOptions, ConfigSettings, IndexStrategy, Reinstall, SetupPyStrategy, + BuildKind, BuildOptions, ConfigSettings, Constraints, IndexStrategy, Reinstall, SetupPyStrategy, }; use uv_configuration::{Concurrency, PreviewMode}; use uv_distribution::DistributionDatabase; @@ -35,6 +35,7 @@ use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrateg pub struct BuildDispatch<'a> { client: &'a RegistryClient, cache: &'a Cache, + constraints: Constraints, interpreter: &'a Interpreter, index_locations: &'a IndexLocations, index_strategy: IndexStrategy, @@ -58,6 +59,7 @@ impl<'a> BuildDispatch<'a> { pub fn new( client: &'a RegistryClient, cache: &'a Cache, + constraints: &'a [Requirement], interpreter: &'a Interpreter, index_locations: &'a IndexLocations, flat_index: &'a FlatIndex, @@ -77,6 +79,7 @@ impl<'a> BuildDispatch<'a> { Self { client, cache, + constraints: Constraints::from_requirements(constraints.iter().cloned()), interpreter, index_locations, flat_index, @@ -140,8 +143,9 @@ impl<'a> BuildContext for BuildDispatch<'a> { let python_requirement = PythonRequirement::from_interpreter(self.interpreter); let markers = self.interpreter.markers(); let tags = self.interpreter.tags()?; + let resolver = Resolver::new( - Manifest::simple(requirements.to_vec()), + Manifest::simple(requirements.to_vec()).with_constraints(self.constraints.clone()), OptionsBuilder::new() .exclude_newer(self.exclude_newer) .index_strategy(self.index_strategy) diff --git a/crates/uv-resolver/src/manifest.rs b/crates/uv-resolver/src/manifest.rs index 06b16fb3c..91a2d68e5 100644 --- a/crates/uv-resolver/src/manifest.rs +++ b/crates/uv-resolver/src/manifest.rs @@ -86,6 +86,12 @@ impl Manifest { } } + #[must_use] + pub fn with_constraints(mut self, constraints: Constraints) -> Self { + self.constraints = constraints; + self + } + /// Return an iterator over all requirements, constraints, and overrides, in priority order, /// such that requirements come first, followed by constraints, followed by overrides. /// diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 32d14d4ad..cffbcfd62 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -48,6 +48,7 @@ pub(crate) async fn pip_compile( requirements: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + build_constraints: &[RequirementsSource], constraints_from_workspace: Vec, overrides_from_workspace: Vec, extras: ExtrasSpecification, @@ -143,6 +144,10 @@ pub(crate) async fn pip_compile( ) .collect(); + // Read build constraints. + let build_constraints = + operations::read_constraints(build_constraints, &client_builder).await?; + // If all the metadata could be statically resolved, validate that every extra was used. If we // need to resolve metadata via PEP 517, we don't know which extras are used until much later. if source_trees.is_empty() { @@ -304,6 +309,7 @@ pub(crate) async fn pip_compile( let build_dispatch = BuildDispatch::new( &client, &cache, + &build_constraints, &interpreter, &index_locations, &flat_index, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 0a9faed6f..13c4dcec0 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -40,6 +40,7 @@ pub(crate) async fn pip_install( requirements: &[RequirementsSource], constraints: &[RequirementsSource], overrides: &[RequirementsSource], + build_constraints: &[RequirementsSource], constraints_from_workspace: Vec, overrides_from_workspace: Vec, extras: &ExtrasSpecification, @@ -105,6 +106,10 @@ pub(crate) async fn pip_install( ) .await?; + // Read build constraints. + let build_constraints = + operations::read_constraints(build_constraints, &client_builder).await?; + let constraints: Vec = constraints .iter() .cloned() @@ -294,6 +299,7 @@ pub(crate) async fn pip_install( let build_dispatch = BuildDispatch::new( &client, &cache, + &build_constraints, interpreter, &index_locations, &flat_index, diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 6c7418161..21390138f 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -71,6 +71,18 @@ pub(crate) async fn read_requirements( .await?) } +/// Resolve a set of constraints. +pub(crate) async fn read_constraints( + constraints: &[RequirementsSource], + client_builder: &BaseClientBuilder<'_>, +) -> Result, Error> { + Ok( + RequirementsSpecification::from_sources(&[], constraints, &[], client_builder) + .await? + .constraints, + ) +} + /// Resolve a set of requirements, similar to running `pip compile`. pub(crate) async fn resolve( requirements: Vec, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index e16729bb1..dafdc91be 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -38,6 +38,7 @@ use crate::printer::Printer; pub(crate) async fn pip_sync( requirements: &[RequirementsSource], constraints: &[RequirementsSource], + build_constraints: &[RequirementsSource], reinstall: Reinstall, link_mode: LinkMode, compile: bool, @@ -103,6 +104,10 @@ pub(crate) async fn pip_sync( ) .await?; + // Read build constraints. + let build_constraints = + operations::read_constraints(build_constraints, &client_builder).await?; + // Validate that the requirements are non-empty. if !allow_empty_requirements { let num_requirements = requirements.len() + source_trees.len(); @@ -240,6 +245,7 @@ pub(crate) async fn pip_sync( let build_dispatch = BuildDispatch::new( &client, &cache, + &build_constraints, interpreter, &index_locations, &flat_index, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 3f0555087..dca37e5e7 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -123,10 +123,14 @@ pub(crate) async fn add( FlatIndex::from_entries(entries, Some(&tags), &hasher, &settings.build_options) }; + // TODO: read locked build constraints + let build_constraints = []; + // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, venv.interpreter(), &settings.index_locations, &flat_index, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index ecf2ce652..cff94e7da 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -403,10 +403,13 @@ async fn do_lock( // Prefill the index with the lockfile metadata. let index = lock.to_index(workspace.install_path(), upgrade)?; + // TODO: read locked build constraints + let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, @@ -479,10 +482,13 @@ async fn do_lock( None => { debug!("Starting clean resolution"); + // TODO: read locked build constraints + let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index b595d8c89..c61eec186 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -405,10 +405,13 @@ pub(crate) async fn resolve_names( let setup_py = SetupPyStrategy::default(); let flat_index = FlatIndex::default(); + // TODO: read locked build constraints + let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, @@ -525,10 +528,13 @@ pub(crate) async fn resolve_environment<'a>( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; + // TODO: read locked build constraints + let build_constraints = []; // Create a build dispatch. let resolve_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, @@ -638,10 +644,13 @@ pub(crate) async fn sync_environment( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; + // TODO: read locked build constraints + let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, @@ -799,10 +808,14 @@ pub(crate) async fn update_environment( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; + // TODO: read locked build constraints + let build_constraints = []; + // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 0f90c7dce..af77f2fc6 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -209,10 +209,13 @@ pub(super) async fn do_sync( FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) }; + // TODO: read locked build constraints + let build_constraints = []; // Create a build dispatch. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, venv.interpreter(), index_locations, &flat_index, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index f5e1786f1..042ebbbd3 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -276,10 +276,13 @@ async fn venv_impl( // Do not allow builds let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); + let build_constraints = []; + // Prep the build context. let build_dispatch = BuildDispatch::new( &client, cache, + &build_constraints, interpreter, index_locations, &flat_index, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 554172e49..e379fa987 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -236,11 +236,17 @@ async fn run(cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_overrides_txt) .collect::>(); + let build_constraints = args + .build_constraint + .into_iter() + .map(RequirementsSource::from_constraints_txt) + .collect::>(); commands::pip_compile( &requirements, &constraints, &overrides, + &build_constraints, args.constraints_from_workspace, args.overrides_from_workspace, args.settings.extras, @@ -316,10 +322,16 @@ async fn run(cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_constraints_txt) .collect::>(); + let build_constraints = args + .build_constraint + .into_iter() + .map(RequirementsSource::from_constraints_txt) + .collect::>(); commands::pip_sync( &requirements, &constraints, + &build_constraints, args.settings.reinstall, args.settings.link_mode, args.settings.compile_bytecode, @@ -392,10 +404,17 @@ async fn run(cli: Cli) -> Result { .map(RequirementsSource::from_overrides_txt) .collect::>(); + let build_constraints = args + .build_constraint + .into_iter() + .map(RequirementsSource::from_overrides_txt) + .collect::>(); + commands::pip_install( &requirements, &constraints, &overrides, + &build_constraints, args.constraints_from_workspace, args.overrides_from_workspace, &args.settings.extras, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 168c2dc6b..0e2409f00 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -814,6 +814,7 @@ pub(crate) struct PipCompileSettings { pub(crate) r#override: Vec, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, + pub(crate) build_constraint: Vec, pub(crate) refresh: Refresh, pub(crate) settings: PipSettings, } @@ -828,6 +829,7 @@ impl PipCompileSettings { extra, all_extras, no_all_extras, + build_constraint, refresh, no_deps, deps, @@ -908,6 +910,10 @@ impl PipCompileSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + build_constraint: build_constraint + .into_iter() + .filter_map(Maybe::into_option) + .collect(), r#override: r#override .into_iter() .filter_map(Maybe::into_option) @@ -961,6 +967,7 @@ impl PipCompileSettings { pub(crate) struct PipSyncSettings { pub(crate) src_file: Vec, pub(crate) constraint: Vec, + pub(crate) build_constraint: Vec, pub(crate) dry_run: bool, pub(crate) refresh: Refresh, pub(crate) settings: PipSettings, @@ -972,6 +979,7 @@ impl PipSyncSettings { let PipSyncArgs { src_file, constraint, + build_constraint, installer, refresh, require_hashes, @@ -1009,6 +1017,10 @@ impl PipSyncSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + build_constraint: build_constraint + .into_iter() + .filter_map(Maybe::into_option) + .collect(), dry_run, refresh: Refresh::from(refresh), settings: PipSettings::combine( @@ -1052,6 +1064,7 @@ pub(crate) struct PipInstallSettings { pub(crate) editable: Vec, pub(crate) constraint: Vec, pub(crate) r#override: Vec, + pub(crate) build_constraint: Vec, pub(crate) dry_run: bool, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, @@ -1071,6 +1084,7 @@ impl PipInstallSettings { extra, all_extras, no_all_extras, + build_constraint, refresh, no_deps, deps, @@ -1142,6 +1156,10 @@ impl PipInstallSettings { .into_iter() .filter_map(Maybe::into_option) .collect(), + build_constraint: build_constraint + .into_iter() + .filter_map(Maybe::into_option) + .collect(), dry_run, constraints_from_workspace, overrides_from_workspace, diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index a9844e894..f39d647c9 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -413,10 +413,10 @@ impl TestContext { } /// Create a `uv help` command with options shared across scenarios. - #[allow(clippy::unused_self)] pub fn help(&self) -> Command { let mut command = Command::new(get_bin()); command.arg("help"); + self.add_shared_args(&mut command); command } diff --git a/crates/uv/tests/help.rs b/crates/uv/tests/help.rs index dc92bf57d..7a2519973 100644 --- a/crates/uv/tests/help.rs +++ b/crates/uv/tests/help.rs @@ -627,7 +627,7 @@ fn help_unknown_subsubcommand() { fn help_with_global_option() { let context = TestContext::new_with_versions(&[]); - uv_snapshot!(context.filters(), context.help().arg("--cache-dir").arg("/dev/null"), @r###" + uv_snapshot!(context.filters(), context.help().arg("--no-cache"), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 8e04ccf77..c5d563d81 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -11508,3 +11508,63 @@ fn ignore_invalid_constraint() -> Result<()> { Ok(()) } + +/// Include a `build_constraints.txt` file with an incompatible constraint. +#[test] +fn incompatible_build_constraint() -> Result<()> { + let context = TestContext::new("3.8"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("requests==1.2")?; + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==1")?; + + uv_snapshot!(context.pip_compile() + .arg("requirements.txt") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build `requests==1.2.0` + Caused by: Failed to build: `requests==1.2.0` + Caused by: Failed to install requirements from setup.py build (resolve) + Caused by: No solution found when resolving: setuptools>=40.8.0 + Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include a `build_constraints.txt` file with a compatible constraint. +#[test] +fn compatible_build_constraint() -> Result<()> { + let context = TestContext::new("3.8"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("requests==1.2")?; + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + + uv_snapshot!(context.pip_compile() + .arg("requirements.txt") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] requirements.txt --build-constraint build_constraints.txt + requests==1.2.0 + # via -r requirements.txt + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 7f21b3999..6f0160103 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -6244,3 +6244,58 @@ fn install_relocatable() -> Result<()> { Ok(()) } + +/// Include a `build_constraints.txt` file with an incompatible constraint. +#[test] +fn incompatible_build_constraint() -> Result<()> { + let context = TestContext::new("3.8"); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==1")?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build `requests==1.2.0` + Caused by: Failed to build: `requests==1.2.0` + Caused by: Failed to install requirements from setup.py build (resolve) + Caused by: No solution found when resolving: setuptools>=40.8.0 + Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include a `build_constraints.txt` file with a compatible constraint. +#[test] +fn compatible_build_constraint() -> Result<()> { + let context = TestContext::new("3.8"); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + requests==1.2.0 + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 622937a06..6934ee2d4 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -5349,3 +5349,62 @@ fn preserve_markers() -> Result<()> { Ok(()) } + +/// Include a `build_constraints.txt` file with an incompatible constraint. +#[test] +fn incompatible_build_constraint() -> Result<()> { + let context = TestContext::new("3.8"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("requests==1.2")?; + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==1")?; + + uv_snapshot!(context.pip_sync() + .arg("requirements.txt") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build `requests==1.2.0` + Caused by: Failed to build: `requests==1.2.0` + Caused by: Failed to install requirements from setup.py build (resolve) + Caused by: No solution found when resolving: setuptools>=40.8.0 + Caused by: Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include a `build_constraints.txt` file with a compatible constraint. +#[test] +fn compatible_build_constraint() -> Result<()> { + let context = TestContext::new("3.8"); + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("requests==1.2")?; + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + + uv_snapshot!(context.pip_sync() + .arg("requirements.txt") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + requests==1.2.0 + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index d1951515a..c25ebc2be 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -74,6 +74,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -207,6 +208,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -341,6 +343,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -507,6 +510,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -642,6 +646,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -763,6 +768,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -921,6 +927,7 @@ fn resolve_index_url() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -1079,6 +1086,7 @@ fn resolve_index_url() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -1282,6 +1290,7 @@ fn resolve_find_links() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -1439,6 +1448,7 @@ fn resolve_top_level() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -1566,6 +1576,7 @@ fn resolve_top_level() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -1721,6 +1732,7 @@ fn resolve_top_level() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -1900,6 +1912,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -2017,6 +2030,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -2134,6 +2148,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -2253,6 +2268,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -2397,6 +2413,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { @@ -2542,6 +2559,7 @@ fn resolve_both() -> anyhow::Result<()> { override: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraint: [], refresh: None( Timestamp( SystemTime { diff --git a/docs/reference/cli.md b/docs/reference/cli.md index a0611fdaa..09b664025 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -108,6 +108,10 @@ uv pip compile [OPTIONS] ...

While constraints are additive, in that they’re combined with the requirements of the constituent packages, overrides are absolute, in that they completely replace the requirements of the constituent packages.

+
--build-constraint, -b build-constraint

Constrain build dependencies using the given requirements files when building source distributions.

+ +

Constraints files are requirements.txt-like files that only control the version of a requirement that’s installed. However, including a package in a constraints file will not trigger the installation of that package.

+
--extra extra

Include optional dependencies from the extra group name; may be provided more than once.

Only applies to pyproject.toml, setup.py, and setup.cfg sources.

@@ -254,6 +258,10 @@ uv pip sync [OPTIONS] ...

This is equivalent to pip’s --constraint option.

+
--build-constraint, -b build-constraint

Constrain build dependencies using the given requirements files when building source distributions.

+ +

Constraints files are requirements.txt-like files that only control the version of a requirement that’s installed. However, including a package in a constraints file will not trigger the installation of that package.

+
--index-url, -i index-url

The URL of the Python package index (by default: <https://pypi.org/simple>).

Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.

@@ -390,6 +398,10 @@ uv pip install [OPTIONS] |--editable While constraints are additive, in that they’re combined with the requirements of the constituent packages, overrides are absolute, in that they completely replace the requirements of the constituent packages.

+
--build-constraint, -b build-constraint

Constrain build dependencies using the given requirements files when building source distributions.

+ +

Constraints files are requirements.txt-like files that only control the version of a requirement that’s installed. However, including a package in a constraints file will not trigger the installation of that package.

+
--extra extra

Include optional dependencies from the extra group name; may be provided more than once.

Only applies to pyproject.toml, setup.py, and setup.cfg sources.