Never apply sources

This commit is contained in:
Zanie Blue 2025-07-18 12:37:01 -05:00
parent a0876c43c3
commit c337ceabce
2 changed files with 320 additions and 49 deletions

View File

@ -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<Requirement>,
build_stack: &BuildStack,
) -> Result<Resolution, Error> {
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<Project>), Box<Error>> {
@ -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())),

View File

@ -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(())
}