Respect build constraints for workspace members (#18350)

## Summary

These are respected in `uv sync` and `uv pip install`, but not `uv
build`.

Closes https://github.com/astral-sh/uv/issues/18283
This commit is contained in:
Charlie Marsh
2026-03-06 11:53:07 -05:00
committed by GitHub
parent 24e9b47900
commit 12caaf3160
5 changed files with 305 additions and 3 deletions
+8 -2
View File
@@ -24,7 +24,7 @@ use uv_distribution_filename::{
};
use uv_distribution_types::{
ConfigSettings, DependencyMetadata, ExtraBuildVariables, Index, IndexLocations,
PackageConfigSettings, RequiresPython, SourceDist,
PackageConfigSettings, Requirement, RequiresPython, SourceDist,
};
use uv_fs::{Simplified, relative_to};
use uv_install_wheel::LinkMode;
@@ -113,6 +113,7 @@ pub(crate) async fn build_frontend(
force_pep517: bool,
clear: bool,
build_constraints: Vec<RequirementsSource>,
build_constraints_from_workspace: Vec<Requirement>,
hash_checking: Option<HashCheckingMode>,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
@@ -141,6 +142,7 @@ pub(crate) async fn build_frontend(
force_pep517,
clear,
&build_constraints,
&build_constraints_from_workspace,
hash_checking,
python.as_deref(),
install_mirrors,
@@ -189,6 +191,7 @@ async fn build_impl(
force_pep517: bool,
clear: bool,
build_constraints: &[RequirementsSource],
build_constraints_from_workspace: &[Requirement],
hash_checking: Option<HashCheckingMode>,
python_request: Option<&str>,
install_mirrors: PythonInstallMirrors,
@@ -358,6 +361,7 @@ async fn build_impl(
force_pep517,
clear,
build_constraints,
build_constraints_from_workspace,
build_isolation,
extra_build_dependencies,
extra_build_variables,
@@ -468,6 +472,7 @@ async fn build_package(
force_pep517: bool,
clear: bool,
build_constraints: &[RequirementsSource],
build_constraints_from_workspace: &[Requirement],
build_isolation: &BuildIsolation,
extra_build_dependencies: &ExtraBuildDependencies,
extra_build_variables: &ExtraBuildVariables,
@@ -571,7 +576,8 @@ async fn build_package(
let build_constraints = Constraints::from_requirements(
build_constraints
.into_iter()
.map(|constraint| constraint.requirement),
.map(|constraint| constraint.requirement)
.chain(build_constraints_from_workspace.iter().cloned()),
);
// Initialize the registry client.
+1
View File
@@ -1173,6 +1173,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
args.force_pep517,
args.clear,
build_constraints,
args.build_constraints_from_workspace,
args.hash_checking,
args.python,
args.install_mirrors,
+15
View File
@@ -3364,6 +3364,7 @@ pub(crate) struct BuildSettings {
pub(crate) force_pep517: bool,
pub(crate) clear: bool,
pub(crate) build_constraints: Vec<PathBuf>,
pub(crate) build_constraints_from_workspace: Vec<Requirement>,
pub(crate) hash_checking: Option<HashCheckingMode>,
pub(crate) python: Option<String>,
pub(crate) install_mirrors: PythonInstallMirrors,
@@ -3406,6 +3407,19 @@ impl BuildSettings {
Some(fs) => fs.install_mirrors.clone(),
None => PythonInstallMirrors::default(),
};
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 {
src,
@@ -3424,6 +3438,7 @@ impl BuildSettings {
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
build_constraints_from_workspace,
hash_checking: HashCheckingMode::from_args(
flag(require_hashes, no_require_hashes, "require-hashes"),
flag(verify_hashes, no_verify_hashes, "verify-hashes"),
+185 -1
View File
@@ -7,7 +7,7 @@ use insta::assert_snapshot;
use predicates::prelude::predicate;
use std::env::current_dir;
use uv_static::EnvVars;
use uv_test::{DEFAULT_PYTHON_VERSION, uv_snapshot};
use uv_test::{DEFAULT_PYTHON_VERSION, apply_filters, uv_snapshot};
use zip::ZipArchive;
#[test]
@@ -894,6 +894,190 @@ fn build_constraints() -> Result<()> {
Ok(())
}
/// Regression test for <https://github.com/astral-sh/uv/issues/18283>.
///
/// `uv build --all` should respect `tool.uv.build-constraint-dependencies` declared at the
/// workspace root.
#[test]
fn build_all_respects_workspace_build_constraint_dependencies() -> Result<()> {
let context = uv_test::test_context!("3.12");
let filters = context
.filters()
.into_iter()
.chain([(r"\\\.", ""), (r"\[member\]", "[PKG]")])
.collect::<Vec<_>>();
let project = context.temp_dir.child("project");
project.child("pyproject.toml").write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv]
build-constraint-dependencies = ["hatchling==0.1.0"]
[tool.uv.workspace]
members = ["packages/*"]
"#,
)?;
project
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
project.child("README").touch()?;
let member = project.child("packages").child("member");
fs_err::create_dir_all(member.path())?;
member.child("pyproject.toml").write_str(
r#"
[project]
name = "member"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling>=1.0"]
build-backend = "hatchling.build"
"#,
)?;
member
.child("src")
.child("member")
.child("__init__.py")
.touch()?;
member.child("README").touch()?;
let output = context
.build()
.arg("--all")
.arg("--no-build-logs")
.current_dir(&project)
.output()?;
let stderr = apply_filters(
String::from_utf8_lossy(&output.stderr).into_owned(),
&filters,
);
assert!(
!output.status.success(),
"expected `uv build --all` to fail when workspace build constraints make `hatchling` \
unsatisfiable, but it succeeded:\n{stderr}"
);
assert_eq!(output.status.code(), Some(2));
assert!(
stderr.contains("Failed to resolve requirements from `build-system.requires`"),
"expected build constraint failure in stderr:\n{stderr}"
);
assert!(
stderr.contains(
"Because you require hatchling>=1.0 and hatchling==0.1.0, we can conclude that your requirements are unsatisfiable."
),
"expected incompatible build constraints in stderr:\n{stderr}"
);
project
.child("dist")
.child("member-0.1.0.tar.gz")
.assert(predicate::path::missing());
project
.child("dist")
.child("member-0.1.0-py3-none-any.whl")
.assert(predicate::path::missing());
Ok(())
}
/// Limitation: when `uv build` is invoked with an explicit source path, configuration is still
/// loaded from the invocation directory instead of the source workspace. As a result, workspace
/// `build-constraint-dependencies` are not applied here.
#[test]
fn build_source_path_ignores_workspace_build_constraint_dependencies() -> Result<()> {
let context = uv_test::test_context!("3.12");
let filters = context
.filters()
.into_iter()
.chain([(r"\\\.", ""), (r"\[member\]", "[PKG]")])
.collect::<Vec<_>>();
let project = context.temp_dir.child("project");
project.child("pyproject.toml").write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
[tool.uv]
build-constraint-dependencies = ["hatchling==0.1.0"]
[tool.uv.workspace]
members = ["packages/*"]
"#,
)?;
project
.child("src")
.child("project")
.child("__init__.py")
.touch()?;
project.child("README").touch()?;
let member = project.child("packages").child("member");
fs_err::create_dir_all(member.path())?;
member.child("pyproject.toml").write_str(
r#"
[project]
name = "member"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["hatchling>=1.0"]
build-backend = "hatchling.build"
"#,
)?;
member
.child("src")
.child("member")
.child("__init__.py")
.touch()?;
member.child("README").touch()?;
uv_snapshot!(&filters, context.build().arg("./project").arg("--all").arg("--no-build-logs"), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[PKG] Building source distribution...
[PKG] Building wheel from source distribution...
Successfully built project/dist/member-0.1.0.tar.gz
Successfully built project/dist/member-0.1.0-py3-none-any.whl
");
project
.child("dist")
.child("member-0.1.0.tar.gz")
.assert(predicate::path::is_file());
project
.child("dist")
.child("member-0.1.0-py3-none-any.whl")
.assert(predicate::path::is_file());
Ok(())
}
#[test]
fn build_sha() -> Result<()> {
let context = uv_test::test_context!(DEFAULT_PYTHON_VERSION);
+96
View File
@@ -12342,6 +12342,102 @@ fn sync_build_constraints() -> Result<()> {
Ok(())
}
/// `uv sync --package <member>` should respect workspace build constraints for the selected
/// member's transitive build dependencies.
///
/// See: <https://github.com/astral-sh/uv/issues/12434>
#[test]
fn sync_workspace_member_build_constraints() -> Result<()> {
let context = uv_test::test_context!("3.12")
.with_exclude_newer("2025-03-24T19:00:00Z")
.with_filtered_counts();
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[tool.uv]
build-constraint-dependencies = ["setuptools<78"]
[tool.uv.sources]
child = { workspace = true }
[tool.uv.workspace]
members = ["child"]
"#,
)?;
context
.temp_dir
.child("src")
.child("root")
.child("__init__.py")
.touch()?;
let child = context.temp_dir.child("child");
child.child("pyproject.toml").write_str(
r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["json-merge-patch"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#,
)?;
child
.child("src")
.child("child")
.child("__init__.py")
.touch()?;
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--no-binary-package").arg("json-merge-patch"), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ json-merge-patch==0.2
");
let lock = context.read("uv.lock");
assert!(
lock.contains(r#"build-constraints = [{ name = "setuptools", specifier = "<78" }]"#),
"expected workspace build constraints to be recorded in the lockfile:\n{lock}"
);
fs_err::remove_dir_all(&context.cache_dir)?;
fs_err::remove_dir_all(&context.venv)?;
uv_snapshot!(context.filters(), context.sync().arg("--package").arg("child").arg("--locked").arg("--no-binary-package").arg("json-merge-patch"), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ json-merge-patch==0.2
");
Ok(())
}
// Test that we recreate a virtual environment when `pyvenv.cfg` version
// is incompatible with the interpreter version.
#[test]