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-constraintConstrain 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 extraInclude 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-constraintConstrain 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-urlThe 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-constraintConstrain 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 extraInclude optional dependencies from the extra group name; may be provided more than once.
Only applies to pyproject.toml, setup.py, and setup.cfg sources.