From d02eef5b32386d72010d0cab50a9a93c2689b622 Mon Sep 17 00:00:00 2001 From: Chao Ning Date: Sun, 16 Feb 2025 00:35:20 +0000 Subject: [PATCH 1/4] Add 'tool.uv.build-constraint-dependencies' to pyproject.toml. Add tests for `uv lock`, `uv add`, `uv pip isntall`, and `uv pip compile`. --- crates/uv-scripts/src/lib.rs | 1 + crates/uv-settings/src/settings.rs | 6 + crates/uv-workspace/src/pyproject.rs | 30 +++ crates/uv-workspace/src/workspace.rs | 20 ++ crates/uv/src/commands/pip/compile.rs | 14 +- crates/uv/src/commands/pip/install.rs | 19 +- crates/uv/src/commands/project/lock.rs | 5 +- crates/uv/src/commands/project/lock_target.rs | 17 ++ crates/uv/src/lib.rs | 2 + crates/uv/src/settings.rs | 32 +++ crates/uv/tests/it/edit.rs | 56 ++++ crates/uv/tests/it/lock.rs | 144 ++++++++++ crates/uv/tests/it/pip_compile.rs | 253 ++++++++++++++++++ crates/uv/tests/it/pip_install.rs | 160 +++++++++++ crates/uv/tests/it/show_settings.rs | 36 ++- uv.schema.json | 10 + 16 files changed, 797 insertions(+), 8 deletions(-) diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index 377cc4a541ee..e969ea329692 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -331,6 +331,7 @@ pub struct ToolUv { pub top_level: ResolverInstallerOptions, pub override_dependencies: Option>>, pub constraint_dependencies: Option>>, + pub build_constraint_dependencies: Option>>, pub sources: Option>, } diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 4b9450898d3a..a91300b64ea7 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -107,6 +107,9 @@ pub struct Options { #[cfg_attr(feature = "schemars", schemars(skip))] pub constraint_dependencies: Option>>, + #[cfg_attr(feature = "schemars", schemars(skip))] + pub build_constraint_dependencies: Option>>, + #[cfg_attr(feature = "schemars", schemars(skip))] pub environments: Option, @@ -1792,6 +1795,7 @@ pub struct OptionsWire { // They're respected in both `pyproject.toml` and `uv.toml` files. override_dependencies: Option>>, constraint_dependencies: Option>>, + build_constraint_dependencies: Option>>, environments: Option, required_environments: Option, @@ -1858,6 +1862,7 @@ impl From for Options { cache_keys, override_dependencies, constraint_dependencies, + build_constraint_dependencies, environments, required_environments, conflicts, @@ -1922,6 +1927,7 @@ impl From for Options { cache_keys, override_dependencies, constraint_dependencies, + build_constraint_dependencies, environments, required_environments, install_mirrors: PythonInstallMirrors::resolve( diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 772f86248128..df14e3ae94e7 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -479,6 +479,36 @@ pub struct ToolUv { )] pub constraint_dependencies: Option>>, + /// Constrains to build dependencies using the given requirements files when building source + /// distributions. + /// + /// Including a package as a constraint will _not_ trigger installation of the package on its + /// own; instead, the package must be requested elsewhere in the project's first-party or + /// transitive dependencies. + /// + /// !!! note + /// In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from + /// the `pyproject.toml` at the workspace root, and will ignore any declarations in other + /// workspace members or `uv.toml` files. + /// + #[cfg_attr( + feature = "schemars", + schemars( + with = "Option>", + description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." + ) + )] + #[option( + default = "[]", + value_type = "list[str]", + example = r#" + # Ensure that the `setuptools 60.0.0` is used to build any packages with a build dependency + on `setuptools`. + build-constraint-dependencies = ["setuptools==60.0.0"] + "# + )] + pub build_constraint_dependencies: Option>>, + /// A list of supported environments against which to resolve dependencies. /// /// By default, uv will resolve for all possible environments during a `uv lock` operation. diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index d973dd40aae5..b2feefe62fb2 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -496,6 +496,20 @@ impl Workspace { constraints.clone() } + /// Returns the set of build constraints for the workspace. + pub fn build_constraints(&self) -> Vec> { + let Some(build_constraints) = self + .pyproject_toml + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.build_constraint_dependencies.as_ref()) + else { + return vec![]; + }; + build_constraints.clone() + } + /// The path to the workspace root, the directory containing the top level `pyproject.toml` with /// the `uv.tool.workspace`, or the `pyproject.toml` in an implicit single workspace project. pub fn install_path(&self) -> &PathBuf { @@ -1725,6 +1739,7 @@ mod tests { "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, + "build-constraint-dependencies": null, "environments": null, "required-environments": null, "conflicts": null @@ -1818,6 +1833,7 @@ mod tests { "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, + "build-constraint-dependencies": null, "environments": null, "required-environments": null, "conflicts": null @@ -2025,6 +2041,7 @@ mod tests { "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, + "build-constraint-dependencies": null, "environments": null, "required-environments": null, "conflicts": null @@ -2130,6 +2147,7 @@ mod tests { "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, + "build-constraint-dependencies": null, "environments": null, "required-environments": null, "conflicts": null @@ -2248,6 +2266,7 @@ mod tests { "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, + "build-constraint-dependencies": null, "environments": null, "required-environments": null, "conflicts": null @@ -2340,6 +2359,7 @@ mod tests { "dev-dependencies": null, "override-dependencies": null, "constraint-dependencies": null, + "build-constraint-dependencies": null, "environments": null, "required-environments": null, "conflicts": null diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index e16410b8ecc5..8fec7d99129f 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -56,6 +56,7 @@ pub(crate) async fn pip_compile( build_constraints: &[RequirementsSource], constraints_from_workspace: Vec, overrides_from_workspace: Vec, + build_constraints_from_workspace: Vec, environments: SupportedEnvironments, extras: ExtrasSpecification, groups: DevGroupsSpecification, @@ -184,8 +185,17 @@ pub(crate) async fn pip_compile( .collect(); // Read build constraints. - let build_constraints = - operations::read_constraints(build_constraints, &client_builder).await?; + let build_constraints: Vec = + operations::read_constraints(build_constraints, &client_builder) + .await? + .iter() + .cloned() + .chain( + build_constraints_from_workspace + .into_iter() + .map(NameRequirementSpecification::from), + ) + .collect(); // 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. diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 525074f65923..b7160b33b3c7 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -51,6 +51,7 @@ pub(crate) async fn pip_install( build_constraints: &[RequirementsSource], constraints_from_workspace: Vec, overrides_from_workspace: Vec, + build_constraints_from_workspace: Vec, extras: &ExtrasSpecification, groups: &DevGroupsSpecification, resolution_mode: ResolutionMode, @@ -123,10 +124,6 @@ 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() @@ -147,6 +144,20 @@ pub(crate) async fn pip_install( ) .collect(); + // Read build constraints. + let build_constraints: Vec = + operations::read_constraints(build_constraints, &client_builder) + .await? + .iter() + .cloned() + .chain( + build_constraints_from_workspace + .iter() + .cloned() + .map(NameRequirementSpecification::from), + ) + .collect(); + // Detect the current Python interpreter. let environment = if target.is_some() || prefix.is_some() { let installation = PythonInstallation::find( diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 03c248416e99..701a7fc40411 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -366,6 +366,7 @@ async fn do_lock( let requirements = target.requirements(); let overrides = target.overrides(); let constraints = target.constraints(); + let build_constraints = target.build_constraints(); let dependency_groups = target.dependency_groups()?; let source_trees = vec![]; @@ -373,6 +374,7 @@ async fn do_lock( let requirements = target.lower(requirements, index_locations, sources)?; let overrides = target.lower(overrides, index_locations, sources)?; let constraints = target.lower(constraints, index_locations, sources)?; + let build_constraints = target.lower(build_constraints, index_locations, sources)?; let dependency_groups = dependency_groups .into_iter() .map(|(name, requirements)| { @@ -556,9 +558,10 @@ async fn do_lock( .build(); let hasher = HashStrategy::Generate(HashGeneration::Url); + let build_constraints = Constraints::from_requirements(build_constraints.iter().cloned()); + // TODO(charlie): These are all default values. We should consider whether we want to make them // optional on the downstream APIs. - let build_constraints = Constraints::default(); let build_hasher = HashStrategy::default(); let extras = ExtrasSpecification::default(); let groups = DevGroupsSpecification::default(); diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index b7c8c209f6b7..ebd67d5b4951 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -79,6 +79,23 @@ impl<'lock> LockTarget<'lock> { } } + /// Returns the set of build constraints for the [`LockTarget`]. + pub(crate) fn build_constraints(self) -> Vec> { + match self { + Self::Workspace(workspace) => workspace.build_constraints(), + Self::Script(script) => script + .metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.build_constraint_dependencies.as_ref()) + .into_iter() + .flatten() + .cloned() + .collect(), + } + } + /// Return the dependency groups that are attached to the target directly, as opposed to being /// attached to any members within the target. pub(crate) fn dependency_groups( diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index d26b4ad3aa00..85b8dcc09394 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -389,6 +389,7 @@ async fn run(mut cli: Cli) -> Result { &build_constraints, args.constraints_from_workspace, args.overrides_from_workspace, + args.build_constraints_from_workspace, args.environments, args.settings.extras, args.settings.groups, @@ -561,6 +562,7 @@ async fn run(mut cli: Cli) -> Result { &build_constraints, args.constraints_from_workspace, args.overrides_from_workspace, + args.build_constraints_from_workspace, &args.settings.extras, &args.settings.groups, args.settings.resolution, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 63ed91655781..f483c03a48aa 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1540,6 +1540,7 @@ pub(crate) struct PipCompileSettings { pub(crate) build_constraints: Vec, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, + pub(crate) build_constraints_from_workspace: Vec, pub(crate) environments: SupportedEnvironments, pub(crate) refresh: Refresh, pub(crate) settings: PipSettings, @@ -1626,6 +1627,20 @@ impl PipCompileSettings { Vec::new() }; + let build_constraints_from_workspace = if let Some(configuration) = &filesystem { + configuration + .build_constraint_dependencies + .clone() + .unwrap_or_default() + .into_iter() + .map(|requirement| { + Requirement::from(requirement.with_origin(RequirementOrigin::Workspace)) + }) + .collect() + } else { + Vec::new() + }; + let environments = if let Some(configuration) = &filesystem { configuration.environments.clone().unwrap_or_default() } else { @@ -1648,6 +1663,7 @@ impl PipCompileSettings { .collect(), constraints_from_workspace, overrides_from_workspace, + build_constraints_from_workspace, environments, refresh: Refresh::from(refresh), settings: PipSettings::combine( @@ -1783,6 +1799,7 @@ pub(crate) struct PipInstallSettings { pub(crate) dry_run: DryRun, pub(crate) constraints_from_workspace: Vec, pub(crate) overrides_from_workspace: Vec, + pub(crate) build_constraints_from_workspace: Vec, pub(crate) modifications: Modifications, pub(crate) refresh: Refresh, pub(crate) settings: PipSettings, @@ -1858,6 +1875,20 @@ impl PipInstallSettings { Vec::new() }; + let build_constraints_from_workspace = if let Some(configuration) = &filesystem { + configuration + .build_constraint_dependencies + .clone() + .unwrap_or_default() + .into_iter() + .map(|requirement| { + Requirement::from(requirement.with_origin(RequirementOrigin::Workspace)) + }) + .collect() + } else { + Vec::new() + }; + Self { package, requirements, @@ -1877,6 +1908,7 @@ impl PipInstallSettings { dry_run: DryRun::from_args(dry_run), constraints_from_workspace, overrides_from_workspace, + build_constraints_from_workspace, modifications: if flag(exact, inexact).unwrap_or(false) { Modifications::Exact } else { diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 3f67356461e3..6e06a7fc44af 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9714,3 +9714,59 @@ fn repeated_index_cli_reversed() -> Result<()> { Ok(()) } + +#[test] +fn add_with_build_constraints() -> Result<()> { + let context = TestContext::new("3.8"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = [] + + [tool.uv] + build-constraint-dependencies = ["setuptools==1"] + "###})?; + + uv_snapshot!(context.filters(), context.add().arg("requests==1.2"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + help: `requests` (v1.2.0) was included because `project` (v0.1.0) depends on `requests==1.2` + "); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = [] + + [tool.uv] + build-constraint-dependencies = ["setuptools>=40"] + "###})?; + + uv_snapshot!(context.filters(), context.add().arg("requests==1.2"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + requests==1.2.0 + "###); + + Ok(()) +} diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 373c918780cf..565bfe3e6e42 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -1883,6 +1883,150 @@ fn lock_project_with_constraint_sources() -> Result<()> { Ok(()) } +/// Lock a project with `uv.tool.build-constraint-dependencies`. +#[test] +fn lock_project_with_build_constraints() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [tool.uv] + constraint-dependencies = ["idna<3.4"] + + # This should be ignored because none of the dependecies requires 'setuptools' + build-constraint-dependencies = ["setuptools==1"] + "###, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // Install the base dependencies from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.3 + + sniffio==1.3.1 + "###); + + // + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r###" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.8" + dependencies = ["requests==1.2"] + + [tool.uv] + build-constraint-dependencies = ["setuptools==1"] + "###, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + help: `requests` (v1.2.0) was included because `project` (v0.1.0) depends on `requests==1.2` + "); + + Ok(()) +} + +/// Lock a project with `uv.tool.build-constraint-dependencies` that reference `tool.uv.sources`. +#[test] +fn lock_project_with_build_constraint_sources() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [tool.uv] + constraint-dependencies = ["idna<3.4"] + + [tool.uv.sources] + idna = { url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" } + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + // Install the base dependencies from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 2 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + idna==3.2 (from https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl) + + sniffio==1.3.1 + "###); + + Ok(()) +} + /// Lock a project with a dependency that has an extra. #[test] fn lock_dependency_extra() -> Result<()> { diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index b20a427231df..3066e8ed5c2e 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -13572,6 +13572,259 @@ fn compatible_build_constraint() -> Result<()> { Ok(()) } +/// Include `build-constraint-dependencies` in pyproject.toml with an incompatible constraint. +#[test] +fn incompatible_build_constraint_in_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [ + "requests==1.2", +] + +[tool.uv] +build-constraint-dependencies = [ + "setuptools==1", +] +"#, + )?; + + uv_snapshot!(context.pip_compile() + .arg("pyproject.toml"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include `build-constraint-dependencies` in pyproject.toml with a compatible constraint. +#[test] +fn compatible_build_constraint_in_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [ + "requests==1.2", +] + +[tool.uv] +build-constraint-dependencies = [ + "setuptools>=40", +] +"#, + )?; + + uv_snapshot!(context.pip_compile() + .arg("pyproject.toml"), @r" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] pyproject.toml + requests==1.2.0 + # via project (pyproject.toml) + + ----- stderr ----- + Resolved 1 package in [TIME] + " + ); + + Ok(()) +} + +/// Merge `build_constraints.txt` with `build-constraint-dependencies` in pyproject.toml with an incompatible constraint. +#[test] +fn incompatible_build_constraint_merged_with_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + + // incompatible setuptools version in pyproject.toml, compatible in build_constraints.txt + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [ + "requests==1.2", +] + +[tool.uv] +build-constraint-dependencies = [ + "setuptools==1", +] +"#, + )?; + + uv_snapshot!(context.pip_compile() + .arg("pyproject.toml") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "### + ); + + // compatible setuptools version in pyproject.toml, incompatible in build_constraints.txt + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==1")?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [ + "requests==1.2", +] + +[tool.uv] +build-constraint-dependencies = [ + "setuptools>=40", +] +"#, + )?; + + uv_snapshot!(context.pip_compile() + .arg("pyproject.toml") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools==1 and setuptools>=40, we can conclude that your requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Merge CLI args `build_constraints.txt` with `build-constraint-dependencies` in pyproject.toml with a compatible constraint. +#[test] +fn compatible_build_constraint_merged_with_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + + // incompatible setuptools version in pyproject.toml, compatible in build_constraints.txt + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [ + "requests==1.2", +] + +[tool.uv] +build-constraint-dependencies = [ + "setuptools>=1", +] +"#, + )?; + + uv_snapshot!(context.pip_compile() + .arg("pyproject.toml") + .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] pyproject.toml --build-constraint build_constraints.txt + requests==1.2.0 + # via project (pyproject.toml) + + ----- stderr ----- + Resolved 1 package in [TIME] + " + ); + + // compatible setuptools version in pyproject.toml, incompatible in build_constraints.txt + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=1")?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[build-system] +requires = ["setuptools"] + +[project] +name = "project" +version = "0.1.0" +dependencies = [ + "requests==1.2", +] + +[tool.uv] +build-constraint-dependencies = [ + "setuptools>=40", +] +"#, + )?; + + uv_snapshot!(context.pip_compile() + .arg("pyproject.toml") + .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] pyproject.toml --build-constraint build_constraints.txt + requests==1.2.0 + # via project (pyproject.toml) + + ----- stderr ----- + Resolved 1 package in [TIME] + " + ); + + Ok(()) +} + /// Ensure that we treat invalid extra markers as `false`, i.e., in projects that define /// non-spec-compliant extras. #[test] diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 2711a6d3fb50..b0f95bd95adf 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7797,6 +7797,166 @@ fn compatible_build_constraint() -> Result<()> { Ok(()) } +/// Include `build-constraint-dependencies` in pyproject.toml with an incompatible constraint. +#[test] +fn incompatible_build_constraint_in_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[tool.uv] +build-constraint-dependencies = [ + "setuptools==1", +] +"#, + )?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40.8.0 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include a `build_constraints.txt` file with a compatible constraint. +#[test] +fn compatible_build_constraint_in_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[tool.uv] +build-constraint-dependencies = [ + "setuptools>=40", +] +"#, + )?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2"), @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(()) +} + +/// Merge `build_constraints.txt` with `build-constraint-dependencies` in pyproject.toml with an incompatible constraint. +#[test] +fn incompatible_build_constraint_merged_with_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + + // incompatible setuptools version in pyproject.toml, compatible in build_constraints.txt + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[tool.uv] +build-constraint-dependencies = [ + "setuptools==1", +] +"#, + )?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools>=40 and setuptools==1, we can conclude that your requirements are unsatisfiable. + "### + ); + + // compatible setuptools version in pyproject.toml, incompatible in build_constraints.txt + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools==1")?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[tool.uv] +build-constraint-dependencies = [ + "setuptools>=40", +] +"#, + )?; + + uv_snapshot!(context.pip_install() + .arg("requests==1.2") + .arg("--build-constraint") + .arg("build_constraints.txt"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × Failed to download and build `requests==1.2.0` + ├─▶ Failed to resolve requirements from `setup.py` build + ├─▶ No solution found when resolving: `setuptools>=40.8.0` + ╰─▶ Because you require setuptools==1 and setuptools>=40, we can conclude that your requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Merge `build_constraints.txt` with `build-constraint-dependencies` in pyproject.toml with a compatible constraint. +#[test] +fn compatible_build_constraint_merged_with_pyproject_toml() -> Result<()> { + let context = TestContext::new("3.8"); + + let constraints_txt = context.temp_dir.child("build_constraints.txt"); + constraints_txt.write_str("setuptools>=40")?; + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[tool.uv] +build-constraint-dependencies = [ + "setuptools>=1", +] +"#, + )?; + + 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(()) +} + #[test] fn install_build_isolation_package() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 9dfd9f97c933..d9b5a3fefc42 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -90,6 +90,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -261,6 +262,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -433,6 +435,7 @@ fn resolve_uv_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -637,6 +640,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -810,6 +814,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -962,6 +967,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -1158,6 +1164,7 @@ fn resolve_index_url() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -1360,6 +1367,7 @@ fn resolve_index_url() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -1616,6 +1624,7 @@ fn resolve_find_links() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -1811,6 +1820,7 @@ fn resolve_top_level() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -1969,6 +1979,7 @@ fn resolve_top_level() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -2169,6 +2180,7 @@ fn resolve_top_level() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -2393,6 +2405,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -2541,6 +2554,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -2689,6 +2703,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -2839,6 +2854,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -3167,6 +3183,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -3343,6 +3360,7 @@ fn resolve_both() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -3637,6 +3655,7 @@ fn resolve_config_file() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -3793,7 +3812,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` + unknown field `project`, expected one of `required-version`, `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `fork-strategy`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `python-install-mirror`, `pypy-install-mirror`, `publish-url`, `trusted-publishing`, `check-url`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `build-constraint-dependencies`, `environments`, `required-environments`, `conflicts`, `workspace`, `sources`, `managed`, `package`, `default-groups`, `dev-dependencies`, `build-backend` "### ); @@ -3909,6 +3928,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -4060,6 +4080,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -4230,6 +4251,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -4392,6 +4414,7 @@ fn index_priority() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -4594,6 +4617,7 @@ fn index_priority() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -4802,6 +4826,7 @@ fn index_priority() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -5005,6 +5030,7 @@ fn index_priority() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -5215,6 +5241,7 @@ fn index_priority() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -5418,6 +5445,7 @@ fn index_priority() -> anyhow::Result<()> { build_constraints: [], constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], environments: SupportedEnvironments( [], ), @@ -5637,6 +5665,7 @@ fn verify_hashes() -> anyhow::Result<()> { dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( Timestamp( @@ -5779,6 +5808,7 @@ fn verify_hashes() -> anyhow::Result<()> { dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( Timestamp( @@ -5919,6 +5949,7 @@ fn verify_hashes() -> anyhow::Result<()> { dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( Timestamp( @@ -6061,6 +6092,7 @@ fn verify_hashes() -> anyhow::Result<()> { dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( Timestamp( @@ -6201,6 +6233,7 @@ fn verify_hashes() -> anyhow::Result<()> { dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( Timestamp( @@ -6342,6 +6375,7 @@ fn verify_hashes() -> anyhow::Result<()> { dry_run: Disabled, constraints_from_workspace: [], overrides_from_workspace: [], + build_constraints_from_workspace: [], modifications: Sufficient, refresh: None( Timestamp( diff --git a/uv.schema.json b/uv.schema.json index df6143bd7ce9..12bce12be932 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -14,6 +14,16 @@ "$ref": "#/definitions/TrustedHost" } }, + "build-constraint-dependencies": { + "description": "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "cache-dir": { "description": "Path to the cache directory.\n\nDefaults to `$HOME/Library/Caches/uv` on macOS, `$XDG_CACHE_HOME/uv` or `$HOME/.cache/uv` on Linux, and `%LOCALAPPDATA%\\uv\\cache` on Windows.", "type": [ From af0640c697341c9fb5a3fc467f50d6324158ca45 Mon Sep 17 00:00:00 2001 From: Chao Ning Date: Mon, 17 Feb 2025 23:06:46 +0000 Subject: [PATCH 2/4] docs: add build-constraint-dependencies documentation --- docs/pip/compile.md | 10 ++++++ docs/reference/settings.md | 35 +++++++++++++++++++ .../troubleshooting/build-failures.md | 14 ++++++++ 3 files changed, 59 insertions(+) diff --git a/docs/pip/compile.md b/docs/pip/compile.md index 95731254b0cb..42ff49c26e68 100644 --- a/docs/pip/compile.md +++ b/docs/pip/compile.md @@ -125,6 +125,16 @@ $ uv pip compile requirements.in --constraint constraints.txt Note that multiple constraints can be defined in each file and multiple files can be used. +uv will also read `constraint-dependencies` from the `pyproject.toml` at the workspace root, and append them to those specified in the constaints file. + +## Adding build constraints + +Similar to `constraints`, but specifically for build-time dependencies, including those required when building runtime dependencies. + +Build constraint files are `requirements.txt`-like files that only control the _version_ of a build-time requirement. However, including a package in a build constraints file will _not_ trigger its installation; instead, constraints apply only when the package is required as a direct or transitive build-time dependency. Build constraints can be used to add bounds to dependencies that are not explicitly declared as build-time dependencies of the current project. + +uv will also read `build-constraint-dependencies` from the `pyproject.toml` at the workspace root, and append them to those specified in the build constraints file. + ## Overriding dependency versions Overrides files are `requirements.txt`-like files that force a specific version of a requirement to diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 4641d7049118..cd73b6e4707b 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1,4 +1,36 @@ ## Project metadata + +### [`build-constraint-dependencies`](#build-constraint-dependencies) {: #build-constraint-dependencies } + +Constraints applied when resolving the project’s build-time dependencies, including build dependencies of runtime dependencies. + +Build constraints restrict the versions of dependencies selected during resolution. They do _not_ trigger installation on their own; instead, they apply only when a package is required as a first-party or transitive build-time dependency. + +When installing a runtime dependency that requires building, its build dependencies will also be constrained by the build-constraint-dependencies settings. + +!!! note + In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from + the `pyproject.toml` at the workspace root, and will ignore any declarations in other + workspace members or `uv.toml` files. + +!!! note + In `uv pip compile` and `uv pip install`, if a build constraints file is provided as `--build-constraints`, uv will read the `build-constraint-dependencies` from `pyproject.toml` at the workspace root, and append them to those specified in the build constraints file. + +**Default value**: `[]` + +**Type**: `list[str]` + +**Example usage**: + +```toml title="pyproject.toml" +[tool.uv] +# Ensure that the setuptools version is always greater than 72.0.0, if it's requested by a +# direct or transitive build-time dependency. +build-constraint-dependencies = ["setuptools>72.0.0"] +``` + +--- + ### [`conflicts`](#conflicts) {: #conflicts } Declare collections of extras or dependency groups that are conflicting @@ -62,6 +94,9 @@ transitive dependencies. the `pyproject.toml` at the workspace root, and will ignore any declarations in other workspace members or `uv.toml` files. +!!! note + In `uv pip compile` and `uv pip install`, if a constraints file is provided as `--constraints`, uv will read the `constraint-dependencies` from `pyproject.toml` at the workspace root, and append them to those specified in the constraints file. + **Default value**: `[]` **Type**: `list[str]` diff --git a/docs/reference/troubleshooting/build-failures.md b/docs/reference/troubleshooting/build-failures.md index f5c751c0da1e..92d6c62e0f3c 100644 --- a/docs/reference/troubleshooting/build-failures.md +++ b/docs/reference/troubleshooting/build-failures.md @@ -288,6 +288,20 @@ uv will avoid using an old version of `apache-beam`. Constraints can also be defined for indirect dependencies using `constraints.txt` files or the [`constraint-dependencies`](../settings.md#constraint-dependencies) setting. +### Old Version of a Build Dependency is Used + +If a package fails to build because `uv` selects an incompatible or outdated version of a build-time dependency, you can enforce constraints specifically for build dependencies. The [`build-constraint-dependencies`](../settings.md#build-constraint-dependencies) setting or a `build-constraints.txt` file can help ensure that `uv` selects an appropriate version of build requirements. + +For example, the issue described in [uv#5551](https://github.com/astral-sh/uv/issues/5551#issuecomment-2256055975) can be addressed by specifying a build constraint that excludes `setuptools` version `72.0.0`, which causes a build failure for certain packages. This can be done as follows: + +```toml title="pyproject.toml" +[tool.uv] +# Prevent setuptools version 72.0.0 from being used as a build dependency +build-constraint-dependencies = ["setuptools!=72.0.0"] +``` + +This ensures that any package requiring `setuptools` during the build process will avoid using the problematic version, preventing build failures caused by incompatible build dependencies. + ### Package is only needed for an unused platform If locking fails due to building a package from a platform you do not need to support, consider From d22fb8eaf5852cd8418cb2dd22bd286a951c4afe Mon Sep 17 00:00:00 2001 From: Chao Ning Date: Mon, 17 Feb 2025 23:23:00 +0000 Subject: [PATCH 3/4] chore: fix typos; reformat --- crates/uv/tests/it/edit.rs | 8 +++---- crates/uv/tests/it/lock.rs | 10 ++++----- docs/pip/compile.md | 15 +++++++++---- docs/reference/settings.md | 22 +++++++------------ .../troubleshooting/build-failures.md | 14 +++++++++--- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 6e06a7fc44af..a6193301478d 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -9720,7 +9720,7 @@ fn add_with_build_constraints() -> Result<()> { let context = TestContext::new("3.8"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! {r###" + pyproject_toml.write_str(indoc! {r#" [project] name = "project" version = "0.1.0" @@ -9729,7 +9729,7 @@ fn add_with_build_constraints() -> Result<()> { [tool.uv] build-constraint-dependencies = ["setuptools==1"] - "###})?; + "#})?; uv_snapshot!(context.filters(), context.add().arg("requests==1.2"), @r" success: false @@ -9745,7 +9745,7 @@ fn add_with_build_constraints() -> Result<()> { "); let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! {r###" + pyproject_toml.write_str(indoc! {r#" [project] name = "project" version = "0.1.0" @@ -9754,7 +9754,7 @@ fn add_with_build_constraints() -> Result<()> { [tool.uv] build-constraint-dependencies = ["setuptools>=40"] - "###})?; + "#})?; uv_snapshot!(context.filters(), context.add().arg("requests==1.2"), @r###" success: true diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 565bfe3e6e42..0c2b49b47532 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -1890,7 +1890,7 @@ fn lock_project_with_build_constraints() -> Result<()> { let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( - r###" + r#" [project] name = "project" version = "0.1.0" @@ -1900,9 +1900,9 @@ fn lock_project_with_build_constraints() -> Result<()> { [tool.uv] constraint-dependencies = ["idna<3.4"] - # This should be ignored because none of the dependecies requires 'setuptools' + # This should be ignored because none of the dependencies requires 'setuptools' build-constraint-dependencies = ["setuptools==1"] - "###, + "#, )?; uv_snapshot!(context.filters(), context.lock(), @r###" @@ -1941,7 +1941,7 @@ fn lock_project_with_build_constraints() -> Result<()> { // let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( - r###" + r#" [project] name = "project" version = "0.1.0" @@ -1950,7 +1950,7 @@ fn lock_project_with_build_constraints() -> Result<()> { [tool.uv] build-constraint-dependencies = ["setuptools==1"] - "###, + "#, )?; uv_snapshot!(context.filters(), context.lock(), @r" diff --git a/docs/pip/compile.md b/docs/pip/compile.md index 42ff49c26e68..490f191ab344 100644 --- a/docs/pip/compile.md +++ b/docs/pip/compile.md @@ -125,15 +125,22 @@ $ uv pip compile requirements.in --constraint constraints.txt Note that multiple constraints can be defined in each file and multiple files can be used. -uv will also read `constraint-dependencies` from the `pyproject.toml` at the workspace root, and append them to those specified in the constaints file. +uv will also read `constraint-dependencies` from the `pyproject.toml` at the workspace root, and +append them to those specified in the constraints file. ## Adding build constraints -Similar to `constraints`, but specifically for build-time dependencies, including those required when building runtime dependencies. +Similar to `constraints`, but specifically for build-time dependencies, including those required +when building runtime dependencies. -Build constraint files are `requirements.txt`-like files that only control the _version_ of a build-time requirement. However, including a package in a build constraints file will _not_ trigger its installation; instead, constraints apply only when the package is required as a direct or transitive build-time dependency. Build constraints can be used to add bounds to dependencies that are not explicitly declared as build-time dependencies of the current project. +Build constraint files are `requirements.txt`-like files that only control the _version_ of a +build-time requirement. However, including a package in a build constraints file will _not_ trigger +its installation; instead, constraints apply only when the package is required as a direct or +transitive build-time dependency. Build constraints can be used to add bounds to dependencies that +are not explicitly declared as build-time dependencies of the current project. -uv will also read `build-constraint-dependencies` from the `pyproject.toml` at the workspace root, and append them to those specified in the build constraints file. +uv will also read `build-constraint-dependencies` from the `pyproject.toml` at the workspace root, +and append them to those specified in the build constraints file. ## Overriding dependency versions diff --git a/docs/reference/settings.md b/docs/reference/settings.md index cd73b6e4707b..6c4f62a25ae2 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1,21 +1,18 @@ ## Project metadata - ### [`build-constraint-dependencies`](#build-constraint-dependencies) {: #build-constraint-dependencies } -Constraints applied when resolving the project’s build-time dependencies, including build dependencies of runtime dependencies. - -Build constraints restrict the versions of dependencies selected during resolution. They do _not_ trigger installation on their own; instead, they apply only when a package is required as a first-party or transitive build-time dependency. +Constrains to build dependencies using the given requirements files when building source +distributions. -When installing a runtime dependency that requires building, its build dependencies will also be constrained by the build-constraint-dependencies settings. +Including a package as a constraint will _not_ trigger installation of the package on its +own; instead, the package must be requested elsewhere in the project's first-party or +transitive dependencies. !!! note In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from the `pyproject.toml` at the workspace root, and will ignore any declarations in other workspace members or `uv.toml` files. -!!! note - In `uv pip compile` and `uv pip install`, if a build constraints file is provided as `--build-constraints`, uv will read the `build-constraint-dependencies` from `pyproject.toml` at the workspace root, and append them to those specified in the build constraints file. - **Default value**: `[]` **Type**: `list[str]` @@ -24,9 +21,9 @@ When installing a runtime dependency that requires building, its build dependenc ```toml title="pyproject.toml" [tool.uv] -# Ensure that the setuptools version is always greater than 72.0.0, if it's requested by a -# direct or transitive build-time dependency. -build-constraint-dependencies = ["setuptools>72.0.0"] +# Ensure that the `setuptools 60.0.0` is used to build any packages with a build dependency +on `setuptools`. +build-constraint-dependencies = ["setuptools==60.0.0"] ``` --- @@ -94,9 +91,6 @@ transitive dependencies. the `pyproject.toml` at the workspace root, and will ignore any declarations in other workspace members or `uv.toml` files. -!!! note - In `uv pip compile` and `uv pip install`, if a constraints file is provided as `--constraints`, uv will read the `constraint-dependencies` from `pyproject.toml` at the workspace root, and append them to those specified in the constraints file. - **Default value**: `[]` **Type**: `list[str]` diff --git a/docs/reference/troubleshooting/build-failures.md b/docs/reference/troubleshooting/build-failures.md index 92d6c62e0f3c..dbda2df5ddcb 100644 --- a/docs/reference/troubleshooting/build-failures.md +++ b/docs/reference/troubleshooting/build-failures.md @@ -290,9 +290,16 @@ Constraints can also be defined for indirect dependencies using `constraints.txt ### Old Version of a Build Dependency is Used -If a package fails to build because `uv` selects an incompatible or outdated version of a build-time dependency, you can enforce constraints specifically for build dependencies. The [`build-constraint-dependencies`](../settings.md#build-constraint-dependencies) setting or a `build-constraints.txt` file can help ensure that `uv` selects an appropriate version of build requirements. +If a package fails to build because `uv` selects an incompatible or outdated version of a build-time +dependency, you can enforce constraints specifically for build dependencies. The +[`build-constraint-dependencies`](../settings.md#build-constraint-dependencies) setting or a +`build-constraints.txt` file can help ensure that `uv` selects an appropriate version of build +requirements. -For example, the issue described in [uv#5551](https://github.com/astral-sh/uv/issues/5551#issuecomment-2256055975) can be addressed by specifying a build constraint that excludes `setuptools` version `72.0.0`, which causes a build failure for certain packages. This can be done as follows: +For example, the issue described in +[uv#5551](https://github.com/astral-sh/uv/issues/5551#issuecomment-2256055975) can be addressed by +specifying a build constraint that excludes `setuptools` version `72.0.0`, which causes a build +failure for certain packages. This can be done as follows: ```toml title="pyproject.toml" [tool.uv] @@ -300,7 +307,8 @@ For example, the issue described in [uv#5551](https://github.com/astral-sh/uv/is build-constraint-dependencies = ["setuptools!=72.0.0"] ``` -This ensures that any package requiring `setuptools` during the build process will avoid using the problematic version, preventing build failures caused by incompatible build dependencies. +This ensures that any package requiring `setuptools` during the build process will avoid using the +problematic version, preventing build failures caused by incompatible build dependencies. ### Package is only needed for an unused platform From 19faba2d80bf399790573e4b90e4879b34fa4911 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 17 Feb 2025 20:19:28 -0500 Subject: [PATCH 4/4] Tweak tests and docs --- crates/uv-workspace/src/pyproject.rs | 17 ++++++----- crates/uv/tests/it/lock.rs | 30 ++++++++----------- crates/uv/tests/it/pip_install.rs | 6 ++-- docs/pip/compile.md | 21 +++++++++++-- docs/reference/settings.md | 16 +++++----- .../troubleshooting/build-failures.md | 20 ++++++------- 6 files changed, 62 insertions(+), 48 deletions(-) diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index df14e3ae94e7..4d7733e63a32 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -479,18 +479,19 @@ pub struct ToolUv { )] pub constraint_dependencies: Option>>, - /// Constrains to build dependencies using the given requirements files when building source - /// distributions. + /// Constraints to apply when solving build dependencies. /// - /// Including a package as a constraint will _not_ trigger installation of the package on its - /// own; instead, the package must be requested elsewhere in the project's first-party or - /// transitive dependencies. + /// Build constraints are used to restrict the versions of build dependencies that are selected + /// when building a package during resolution or installation. + /// + /// Including a package as a constraint will _not_ trigger installation of the package during + /// a build; instead, the package must be requested elsewhere in the project's build dependency + /// graph. /// /// !!! note /// In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from /// the `pyproject.toml` at the workspace root, and will ignore any declarations in other /// workspace members or `uv.toml` files. - /// #[cfg_attr( feature = "schemars", schemars( @@ -502,8 +503,8 @@ pub struct ToolUv { default = "[]", value_type = "list[str]", example = r#" - # Ensure that the `setuptools 60.0.0` is used to build any packages with a build dependency - on `setuptools`. + # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency + # on setuptools. build-constraint-dependencies = ["setuptools==60.0.0"] "# )] diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 0c2b49b47532..94db1c52e676 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -1897,10 +1897,8 @@ fn lock_project_with_build_constraints() -> Result<()> { requires-python = ">=3.12" dependencies = ["anyio==3.7.0"] + # This should be ignored because none of the dependencies requires `setuptools` [tool.uv] - constraint-dependencies = ["idna<3.4"] - - # This should be ignored because none of the dependencies requires 'setuptools' build-constraint-dependencies = ["setuptools==1"] "#, )?; @@ -1934,11 +1932,10 @@ fn lock_project_with_build_constraints() -> Result<()> { Prepared 3 packages in [TIME] Installed 3 packages in [TIME] + anyio==3.7.0 - + idna==3.3 + + idna==3.6 + sniffio==1.3.1 "###); - // let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( r#" @@ -1948,6 +1945,7 @@ fn lock_project_with_build_constraints() -> Result<()> { requires-python = ">=3.8" dependencies = ["requests==1.2"] + # This should fail the operation, since `requests` depends on `setuptools` at build time. [tool.uv] build-constraint-dependencies = ["setuptools==1"] "#, @@ -1972,7 +1970,7 @@ fn lock_project_with_build_constraints() -> Result<()> { /// Lock a project with `uv.tool.build-constraint-dependencies` that reference `tool.uv.sources`. #[test] fn lock_project_with_build_constraint_sources() -> Result<()> { - let context = TestContext::new("3.12"); + let context = TestContext::new("3.9"); let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str( @@ -1980,14 +1978,14 @@ fn lock_project_with_build_constraint_sources() -> Result<()> { [project] name = "project" version = "0.1.0" - requires-python = ">=3.12" - dependencies = ["anyio==3.7.0"] + requires-python = ">=3.8" + dependencies = ["requests==1.2"] [tool.uv] - constraint-dependencies = ["idna<3.4"] + build-constraint-dependencies = ["setuptools==75.8.0"] [tool.uv.sources] - idna = { url = "https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl" } + setuptools = { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl" } "#, )?; @@ -1997,7 +1995,7 @@ fn lock_project_with_build_constraint_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); // Re-run with `--locked`. @@ -2007,7 +2005,7 @@ fn lock_project_with_build_constraint_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 4 packages in [TIME] + Resolved 2 packages in [TIME] "###); // Install the base dependencies from the lockfile. @@ -2017,11 +2015,9 @@ fn lock_project_with_build_constraint_sources() -> Result<()> { ----- stdout ----- ----- stderr ----- - Prepared 2 packages in [TIME] - Installed 3 packages in [TIME] - + anyio==3.7.0 - + idna==3.2 (from https://files.pythonhosted.org/packages/d7/77/ff688d1504cdc4db2a938e2b7b9adee5dd52e34efbd2431051efc9984de9/idna-3.2-py3-none-any.whl) - + sniffio==1.3.1 + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + requests==1.2.0 "###); Ok(()) diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index b0f95bd95adf..7e3ac854aa5b 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -7837,7 +7837,7 @@ fn compatible_build_constraint_in_pyproject_toml() -> Result<()> { pyproject_toml.write_str( r#"[tool.uv] build-constraint-dependencies = [ - "setuptools>=40", + "setuptools==40.8.0", ] "#, )?; @@ -7864,7 +7864,7 @@ build-constraint-dependencies = [ fn incompatible_build_constraint_merged_with_pyproject_toml() -> Result<()> { let context = TestContext::new("3.8"); - // incompatible setuptools version in pyproject.toml, compatible in build_constraints.txt + // Incompatible setuptools version in pyproject.toml, compatible in build_constraints.txt. let constraints_txt = context.temp_dir.child("build_constraints.txt"); constraints_txt.write_str("setuptools>=40")?; let pyproject_toml = context.temp_dir.child("pyproject.toml"); @@ -7892,7 +7892,7 @@ build-constraint-dependencies = [ "### ); - // compatible setuptools version in pyproject.toml, incompatible in build_constraints.txt + // Compatible setuptools version in pyproject.toml, incompatible in build_constraints.txt. let constraints_txt = context.temp_dir.child("build_constraints.txt"); constraints_txt.write_str("setuptools==1")?; let pyproject_toml = context.temp_dir.child("pyproject.toml"); diff --git a/docs/pip/compile.md b/docs/pip/compile.md index 490f191ab344..6451a2ae1024 100644 --- a/docs/pip/compile.md +++ b/docs/pip/compile.md @@ -135,9 +135,24 @@ when building runtime dependencies. Build constraint files are `requirements.txt`-like files that only control the _version_ of a build-time requirement. However, including a package in a build constraints file will _not_ trigger -its installation; instead, constraints apply only when the package is required as a direct or -transitive build-time dependency. Build constraints can be used to add bounds to dependencies that -are not explicitly declared as build-time dependencies of the current project. +its installation at build time; instead, constraints apply only when the package is required as a +direct or transitive build-time dependency. Build constraints can be used to add bounds to +dependencies that are not explicitly declared as build-time dependencies of the current project. + +For example, if a package defines its build dependencies as follows: + +```toml title="pyproject.toml" +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" +``` + +Build constraints could be used to ensure that a specific version of `setuptools` is used for every +package in the workspace: + +```python title="build-constraints.txt" +setuptools==75.0.0 +``` uv will also read `build-constraint-dependencies` from the `pyproject.toml` at the workspace root, and append them to those specified in the build constraints file. diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 6c4f62a25ae2..677eb2dc81ee 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -1,12 +1,14 @@ ## Project metadata ### [`build-constraint-dependencies`](#build-constraint-dependencies) {: #build-constraint-dependencies } -Constrains to build dependencies using the given requirements files when building source -distributions. +Constraints to apply when solving build dependencies. -Including a package as a constraint will _not_ trigger installation of the package on its -own; instead, the package must be requested elsewhere in the project's first-party or -transitive dependencies. +Build constraints are used to restrict the versions of build dependencies that are selected +when building a package during resolution or installation. + +Including a package as a constraint will _not_ trigger installation of the package during +a build; instead, the package must be requested elsewhere in the project's build dependency +graph. !!! note In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from @@ -21,8 +23,8 @@ transitive dependencies. ```toml title="pyproject.toml" [tool.uv] -# Ensure that the `setuptools 60.0.0` is used to build any packages with a build dependency -on `setuptools`. +# Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency +# on setuptools. build-constraint-dependencies = ["setuptools==60.0.0"] ``` diff --git a/docs/reference/troubleshooting/build-failures.md b/docs/reference/troubleshooting/build-failures.md index dbda2df5ddcb..6cfa34754c72 100644 --- a/docs/reference/troubleshooting/build-failures.md +++ b/docs/reference/troubleshooting/build-failures.md @@ -288,27 +288,27 @@ uv will avoid using an old version of `apache-beam`. Constraints can also be defined for indirect dependencies using `constraints.txt` files or the [`constraint-dependencies`](../settings.md#constraint-dependencies) setting. -### Old Version of a Build Dependency is Used +### Old Version of a build dependency is used If a package fails to build because `uv` selects an incompatible or outdated version of a build-time dependency, you can enforce constraints specifically for build dependencies. The -[`build-constraint-dependencies`](../settings.md#build-constraint-dependencies) setting or a -`build-constraints.txt` file can help ensure that `uv` selects an appropriate version of build -requirements. +[`build-constraint-dependencies`](../settings.md#build-constraint-dependencies) setting (or an +analogous `build-constraints.txt` file) can be used to ensure that `uv` selects an appropriate +version of a given build requirements. For example, the issue described in -[uv#5551](https://github.com/astral-sh/uv/issues/5551#issuecomment-2256055975) can be addressed by -specifying a build constraint that excludes `setuptools` version `72.0.0`, which causes a build -failure for certain packages. This can be done as follows: +[#5551](https://github.com/astral-sh/uv/issues/5551#issuecomment-2256055975) could be addressed by +specifying a build constraint that excludes `setuptools` version `72.0.0`: ```toml title="pyproject.toml" [tool.uv] -# Prevent setuptools version 72.0.0 from being used as a build dependency +# Prevent setuptools version 72.0.0 from being used as a build dependency. build-constraint-dependencies = ["setuptools!=72.0.0"] ``` -This ensures that any package requiring `setuptools` during the build process will avoid using the -problematic version, preventing build failures caused by incompatible build dependencies. +The build constraint will thus ensure that any package requiring `setuptools` during the build +process will avoid using the problematic version, preventing build failures caused by incompatible +build dependencies. ### Package is only needed for an unused platform