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.
This commit is contained in:
Ahmed Ilyas 2024-08-02 04:15:58 +02:00 committed by GitHub
parent c558d70690
commit ff9f3dede1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 360 additions and 4 deletions

View file

@ -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 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`. `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 ## `pip compile` defaults
There are a few small but notable differences in the default behaviors of `pip compile` and There are a few small but notable differences in the default behaviors of `pip compile` and

View file

@ -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 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 - `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. 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 - `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. 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 - `UV_LINK_MODE`: Equivalent to the `--link-mode` command-line argument. If set, uv will use this

View file

@ -151,10 +151,12 @@ mod resolver {
let python_requirement = PythonRequirement::from_interpreter(interpreter); let python_requirement = PythonRequirement::from_interpreter(interpreter);
let options = OptionsBuilder::new().exclude_newer(exclude_newer).build(); let options = OptionsBuilder::new().exclude_newer(exclude_newer).build();
let build_constraints = [];
let build_context = BuildDispatch::new( let build_context = BuildDispatch::new(
client, client,
&cache, &cache,
&build_constraints,
interpreter, interpreter,
&index_locations, &index_locations,
&flat_index, &flat_index,

View file

@ -541,6 +541,15 @@ pub struct PipCompileArgs {
#[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)] #[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub r#override: Vec<Maybe<PathBuf>>, pub r#override: Vec<Maybe<PathBuf>>,
/// 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<Maybe<PathBuf>>,
/// Include optional dependencies from the extra group name; may be provided more than once. /// 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. /// 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)] #[arg(long, short, env = "UV_CONSTRAINT", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub constraint: Vec<Maybe<PathBuf>>, pub constraint: Vec<Maybe<PathBuf>>,
/// 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<Maybe<PathBuf>>,
#[command(flatten)] #[command(flatten)]
pub installer: InstallerArgs, pub installer: InstallerArgs,
@ -1111,6 +1129,15 @@ pub struct PipInstallArgs {
#[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)] #[arg(long, env = "UV_OVERRIDE", value_delimiter = ' ', value_parser = parse_maybe_file_path)]
pub r#override: Vec<Maybe<PathBuf>>, pub r#override: Vec<Maybe<PathBuf>>,
/// 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<Maybe<PathBuf>>,
/// Include optional dependencies from the extra group name; may be provided more than once. /// 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. /// Only applies to `pyproject.toml`, `setup.py`, and `setup.cfg` sources.

View file

@ -74,10 +74,12 @@ pub(crate) async fn build(args: BuildArgs) -> Result<PathBuf> {
&cache, &cache,
)?; )?;
let build_options = BuildOptions::default(); let build_options = BuildOptions::default();
let build_constraints = [];
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
&build_constraints,
python.interpreter(), python.interpreter(),
&index_urls, &index_urls,
&flat_index, &flat_index,

View file

@ -17,7 +17,7 @@ use uv_build::{SourceBuild, SourceBuildContext};
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::RegistryClient; use uv_client::RegistryClient;
use uv_configuration::{ use uv_configuration::{
BuildKind, BuildOptions, ConfigSettings, IndexStrategy, Reinstall, SetupPyStrategy, BuildKind, BuildOptions, ConfigSettings, Constraints, IndexStrategy, Reinstall, SetupPyStrategy,
}; };
use uv_configuration::{Concurrency, PreviewMode}; use uv_configuration::{Concurrency, PreviewMode};
use uv_distribution::DistributionDatabase; use uv_distribution::DistributionDatabase;
@ -35,6 +35,7 @@ use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrateg
pub struct BuildDispatch<'a> { pub struct BuildDispatch<'a> {
client: &'a RegistryClient, client: &'a RegistryClient,
cache: &'a Cache, cache: &'a Cache,
constraints: Constraints,
interpreter: &'a Interpreter, interpreter: &'a Interpreter,
index_locations: &'a IndexLocations, index_locations: &'a IndexLocations,
index_strategy: IndexStrategy, index_strategy: IndexStrategy,
@ -58,6 +59,7 @@ impl<'a> BuildDispatch<'a> {
pub fn new( pub fn new(
client: &'a RegistryClient, client: &'a RegistryClient,
cache: &'a Cache, cache: &'a Cache,
constraints: &'a [Requirement],
interpreter: &'a Interpreter, interpreter: &'a Interpreter,
index_locations: &'a IndexLocations, index_locations: &'a IndexLocations,
flat_index: &'a FlatIndex, flat_index: &'a FlatIndex,
@ -77,6 +79,7 @@ impl<'a> BuildDispatch<'a> {
Self { Self {
client, client,
cache, cache,
constraints: Constraints::from_requirements(constraints.iter().cloned()),
interpreter, interpreter,
index_locations, index_locations,
flat_index, flat_index,
@ -140,8 +143,9 @@ impl<'a> BuildContext for BuildDispatch<'a> {
let python_requirement = PythonRequirement::from_interpreter(self.interpreter); let python_requirement = PythonRequirement::from_interpreter(self.interpreter);
let markers = self.interpreter.markers(); let markers = self.interpreter.markers();
let tags = self.interpreter.tags()?; let tags = self.interpreter.tags()?;
let resolver = Resolver::new( let resolver = Resolver::new(
Manifest::simple(requirements.to_vec()), Manifest::simple(requirements.to_vec()).with_constraints(self.constraints.clone()),
OptionsBuilder::new() OptionsBuilder::new()
.exclude_newer(self.exclude_newer) .exclude_newer(self.exclude_newer)
.index_strategy(self.index_strategy) .index_strategy(self.index_strategy)

View file

@ -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, /// Return an iterator over all requirements, constraints, and overrides, in priority order,
/// such that requirements come first, followed by constraints, followed by overrides. /// such that requirements come first, followed by constraints, followed by overrides.
/// ///

View file

@ -48,6 +48,7 @@ pub(crate) async fn pip_compile(
requirements: &[RequirementsSource], requirements: &[RequirementsSource],
constraints: &[RequirementsSource], constraints: &[RequirementsSource],
overrides: &[RequirementsSource], overrides: &[RequirementsSource],
build_constraints: &[RequirementsSource],
constraints_from_workspace: Vec<Requirement>, constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>, overrides_from_workspace: Vec<Requirement>,
extras: ExtrasSpecification, extras: ExtrasSpecification,
@ -143,6 +144,10 @@ pub(crate) async fn pip_compile(
) )
.collect(); .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 // 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. // need to resolve metadata via PEP 517, we don't know which extras are used until much later.
if source_trees.is_empty() { if source_trees.is_empty() {
@ -304,6 +309,7 @@ pub(crate) async fn pip_compile(
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
&build_constraints,
&interpreter, &interpreter,
&index_locations, &index_locations,
&flat_index, &flat_index,

View file

@ -40,6 +40,7 @@ pub(crate) async fn pip_install(
requirements: &[RequirementsSource], requirements: &[RequirementsSource],
constraints: &[RequirementsSource], constraints: &[RequirementsSource],
overrides: &[RequirementsSource], overrides: &[RequirementsSource],
build_constraints: &[RequirementsSource],
constraints_from_workspace: Vec<Requirement>, constraints_from_workspace: Vec<Requirement>,
overrides_from_workspace: Vec<Requirement>, overrides_from_workspace: Vec<Requirement>,
extras: &ExtrasSpecification, extras: &ExtrasSpecification,
@ -105,6 +106,10 @@ pub(crate) async fn pip_install(
) )
.await?; .await?;
// Read build constraints.
let build_constraints =
operations::read_constraints(build_constraints, &client_builder).await?;
let constraints: Vec<Requirement> = constraints let constraints: Vec<Requirement> = constraints
.iter() .iter()
.cloned() .cloned()
@ -294,6 +299,7 @@ pub(crate) async fn pip_install(
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
&build_constraints,
interpreter, interpreter,
&index_locations, &index_locations,
&flat_index, &flat_index,

View file

@ -71,6 +71,18 @@ pub(crate) async fn read_requirements(
.await?) .await?)
} }
/// Resolve a set of constraints.
pub(crate) async fn read_constraints(
constraints: &[RequirementsSource],
client_builder: &BaseClientBuilder<'_>,
) -> Result<Vec<Requirement>, Error> {
Ok(
RequirementsSpecification::from_sources(&[], constraints, &[], client_builder)
.await?
.constraints,
)
}
/// Resolve a set of requirements, similar to running `pip compile`. /// Resolve a set of requirements, similar to running `pip compile`.
pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>( pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
requirements: Vec<UnresolvedRequirementSpecification>, requirements: Vec<UnresolvedRequirementSpecification>,

View file

@ -38,6 +38,7 @@ use crate::printer::Printer;
pub(crate) async fn pip_sync( pub(crate) async fn pip_sync(
requirements: &[RequirementsSource], requirements: &[RequirementsSource],
constraints: &[RequirementsSource], constraints: &[RequirementsSource],
build_constraints: &[RequirementsSource],
reinstall: Reinstall, reinstall: Reinstall,
link_mode: LinkMode, link_mode: LinkMode,
compile: bool, compile: bool,
@ -103,6 +104,10 @@ pub(crate) async fn pip_sync(
) )
.await?; .await?;
// Read build constraints.
let build_constraints =
operations::read_constraints(build_constraints, &client_builder).await?;
// Validate that the requirements are non-empty. // Validate that the requirements are non-empty.
if !allow_empty_requirements { if !allow_empty_requirements {
let num_requirements = requirements.len() + source_trees.len(); let num_requirements = requirements.len() + source_trees.len();
@ -240,6 +245,7 @@ pub(crate) async fn pip_sync(
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
&cache, &cache,
&build_constraints,
interpreter, interpreter,
&index_locations, &index_locations,
&flat_index, &flat_index,

View file

@ -123,10 +123,14 @@ pub(crate) async fn add(
FlatIndex::from_entries(entries, Some(&tags), &hasher, &settings.build_options) FlatIndex::from_entries(entries, Some(&tags), &hasher, &settings.build_options)
}; };
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
venv.interpreter(), venv.interpreter(),
&settings.index_locations, &settings.index_locations,
&flat_index, &flat_index,

View file

@ -403,10 +403,13 @@ async fn do_lock(
// Prefill the index with the lockfile metadata. // Prefill the index with the lockfile metadata.
let index = lock.to_index(workspace.install_path(), upgrade)?; let index = lock.to_index(workspace.install_path(), upgrade)?;
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,
@ -479,10 +482,13 @@ async fn do_lock(
None => { None => {
debug!("Starting clean resolution"); debug!("Starting clean resolution");
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,

View file

@ -405,10 +405,13 @@ pub(crate) async fn resolve_names(
let setup_py = SetupPyStrategy::default(); let setup_py = SetupPyStrategy::default();
let flat_index = FlatIndex::default(); let flat_index = FlatIndex::default();
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,
@ -525,10 +528,13 @@ pub(crate) async fn resolve_environment<'a>(
FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) FlatIndex::from_entries(entries, Some(tags), &hasher, build_options)
}; };
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let resolve_dispatch = BuildDispatch::new( let resolve_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,
@ -638,10 +644,13 @@ pub(crate) async fn sync_environment(
FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) FlatIndex::from_entries(entries, Some(tags), &hasher, build_options)
}; };
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,
@ -799,10 +808,14 @@ pub(crate) async fn update_environment(
FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) FlatIndex::from_entries(entries, Some(tags), &hasher, build_options)
}; };
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,

View file

@ -209,10 +209,13 @@ pub(super) async fn do_sync(
FlatIndex::from_entries(entries, Some(tags), &hasher, build_options) FlatIndex::from_entries(entries, Some(tags), &hasher, build_options)
}; };
// TODO: read locked build constraints
let build_constraints = [];
// Create a build dispatch. // Create a build dispatch.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
venv.interpreter(), venv.interpreter(),
index_locations, index_locations,
&flat_index, &flat_index,

View file

@ -276,10 +276,13 @@ async fn venv_impl(
// Do not allow builds // Do not allow builds
let build_options = BuildOptions::new(NoBinary::None, NoBuild::All); let build_options = BuildOptions::new(NoBinary::None, NoBuild::All);
let build_constraints = [];
// Prep the build context. // Prep the build context.
let build_dispatch = BuildDispatch::new( let build_dispatch = BuildDispatch::new(
&client, &client,
cache, cache,
&build_constraints,
interpreter, interpreter,
index_locations, index_locations,
&flat_index, &flat_index,

View file

@ -236,11 +236,17 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.into_iter() .into_iter()
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let build_constraints = args
.build_constraint
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
commands::pip_compile( commands::pip_compile(
&requirements, &requirements,
&constraints, &constraints,
&overrides, &overrides,
&build_constraints,
args.constraints_from_workspace, args.constraints_from_workspace,
args.overrides_from_workspace, args.overrides_from_workspace,
args.settings.extras, args.settings.extras,
@ -316,10 +322,16 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.into_iter() .into_iter()
.map(RequirementsSource::from_constraints_txt) .map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let build_constraints = args
.build_constraint
.into_iter()
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
commands::pip_sync( commands::pip_sync(
&requirements, &requirements,
&constraints, &constraints,
&build_constraints,
args.settings.reinstall, args.settings.reinstall,
args.settings.link_mode, args.settings.link_mode,
args.settings.compile_bytecode, args.settings.compile_bytecode,
@ -392,10 +404,17 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.map(RequirementsSource::from_overrides_txt) .map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let build_constraints = args
.build_constraint
.into_iter()
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
commands::pip_install( commands::pip_install(
&requirements, &requirements,
&constraints, &constraints,
&overrides, &overrides,
&build_constraints,
args.constraints_from_workspace, args.constraints_from_workspace,
args.overrides_from_workspace, args.overrides_from_workspace,
&args.settings.extras, &args.settings.extras,

View file

@ -814,6 +814,7 @@ pub(crate) struct PipCompileSettings {
pub(crate) r#override: Vec<PathBuf>, pub(crate) r#override: Vec<PathBuf>,
pub(crate) constraints_from_workspace: Vec<Requirement>, pub(crate) constraints_from_workspace: Vec<Requirement>,
pub(crate) overrides_from_workspace: Vec<Requirement>, pub(crate) overrides_from_workspace: Vec<Requirement>,
pub(crate) build_constraint: Vec<PathBuf>,
pub(crate) refresh: Refresh, pub(crate) refresh: Refresh,
pub(crate) settings: PipSettings, pub(crate) settings: PipSettings,
} }
@ -828,6 +829,7 @@ impl PipCompileSettings {
extra, extra,
all_extras, all_extras,
no_all_extras, no_all_extras,
build_constraint,
refresh, refresh,
no_deps, no_deps,
deps, deps,
@ -908,6 +910,10 @@ impl PipCompileSettings {
.into_iter() .into_iter()
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
.collect(), .collect(),
build_constraint: build_constraint
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
r#override: r#override r#override: r#override
.into_iter() .into_iter()
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
@ -961,6 +967,7 @@ impl PipCompileSettings {
pub(crate) struct PipSyncSettings { pub(crate) struct PipSyncSettings {
pub(crate) src_file: Vec<PathBuf>, pub(crate) src_file: Vec<PathBuf>,
pub(crate) constraint: Vec<PathBuf>, pub(crate) constraint: Vec<PathBuf>,
pub(crate) build_constraint: Vec<PathBuf>,
pub(crate) dry_run: bool, pub(crate) dry_run: bool,
pub(crate) refresh: Refresh, pub(crate) refresh: Refresh,
pub(crate) settings: PipSettings, pub(crate) settings: PipSettings,
@ -972,6 +979,7 @@ impl PipSyncSettings {
let PipSyncArgs { let PipSyncArgs {
src_file, src_file,
constraint, constraint,
build_constraint,
installer, installer,
refresh, refresh,
require_hashes, require_hashes,
@ -1009,6 +1017,10 @@ impl PipSyncSettings {
.into_iter() .into_iter()
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
.collect(), .collect(),
build_constraint: build_constraint
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
dry_run, dry_run,
refresh: Refresh::from(refresh), refresh: Refresh::from(refresh),
settings: PipSettings::combine( settings: PipSettings::combine(
@ -1052,6 +1064,7 @@ pub(crate) struct PipInstallSettings {
pub(crate) editable: Vec<String>, pub(crate) editable: Vec<String>,
pub(crate) constraint: Vec<PathBuf>, pub(crate) constraint: Vec<PathBuf>,
pub(crate) r#override: Vec<PathBuf>, pub(crate) r#override: Vec<PathBuf>,
pub(crate) build_constraint: Vec<PathBuf>,
pub(crate) dry_run: bool, pub(crate) dry_run: bool,
pub(crate) constraints_from_workspace: Vec<Requirement>, pub(crate) constraints_from_workspace: Vec<Requirement>,
pub(crate) overrides_from_workspace: Vec<Requirement>, pub(crate) overrides_from_workspace: Vec<Requirement>,
@ -1071,6 +1084,7 @@ impl PipInstallSettings {
extra, extra,
all_extras, all_extras,
no_all_extras, no_all_extras,
build_constraint,
refresh, refresh,
no_deps, no_deps,
deps, deps,
@ -1142,6 +1156,10 @@ impl PipInstallSettings {
.into_iter() .into_iter()
.filter_map(Maybe::into_option) .filter_map(Maybe::into_option)
.collect(), .collect(),
build_constraint: build_constraint
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
dry_run, dry_run,
constraints_from_workspace, constraints_from_workspace,
overrides_from_workspace, overrides_from_workspace,

View file

@ -413,10 +413,10 @@ impl TestContext {
} }
/// Create a `uv help` command with options shared across scenarios. /// Create a `uv help` command with options shared across scenarios.
#[allow(clippy::unused_self)]
pub fn help(&self) -> Command { pub fn help(&self) -> Command {
let mut command = Command::new(get_bin()); let mut command = Command::new(get_bin());
command.arg("help"); command.arg("help");
self.add_shared_args(&mut command);
command command
} }

View file

@ -627,7 +627,7 @@ fn help_unknown_subsubcommand() {
fn help_with_global_option() { fn help_with_global_option() {
let context = TestContext::new_with_versions(&[]); 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 success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----

View file

@ -11508,3 +11508,63 @@ fn ignore_invalid_constraint() -> Result<()> {
Ok(()) 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(())
}

View file

@ -6244,3 +6244,58 @@ fn install_relocatable() -> Result<()> {
Ok(()) 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(())
}

View file

@ -5349,3 +5349,62 @@ fn preserve_markers() -> Result<()> {
Ok(()) 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(())
}

View file

@ -74,6 +74,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -207,6 +208,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -341,6 +343,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -507,6 +510,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -642,6 +646,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -763,6 +768,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -921,6 +927,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -1079,6 +1086,7 @@ fn resolve_index_url() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -1282,6 +1290,7 @@ fn resolve_find_links() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -1439,6 +1448,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -1566,6 +1576,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -1721,6 +1732,7 @@ fn resolve_top_level() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -1900,6 +1912,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -2017,6 +2030,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -2134,6 +2148,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -2253,6 +2268,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -2397,6 +2413,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {
@ -2542,6 +2559,7 @@ fn resolve_both() -> anyhow::Result<()> {
override: [], override: [],
constraints_from_workspace: [], constraints_from_workspace: [],
overrides_from_workspace: [], overrides_from_workspace: [],
build_constraint: [],
refresh: None( refresh: None(
Timestamp( Timestamp(
SystemTime { SystemTime {

View file

@ -108,6 +108,10 @@ uv pip compile [OPTIONS] <SRC_FILE>...
<p>While constraints are <em>additive</em>, in that they&#8217;re combined with the requirements of the constituent packages, overrides are <em>absolute</em>, in that they completely replace the requirements of the constituent packages.</p> <p>While constraints are <em>additive</em>, in that they&#8217;re combined with the requirements of the constituent packages, overrides are <em>absolute</em>, in that they completely replace the requirements of the constituent packages.</p>
</dd><dt><code>--build-constraint</code>, <code>-b</code> <i>build-constraint</i></dt><dd><p>Constrain build dependencies using the given requirements files when building source distributions.</p>
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a requirement that&#8217;s installed. However, including a package in a constraints file will <em>not</em> trigger the installation of that package.</p>
</dd><dt><code>--extra</code> <i>extra</i></dt><dd><p>Include optional dependencies from the extra group name; may be provided more than once.</p> </dd><dt><code>--extra</code> <i>extra</i></dt><dd><p>Include optional dependencies from the extra group name; may be provided more than once.</p>
<p>Only applies to <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p> <p>Only applies to <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>
@ -254,6 +258,10 @@ uv pip sync [OPTIONS] <SRC_FILE>...
<p>This is equivalent to pip&#8217;s <code>--constraint</code> option.</p> <p>This is equivalent to pip&#8217;s <code>--constraint</code> option.</p>
</dd><dt><code>--build-constraint</code>, <code>-b</code> <i>build-constraint</i></dt><dd><p>Constrain build dependencies using the given requirements files when building source distributions.</p>
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a requirement that&#8217;s installed. However, including a package in a constraints file will <em>not</em> trigger the installation of that package.</p>
</dd><dt><code>--index-url</code>, <code>-i</code> <i>index-url</i></dt><dd><p>The URL of the Python package index (by default: &lt;https://pypi.org/simple&gt;).</p> </dd><dt><code>--index-url</code>, <code>-i</code> <i>index-url</i></dt><dd><p>The URL of the Python package index (by default: &lt;https://pypi.org/simple&gt;).</p>
<p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p> <p>Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.</p>
@ -390,6 +398,10 @@ uv pip install [OPTIONS] <PACKAGE|--requirement <REQUIREMENT>|--editable <EDITAB
<p>While constraints are <em>additive</em>, in that they&#8217;re combined with the requirements of the constituent packages, overrides are <em>absolute</em>, in that they completely replace the requirements of the constituent packages.</p> <p>While constraints are <em>additive</em>, in that they&#8217;re combined with the requirements of the constituent packages, overrides are <em>absolute</em>, in that they completely replace the requirements of the constituent packages.</p>
</dd><dt><code>--build-constraint</code>, <code>-b</code> <i>build-constraint</i></dt><dd><p>Constrain build dependencies using the given requirements files when building source distributions.</p>
<p>Constraints files are <code>requirements.txt</code>-like files that only control the <em>version</em> of a requirement that&#8217;s installed. However, including a package in a constraints file will <em>not</em> trigger the installation of that package.</p>
</dd><dt><code>--extra</code> <i>extra</i></dt><dd><p>Include optional dependencies from the extra group name; may be provided more than once.</p> </dd><dt><code>--extra</code> <i>extra</i></dt><dd><p>Include optional dependencies from the extra group name; may be provided more than once.</p>
<p>Only applies to <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p> <p>Only applies to <code>pyproject.toml</code>, <code>setup.py</code>, and <code>setup.cfg</code> sources.</p>