Perform wheel lockfile filtering based on platform and OS intersection (#14976)

## Summary

Ensures that if the user filters to macOS ARM, we don't include macOS
x86_64 wheels.

Closes https://github.com/astral-sh/uv/issues/14901.
This commit is contained in:
Charlie Marsh 2025-07-30 11:12:22 -04:00 committed by GitHub
parent 6856a27711
commit 9b8ff44a04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 224 additions and 3 deletions

View File

@ -105,6 +105,11 @@ impl PlatformTag {
}
impl PlatformTag {
/// Returns `true` if the platform is "any" (i.e., not specific to a platform).
pub fn is_any(&self) -> bool {
matches!(self, Self::Any)
}
/// Returns `true` if the platform is manylinux-only.
pub fn is_manylinux(&self) -> bool {
matches!(

View File

@ -106,6 +106,51 @@ static X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
.unwrap();
UniversalMarker::new(pep508, ConflictMarker::TRUE)
});
static LINUX_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *LINUX_MARKERS;
marker.and(*ARM_MARKERS);
marker
});
static LINUX_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *LINUX_MARKERS;
marker.and(*X86_64_MARKERS);
marker
});
static LINUX_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *LINUX_MARKERS;
marker.and(*X86_MARKERS);
marker
});
static WINDOWS_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *WINDOWS_MARKERS;
marker.and(*ARM_MARKERS);
marker
});
static WINDOWS_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *WINDOWS_MARKERS;
marker.and(*X86_64_MARKERS);
marker
});
static WINDOWS_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *WINDOWS_MARKERS;
marker.and(*X86_MARKERS);
marker
});
static MAC_ARM_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *MAC_MARKERS;
marker.and(*ARM_MARKERS);
marker
});
static MAC_X86_64_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *MAC_MARKERS;
marker.and(*X86_64_MARKERS);
marker
});
static MAC_X86_MARKERS: LazyLock<UniversalMarker> = LazyLock::new(|| {
let mut marker = *MAC_MARKERS;
marker.and(*X86_MARKERS);
marker
});
#[derive(Clone, Debug, serde::Deserialize)]
#[serde(try_from = "LockWire")]
@ -336,14 +381,61 @@ impl Lock {
// a single disjointness check with the intersection is sufficient, so we have one
// constant per platform.
let platform_tags = wheel.filename.platform_tags();
if platform_tags.iter().all(PlatformTag::is_any) {
return true;
}
if platform_tags.iter().all(PlatformTag::is_linux) {
if graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) {
if platform_tags.iter().all(PlatformTag::is_arm) {
if graph.graph[node_index]
.marker()
.is_disjoint(*LINUX_ARM_MARKERS)
{
return false;
}
} else if platform_tags.iter().all(PlatformTag::is_x86_64) {
if graph.graph[node_index]
.marker()
.is_disjoint(*LINUX_X86_64_MARKERS)
{
return false;
}
} else if platform_tags.iter().all(PlatformTag::is_x86) {
if graph.graph[node_index]
.marker()
.is_disjoint(*LINUX_X86_MARKERS)
{
return false;
}
} else if graph.graph[node_index].marker().is_disjoint(*LINUX_MARKERS) {
return false;
}
}
if platform_tags.iter().all(PlatformTag::is_windows) {
if graph.graph[node_index]
if platform_tags.iter().all(PlatformTag::is_arm) {
if graph.graph[node_index]
.marker()
.is_disjoint(*WINDOWS_ARM_MARKERS)
{
return false;
}
} else if platform_tags.iter().all(PlatformTag::is_x86_64) {
if graph.graph[node_index]
.marker()
.is_disjoint(*WINDOWS_X86_64_MARKERS)
{
return false;
}
} else if platform_tags.iter().all(PlatformTag::is_x86) {
if graph.graph[node_index]
.marker()
.is_disjoint(*WINDOWS_X86_MARKERS)
{
return false;
}
} else if graph.graph[node_index]
.marker()
.is_disjoint(*WINDOWS_MARKERS)
{
@ -352,7 +444,28 @@ impl Lock {
}
if platform_tags.iter().all(PlatformTag::is_macos) {
if graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) {
if platform_tags.iter().all(PlatformTag::is_arm) {
if graph.graph[node_index]
.marker()
.is_disjoint(*MAC_ARM_MARKERS)
{
return false;
}
} else if platform_tags.iter().all(PlatformTag::is_x86_64) {
if graph.graph[node_index]
.marker()
.is_disjoint(*MAC_X86_64_MARKERS)
{
return false;
}
} else if platform_tags.iter().all(PlatformTag::is_x86) {
if graph.graph[node_index]
.marker()
.is_disjoint(*MAC_X86_MARKERS)
{
return false;
}
} else if graph.graph[node_index].marker().is_disjoint(*MAC_MARKERS) {
return false;
}
}

View File

@ -30040,3 +30040,106 @@ fn lock_circular_path_dependency_explicit_index() -> Result<()> {
Ok(())
}
#[test]
fn lock_required_intersection() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"numpy",
]
[tool.uv]
environments = [
"(sys_platform=='linux' and platform_machine=='x86_64')",
"(platform_machine=='arm64' and sys_platform=='darwin')"
]
"#,
)?;
uv_snapshot!(context.filters(), context.lock(), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
");
let lock = context.read("uv.lock");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r#"
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
"platform_machine == 'arm64' and sys_platform == 'darwin'",
]
supported-markers = [
"platform_machine == 'x86_64' and sys_platform == 'linux'",
"platform_machine == 'arm64' and sys_platform == 'darwin'",
]
[options]
exclude-newer = "2024-03-25T00:00:00Z"
[[package]]
name = "numpy"
version = "1.26.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" },
{ url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
{ url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
]
[[package]]
name = "project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "numpy", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
]
[package.metadata]
requires-dist = [{ name = "numpy" }]
"#
);
});
// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
");
// Re-run with `--offline`. We shouldn't need a network connection to validate an
// already-correct lockfile with immutable metadata.
uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
"###);
Ok(())
}