From c337ceabcece58e40f062fe58c26f3e628d429ca Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 18 Jul 2025 12:37:01 -0500 Subject: [PATCH] Never apply sources --- crates/uv-build-frontend/src/lib.rs | 60 +++--- crates/uv/tests/it/sync.rs | 309 ++++++++++++++++++++++++++-- 2 files changed, 320 insertions(+), 49 deletions(-) diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index a2f23a904..7f1685bc1 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -4,6 +4,7 @@ mod error; +use std::borrow::Cow; use std::ffi::OsString; use std::fmt::Formatter; use std::fmt::Write; @@ -306,7 +307,6 @@ impl SourceBuild { fallback_package_name, locations, source_strategy, - extra_build_dependencies, workspace_cache, &default_backend, ) @@ -324,6 +324,14 @@ impl SourceBuild { .or(fallback_package_version) .cloned(); + let extra_build_dependencies = package_name + .as_ref() + .and_then(|name| extra_build_dependencies.get(name).cloned()) + .unwrap_or_default() + .into_iter() + .map(Requirement::from) + .collect(); + // Create a virtual environment, or install into the shared environment if requested. let venv = if let Some(venv) = build_isolation.shared_environment(package_name.as_ref()) { venv.clone() @@ -351,10 +359,13 @@ impl SourceBuild { source_build_context, &default_backend, &pep517_backend, + extra_build_dependencies, build_stack, ) .await?; + // TODO(zanieb): We'll report `build-system.requires` here but it may include + // `extra-build-dependencies` build_context .install(&resolved_requirements, &venv, build_stack) .await @@ -473,10 +484,13 @@ impl SourceBuild { source_build_context: SourceBuildContext, default_backend: &Pep517Backend, pep517_backend: &Pep517Backend, + extra_build_dependencies: Vec, build_stack: &BuildStack, ) -> Result { Ok( - if pep517_backend.requirements == default_backend.requirements { + if pep517_backend.requirements == default_backend.requirements + && extra_build_dependencies.is_empty() + { let mut resolution = source_build_context.default_resolution.lock().await; if let Some(resolved_requirements) = &*resolution { resolved_requirements.clone() @@ -491,8 +505,21 @@ impl SourceBuild { resolved_requirements } } else { + // TODO(zanieb): It's unclear if we actually want to solve these together. We might + // want to perform a separate solve to allow conflicts? + let requirements = if extra_build_dependencies.is_empty() { + Cow::Borrowed(&pep517_backend.requirements) + } else { + // If there are extra build dependencies, we need to resolve them together with + // the backend requirements. + let mut requirements = pep517_backend.requirements.clone(); + requirements.extend(extra_build_dependencies); + Cow::Owned(requirements) + }; + // TODO(zanieb): We'll report `build-system.requires` here but it may include + // `extra-build-dependencies` build_context - .resolve(&pep517_backend.requirements, build_stack) + .resolve(&requirements, build_stack) .await .map_err(|err| { Error::RequirementsResolve("`build-system.requires`", err.into()) @@ -508,7 +535,6 @@ impl SourceBuild { package_name: Option<&PackageName>, locations: &IndexLocations, source_strategy: SourceStrategy, - extra_build_dependencies: &ExtraBuildDependencies, workspace_cache: &WorkspaceCache, default_backend: &Pep517Backend, ) -> Result<(Pep517Backend, Option), Box> { @@ -525,10 +551,6 @@ impl SourceBuild { .as_ref() .map(|project| &project.name) .or(package_name); - let extra_build_dependencies = name - .as_ref() - .and_then(|name| extra_build_dependencies.get(name).cloned()) - .unwrap_or_default(); let backend = if let Some(build_system) = pyproject_toml.build_system { // If necessary, lower the requirements. @@ -554,9 +576,6 @@ impl SourceBuild { .requires .into_iter() .map(Requirement::from) - .chain( - extra_build_dependencies.into_iter().map(Requirement::from), - ) .collect() } } @@ -564,7 +583,6 @@ impl SourceBuild { .requires .into_iter() .map(Requirement::from) - .chain(extra_build_dependencies.into_iter().map(Requirement::from)) .collect(), }; @@ -617,12 +635,8 @@ impl SourceBuild { ); } } - let mut backend = default_backend.clone(); - // Apply extra build dependencies - backend - .requirements - .extend(extra_build_dependencies.into_iter().map(Requirement::from)); - backend + + default_backend.clone() }; Ok((backend, pyproject_toml.project)) } @@ -637,15 +651,7 @@ impl SourceBuild { // the default backend, to match `build`. `pip` uses `setup.py` directly in this // case, but plans to make PEP 517 builds the default in the future. // See: https://github.com/pypa/pip/issues/9175. - let mut backend = default_backend.clone(); - // Apply extra build dependencies - let extra_build_dependencies = package_name - .as_ref() - .and_then(|name| extra_build_dependencies.get(name).cloned()) - .unwrap_or_default(); - backend - .requirements - .extend(extra_build_dependencies.into_iter().map(Requirement::from)); + let backend = default_backend.clone(); Ok((backend, None)) } Err(err) => Err(Box::new(err.into())), diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 44acaf2ab..73c628657 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -1527,15 +1527,17 @@ fn sync_build_isolation_extra() -> Result<()> { Ok(()) } -/// Use dedicated extra groups to install dependencies for `--no-build-isolation-package`. #[test] -fn sync_build_isolation_fail() -> Result<()> { +fn sync_extra_build_dependencies() -> Result<()> { let context = TestContext::new("3.12").with_filtered_counts(); - let pyproject_toml = context.temp_dir.child("pyproject.toml"); - pyproject_toml.write_str(indoc! {r#" + // Write a test package that arbitrarily requires `anyio` at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" [project] - name = "project" + name = "child" version = "0.1.0" requires-python = ">=3.9" @@ -1544,7 +1546,7 @@ fn sync_build_isolation_fail() -> Result<()> { backend-path = ["."] build-backend = "build_backend" "#})?; - let build_backend = context.temp_dir.child("build_backend.py"); + let build_backend = child.child("build_backend.py"); build_backend.write_str(indoc! {r#" import sys @@ -1553,46 +1555,59 @@ fn sync_build_isolation_fail() -> Result<()> { try: import anyio except ModuleNotFoundError: - print("Missing `anyio` module to build package", file=sys.stderr) + print("Missing `anyio` module", file=sys.stderr) sys.exit(1) "#})?; - context.temp_dir.child("src/project/__init__.py").touch()?; + child.child("src/child/__init__.py").touch()?; + + let parent = &context.temp_dir; + let pyproject_toml = parent.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child"] + + [tool.uv.sources] + child = { path = "child" } + "#})?; // Running `uv sync` should fail due to missing build-dependencies - uv_snapshot!(context.filters(), context.sync(), @r" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- Resolved [N] packages in [TIME] - × Failed to build `project @ file://[TEMP_DIR]/` + × Failed to build `child @ file://[TEMP_DIR]/child` ├─▶ The build backend returned an error - ╰─▶ Call to `build_backend.build_editable` failed (exit status: 1) + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) [stderr] - Missing `anyio` module to build package + Missing `anyio` module hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `parent` (v0.1.0) depends on `child` "); - // Adding extra-build-dependencies should solve the issue + // Adding `extra-build-dependencies` should solve the issue pyproject_toml.write_str(indoc! {r#" [project] - name = "project" + name = "parent" version = "0.1.0" requires-python = ">=3.9" + dependencies = ["child"] - [build-system] - requires = ["hatchling"] - backend-path = ["."] - build-backend = "build_backend" + [tool.uv.sources] + child = { path = "child" } [tool.uv.extra-build-dependencies] - project = ["anyio"] + child = ["anyio"] "#})?; - uv_snapshot!(context.filters(), context.sync(), @r" + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1601,10 +1616,260 @@ fn sync_build_isolation_fail() -> Result<()> { Resolved [N] packages in [TIME] Prepared [N] packages in [TIME] Installed [N] packages in [TIME] - + project==0.1.0 (from file://[TEMP_DIR]/) + + child==0.1.0 (from file://[TEMP_DIR]/child) "); - assert!(context.temp_dir.child("uv.lock").exists()); + // Adding `extra-build-dependencies` with the wrong name should not + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child"] + + [tool.uv.sources] + child = { path = "child" } + + [tool.uv.extra-build-dependencies] + wrong_name = ["anyio"] + "#})?; + + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Missing `anyio` module + + hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `parent` (v0.1.0) depends on `child` + "); + + // Write a test package that arbitrarily bans `anyio` at build time + let bad_child = context.temp_dir.child("bad_child"); + bad_child.create_dir_all()?; + let bad_child_pyproject_toml = bad_child.child("pyproject.toml"); + bad_child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "bad_child" + version = "0.1.0" + requires-python = ">=3.9" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = bad_child.child("build_backend.py"); + build_backend.write_str(indoc! {r#" + import sys + + from hatchling.build import * + + try: + import anyio + except ModuleNotFoundError: + pass + else: + print("Found `anyio` module", file=sys.stderr) + sys.exit(1) + "#})?; + bad_child.child("src/bad_child/__init__.py").touch()?; + + // Depend on `bad_child` too + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child", "bad_child"] + + [tool.uv.sources] + child = { path = "child" } + bad_child = { path = "bad_child" } + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + bad_child = ["anyio"] + "#})?; + + // Confirm that `bad_child` fails if anyio is provided + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `bad-child @ file://[TEMP_DIR]/bad_child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Found `anyio` module + + hint: This usually indicates a problem with the package or the build environment. + help: `bad-child` was included because `parent` (v0.1.0) depends on `bad-child` + "); + + // But `anyio` is not provided to `bad_child` if scoped to `child` + pyproject_toml.write_str(indoc! {r#" + [project] + name = "parent" + version = "0.1.0" + requires-python = ">=3.9" + dependencies = ["child", "bad_child"] + + [tool.uv.sources] + child = { path = "child" } + bad_child = { path = "bad_child" } + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + "#})?; + + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + Prepared [N] packages in [TIME] + Uninstalled [N] packages in [TIME] + Installed [N] packages in [TIME] + + bad-child==0.1.0 (from file://[TEMP_DIR]/bad_child) + ~ child==0.1.0 (from file://[TEMP_DIR]/child) + "); + + Ok(()) +} + +#[test] +fn sync_extra_build_dependencies_sources() -> Result<()> { + let context = TestContext::new("3.12").with_filtered_counts(); + + let anyio_local = context.workspace_root.join("scripts/packages/anyio_local"); + + // Write a test package that arbitrarily requires `anyio` at a specific _path_ at build time + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(indoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + "#})?; + let build_backend = child.child("build_backend.py"); + build_backend.write_str(&formatdoc! {r#" + import sys + + from hatchling.build import * + + try: + import anyio + except ModuleNotFoundError: + print("Missing `anyio` module", file=sys.stderr) + sys.exit(1) + + if not anyio.__file__.startswith("{0}"): + print("Found anyio at", anyio.__file__, file=sys.stderr) + print("Expected {0}", file=sys.stderr) + sys.exit(1) + "#, anyio_local.display() + })?; + child.child("src/child/__init__.py").touch()?; + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&formatdoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["child"] + + [tool.uv.sources] + anyio = {{ path = "{anyio_local}" }} + child = {{ path = "child" }} + + [tool.uv.extra-build-dependencies] + child = ["anyio"] + "#, + anyio_local = anyio_local.portable_display(), + })?; + + // Running `uv sync` should fail due to the unapplied source + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Found anyio at [CACHE_DIR]/builds-v0/[TMP]/__init__.py + Expected [WORKSPACE]/scripts/packages/anyio_local + + hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `project` (v0.1.0) depends on `child` + "); + + // We also don't apply sources from the child + let child = context.temp_dir.child("child"); + child.create_dir_all()?; + let child_pyproject_toml = child.child("pyproject.toml"); + child_pyproject_toml.write_str(&formatdoc! {r#" + [project] + name = "child" + version = "0.1.0" + requires-python = ">=3.9" + + [build-system] + requires = ["hatchling"] + backend-path = ["."] + build-backend = "build_backend" + + [tool.uv.sources] + anyio = {{ path = "{}" }} + "#, anyio_local.display() + })?; + + // Running `uv sync` should fail due to the unapplied source + uv_snapshot!(context.filters(), context.sync().arg("--reinstall").arg("--refresh"), @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved [N] packages in [TIME] + × Failed to build `child @ file://[TEMP_DIR]/child` + ├─▶ The build backend returned an error + ╰─▶ Call to `build_backend.build_wheel` failed (exit status: 1) + + [stderr] + Found anyio at [CACHE_DIR]/builds-v0/[TMP]/__init__.py + Expected [WORKSPACE]/scripts/packages/anyio_local + + hint: This usually indicates a problem with the package or the build environment. + help: `child` was included because `project` (v0.1.0) depends on `child` + "); Ok(()) }