Support match-runtime = true in the uv pip CLI (#15087)

## Summary

Pretty straightforward, a ~one line change plus recreating the
`BuildDispatch` (which I tried to avoid, but ran into lifetime issues).
This commit is contained in:
Charlie Marsh
2025-08-05 21:03:10 +01:00
committed by GitHub
parent 1fb0fa045c
commit bda9ea957a
5 changed files with 272 additions and 9 deletions
@@ -71,10 +71,7 @@ impl CacheKey for ExtraBuildRequirement {
impl ExtraBuildRequires {
/// Apply runtime constraints from a resolution to the extra build requirements.
pub fn match_runtime(
self,
resolution: &Resolution,
) -> Result<ExtraBuildRequires, ExtraBuildRequiresError> {
pub fn match_runtime(self, resolution: &Resolution) -> Result<Self, ExtraBuildRequiresError> {
self.into_iter()
.map(|(name, requirements)| {
let requirements = requirements
@@ -104,6 +101,6 @@ impl ExtraBuildRequires {
.collect::<Result<Vec<_>, _>>()?;
Ok::<_, ExtraBuildRequiresError>((name, requirements))
})
.collect::<Result<ExtraBuildRequires, _>>()
.collect::<Result<Self, _>>()
}
}
+2 -2
View File
@@ -821,8 +821,8 @@ impl TryFrom<ExtraBuildDependencyWire> for ExtraBuildDependency {
}
impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
fn from(item: ExtraBuildDependency) -> ExtraBuildDependencyWire {
ExtraBuildDependencyWire::Annotated {
fn from(item: ExtraBuildDependency) -> Self {
Self::Annotated {
requirement: item.requirement,
match_runtime: item.match_runtime,
}
+29 -1
View File
@@ -511,7 +511,7 @@ pub(crate) async fn pip_install(
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
.dependency_mode(dependency_mode)
.exclude_newer(exclude_newer)
.exclude_newer(exclude_newer.clone())
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
@@ -559,6 +559,34 @@ pub(crate) async fn pip_install(
(resolution, hasher)
};
// Constrain any build requirements marked as `match-runtime = true`.
let extra_build_requires = extra_build_requires.match_runtime(&resolution)?;
// Create a build dispatch.
let build_dispatch = BuildDispatch::new(
&client,
&cache,
&build_constraints,
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
state.clone(),
index_strategy,
config_settings,
config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
&build_options,
&hasher,
exclude_newer.clone(),
sources,
WorkspaceCache::default(),
concurrency,
preview,
);
// Sync the environment.
match operations::install(
&resolution,
+29 -1
View File
@@ -449,7 +449,7 @@ pub(crate) async fn pip_sync(
.resolution_mode(resolution_mode)
.prerelease_mode(prerelease_mode)
.dependency_mode(dependency_mode)
.exclude_newer(exclude_newer)
.exclude_newer(exclude_newer.clone())
.index_strategy(index_strategy)
.torch_backend(torch_backend)
.build_options(build_options.clone())
@@ -496,6 +496,34 @@ pub(crate) async fn pip_sync(
(resolution, hasher)
};
// Constrain any build requirements marked as `match-runtime = true`.
let extra_build_requires = extra_build_requires.match_runtime(&resolution)?;
// Create a build dispatch.
let build_dispatch = BuildDispatch::new(
&client,
&cache,
&build_constraints,
interpreter,
&index_locations,
&flat_index,
&dependency_metadata,
state.clone(),
index_strategy,
config_settings,
config_settings_package,
build_isolation,
&extra_build_requires,
link_mode,
&build_options,
&build_hasher,
exclude_newer.clone(),
sources,
WorkspaceCache::default(),
concurrency,
preview,
);
// Sync the environment.
match operations::install(
&resolution,
+210
View File
@@ -12033,3 +12033,213 @@ fn config_settings_package() -> Result<()> {
Ok(())
}
/// Test that build dependencies respect locked versions from the resolution.
#[test]
fn pip_install_build_dependencies_respect_locked_versions() -> Result<()> {
let context = TestContext::new("3.12").with_filtered_counts();
// 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 = "child"
version = "0.1.0"
requires-python = ">=3.9"
[build-system]
requires = ["hatchling", "anyio"]
backend-path = ["."]
build-backend = "build_backend"
"#})?;
// Create a build backend that checks for a specific version of anyio
let build_backend = child.child("build_backend.py");
build_backend.write_str(indoc! {r#"
import os
import sys
from hatchling.build import *
expected_version = os.environ.get("EXPECTED_ANYIO_VERSION", "")
if not expected_version:
print("`EXPECTED_ANYIO_VERSION` not set", file=sys.stderr)
sys.exit(1)
try:
import anyio
except ModuleNotFoundError:
print("Missing `anyio` module", file=sys.stderr)
sys.exit(1)
from importlib.metadata import version
anyio_version = version("anyio")
if not anyio_version.startswith(expected_version):
print(f"Expected `anyio` version {expected_version} but got {anyio_version}", file=sys.stderr)
sys.exit(1)
print(f"Found expected `anyio` version {anyio_version}", file=sys.stderr)
"#})?;
child.child("src/child/__init__.py").touch()?;
// Create a project that will resolve to a non-latest version of `anyio`
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 = ["anyio<4.1"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
"#})?;
context
.temp_dir
.child("src")
.child("parent")
.child("__init__.py")
.touch()?;
// Now add the child dependency.
pyproject_toml.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["anyio<4.1", "child"]
[tool.uv.sources]
child = { path = "child" }
"#})?;
// Ensure our build backend is checking the version correctly
uv_snapshot!(context.filters(), context.pip_install().arg(".").env("EXPECTED_ANYIO_VERSION", "3.0"), @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]
Expected `anyio` version 3.0 but got 4.3.0
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`
");
// Now constrain the `anyio` build dependency to match the runtime
pyproject_toml.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["anyio<4.1", "child"]
[tool.uv.sources]
child = { path = "child" }
[tool.uv.extra-build-dependencies]
child = [{ requirement = "anyio", match-runtime = true }]
"#})?;
// The child should be built with anyio 4.0
uv_snapshot!(context.filters(), context.pip_install().arg(".").env("EXPECTED_ANYIO_VERSION", "4.0"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ anyio==4.0.0
+ child==0.1.0 (from file://[TEMP_DIR]/child)
+ idna==3.6
+ parent==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
");
// Change the constraints on anyio
pyproject_toml.write_str(indoc! {r#"
[project]
name = "parent"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = ["anyio<3.8", "child"]
[tool.uv.sources]
child = { path = "child" }
[tool.uv.extra-build-dependencies]
child = [{ requirement = "anyio", match-runtime = true }]
"#})?;
// The child should be rebuilt with anyio 3.7, without `--reinstall`
uv_snapshot!(context.filters(), context.pip_install().arg(".")
.arg("--reinstall-package").arg("child").env("EXPECTED_ANYIO_VERSION", "4.0"), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
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]
Expected `anyio` version 4.0 but got 3.7.1
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`
");
uv_snapshot!(context.filters(), context.pip_install().arg(".")
.arg("--reinstall-package").arg("child").env("EXPECTED_ANYIO_VERSION", "3.7"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: The `extra-build-dependencies` option is experimental and may change without warning. Pass `--preview-features extra-build-dependencies` to disable this warning.
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Uninstalled [N] packages in [TIME]
Installed [N] packages in [TIME]
- anyio==4.0.0
+ anyio==3.7.1
~ child==0.1.0 (from file://[TEMP_DIR]/child)
~ parent==0.1.0 (from file://[TEMP_DIR]/)
");
// With preview enabled, there's no warning
uv_snapshot!(context.filters(), context.pip_install().arg(".")
.arg("--preview-features").arg("extra-build-dependencies")
.arg("--reinstall-package").arg("child")
.env("EXPECTED_ANYIO_VERSION", "3.7"), @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]
~ child==0.1.0 (from file://[TEMP_DIR]/child)
~ parent==0.1.0 (from file://[TEMP_DIR]/)
");
Ok(())
}