diff --git a/Cargo.lock b/Cargo.lock index 289e3c8f1..45ce5fe67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2458,6 +2458,7 @@ dependencies = [ "itertools 0.11.0", "mimalloc", "pep508_rs", + "petgraph", "platform-host", "platform-tags", "puffin-build", @@ -2465,6 +2466,7 @@ dependencies = [ "puffin-client", "puffin-dispatch", "puffin-interpreter", + "puffin-resolver", "puffin-traits", "pypi-types", "tempfile", diff --git a/crates/puffin-dev/Cargo.toml b/crates/puffin-dev/Cargo.toml index 6ded24d44..6a8f852cb 100644 --- a/crates/puffin-dev/Cargo.toml +++ b/crates/puffin-dev/Cargo.toml @@ -21,6 +21,7 @@ puffin-cache = { path = "../puffin-cache", features = ["clap"] } puffin-client = { path = "../puffin-client" } puffin-dispatch = { path = "../puffin-dispatch" } puffin-interpreter = { path = "../puffin-interpreter" } +puffin-resolver = { path = "../puffin-resolver" } pypi-types = { path = "../pypi-types" } puffin-traits = { path = "../puffin-traits" } @@ -32,6 +33,7 @@ fs-err = { workspace = true } futures = { workspace = true } indicatif = { workspace = true } itertools = { workspace = true } +petgraph = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/crates/puffin-dev/src/resolve_cli.rs b/crates/puffin-dev/src/resolve_cli.rs index aa32564e3..a4bff7c7f 100644 --- a/crates/puffin-dev/src/resolve_cli.rs +++ b/crates/puffin-dev/src/resolve_cli.rs @@ -1,22 +1,29 @@ use std::fs; +use std::io::{BufWriter, Write}; +use std::path::PathBuf; use anstream::println; +use anyhow::Context; use clap::Parser; +use fs_err::File; use itertools::Itertools; +use petgraph::dot::{Config as DotConfig, Dot}; use pep508_rs::Requirement; use platform_host::Platform; +use platform_tags::Tags; use puffin_cache::{CacheArgs, CacheDir}; use puffin_client::RegistryClientBuilder; use puffin_dispatch::BuildDispatch; use puffin_interpreter::Virtualenv; -use puffin_traits::BuildContext; +use puffin_resolver::{Manifest, PreReleaseMode, ResolutionMode, Resolver}; #[derive(Parser)] pub(crate) struct ResolveCliArgs { requirements: Vec, + /// Write debug output in DOT format for graphviz to this file #[clap(long)] - limit: Option, + graphviz: Option, /// Don't build source distributions. This means resolving will not run arbitrary code. The /// cached wheels of already built source distributions will be reused. #[clap(long)] @@ -30,15 +37,61 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> anyhow::Result<()> { let platform = Platform::current()?; let venv = Virtualenv::from_env(platform, Some(cache_dir.path()))?; + let client = RegistryClientBuilder::new(cache_dir.path().clone()).build(); let build_dispatch = BuildDispatch::new( - RegistryClientBuilder::new(cache_dir.path().clone()).build(), + client.clone(), cache_dir.path().clone(), venv.interpreter_info().clone(), fs::canonicalize(venv.python_executable())?, args.no_build, ); - let mut resolution = build_dispatch.resolve(&args.requirements).await?; + // Copied from `BuildDispatch` + let tags = Tags::from_env( + venv.interpreter_info().platform(), + venv.interpreter_info().simple_version(), + )?; + let resolver = Resolver::new( + // TODO(konstin): Split settings (for all resolutions) and inputs (only for this + // resolution) and attach the former to Self. + Manifest::new( + args.requirements.clone(), + Vec::default(), + Vec::default(), + ResolutionMode::default(), + PreReleaseMode::default(), + None, // TODO(zanieb): We may want to provide a project name here + None, + ), + venv.interpreter_info().markers(), + &tags, + &client, + &build_dispatch, + ); + let resolution_graph = resolver.resolve().await.with_context(|| { + format!( + "No solution found when resolving: {}", + args.requirements.iter().map(ToString::to_string).join(", "), + ) + })?; + + if let Some(graphviz) = args.graphviz { + let mut writer = BufWriter::new(File::create(graphviz)?); + let graphviz = Dot::with_attr_getters( + resolution_graph.petgraph(), + &[DotConfig::NodeNoLabel, DotConfig::EdgeNoLabel], + &|_graph, edge_ref| format!("label={:?}", edge_ref.weight().to_string()), + &|_graph, (_node_index, dist)| { + format!( + "label={:?}", + dist.to_string().replace("==", "\n").to_string() + ) + }, + ); + write!(&mut writer, "{graphviz:?}")?; + } + + let mut resolution = resolution_graph.requirements(); resolution.sort_unstable_by(|a, b| a.name.cmp(&b.name)); // Concise format for dev diff --git a/crates/puffin-resolver/src/resolution.rs b/crates/puffin-resolver/src/resolution.rs index 29e976543..0d0f472fe 100644 --- a/crates/puffin-resolver/src/resolution.rs +++ b/crates/puffin-resolver/src/resolution.rs @@ -3,6 +3,7 @@ use std::hash::BuildHasherDefault; use colored::Colorize; use fxhash::FxHashMap; use petgraph::visit::EdgeRef; +use petgraph::Direction; use pubgrub::range::Range; use pubgrub::solver::{Kind, State}; use pubgrub::type_aliases::SelectedDependencies; @@ -51,7 +52,7 @@ impl Resolution { /// A complete resolution graph in which every node represents a pinned package and every edge /// represents a dependency between two pinned packages. #[derive(Debug)] -pub struct Graph(petgraph::graph::Graph); +pub struct Graph(petgraph::graph::Graph, petgraph::Directed>); impl Graph { /// Create a new graph from the resolved `PubGrub` state. @@ -98,8 +99,12 @@ impl Graph { // Add every edge to the graph. for (package, version) in selection { for id in &state.incompatibilities[package] { - if let Kind::FromDependencyOf(self_package, self_version, dependency_package, _) = - &state.incompatibility_store[*id].kind + if let Kind::FromDependencyOf( + self_package, + self_version, + dependency_package, + dependency_range, + ) = &state.incompatibility_store[*id].kind { let PubGrubPackage::Package(self_package, None, _) = self_package else { continue; @@ -112,7 +117,7 @@ impl Graph { if self_version.contains(version) { let self_index = &inverse[self_package]; let dependency_index = &inverse[dependency_package]; - graph.update_edge(*dependency_index, *self_index, ()); + graph.update_edge(*self_index, *dependency_index, dependency_range.clone()); } } } @@ -179,6 +184,13 @@ impl Graph { }) .collect() } + + /// Return the underlying graph. + pub fn petgraph( + &self, + ) -> &petgraph::graph::Graph, petgraph::Directed> { + &self.0 + } } /// Write the graph in the `{name}=={version}` format of requirements.txt that pip uses. @@ -198,8 +210,8 @@ impl std::fmt::Display for Graph { let mut edges = self .0 - .edges(index) - .map(|edge| &self.0[edge.target()]) + .edges_directed(index, Direction::Incoming) + .map(|edge| &self.0[edge.source()]) .collect::>(); edges.sort_unstable_by_key(|package| package.name());