mirror of
https://github.com/astral-sh/uv
synced 2026-06-21 13:47:25 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user