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:
@@ -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, _>>()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user