From 5f6529a69a4764a9e9cd6ec9233e726969628a70 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 20 Feb 2025 12:19:46 -0800 Subject: [PATCH] Support conflict markers in `uv export` (#11643) ## Summary Today, if you have a lockfile that includes conflict markers, we write those markers out to `requirements.txt` in `uv export`. This is problematic, since no tool will ever evaluate those markers correctly downstream. This PR adds handling for the conflict markers, though it's quite involved. Specifically, we have a new reachability algorithm that tracks, for each node, the reachable marker for that node _and_ the marker conditions under which each conflict item is `true` (at that node). I'm slightly worried that this algorithm could be wrong for graphs with cycles, but we only use this logic for lockfiles with conflicts anyway, so I think it's a strict improvement over the status quo. Closes https://github.com/astral-sh/uv/issues/11559. Closes https://github.com/astral-sh/uv/issues/11548. --- crates/uv-resolver/src/graph_ops.rs | 75 ++-- .../uv-resolver/src/lock/requirements_txt.rs | 272 +++++++++++++- crates/uv-resolver/src/universal_marker.rs | 154 +++++++- crates/uv/tests/it/export.rs | 344 ++++++++++++++++++ 4 files changed, 796 insertions(+), 49 deletions(-) diff --git a/crates/uv-resolver/src/graph_ops.rs b/crates/uv-resolver/src/graph_ops.rs index f9f2ce061..63ffed950 100644 --- a/crates/uv-resolver/src/graph_ops.rs +++ b/crates/uv-resolver/src/graph_ops.rs @@ -18,10 +18,14 @@ use crate::universal_marker::UniversalMarker; /// marker), we re-queue the node and update all its children. This implicitly handles cycles, /// whenever we re-reach a node through a cycle the marker we have is a more /// specific marker/longer path, so we don't update the node and don't re-queue it. -pub(crate) fn marker_reachability( +pub(crate) fn marker_reachability< + Marker: Boolean + Copy + PartialEq, + Node, + Edge: Reachable, +>( graph: &Graph, fork_markers: &[Edge], -) -> FxHashMap { +) -> FxHashMap { // Note that we build including the virtual packages due to how we propagate markers through // the graph, even though we then only read the markers for base packages. let mut reachability = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher); @@ -47,8 +51,8 @@ pub(crate) fn marker_reachability( } else { fork_markers .iter() - .fold(Edge::false_marker(), |mut acc, marker| { - acc.or(*marker); + .fold(Edge::false_marker(), |mut acc, edge| { + acc.or(edge.marker()); acc }) }; @@ -62,7 +66,7 @@ pub(crate) fn marker_reachability( let marker = reachability[&parent_index]; for child_edge in graph.edges_directed(parent_index, Direction::Outgoing) { // The marker for all paths to the child through the parent. - let mut child_marker = *child_edge.weight(); + let mut child_marker = child_edge.weight().marker(); child_marker.and(marker); match reachability.entry(child_edge.target()) { Entry::Occupied(mut existing) => { @@ -283,14 +287,47 @@ pub(crate) fn simplify_conflict_markers( } } -/// A trait for types that can be used as markers in the dependency graph. -pub(crate) trait Reachable { +pub(crate) trait Reachable { /// The marker representing the "true" value. - fn true_marker() -> Self; + fn true_marker() -> T; /// The marker representing the "false" value. - fn false_marker() -> Self; + fn false_marker() -> T; + /// The marker attached to the edge. + fn marker(&self) -> T; +} + +impl Reachable for MarkerTree { + fn true_marker() -> MarkerTree { + MarkerTree::TRUE + } + + fn false_marker() -> MarkerTree { + MarkerTree::FALSE + } + + fn marker(&self) -> MarkerTree { + *self + } +} + +impl Reachable for UniversalMarker { + fn true_marker() -> UniversalMarker { + UniversalMarker::TRUE + } + + fn false_marker() -> UniversalMarker { + UniversalMarker::FALSE + } + + fn marker(&self) -> UniversalMarker { + *self + } +} + +/// A trait for types that can be used as markers in the dependency graph. +pub(crate) trait Boolean { /// Perform a logical AND operation with another marker. fn and(&mut self, other: Self); @@ -298,15 +335,7 @@ pub(crate) trait Reachable { fn or(&mut self, other: Self); } -impl Reachable for UniversalMarker { - fn true_marker() -> Self { - Self::TRUE - } - - fn false_marker() -> Self { - Self::FALSE - } - +impl Boolean for UniversalMarker { fn and(&mut self, other: Self) { self.and(other); } @@ -316,15 +345,7 @@ impl Reachable for UniversalMarker { } } -impl Reachable for MarkerTree { - fn true_marker() -> Self { - Self::TRUE - } - - fn false_marker() -> Self { - Self::FALSE - } - +impl Boolean for MarkerTree { fn and(&mut self, other: Self) { self.and(other); } diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index 040b2407e..916592354 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -5,8 +5,10 @@ use std::fmt::Formatter; use std::path::{Component, Path, PathBuf}; use either::Either; +use petgraph::graph::NodeIndex; +use petgraph::prelude::EdgeRef; use petgraph::visit::IntoNodeReferences; -use petgraph::Graph; +use petgraph::{Direction, Graph}; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use url::Url; @@ -14,12 +16,13 @@ use uv_configuration::{DevGroupsManifest, EditableMode, ExtrasSpecification, Ins use uv_distribution_filename::{DistExtension, SourceDistExtension}; use uv_fs::Simplified; use uv_git_types::GitReference; -use uv_normalize::{ExtraName, PackageName}; +use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_pep508::MarkerTree; -use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl}; +use uv_pypi_types::{ConflictItem, ParsedArchiveUrl, ParsedGitUrl}; -use crate::graph_ops::marker_reachability; +use crate::graph_ops::{marker_reachability, Reachable}; use crate::lock::{Package, PackageId, Source}; +use crate::universal_marker::resolve_conflicts; use crate::{Installable, LockError}; /// An export of a [`Lock`] that renders in `requirements.txt` format. @@ -41,13 +44,18 @@ impl<'lock> RequirementsTxtExport<'lock> { install_options: &'lock InstallOptions, ) -> Result { let size_guess = target.lock().packages.len(); - let mut petgraph = Graph::with_capacity(size_guess, size_guess); + let mut graph = Graph::, Edge<'lock>>::with_capacity(size_guess, size_guess); let mut inverse = FxHashMap::with_capacity_and_hasher(size_guess, FxBuildHasher); let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new(); let mut seen = FxHashSet::default(); + let mut conflicts = if target.lock().conflicts.is_empty() { + None + } else { + Some(FxHashMap::default()) + }; - let root = petgraph.add_node(Node::Root); + let root = graph.add_node(Node::Root); // Add the workspace packages to the queue. for root_name in target.roots() { @@ -64,33 +72,49 @@ impl<'lock> RequirementsTxtExport<'lock> { if dev.prod() { // Add the workspace package to the graph. if let Entry::Vacant(entry) = inverse.entry(&dist.id) { - entry.insert(petgraph.add_node(Node::Package(dist))); + entry.insert(graph.add_node(Node::Package(dist))); } // Add an edge from the root. let index = inverse[&dist.id]; - petgraph.add_edge(root, index, MarkerTree::TRUE); + graph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE)); // Push its dependencies on the queue. queue.push_back((dist, None)); for extra in extras.extra_names(dist.optional_dependencies.keys()) { queue.push_back((dist, Some(extra))); + + // Track the activated extra in the list of known conflicts. + if let Some(conflicts) = conflicts.as_mut() { + conflicts.insert( + ConflictItem::from((dist.id.name.clone(), extra.clone())), + MarkerTree::TRUE, + ); + } } } // Add any development dependencies. - for dep in dist + for (group, dep) in dist .dependency_groups .iter() .filter_map(|(group, deps)| { if dev.contains(group) { - Some(deps) + Some(deps.iter().map(move |dep| (group, dep))) } else { None } }) .flatten() { + // Track the activated group in the list of known conflicts. + if let Some(conflicts) = conflicts.as_mut() { + conflicts.insert( + ConflictItem::from((dist.id.name.clone(), group.clone())), + MarkerTree::TRUE, + ); + } + if prune.contains(&dep.package_id.name) { continue; } @@ -99,17 +123,17 @@ impl<'lock> RequirementsTxtExport<'lock> { // Add the dependency to the graph. if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) { - entry.insert(petgraph.add_node(Node::Package(dep_dist))); + entry.insert(graph.add_node(Node::Package(dep_dist))); } // Add an edge from the root. Development dependencies may be installed without // installing the workspace package itself (which can never have markers on it // anyway), so they're directly connected to the root. let dep_index = inverse[&dep.package_id]; - petgraph.add_edge( + graph.add_edge( root, dep_index, - dep.simplified_marker.as_simplified_marker_tree(), + Edge::Dev(group, dep.simplified_marker.as_simplified_marker_tree()), ); // Push its dependencies on the queue. @@ -189,12 +213,12 @@ impl<'lock> RequirementsTxtExport<'lock> { // Add the dependency to the graph. if let Entry::Vacant(entry) = inverse.entry(&dist.id) { - entry.insert(petgraph.add_node(Node::Package(dist))); + entry.insert(graph.add_node(Node::Package(dist))); } // Add an edge from the root. let dep_index = inverse[&dist.id]; - petgraph.add_edge(root, dep_index, marker); + graph.add_edge(root, dep_index, Edge::Prod(marker)); // Push its dependencies on the queue. if seen.insert((&dist.id, None)) { @@ -230,19 +254,24 @@ impl<'lock> RequirementsTxtExport<'lock> { continue; } + // Evaluate the conflict marker. let dep_dist = target.lock().find_by_id(&dep.package_id); // Add the dependency to the graph. if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) { - entry.insert(petgraph.add_node(Node::Package(dep_dist))); + entry.insert(graph.add_node(Node::Package(dep_dist))); } // Add the edge. let dep_index = inverse[&dep.package_id]; - petgraph.add_edge( + graph.add_edge( index, dep_index, - dep.simplified_marker.as_simplified_marker_tree(), + if let Some(extra) = extra { + Edge::Optional(extra, dep.simplified_marker.as_simplified_marker_tree()) + } else { + Edge::Prod(dep.simplified_marker.as_simplified_marker_tree()) + }, ); // Push its dependencies on the queue. @@ -257,10 +286,15 @@ impl<'lock> RequirementsTxtExport<'lock> { } } - let mut reachability = marker_reachability(&petgraph, &[]); + // Determine the reachability of each node in the graph. + let mut reachability = if let Some(conflicts) = conflicts.as_ref() { + conflict_marker_reachability(&graph, &[], conflicts) + } else { + marker_reachability(&graph, &[]) + }; // Collect all packages. - let mut nodes = petgraph + let mut nodes = graph .node_references() .filter_map(|(index, node)| match node { Node::Root => None, @@ -277,6 +311,7 @@ impl<'lock> RequirementsTxtExport<'lock> { package, marker: reachability.remove(&index).unwrap_or_default(), }) + .filter(|requirement| !requirement.marker.is_false()) .collect::>(); // Sort the nodes, such that unnamed URLs (editables) appear at the top. @@ -292,6 +327,170 @@ impl<'lock> RequirementsTxtExport<'lock> { } } +/// Determine the markers under which a package is reachable in the dependency tree, taking into +/// account conflicts. +/// +/// This method is structurally similar to [`marker_reachability`], but it _also_ attempts to resolve +/// conflict markers. Specifically, in addition to tracking the reachability marker for each node, +/// we also track (for each node) the conditions under which each conflict item is `true`. Then, +/// when evaluating the marker for the node, we inline the conflict marker conditions, thus removing +/// all conflict items from the marker expression. +fn conflict_marker_reachability<'lock>( + graph: &Graph, Edge<'lock>>, + fork_markers: &[Edge<'lock>], + known_conflicts: &FxHashMap, +) -> FxHashMap { + // For each node, track the conditions under which each conflict item is enabled. + let mut conflict_maps = + FxHashMap::>::with_capacity_and_hasher( + graph.node_count(), + FxBuildHasher, + ); + + // Note that we build including the virtual packages due to how we propagate markers through + // the graph, even though we then only read the markers for base packages. + let mut reachability = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher); + + // Collect the root nodes. + // + // Besides the actual virtual root node, virtual dev dependencies packages are also root + // nodes since the edges don't cover dev dependencies. + let mut queue: Vec<_> = graph + .node_indices() + .filter(|node_index| { + graph + .edges_directed(*node_index, Direction::Incoming) + .next() + .is_none() + }) + .collect(); + + // The root nodes are always applicable, unless the user has restricted resolver + // environments with `tool.uv.environments`. + let root_markers = if fork_markers.is_empty() { + MarkerTree::TRUE + } else { + fork_markers + .iter() + .fold(MarkerTree::FALSE, |mut acc, edge| { + acc.or(*edge.marker()); + acc + }) + }; + for root_index in &queue { + reachability.insert(*root_index, root_markers); + } + + // Propagate all markers through the graph, so that the eventual marker for each node is the + // union of the markers of each path we can reach the node by. + while let Some(parent_index) = queue.pop() { + // Resolve any conflicts in the parent marker. + reachability.entry(parent_index).and_modify(|marker| { + let conflict_map = conflict_maps.get(&parent_index).unwrap_or(known_conflicts); + *marker = resolve_conflicts(*marker, conflict_map); + }); + + // When we see an edge like `parent [dotenv]> flask`, we should take the reachability + // on `parent`, combine it with the marker on the edge, then add `flask[dotenv]` to + // the inference map on the `flask` node. + for child_edge in graph.edges_directed(parent_index, Direction::Outgoing) { + let mut parent_marker = reachability[&parent_index]; + + // The marker for all paths to the child through the parent. + let mut parent_map = conflict_maps + .get(&parent_index) + .cloned() + .unwrap_or_else(|| known_conflicts.clone()); + + match child_edge.weight() { + Edge::Prod(marker) => { + // Resolve any conflicts on the edge. + let marker = resolve_conflicts(*marker, &parent_map); + + // Propagate the edge to the known conflicts. + for value in parent_map.values_mut() { + value.and(marker); + } + + // Propagate the edge to the node itself. + parent_marker.and(marker); + } + Edge::Optional(extra, marker) => { + // Resolve any conflicts on the edge. + let marker = resolve_conflicts(*marker, &parent_map); + + // Propagate the edge to the known conflicts. + for value in parent_map.values_mut() { + value.and(marker); + } + + // Propagate the edge to the node itself. + parent_marker.and(marker); + + // Add a known conflict item for the extra. + if let Node::Package(parent) = graph[parent_index] { + let item = ConflictItem::from((parent.name().clone(), (*extra).clone())); + parent_map.insert(item, parent_marker); + } + } + Edge::Dev(group, marker) => { + // Resolve any conflicts on the edge. + let marker = resolve_conflicts(*marker, &parent_map); + + // Propagate the edge to the known conflicts. + for value in parent_map.values_mut() { + value.and(marker); + } + + // Propagate the edge to the node itself. + parent_marker.and(marker); + + // Add a known conflict item for the group. + if let Node::Package(parent) = graph[parent_index] { + let item = ConflictItem::from((parent.name().clone(), (*group).clone())); + parent_map.insert(item, parent_marker); + } + } + } + + // Combine the inferred conflicts with the existing conflicts on the node. + match conflict_maps.entry(child_edge.target()) { + Entry::Occupied(mut existing) => { + let child_map = existing.get_mut(); + for (key, value) in parent_map { + let mut after = child_map.get(&key).copied().unwrap_or(MarkerTree::FALSE); + after.or(value); + child_map.entry(key).or_insert(MarkerTree::FALSE).or(value); + } + } + Entry::Vacant(vacant) => { + vacant.insert(parent_map); + } + } + + // Combine the inferred marker with the existing marker on the node. + match reachability.entry(child_edge.target()) { + Entry::Occupied(existing) => { + // If the marker is a subset of the existing marker (A ⊆ B exactly if + // A ∪ B = A), updating the child wouldn't change child's marker. + parent_marker.or(*existing.get()); + if parent_marker != *existing.get() { + queue.push(child_edge.target()); + } + } + Entry::Vacant(vacant) => { + vacant.insert(parent_marker); + queue.push(child_edge.target()); + } + } + + queue.push(child_edge.target()); + } + } + + reachability +} + impl std::fmt::Display for RequirementsTxtExport<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { // Write out each package. @@ -399,6 +598,39 @@ enum Node<'lock> { Package(&'lock Package), } +/// An edge in the resolution graph, along with the marker that must be satisfied to traverse it. +#[derive(Debug, Clone)] +enum Edge<'lock> { + Prod(MarkerTree), + Optional(&'lock ExtraName, MarkerTree), + Dev(&'lock GroupName, MarkerTree), +} + +impl Edge<'_> { + /// Return the [`MarkerTree`] for this edge. + fn marker(&self) -> &MarkerTree { + match self { + Self::Prod(marker) => marker, + Self::Optional(_, marker) => marker, + Self::Dev(_, marker) => marker, + } + } +} + +impl Reachable for Edge<'_> { + fn true_marker() -> MarkerTree { + MarkerTree::TRUE + } + + fn false_marker() -> MarkerTree { + MarkerTree::FALSE + } + + fn marker(&self) -> MarkerTree { + *self.marker() + } +} + /// A flat requirement, with its associated marker. #[derive(Debug, Clone, PartialEq, Eq)] struct Requirement<'lock> { diff --git a/crates/uv-resolver/src/universal_marker.rs b/crates/uv-resolver/src/universal_marker.rs index 587440939..67b8be623 100644 --- a/crates/uv-resolver/src/universal_marker.rs +++ b/crates/uv-resolver/src/universal_marker.rs @@ -1,9 +1,12 @@ use std::borrow::Borrow; use itertools::Itertools; - +use rustc_hash::FxHashMap; use uv_normalize::{ExtraName, GroupName, PackageName}; -use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder, MarkerOperator, MarkerTree}; +use uv_pep508::{ + ExtraOperator, MarkerEnvironment, MarkerEnvironmentBuilder, MarkerExpression, MarkerOperator, + MarkerTree, +}; use uv_pypi_types::{ConflictItem, ConflictPackage, Conflicts}; use crate::ResolveError; @@ -616,6 +619,110 @@ impl<'a> ParsedRawExtra<'a> { } } +/// Resolve the conflict markers in a [`MarkerTree`] based on the conditions under which each +/// conflict item is known to be true. +/// +/// For example, if the `cpu` extra is known to be enabled when `sys_platform == 'darwin'`, then +/// given the combined marker `python_version >= '3.8' and extra == 'extra-7-project-cpu'`, this +/// method would return `python_version >= '3.8' and sys_platform == 'darwin'`. +/// +/// If a conflict item isn't present in the map of known conflicts, it's assumed to be false in all +/// environments. +pub(crate) fn resolve_conflicts( + marker: MarkerTree, + known_conflicts: &FxHashMap, +) -> MarkerTree { + if marker.is_true() || marker.is_false() { + return marker; + } + + let mut transformed = MarkerTree::FALSE; + + // Convert the marker to DNF, then re-build it. + for dnf in marker.to_dnf() { + let mut or = MarkerTree::TRUE; + + for marker in dnf { + let MarkerExpression::Extra { + ref operator, + ref name, + } = marker + else { + or.and(MarkerTree::expression(marker)); + continue; + }; + + let Some(name) = name.as_extra() else { + or.and(MarkerTree::expression(marker)); + continue; + }; + + // Given an extra marker (like `extra == 'extra-7-project-cpu'`), search for the + // corresponding conflict; once found, inline the marker of conditions under which the + // conflict is known to be true. + let mut found = false; + for (conflict_item, conflict_marker) in known_conflicts { + // Search for the conflict item as an extra. + if let Some(extra) = conflict_item.extra() { + let package = conflict_item.package(); + let encoded = encode_package_extra(package, extra); + if encoded == *name { + match operator { + ExtraOperator::Equal => { + or.and(*conflict_marker); + found = true; + break; + } + ExtraOperator::NotEqual => { + or.and(conflict_marker.negate()); + found = true; + break; + } + } + } + } + + // Search for the conflict item as a group. + if let Some(group) = conflict_item.group() { + let package = conflict_item.package(); + let encoded = encode_package_group(package, group); + if encoded == *name { + match operator { + ExtraOperator::Equal => { + or.and(*conflict_marker); + found = true; + break; + } + ExtraOperator::NotEqual => { + or.and(conflict_marker.negate()); + found = true; + break; + } + } + } + } + } + + // If we didn't find the marker in the list of known conflicts, assume it's always + // false. + if !found { + match operator { + ExtraOperator::Equal => { + or.and(MarkerTree::FALSE); + } + ExtraOperator::NotEqual => { + or.and(MarkerTree::TRUE); + } + } + } + } + + transformed.or(or); + } + + transformed +} + #[cfg(test)] mod tests { use super::*; @@ -661,6 +768,25 @@ mod tests { ConflictMarker::extra(&create_package("pkg"), &create_extra(name)) } + /// Shortcut for creating a conflict item from an extra name. + fn create_extra_item(name: &str) -> ConflictItem { + ConflictItem::from((create_package("pkg"), create_extra(name))) + } + + /// Shortcut for creating a conflict map. + fn create_known_conflicts<'a>( + it: impl IntoIterator, + ) -> FxHashMap { + it.into_iter() + .map(|(extra, marker)| { + ( + create_extra_item(extra), + MarkerTree::from_str(marker).unwrap(), + ) + }) + .collect() + } + /// Returns a string representation of the given conflict marker. /// /// This is just the underlying marker. And if it's `true`, then a @@ -781,4 +907,28 @@ mod tests { dep_conflict_marker.imbibe(conflicts_marker); assert_eq!(format!("{dep_conflict_marker:?}"), "true"); } + + #[test] + fn resolve() { + let known_conflicts = create_known_conflicts([("foo", "sys_platform == 'darwin'")]); + let cm = MarkerTree::from_str("(python_version >= '3.10' and extra == 'extra-3-pkg-foo') or (python_version < '3.10' and extra != 'extra-3-pkg-foo')").unwrap(); + let cm = resolve_conflicts(cm, &known_conflicts); + assert_eq!( + cm.try_to_string().as_deref(), + Some("(python_full_version < '3.10' and sys_platform != 'darwin') or (python_full_version >= '3.10' and sys_platform == 'darwin')") + ); + + let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-foo'") + .unwrap(); + let cm = resolve_conflicts(cm, &known_conflicts); + assert_eq!( + cm.try_to_string().as_deref(), + Some("python_full_version >= '3.10' and sys_platform == 'darwin'") + ); + + let cm = MarkerTree::from_str("python_version >= '3.10' and extra == 'extra-3-pkg-bar'") + .unwrap(); + let cm = resolve_conflicts(cm, &known_conflicts); + assert!(cm.is_false()); + } } diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 9df8f2a83..fa890697b 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -2424,3 +2424,347 @@ fn conflicts() -> Result<()> { Ok(()) } + +#[test] +fn simple_conflict_markers() -> 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.0" + dependencies = ["anyio"] + + [project.optional-dependencies] + cpu = [ + "idna<=1", + ] + cu124 = [ + "idna<=2", + ] + + [tool.uv] + conflicts = [ + [ + { extra = "cpu" }, + { extra = "cu124" }, + ], + ] + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] + anyio==1.3.1 \ + --hash=sha256:a46bb2b7743455434afd9adea848a3c4e0b7321aee3e9d08844b11d348d3b5a0 \ + --hash=sha256:f21b4fafeec1b7db81e09a907e44e374a1e39718d782a488fdfcdcf949c8950c + async-generator==1.10 \ + --hash=sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b \ + --hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144 + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--extra").arg("cpu"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --extra cpu + anyio==1.3.1 \ + --hash=sha256:a46bb2b7743455434afd9adea848a3c4e0b7321aee3e9d08844b11d348d3b5a0 \ + --hash=sha256:f21b4fafeec1b7db81e09a907e44e374a1e39718d782a488fdfcdcf949c8950c + async-generator==1.10 \ + --hash=sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b \ + --hash=sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144 + idna==1.0 \ + --hash=sha256:c31140a69ecae014d65e936e9a45d8a66e2ee29f5abbc656f69c705ad2f1507d + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + + ----- stderr ----- + Resolved 6 packages in [TIME] + "###); + + Ok(()) +} + +#[test] +fn complex_conflict_markers() -> Result<()> { + let context = TestContext::new("3.12").with_exclude_newer("2025-01-30T00:00:00Z"); + + 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.0" + dependencies = ["torch"] + + [project.optional-dependencies] + cpu = [ + "torch>=2.6.0", + "torchvision>=0.21.0", + ] + cu124 = [ + "torch>=2.6.0", + "torchvision>=0.21.0", + ] + + [tool.uv] + conflicts = [ + [ + { extra = "cpu" }, + { extra = "cu124" }, + ], + ] + + [tool.uv.sources] + torch = [ + { index = "pytorch-cpu", extra = "cpu" }, + { index = "pytorch-cu124", extra = "cu124" }, + ] + torchvision = [ + { index = "pytorch-cpu", extra = "cpu" }, + { index = "pytorch-cu124", extra = "cu124" }, + ] + + [[tool.uv.index]] + name = "pytorch-cpu" + url = "https://astral-sh.github.io/pytorch-mirror/whl/cpu" + explicit = true + + [[tool.uv.index]] + name = "pytorch-cu124" + url = "https://astral-sh.github.io/pytorch-mirror/whl/cu124" + explicit = true + + "#, + )?; + + context.lock().assert().success(); + + uv_snapshot!(context.filters(), context.export(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] + nvidia-cublas-cu12==12.4.5.8 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:0f8aa1706812e00b9f19dfe0cdb3999b092ccb8ca168c0db5b8ea712456fd9b3 \ + --hash=sha256:2fc8da60df463fdefa81e323eef2e36489e1c94335b5358bcb38360adf75ac9b \ + --hash=sha256:5a796786da89203a0657eda402bcdcec6180254a8ac22d72213abc42069522dc + nvidia-cuda-cupti-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:5688d203301ab051449a2b1cb6690fbe90d2b372f411521c86018b950f3d7922 \ + --hash=sha256:79279b35cf6f91da114182a5ce1864997fd52294a87a16179ce275773799458a \ + --hash=sha256:9dec60f5ac126f7bb551c055072b69d85392b13311fcc1bcda2202d172df30fb + nvidia-cuda-nvrtc-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:0eedf14185e04b76aa05b1fea04133e59f465b6f960c0cbf4e37c3cb6b0ea198 \ + --hash=sha256:a178759ebb095827bd30ef56598ec182b85547f1508941a3d560eb7ea1fbf338 \ + --hash=sha256:a961b2f1d5f17b14867c619ceb99ef6fcec12e46612711bcec78eb05068a60ec + nvidia-cuda-runtime-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:09c2e35f48359752dfa822c09918211844a3d93c100a715d79b59591130c5e1e \ + --hash=sha256:64403288fa2136ee8e467cdc9c9427e0434110899d07c779f25b5c068934faa5 \ + --hash=sha256:961fe0e2e716a2a1d967aab7caee97512f71767f852f67432d572e36cb3a11f3 + nvidia-cudnn-cu12==9.1.0.70 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f \ + --hash=sha256:6278562929433d68365a07a4a1546c237ba2849852c0d4b2262a486e805b977a + nvidia-cufft-cu12==11.2.1.3 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:5dad8008fc7f92f5ddfa2101430917ce2ffacd86824914c82e28990ad7f00399 \ + --hash=sha256:d802f4954291101186078ccbe22fc285a902136f974d369540fd4a5333d1440b \ + --hash=sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9 + nvidia-curand-cu12==10.3.5.147 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:1f173f09e3e3c76ab084aba0de819c49e56614feae5c12f69883f4ae9bb5fad9 \ + --hash=sha256:a88f583d4e0bb643c49743469964103aa59f7f708d862c3ddb0fc07f851e3b8b \ + --hash=sha256:f307cc191f96efe9e8f05a87096abc20d08845a841889ef78cb06924437f6771 + nvidia-cusolver-cu12==11.6.1.9 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260 \ + --hash=sha256:d338f155f174f90724bbde3758b7ac375a70ce8e706d70b018dd3375545fc84e \ + --hash=sha256:e77314c9d7b694fcebc84f58989f3aa4fb4cb442f12ca1a9bde50f5e8f6d1b9c + nvidia-cusparse-cu12==12.3.1.170 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:9bc90fb087bc7b4c15641521f31c0371e9a612fc2ba12c338d3ae032e6b6797f \ + --hash=sha256:9d32f62896231ebe0480efd8a7f702e143c98cfaa0e8a76df3386c1ba2b54df3 \ + --hash=sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1 + nvidia-cusparselt-cu12==0.6.2 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:0057c91d230703924c0422feabe4ce768841f9b4b44d28586b6f6d2eb86fbe70 \ + --hash=sha256:067a7f6d03ea0d4841c85f0c6f1991c5dda98211f6302cb83a4ab234ee95bef8 \ + --hash=sha256:df2c24502fd76ebafe7457dbc4716b2fec071aabaed4fb7691a201cde03704d9 + nvidia-nccl-cu12==2.21.5 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:8579076d30a8c24988834445f8d633c697d42397e92ffc3f63fa26766d25e0a0 + nvidia-nvjitlink-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57 \ + --hash=sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83 \ + --hash=sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1 + nvidia-nvtx-cu12==12.4.127 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:641dccaaa1139f3ffb0d3164b4b84f9d253397e38246a4f2f36728b48566d485 \ + --hash=sha256:781e950d9b9f60d8241ccea575b32f5105a5baf4c2351cab5256a24869f12a1a \ + --hash=sha256:7959ad635db13edf4fc65c06a6e9f9e55fc2f92596db928d169c0bb031e88ef3 + torch==2.6.0 \ + --hash=sha256:2bb8987f3bb1ef2675897034402373ddfc8f5ef0e156e2d8cfc47cacafdda4a9 \ + --hash=sha256:4874a73507a300a5d089ceaff616a569e7bb7c613c56f37f63ec3ffac65259cf \ + --hash=sha256:510c73251bee9ba02ae1cb6c9d4ee0907b3ce6020e62784e2d7598e0cfa4d6cc \ + --hash=sha256:7e1448426d0ba3620408218b50aa6ada88aeae34f7a239ba5431f6c8774b1239 \ + --hash=sha256:9a610afe216a85a8b9bc9f8365ed561535c93e804c2a317ef7fabcc5deda0989 \ + --hash=sha256:a0d5e1b9874c1a6c25556840ab8920569a7a4137afa8a63a32cee0bc7d89bd4b \ + --hash=sha256:b789069020c5588c70d5c2158ac0aa23fd24a028f34a8b4fcb8fcb4d7efcf5fb \ + --hash=sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2 + triton==3.2.0 ; platform_machine == 'x86_64' and sys_platform == 'linux' \ + --hash=sha256:8d9b215efc1c26fa7eefb9a157915c92d52e000d2bf83e5f69704047e63f125c \ + --hash=sha256:e5dfa23ba84541d7c0a531dfce76d8bcd19159d50a4a8b14ad01e91734a5c1b0 + + ----- stderr ----- + Resolved 33 packages in [TIME] + "###); + + uv_snapshot!(context.filters(), context.export().arg("--extra").arg("cpu"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv export --cache-dir [CACHE_DIR] --extra cpu + filelock==3.17.0 ; sys_platform == 'darwin' \ + --hash=sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338 \ + --hash=sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e + fsspec==2024.12.0 ; sys_platform == 'darwin' \ + --hash=sha256:670700c977ed2fb51e0d9f9253177ed20cbde4a3e5c0283cc5385b5870c8533f \ + --hash=sha256:b520aed47ad9804237ff878b504267a3b0b441e97508bd6d2d8774e3db85cee2 + jinja2==3.1.5 ; sys_platform == 'darwin' \ + --hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \ + --hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb + markupsafe==3.0.2 ; sys_platform == 'darwin' \ + --hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \ + --hash=sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9 \ + --hash=sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396 \ + --hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \ + --hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \ + --hash=sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a \ + --hash=sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c \ + --hash=sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c \ + --hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \ + --hash=sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094 \ + --hash=sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5 \ + --hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \ + --hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \ + --hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \ + --hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \ + --hash=sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb \ + --hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \ + --hash=sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c \ + --hash=sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6 \ + --hash=sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd \ + --hash=sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1 \ + --hash=sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d \ + --hash=sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca \ + --hash=sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a \ + --hash=sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe \ + --hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \ + --hash=sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f \ + --hash=sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f \ + --hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0 \ + --hash=sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79 \ + --hash=sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430 + mpmath==1.3.0 ; sys_platform == 'darwin' \ + --hash=sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f \ + --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c + networkx==3.4.2 ; sys_platform == 'darwin' \ + --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ + --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f + numpy==2.2.2 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' \ + --hash=sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0 \ + --hash=sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2 \ + --hash=sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4 \ + --hash=sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648 \ + --hash=sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be \ + --hash=sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb \ + --hash=sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd \ + --hash=sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a \ + --hash=sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84 \ + --hash=sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748 \ + --hash=sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825 \ + --hash=sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317 \ + --hash=sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283 \ + --hash=sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278 \ + --hash=sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9 \ + --hash=sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de \ + --hash=sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369 \ + --hash=sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb \ + --hash=sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49 \ + --hash=sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37 \ + --hash=sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39 \ + --hash=sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576 \ + --hash=sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba \ + --hash=sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7 \ + --hash=sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467 \ + --hash=sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc \ + --hash=sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391 \ + --hash=sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0 \ + --hash=sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369 \ + --hash=sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff \ + --hash=sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f + pillow==11.1.0 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' \ + --hash=sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65 \ + --hash=sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a \ + --hash=sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352 \ + --hash=sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20 \ + --hash=sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c \ + --hash=sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114 \ + --hash=sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91 \ + --hash=sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5 \ + --hash=sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c \ + --hash=sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756 \ + --hash=sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861 \ + --hash=sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1 \ + --hash=sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a \ + --hash=sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081 \ + --hash=sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5 \ + --hash=sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1 \ + --hash=sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3 \ + --hash=sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f \ + --hash=sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c \ + --hash=sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf \ + --hash=sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b \ + --hash=sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe \ + --hash=sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc \ + --hash=sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec \ + --hash=sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3 \ + --hash=sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0 \ + --hash=sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6 \ + --hash=sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547 \ + --hash=sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9 \ + --hash=sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab \ + --hash=sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9 + setuptools==75.8.0 ; sys_platform == 'darwin' \ + --hash=sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6 \ + --hash=sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3 + sympy==1.13.1 ; sys_platform == 'darwin' \ + --hash=sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f \ + --hash=sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8 + torch==2.6.0 ; sys_platform == 'darwin' + torch==2.6.0+cpu ; sys_platform != 'darwin' + torchvision==0.21.0 ; (platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' + torchvision==0.21.0+cpu ; (platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux') + typing-extensions==4.12.2 ; sys_platform == 'darwin' \ + --hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \ + --hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8 + + ----- stderr ----- + Resolved 33 packages in [TIME] + "###); + + Ok(()) +}