Skip to content

Commit

Permalink
Propagate fork markers to extras (#6065)
Browse files Browse the repository at this point in the history
## Summary

When constructing the `Resolution`, we only propagated the fork markers
to the package node, but not the extras node. This led to cases in which
an extra could be included unconditionally or otherwise diverge from the
base package version.

Closes #6062.
  • Loading branch information
charliermarsh authored Aug 14, 2024
1 parent 8c8f723 commit 4a902a7
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 83 deletions.
31 changes: 26 additions & 5 deletions crates/uv-resolver/src/resolution/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,30 @@ impl ResolutionGraph {
)?;
}
}

let mut seen = FxHashSet::default();
for resolution in resolutions {
// Add every edge to the graph.
let marker = resolution
.markers
.fork_markers()
.cloned()
.unwrap_or_default();

// Add every edge to the graph, propagating the marker for the current fork, if
// necessary.
for edge in &resolution.edges {
if !seen.insert(edge) {
if !seen.insert((edge, marker.clone())) {
// Insert each node only once.
continue;
}

Self::add_edge(&mut petgraph, &mut inverse, root_index, edge);
Self::add_edge(
&mut petgraph,
&mut inverse,
root_index,
edge,
marker.clone(),
);
}
}

Expand Down Expand Up @@ -216,6 +230,7 @@ impl ResolutionGraph {
inverse: &mut FxHashMap<PackageRef<'_>, NodeIndex>,
root_index: NodeIndex,
edge: &ResolutionDependencyEdge,
marker: MarkerTree,
) {
let from_index = edge.from.as_ref().map_or(root_index, |from| {
inverse[&PackageRef {
Expand All @@ -234,15 +249,21 @@ impl ResolutionGraph {
group: edge.to_dev.as_ref(),
}];

let edge_marker = {
let mut edge_marker = edge.marker.clone();
edge_marker.and(marker);
edge_marker
};

if let Some(marker) = petgraph
.find_edge(from_index, to_index)
.and_then(|edge| petgraph.edge_weight_mut(edge))
{
// If either the existing marker or new marker is `true`, then the dependency is
// included unconditionally, and so the combined marker is `true`.
marker.or(edge.marker.clone());
marker.or(edge_marker);
} else {
petgraph.update_edge(from_index, to_index, edge.marker.clone());
petgraph.update_edge(from_index, to_index, edge_marker);
}
}

Expand Down
31 changes: 1 addition & 30 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2353,36 +2353,7 @@ impl ForkState {
to_url: to_url.cloned(),
to_extra: dependency_extra.clone(),
to_dev: dependency_dev.clone(),
// This propagates markers from the fork to
// packages without any markers. These might wind
// up be duplicative (and are even further merged
// via disjunction when a ResolutionGraph is
// constructed), but normalization should simplify
// most such cases.
//
// In a previous implementation of marker
// propagation, markers were propagated at the
// time a fork was created. But this was crucially
// missing a key detail: the specific version of
// a package outside of a fork can be determined
// by the forks of its dependencies, even when
// that package is not part of a fork at the time
// the forks were created. In that case, it was
// possible for two versions of the same package
// to be unconditionally included in a resolution,
// which must never be.
//
// See https://github.com/astral-sh/uv/pull/5583
// for an example of where this occurs with
// `Sphinx`.
//
// Here, instead, we do the marker propagation
// after resolution has completed. This relies
// on the fact that the markers aren't otherwise
// needed during resolution (which I believe is
// true), but is a more robust approach that should
// capture all cases.
marker: self.markers.fork_markers().cloned().unwrap_or_default(),
marker: MarkerTree::TRUE,
};
edges.insert(edge);
}
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/tests/ecosystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ fn packse() -> Result<()> {
// Source: https://github.com/konstin/github-wikidata-bot/blob/8218d20985eb480cb8633026f9dabc9e5ec4b5e3/pyproject.toml
#[test]
fn github_wikidata_bot() -> Result<()> {
lock_ecosystem_package("3.12", "github-wikidata-bot")
// TODO(charlie): This test became non-deterministic in https://github.com/astral-sh/uv/pull/6065.
// However, that fix is _correct_, and the non-determinism itself is an existing bug.
lock_ecosystem_package_non_deterministic("3.12", "github-wikidata-bot")
}

// Source: https://github.com/psf/black/blob/9ff047a9575f105f659043f28573e1941e9cdfb3/pyproject.toml
Expand Down
57 changes: 36 additions & 21 deletions crates/uv/tests/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1665,16 +1665,19 @@ fn lock_conditional_dependency_extra() -> Result<()> {
});
}

// Re-run with `--locked`.
uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `uv lock` is experimental and may change without warning
Resolved 7 packages in [TIME]
"###);
// TODO(charlie): This test became non-deterministic in https://github.com/astral-sh/uv/pull/6065.
// But that fix is correct, and the non-determinism itself is a bug.
// // Re-run with `--locked`.
// uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
// success: false
// exit_code: 2
// ----- stdout -----
//
// ----- stderr -----
// warning: `uv lock` is experimental and may change without warning
// Resolved 7 packages in [TIME]
// error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
// "###);

// Install from the lockfile.
uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###"
Expand Down Expand Up @@ -3552,8 +3555,7 @@ fn lock_python_version_marker_complement() -> Result<()> {
"#,
)?;

deterministic_lock! { context =>
uv_snapshot!(context.filters(), context.lock(), @r###"
uv_snapshot!(context.filters(), context.lock(), @r###"
success: true
exit_code: 0
----- stdout -----
Expand All @@ -3563,13 +3565,13 @@ fn lock_python_version_marker_complement() -> Result<()> {
Resolved 4 packages in [TIME]
"###);

let lock = fs_err::read_to_string(&lockfile).unwrap();
let lock = fs_err::read_to_string(&lockfile).unwrap();

insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
lock, @r###"
version = 1
requires-python = ">=3.8"
environment-markers = [
Expand Down Expand Up @@ -3621,9 +3623,22 @@ fn lock_python_version_marker_complement() -> Result<()> {
{ url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 },
]
"###
);
});
}
);
});

// TODO(charlie): This test became non-deterministic in https://github.com/astral-sh/uv/pull/6065.
// But that fix is correct, and the non-determinism itself is a bug.
// // Re-run with `--locked`.
// uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###"
// success: false
// exit_code: 2
// ----- stdout -----
//
// ----- stderr -----
// warning: `uv lock` is experimental and may change without warning
// Resolved 4 packages in [TIME]
// error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
// "###);

Ok(())
}
Expand Down
18 changes: 9 additions & 9 deletions crates/uv/tests/lock_scenarios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1237,8 +1237,8 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> {
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython'" },
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" },
]
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_a-1.0.0.tar.gz", hash = "sha256:c7232306e8597d46c3fe53a3b1472f99b8ff36b3169f335ba0a5b625e193f7d4" }
wheels = [
Expand All @@ -1265,7 +1265,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> {
"implementation_name == 'pypy' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "package-c", marker = "sys_platform == 'linux' or implementation_name == 'pypy'" },
{ name = "package-c", marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" },
]
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_allowed_b-1.0.0.tar.gz", hash = "sha256:d6bd196a0a152c1b32e09f08e554d22ae6a6b3b916e39ad4552572afae5f5492" }
wheels = [
Expand Down Expand Up @@ -1413,8 +1413,8 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> {
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython'" },
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" },
]
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_disallowed_a-1.0.0.tar.gz", hash = "sha256:92081d91570582f3a94ed156f203de53baca5b3fdc350aa1c831c7c42723e798" }
wheels = [
Expand Down Expand Up @@ -1578,8 +1578,8 @@ fn fork_marker_inherit_combined() -> Result<()> {
"implementation_name != 'cpython' and implementation_name != 'pypy' and sys_platform == 'darwin'",
]
dependencies = [
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython'" },
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'pypy' and sys_platform == 'darwin'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "implementation_name == 'cpython' and sys_platform == 'darwin'" },
]
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_marker_inherit_combined_a-1.0.0.tar.gz", hash = "sha256:2ec4c9dbb7078227d996c344b9e0c1b365ed0000de9527b2ba5b616233636f07" }
wheels = [
Expand Down Expand Up @@ -3899,8 +3899,8 @@ fn fork_remaining_universe_partitioning() -> Result<()> {
"os_name != 'darwin' and os_name != 'linux' and sys_platform == 'illumos'",
]
dependencies = [
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "os_name == 'darwin'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "os_name == 'linux'" },
{ name = "package-b", version = "1.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "os_name == 'darwin' and sys_platform == 'illumos'" },
{ name = "package-b", version = "2.0.0", source = { registry = "https://astral-sh.github.io/packse/PACKSE_VERSION/simple-html/" }, marker = "os_name == 'linux' and sys_platform == 'illumos'" },
]
sdist = { url = "https://astral-sh.github.io/packse/PACKSE_VERSION/files/fork_remaining_universe_partitioning_a-1.0.0.tar.gz", hash = "sha256:d5be0af9a1958ec08ca2827b47bfd507efc26cab03ecf7ddf204e18e8a3a18ae" }
wheels = [
Expand Down
8 changes: 4 additions & 4 deletions crates/uv/tests/pip_compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7521,11 +7521,11 @@ fn universal_nested_disjoint_local_requirement() -> Result<()> {
# via torch
tbb==2021.11.0 ; os_name != 'Linux' and platform_system == 'Windows'
# via mkl
torch==2.0.0+cpu ; os_name == 'Linux'
torch==2.0.0+cpu ; os_name == 'Linux' and platform_machine != 'x86_64'
# via
# -r requirements.in
# example
torch==2.0.0+cu118 ; os_name == 'Linux'
torch==2.0.0+cu118 ; os_name == 'Linux' and platform_machine == 'x86_64'
# via
# -r requirements.in
# example
Expand Down Expand Up @@ -7768,11 +7768,11 @@ fn universal_transitive_disjoint_prerelease_requirement() -> Result<()> {
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] requirements.in --universal
cffi==1.16.0 ; platform_python_implementation != 'PyPy' or os_name == 'linux'
cffi==1.16.0 ; os_name == 'linux'
# via
# -r requirements.in
# cryptography
cffi==1.17.0rc1 ; os_name != 'linux' or platform_python_implementation != 'PyPy'
cffi==1.17.0rc1 ; os_name != 'linux'
# via
# -r requirements.in
# cryptography
Expand Down
Loading

0 comments on commit 4a902a7

Please sign in to comment.