diff --git a/Cargo.lock b/Cargo.lock index 006dfea70dca..ac2f96bd5cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4883,6 +4883,7 @@ dependencies = [ "fs-err", "itertools 0.13.0", "jiff", + "petgraph", "rkyv", "rustc-hash", "schemars", diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index 92b9c18712b5..123a55ad41f2 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -33,6 +33,7 @@ bitflags = { workspace = true } fs-err = { workspace = true } itertools = { workspace = true } jiff = { workspace = true } +petgraph = { workspace = true } rkyv = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } diff --git a/crates/uv-distribution-types/src/resolution.rs b/crates/uv-distribution-types/src/resolution.rs index 46922af9f96c..ed935a0909ef 100644 --- a/crates/uv-distribution-types/src/resolution.rs +++ b/crates/uv-distribution-types/src/resolution.rs @@ -1,6 +1,8 @@ -use std::collections::BTreeMap; +use petgraph::{Directed, Graph}; + use uv_distribution_filename::DistExtension; use uv_normalize::{ExtraName, GroupName, PackageName}; +use uv_pep508::MarkerTree; use uv_pypi_types::{HashDigest, RequirementSource}; use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist}; @@ -8,48 +10,65 @@ use crate::{BuiltDist, Diagnostic, Dist, Name, ResolvedDist, SourceDist}; /// A set of packages pinned at specific versions. #[derive(Debug, Default, Clone)] pub struct Resolution { - packages: BTreeMap, - hashes: BTreeMap>, + graph: Graph, diagnostics: Vec, } impl Resolution { /// Create a new resolution from the given pinned packages. - pub fn new( - packages: BTreeMap, - hashes: BTreeMap>, - diagnostics: Vec, - ) -> Self { + pub fn new(graph: Graph) -> Self { Self { - packages, - hashes, - diagnostics, + graph, + diagnostics: Vec::new(), } } - /// Return the hashes for the given package name, if they exist. - pub fn get_hashes(&self, package_name: &PackageName) -> &[HashDigest] { - self.hashes.get(package_name).map_or(&[], Vec::as_slice) + /// Return the underlying graph of the resolution. + pub fn graph(&self) -> &Graph { + &self.graph + } + + /// Add [`Diagnostics`] to the resolution. + #[must_use] + pub fn with_diagnostics(mut self, diagnostics: Vec) -> Self { + self.diagnostics.extend(diagnostics); + self } - /// Iterate over the [`PackageName`] entities in this resolution. - pub fn packages(&self) -> impl Iterator { - self.packages.keys() + /// Return the hashes for the given package name, if they exist. + pub fn hashes(&self) -> impl Iterator { + self.graph + .node_indices() + .filter_map(move |node| match &self.graph[node] { + Node::Dist { + dist, + hashes, + install, + .. + } if *install => Some((dist, hashes.as_slice())), + _ => None, + }) } /// Iterate over the [`ResolvedDist`] entities in this resolution. pub fn distributions(&self) -> impl Iterator { - self.packages.values() + self.graph + .raw_nodes() + .iter() + .filter_map(|node| match &node.weight { + Node::Dist { dist, install, .. } if *install => Some(dist), + _ => None, + }) } /// Return the number of distributions in this resolution. pub fn len(&self) -> usize { - self.packages.len() + self.distributions().count() } /// Return `true` if there are no pinned packages in this resolution. pub fn is_empty(&self) -> bool { - self.packages.is_empty() + self.distributions().next().is_none() } /// Return the [`ResolutionDiagnostic`]s that were produced during resolution. @@ -59,44 +78,31 @@ impl Resolution { /// Filter the resolution to only include packages that match the given predicate. #[must_use] - pub fn filter(self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self { - let packages = self - .packages - .into_iter() - .filter(|(_, dist)| predicate(dist)) - .collect::>(); - let hashes = self - .hashes - .into_iter() - .filter(|(name, _)| packages.contains_key(name)) - .collect(); - let diagnostics = self.diagnostics.clone(); - Self { - packages, - hashes, - diagnostics, + pub fn filter(mut self, predicate: impl Fn(&ResolvedDist) -> bool) -> Self { + for node in self.graph.node_weights_mut() { + if let Node::Dist { dist, install, .. } = node { + if !predicate(dist) { + *install = false; + } + } } + self } /// Map over the resolved distributions in this resolution. + /// + /// For efficiency, the map function should return `None` if the resolved distribution is + /// unchanged. #[must_use] - pub fn map(self, predicate: impl Fn(ResolvedDist) -> ResolvedDist) -> Self { - let packages = self - .packages - .into_iter() - .map(|(name, dist)| (name, predicate(dist))) - .collect::>(); - let hashes = self - .hashes - .into_iter() - .filter(|(name, _)| packages.contains_key(name)) - .collect(); - let diagnostics = self.diagnostics.clone(); - Self { - packages, - hashes, - diagnostics, + pub fn map(mut self, predicate: impl Fn(&ResolvedDist) -> Option) -> Self { + for node in self.graph.node_weights_mut() { + if let Node::Dist { dist, .. } = node { + if let Some(transformed) = predicate(dist) { + *dist = transformed; + } + } } + self } } @@ -166,15 +172,48 @@ impl Diagnostic for ResolutionDiagnostic { } } +/// A node in the resolution, along with whether its been filtered out. +/// +/// This is similar to [`ResolutionGraph`], but represents a resolution for a single platform. +/// +/// We retain filtered nodes as we still need to be able to trace dependencies through the graph +/// (e.g., to determine why a package was install in the resolution). +#[derive(Debug, Clone)] +pub enum Node { + Root, + Dist { + dist: ResolvedDist, + hashes: Vec, + install: bool, + }, +} + +/// An edge in the resolution graph, along with the marker that must be satisfied to traverse it. +#[derive(Debug, Clone)] +pub enum Edge { + Prod(MarkerTree), + Optional(ExtraName, MarkerTree), + Dev(GroupName, MarkerTree), +} + +impl Edge { + /// Return the [`MarkerTree`] for this edge. + pub fn marker(&self) -> &MarkerTree { + match self { + Self::Prod(marker) => marker, + Self::Optional(_, marker) => marker, + Self::Dev(_, marker) => marker, + } + } +} + impl From<&ResolvedDist> for RequirementSource { fn from(resolved_dist: &ResolvedDist) -> Self { match resolved_dist { - ResolvedDist::Installable { dist, .. } => match dist { + ResolvedDist::Installable { dist, version } => match dist { Dist::Built(BuiltDist::Registry(wheels)) => RequirementSource::Registry { specifier: uv_pep440::VersionSpecifiers::from( - uv_pep440::VersionSpecifier::equals_version( - wheels.best_wheel().filename.version.clone(), - ), + uv_pep440::VersionSpecifier::equals_version(version.clone()), ), index: Some(wheels.best_wheel().index.url().clone()), }, diff --git a/crates/uv-resolver/src/lock/requirements_txt.rs b/crates/uv-resolver/src/lock/requirements_txt.rs index df7f900c63d0..e3edcfd01d27 100644 --- a/crates/uv-resolver/src/lock/requirements_txt.rs +++ b/crates/uv-resolver/src/lock/requirements_txt.rs @@ -6,7 +6,7 @@ use std::path::{Component, Path, PathBuf}; use either::Either; use petgraph::visit::IntoNodeReferences; -use petgraph::{Directed, Graph}; +use petgraph::Graph; use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use url::Url; @@ -22,8 +22,6 @@ use crate::graph_ops::marker_reachability; use crate::lock::{Package, PackageId, Source}; use crate::{InstallTarget, LockError}; -type LockGraph<'lock> = Graph, Edge, Directed>; - /// An export of a [`Lock`] that renders in `requirements.txt` format. #[derive(Debug)] pub struct RequirementsTxtExport<'lock> { @@ -42,7 +40,7 @@ impl<'lock> RequirementsTxtExport<'lock> { install_options: &'lock InstallOptions, ) -> Result { let size_guess = target.lock().packages.len(); - let mut petgraph = LockGraph::with_capacity(size_guess, size_guess); + let mut petgraph = Graph::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(); @@ -282,16 +280,13 @@ impl std::fmt::Display for RequirementsTxtExport<'_> { } } -/// A node in the [`LockGraph`]. +/// A node in the graph. #[derive(Debug, Clone, PartialEq, Eq)] enum Node<'lock> { Root, Package(&'lock Package), } -/// The edges of the [`LockGraph`]. -type Edge = MarkerTree; - /// A flat requirement, with its associated marker. #[derive(Debug, Clone, PartialEq, Eq)] struct Requirement<'lock> { diff --git a/crates/uv-resolver/src/lock/target.rs b/crates/uv-resolver/src/lock/target.rs index d0fdd4ea4d51..529939a18dd9 100644 --- a/crates/uv-resolver/src/lock/target.rs +++ b/crates/uv-resolver/src/lock/target.rs @@ -1,11 +1,13 @@ -use std::collections::{BTreeMap, VecDeque}; - use either::Either; -use rustc_hash::FxHashSet; +use petgraph::Graph; +use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; +use std::collections::hash_map::Entry; +use std::collections::{BTreeMap, VecDeque}; use uv_configuration::{BuildOptions, DevGroupsManifest, ExtrasSpecification, InstallOptions}; -use uv_distribution_types::{Resolution, ResolvedDist}; +use uv_distribution_types::{Edge, Node, Resolution, ResolvedDist}; use uv_normalize::{ExtraName, GroupName, PackageName, DEV_DEPENDENCIES}; +use uv_pep508::MarkerTree; use uv_platform_tags::Tags; use uv_pypi_types::{ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroups}; @@ -155,12 +157,57 @@ impl<'env> InstallTarget<'env> { build_options: &BuildOptions, install_options: &InstallOptions, ) -> Result { + /// Convert a lockfile entry to an installable distribution. + macro_rules! node { + ($dist:expr) => { + if install_options.include_package( + $dist.name(), + self.project_name(), + self.lock().members(), + ) { + let dist = $dist.to_dist( + self.workspace().install_path(), + TagPolicy::Required(tags), + build_options, + )?; + let version = $dist.version().clone(); + let dist = ResolvedDist::Installable { dist, version }; + let hashes = $dist.hashes(); + Node::Dist { + dist, + hashes, + install: true, + } + } else { + let dist = $dist.to_dist( + self.workspace().install_path(), + TagPolicy::Preferred(tags), + &BuildOptions::default(), + )?; + let version = $dist.version().clone(); + let dist = ResolvedDist::Installable { dist, version }; + let hashes = $dist.hashes(); + Node::Dist { + dist, + hashes, + install: false, + } + } + }; + } + + let size_guess = self.lock().packages.len(); + let mut petgraph = Graph::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 root = petgraph.add_node(Node::Root); + // Add the workspace packages to the queue. for root_name in self.packages() { - let root = self + let dist = self .lock() .find_by_name(root_name) .map_err(|_| LockErrorKind::MultipleRootPackages { @@ -171,20 +218,27 @@ impl<'env> InstallTarget<'env> { })?; if dev.prod() { - // Add the base package. - queue.push_back((root, None)); + // Add the workspace package to the graph. + if let Entry::Vacant(entry) = inverse.entry(&dist.id) { + entry.insert(petgraph.add_node(node!(dist))); + } + + // Add an edge from the root. + let index = inverse[&dist.id]; + petgraph.add_edge(root, index, Edge::Prod(MarkerTree::TRUE)); - // Add any extras. + // Push its dependencies on the queue. + queue.push_back((dist, None)); match extras { ExtrasSpecification::None => {} ExtrasSpecification::All => { - for extra in root.optional_dependencies.keys() { - queue.push_back((root, Some(extra))); + for extra in dist.optional_dependencies.keys() { + queue.push_back((dist, Some(extra))); } } ExtrasSpecification::Some(extras) => { for extra in extras { - queue.push_back((root, Some(extra))); + queue.push_back((dist, Some(extra))); } } } @@ -192,9 +246,26 @@ impl<'env> InstallTarget<'env> { // Add any dev dependencies. for group in dev.iter() { - for dep in root.dependency_groups.get(group).into_iter().flatten() { + for dep in dist.dependency_groups.get(group).into_iter().flatten() { if dep.complexified_marker.evaluate(marker_env, &[]) { let dep_dist = self.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!(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( + root, + dep_index, + Edge::Dev(group.clone(), MarkerTree::TRUE), + ); + + // Push its dependencies on the queue. if seen.insert((&dep.package_id, None)) { queue.push_back((dep_dist, None)); } @@ -217,7 +288,7 @@ impl<'env> InstallTarget<'env> { for dependency in groups.get(group).into_iter().flatten() { if dependency.marker.evaluate(marker_env, &[]) { let root_name = &dependency.name; - let root = self + let dist = self .lock() .find_by_markers(root_name, marker_env) .map_err(|_| LockErrorKind::MultipleRootPackages { @@ -227,28 +298,64 @@ impl<'env> InstallTarget<'env> { name: root_name.clone(), })?; - // Add the base package. - queue.push_back((root, None)); + // Add the workspace package to the graph. + if let Entry::Vacant(entry) = inverse.entry(&dist.id) { + entry.insert(petgraph.add_node(node!(dist))); + } + + // Add an edge from the root. + let index = inverse[&dist.id]; + petgraph.add_edge( + root, + index, + Edge::Dev(group.clone(), dependency.marker.clone()), + ); - // Add any extras. + // Push its dependencies on the queue. + queue.push_back((dist, None)); for extra in &dependency.extras { - queue.push_back((root, Some(extra))); + queue.push_back((dist, Some(extra))); } } } } - let mut map = BTreeMap::default(); - let mut hashes = BTreeMap::default(); - while let Some((dist, extra)) = queue.pop_front() { + while let Some((package, extra)) = queue.pop_front() { + let index = inverse[&package.id]; + let deps = if let Some(extra) = extra { - Either::Left(dist.optional_dependencies.get(extra).into_iter().flatten()) + Either::Left( + package + .optional_dependencies + .get(extra) + .into_iter() + .flatten(), + ) } else { - Either::Right(dist.dependencies.iter()) + Either::Right(package.dependencies.iter()) }; for dep in deps { if dep.complexified_marker.evaluate(marker_env, &[]) { let dep_dist = self.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!(dep_dist))); + } + + // Add the edge. + let dep_index = inverse[&dep.package_id]; + petgraph.add_edge( + index, + dep_index, + if let Some(extra) = extra { + Edge::Optional(extra.clone(), dep.complexified_marker.clone()) + } else { + Edge::Prod(dep.complexified_marker.clone()) + }, + ); + + // Push its dependencies on the queue. if seen.insert((&dep.package_id, None)) { queue.push_back((dep_dist, None)); } @@ -259,26 +366,8 @@ impl<'env> InstallTarget<'env> { } } } - if install_options.include_package( - &dist.id.name, - self.project_name(), - self.lock().members(), - ) { - map.insert( - dist.id.name.clone(), - ResolvedDist::Installable { - dist: dist.to_dist( - self.workspace().install_path(), - TagPolicy::Required(tags), - build_options, - )?, - version: dist.id.version.clone(), - }, - ); - hashes.insert(dist.id.name.clone(), dist.hashes()); - } } - let diagnostics = vec![]; - Ok(Resolution::new(map, hashes, diagnostics)) + + Ok(Resolution::new(petgraph)) } } diff --git a/crates/uv-resolver/src/resolution/display.rs b/crates/uv-resolver/src/resolution/display.rs index c811c3e68501..bae08bcc62f1 100644 --- a/crates/uv-resolver/src/resolution/display.rs +++ b/crates/uv-resolver/src/resolution/display.rs @@ -2,12 +2,10 @@ use std::collections::BTreeSet; use owo_colors::OwoColorize; use petgraph::visit::EdgeRef; -use petgraph::Direction; +use petgraph::{Directed, Direction, Graph}; use rustc_hash::{FxBuildHasher, FxHashMap}; -use uv_distribution_types::{ - DistributionMetadata, Name, SourceAnnotation, SourceAnnotations, VersionId, -}; +use uv_distribution_types::{DistributionMetadata, Name, SourceAnnotation, SourceAnnotations}; use uv_normalize::PackageName; use uv_pep508::MarkerTree; @@ -305,11 +303,9 @@ pub enum AnnotationStyle { } /// We don't need the edge markers anymore since we switched to propagated markers. -type IntermediatePetGraph<'dist> = - petgraph::graph::Graph, (), petgraph::Directed>; +type IntermediatePetGraph<'dist> = Graph, (), Directed>; -type RequirementsTxtGraph<'dist> = - petgraph::graph::Graph, (), petgraph::Directed>; +type RequirementsTxtGraph<'dist> = Graph, (), Directed>; /// Reduce the graph, such that all nodes for a single package are combined, regardless of /// the extras, as long as they have the same version and markers. @@ -324,8 +320,10 @@ type RequirementsTxtGraph<'dist> = /// We also remove the root node, to simplify the graph structure. fn combine_extras<'dist>(graph: &IntermediatePetGraph<'dist>) -> RequirementsTxtGraph<'dist> { /// Return the key for a node. - fn version_marker(dist: &RequirementsTxtDist) -> (VersionId, MarkerTree) { - (dist.version_id(), dist.markers.clone()) + fn version_marker<'dist>( + dist: &'dist RequirementsTxtDist, + ) -> (&'dist PackageName, &'dist MarkerTree) { + (dist.name(), dist.markers) } let mut next = RequirementsTxtGraph::with_capacity(graph.node_count(), graph.edge_count()); diff --git a/crates/uv-resolver/src/resolution/output.rs b/crates/uv-resolver/src/resolution/output.rs index bb4f2f928807..c44a99b150ab 100644 --- a/crates/uv-resolver/src/resolution/output.rs +++ b/crates/uv-resolver/src/resolution/output.rs @@ -10,8 +10,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use uv_configuration::{Constraints, Overrides}; use uv_distribution::Metadata; use uv_distribution_types::{ - Dist, DistributionMetadata, IndexUrl, Name, ResolutionDiagnostic, ResolvedDist, VersionId, - VersionOrUrlRef, + Dist, DistributionMetadata, Edge, IndexUrl, Name, Node, ResolutionDiagnostic, ResolvedDist, + VersionId, VersionOrUrlRef, }; use uv_git::GitResolver; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -814,19 +814,79 @@ impl Display for ConflictingDistributionError { } } +/// Convert a [`ResolutionGraph`] into a [`uv_distribution_types::Resolution`]. +/// +/// This involves converting [`ResolutionGraphNode`]s into [`Node`]s, which in turn involves +/// dropping any extras and dependency groups from the graph nodes. Instead, each package is +/// collapsed into a single node, with extras and dependency groups annotating the _edges_, rather +/// than being represented as separate nodes. This is a more natural representation, but a further +/// departure from the PubGrub model. +/// +/// For simplicity, this transformation makes the assumption that the resolution only applies to a +/// single platform, i.e., it shouldn't be called on universal resolutions. impl From for uv_distribution_types::Resolution { - fn from(graph: ResolverOutput) -> Self { - Self::new( - graph - .dists() - .map(|node| (node.name().clone(), node.dist.clone())) - .collect(), - graph - .dists() - .map(|node| (node.name().clone(), node.hashes.clone())) - .collect(), - graph.diagnostics, - ) + fn from(output: ResolverOutput) -> Self { + let ResolverOutput { + graph, diagnostics, .. + } = output; + + let mut transformed = Graph::with_capacity(graph.node_count(), graph.edge_count()); + let mut inverse = FxHashMap::with_capacity_and_hasher(graph.node_count(), FxBuildHasher); + + // Create the root node. + let root = transformed.add_node(Node::Root); + + // Re-add the nodes to the reduced graph. + for index in graph.node_indices() { + let ResolutionGraphNode::Dist(dist) = &graph[index] else { + continue; + }; + if dist.is_base() { + inverse.insert( + &dist.name, + transformed.add_node(Node::Dist { + dist: dist.dist.clone(), + hashes: dist.hashes.clone(), + install: true, + }), + ); + } + } + + // Re-add the edges to the reduced petgraph. + for edge in graph.edge_indices() { + let (source, target) = graph.edge_endpoints(edge).unwrap(); + let marker = graph[edge].clone(); + + match (&graph[source], &graph[target]) { + (ResolutionGraphNode::Root, ResolutionGraphNode::Dist(target_dist)) => { + let target = inverse[&target_dist.name()]; + transformed.update_edge(root, target, Edge::Prod(marker)); + } + ( + ResolutionGraphNode::Dist(source_dist), + ResolutionGraphNode::Dist(target_dist), + ) => { + let source = inverse[&source_dist.name()]; + let target = inverse[&target_dist.name()]; + + let edge = if let Some(extra) = source_dist.extra.as_ref() { + Edge::Optional(extra.clone(), marker) + } else if let Some(dev) = source_dist.dev.as_ref() { + Edge::Dev(dev.clone(), marker) + } else { + Edge::Prod(marker) + }; + + transformed.add_edge(source, target, edge); + } + _ => { + unreachable!("root should not contain incoming edges"); + } + } + } + + uv_distribution_types::Resolution::new(transformed).with_diagnostics(diagnostics) } } diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 8696538f0082..2ac08effa3b5 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -274,8 +274,7 @@ impl HashStrategy { ) -> Result { let mut hashes = FxHashMap::>::default(); - for dist in resolution.distributions() { - let digests = resolution.get_hashes(dist.name()); + for (dist, digests) in resolution.hashes() { if digests.is_empty() { // Under `--require-hashes`, every requirement must include a hash. if mode.is_require() { diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 3bc3eab72bde..e427922c19eb 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -733,8 +733,8 @@ pub(crate) fn diagnose_environment( for diagnostic in site_packages.diagnostics(markers)? { // Only surface diagnostics that are "relevant" to the current resolution. if resolution - .packages() - .any(|package| diagnostic.includes(package)) + .distributions() + .any(|dist| diagnostic.includes(dist.name())) { writeln!( printer.stderr(), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index abeb535cb5a8..8f8593a83f55 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -510,19 +510,19 @@ fn apply_editable_mode( version, } = dist else { - return dist; + return None; }; - ResolvedDist::Installable { + Some(ResolvedDist::Installable { dist: Dist::Source(SourceDist::Directory(DirectorySourceDist { - name, - install_path, + name: name.clone(), + install_path: install_path.clone(), editable: false, r#virtual: false, - url, + url: url.clone(), })), - version, - } + version: version.clone(), + }) }), } }