cargo_guppy/
lib.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! A command-line frontend for `guppy`.
5//!
6//! `cargo-guppy` provides a frontend for running `guppy` queries.
7//!
8//! # Installing
9//!
10//! `cargo-guppy` is currently a work in progress, and not yet on `crates.io`. To install it, ensure
11//! you have `cargo` installed (preferably through [rustup](https://rustup.rs/)), then run:
12//!
13//! ```bash
14//! cargo install --git https://github.com/guppy-rs/guppy --branch main cargo-guppy
15//! ```
16//!
17//! This will make the `cargo guppy` command available.
18//!
19//! # Commands
20//!
21//! The list of commands is not currently stable and is subject to change.
22//!
23//! ## Query commands
24//!
25//! * `select`: query packages and their transitive dependencies
26//! * `resolve-cargo`: query packages and features as would be built by cargo
27//! * `subtree-size`: print dependencies along with their unique subtree size
28//! * `dups`: print duplicate packages
29//!
30//! ## Diff commands
31//!
32//! * `diff`: perform a diff of two `cargo metadata` JSON outputs
33//! * `diff-summaries`: perform a diff of two [summaries](https://github.com/guppy-rs/guppy/tree/main/guppy-summaries)
34//!
35//! ## Workspace manipulations
36//!
37//! * `mv`: move crates to a new location in a workspace, updating paths along the way
38
39mod core;
40mod diff;
41mod mv;
42
43pub use crate::{core::*, mv::*};
44
45use ahash::AHashMap;
46use camino::Utf8PathBuf;
47use clap::{ArgEnum, Parser};
48use color_eyre::eyre::{bail, Result, WrapErr};
49use guppy::{
50    graph::{
51        cargo::{CargoOptions, CargoSet},
52        feature::{FeatureSet, StandardFeatures},
53        summaries::Summary,
54        DependencyDirection, DotWrite, PackageDotVisitor, PackageGraph, PackageLink,
55        PackageMetadata,
56    },
57    PackageId,
58};
59use guppy_cmdlib::{
60    string_to_platform_spec, CargoMetadataOptions, CargoResolverOpts, PackagesAndFeatures,
61};
62use std::{borrow::Cow, cmp, collections::HashSet, fmt, fs, io::Write, iter, path::PathBuf};
63
64pub fn cmd_cycles(metadata_opts: CargoMetadataOptions, features: bool) -> Result<()> {
65    let command = metadata_opts.make_command();
66    let pkg_graph = command.build_graph()?;
67
68    if features {
69        let feature_graph = pkg_graph.feature_graph();
70        let cycles = feature_graph.cycles();
71
72        let mut any_cycles = false;
73        for cycle in cycles.all_cycles() {
74            any_cycles = true;
75
76            for (n, feature_id) in cycle.iter().enumerate() {
77                if n > 0 {
78                    print!(" -> ");
79                }
80                println!("{}", feature_id);
81            }
82            println!(" -> {}", cycle[0]);
83        }
84        if !any_cycles {
85            println!("no feature cycles found");
86        }
87    } else {
88        let cycles = pkg_graph.cycles();
89
90        let mut any_cycles = false;
91        for cycle in cycles.all_cycles() {
92            any_cycles = true;
93
94            for (n, package_id) in cycle.iter().enumerate() {
95                if n > 0 {
96                    print!(" -> ");
97                }
98                println!("{}", package_id);
99            }
100            println!(" -> {}", cycle[0]);
101        }
102
103        if !any_cycles {
104            println!("no cycles found");
105        }
106    }
107
108    Ok(())
109}
110
111pub fn cmd_diff(json: bool, old: &str, new: &str) -> Result<()> {
112    let old_json = fs::read_to_string(old)?;
113    let new_json = fs::read_to_string(new)?;
114
115    let old_graph = PackageGraph::from_json(old_json)?;
116    let new_graph = PackageGraph::from_json(new_json)?;
117
118    let old_packages: Vec<_> = old_graph.packages().collect();
119    let new_packages: Vec<_> = new_graph.packages().collect();
120
121    let diff = diff::DiffOptions.diff(&old_packages, &new_packages);
122
123    if json {
124        println!("{}", serde_json::to_string_pretty(&diff).unwrap());
125    } else {
126        print!("{}", diff);
127    }
128
129    Ok(())
130}
131
132#[derive(Debug, Parser)]
133pub struct DiffSummariesOptions {
134    /// The old summary
135    #[clap(name = "OLD")]
136    pub old: Utf8PathBuf,
137
138    /// The new summary
139    #[clap(name = "NEW")]
140    pub new: Utf8PathBuf,
141}
142
143impl DiffSummariesOptions {
144    pub fn exec(&self) -> Result<()> {
145        let old_summary = fs::read_to_string(&self.old)
146            .wrap_err_with(|| format!("reading old summary {} failed", self.old))?;
147        let old_summary = Summary::parse(&old_summary)
148            .wrap_err_with(|| format!("parsing old summary {} failed", self.old))?;
149
150        let new_summary = fs::read_to_string(&self.new)
151            .wrap_err_with(|| format!("reading new summary {} failed", self.new))?;
152        let new_summary = Summary::parse(&new_summary)
153            .wrap_err_with(|| format!("parsing new summary {} failed", self.new))?;
154
155        let diff = old_summary.diff(&new_summary);
156
157        println!("{}", diff.report());
158
159        // TODO: different error codes for non-empty diff and failure, similar to git/hg
160        if diff.is_changed() {
161            bail!("non-empty diff");
162        }
163        Ok(())
164    }
165}
166
167#[derive(Debug, Parser)]
168pub struct DupsOptions {
169    #[clap(flatten)]
170    filter_opts: FilterOptions,
171
172    #[clap(flatten)]
173    metadata_opts: CargoMetadataOptions,
174}
175
176pub fn cmd_dups(opts: &DupsOptions) -> Result<()> {
177    let command = opts.metadata_opts.make_command();
178    let pkg_graph = command.build_graph()?;
179
180    let resolver = opts.filter_opts.make_resolver(&pkg_graph)?;
181    let selection = pkg_graph.query_workspace();
182
183    let mut dupe_map: AHashMap<_, Vec<_>> = AHashMap::new();
184    for package in selection
185        .resolve_with_fn(resolver)
186        .packages(DependencyDirection::Forward)
187    {
188        dupe_map.entry(package.name()).or_default().push(package);
189    }
190
191    for (name, dupes) in dupe_map {
192        if dupes.len() <= 1 {
193            continue;
194        }
195
196        let output = itertools::join(dupes.iter().map(|p| p.version()), ", ");
197
198        println!("{} ({})", name, output);
199    }
200
201    Ok(())
202}
203
204#[derive(ArgEnum, Copy, Clone, Debug)]
205pub enum BuildKind {
206    All,
207    Target,
208    ProcMacro,
209    TargetAndProcMacro,
210    Host,
211}
212
213#[derive(Debug, Parser)]
214pub struct ResolveCargoOptions {
215    #[clap(flatten)]
216    pf: PackagesAndFeatures,
217
218    #[clap(flatten)]
219    resolver_opts: CargoResolverOpts,
220
221    #[clap(flatten)]
222    base_filter_opts: BaseFilterOptions,
223
224    #[clap(long = "target-platform")]
225    /// Evaluate against target platform, "current" or "any" (default: any)
226    target_platform: Option<String>,
227
228    #[clap(long = "host-platform")]
229    /// Evaluate against host platform, "current" or "any" (default: any)
230    host_platform: Option<String>,
231
232    #[clap(long, arg_enum, default_value = "all")]
233    /// Print packages built on target, host or both
234    build_kind: BuildKind,
235
236    #[clap(long, parse(from_os_str))]
237    /// Write summary file
238    summary: Option<PathBuf>,
239
240    #[clap(flatten)]
241    metadata_opts: CargoMetadataOptions,
242}
243
244pub fn cmd_resolve_cargo(opts: &ResolveCargoOptions) -> Result<()> {
245    let target_platform = string_to_platform_spec(opts.target_platform.as_deref())?;
246    let host_platform = string_to_platform_spec(opts.host_platform.as_deref())?;
247    let command = opts.metadata_opts.make_command();
248    let pkg_graph = command.build_graph()?;
249
250    let mut cargo_opts = CargoOptions::new();
251    cargo_opts
252        .set_include_dev(opts.resolver_opts.include_dev)
253        .set_resolver(opts.resolver_opts.resolver_version.to_guppy())
254        .set_initials_platform(opts.resolver_opts.initials_platform.to_guppy())
255        .set_target_platform(target_platform)
256        .set_host_platform(host_platform)
257        .add_omitted_packages(opts.base_filter_opts.omitted_package_ids(&pkg_graph));
258
259    let (initials, features_only) = opts.pf.make_feature_sets(&pkg_graph)?;
260    let cargo_set = CargoSet::new(initials, features_only, &cargo_opts)?;
261
262    // Note that for the target+proc macro case, we unify direct deps here. This means that
263    // direct deps of workspace proc macros (e.g. quote) will be included. This feels like it's
264    // what's desired for this request.
265    let direct_deps = match opts.build_kind {
266        BuildKind::All | BuildKind::TargetAndProcMacro => Cow::Owned(
267            cargo_set
268                .host_direct_deps()
269                .union(cargo_set.target_direct_deps()),
270        ),
271        BuildKind::Target => Cow::Borrowed(cargo_set.target_direct_deps()),
272        BuildKind::Host | BuildKind::ProcMacro => Cow::Borrowed(cargo_set.host_direct_deps()),
273    };
274
275    let print_packages = |feature_set: &FeatureSet| {
276        for feature_list in feature_set.packages_with_features(DependencyDirection::Forward) {
277            let package = feature_list.package();
278            let show_package = match opts.base_filter_opts.kind {
279                Kind::All => true,
280                Kind::Workspace => package.in_workspace(),
281                Kind::DirectThirdParty => {
282                    !package.in_workspace()
283                        && direct_deps.contains(package.id()).expect("valid package")
284                }
285                Kind::ThirdParty => !package.in_workspace(),
286            };
287            if show_package {
288                println!(
289                    "{} {}: {}",
290                    package.name(),
291                    package.version(),
292                    feature_list.display_features()
293                );
294            }
295        }
296    };
297
298    let proc_macro_features = || {
299        let proc_macro_ids = cargo_set.proc_macro_links().map(|link| link.to().id());
300        let package_set = pkg_graph.resolve_ids(proc_macro_ids).expect("valid IDs");
301        let feature_set = package_set.to_feature_set(StandardFeatures::All);
302        cargo_set.host_features().intersection(&feature_set)
303    };
304    match opts.build_kind {
305        BuildKind::All => {
306            print_packages(&cargo_set.target_features().union(cargo_set.host_features()))
307        }
308        BuildKind::Target => print_packages(cargo_set.target_features()),
309        BuildKind::ProcMacro => print_packages(&proc_macro_features()),
310        BuildKind::TargetAndProcMacro => {
311            print_packages(&cargo_set.target_features().union(&proc_macro_features()))
312        }
313        BuildKind::Host => print_packages(cargo_set.host_features()),
314    }
315
316    if let Some(summary_path) = &opts.summary {
317        let summary = cargo_set.to_summary(&cargo_opts)?;
318        let mut out = "# This summary file was @generated by cargo-guppy.\n\n".to_string();
319        summary.write_to_string(&mut out)?;
320
321        fs::write(summary_path, out)?;
322    }
323
324    Ok(())
325}
326
327struct NameVisitor;
328
329impl PackageDotVisitor for NameVisitor {
330    fn visit_package(&self, package: PackageMetadata<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result {
331        write!(f, "{}", package.name())
332    }
333
334    fn visit_link(&self, _link: PackageLink<'_>, f: &mut DotWrite<'_, '_>) -> fmt::Result {
335        write!(f, "")
336    }
337}
338
339#[derive(Debug, Parser)]
340pub struct CmdSelectOptions {
341    #[clap(flatten)]
342    filter_opts: FilterOptions,
343
344    #[clap(long = "output-reverse", parse(from_flag = parse_direction))]
345    /// Output results in reverse topological order (default: forward)
346    output_direction: DependencyDirection,
347
348    #[clap(long, rename_all = "kebab-case")]
349    /// Save selection graph in .dot format
350    output_dot: Option<String>,
351
352    #[clap(flatten)]
353    query_opts: QueryOptions,
354
355    #[clap(flatten)]
356    metadata_opts: CargoMetadataOptions,
357}
358
359pub fn cmd_select(options: &CmdSelectOptions) -> Result<()> {
360    let command = options.metadata_opts.make_command();
361    let pkg_graph = command.build_graph()?;
362
363    let query = options.query_opts.apply(&pkg_graph)?;
364    let resolver = options.filter_opts.make_resolver(&pkg_graph)?;
365    let package_set = query.resolve_with_fn(resolver);
366
367    for package_id in package_set.package_ids(options.output_direction) {
368        let package = pkg_graph.metadata(package_id).unwrap();
369        let in_workspace = package.in_workspace();
370        let direct_dep = package
371            .reverse_direct_links()
372            .any(|link| link.from().in_workspace() && !link.to().in_workspace());
373        let show_package = match options.filter_opts.base_opts.kind {
374            Kind::All => true,
375            Kind::Workspace => in_workspace,
376            Kind::DirectThirdParty => direct_dep,
377            Kind::ThirdParty => !in_workspace,
378        };
379        if show_package {
380            println!("{}", package_id);
381        }
382    }
383
384    if let Some(ref output_file) = options.output_dot {
385        let dot = package_set.display_dot(NameVisitor);
386        let mut f = fs::File::create(output_file)?;
387        write!(f, "{}", dot)?;
388    }
389
390    Ok(())
391}
392
393#[derive(Debug, Parser)]
394pub struct SubtreeSizeOptions {
395    #[clap(flatten)]
396    filter_opts: FilterOptions,
397
398    // TODO: potentially replace this with SelectOptions
399    #[clap(rename_all = "screaming_snake_case")]
400    /// The root packages to start the selection from
401    root: Option<String>,
402
403    #[clap(flatten)]
404    metadata_opts: CargoMetadataOptions,
405}
406
407pub fn cmd_subtree_size(options: &SubtreeSizeOptions) -> Result<()> {
408    let command = options.metadata_opts.make_command();
409    let pkg_graph = command.build_graph()?;
410
411    let resolver = options.filter_opts.make_resolver(&pkg_graph)?;
412
413    let mut dep_cache = pkg_graph.new_depends_cache();
414
415    let root_id = options
416        .root
417        .as_ref()
418        .and_then(|root_name| {
419            pkg_graph
420                .packages()
421                .find(|metadata| root_name == metadata.name())
422        })
423        .map(|metadata| metadata.id());
424    let selection = if options.root.is_some() {
425        pkg_graph.query_forward(iter::once(root_id.unwrap()))?
426    } else {
427        pkg_graph.query_workspace()
428    };
429
430    let mut unique_deps: AHashMap<&PackageId, HashSet<&PackageId>> = AHashMap::new();
431    for package_id in selection
432        .resolve_with_fn(&resolver)
433        .package_ids(DependencyDirection::Forward)
434    {
435        let subtree_package_set: HashSet<&PackageId> = pkg_graph
436            .query_forward(iter::once(package_id))?
437            .resolve_with_fn(&resolver)
438            .package_ids(DependencyDirection::Forward)
439            .collect();
440        let mut nonunique_deps_set: HashSet<&PackageId> = HashSet::new();
441        for dep_package_id in &subtree_package_set {
442            // don't count ourself
443            if *dep_package_id == package_id {
444                continue;
445            }
446
447            let mut unique = true;
448            let dep_package = pkg_graph.metadata(dep_package_id).unwrap();
449            for link in dep_package.reverse_direct_links() {
450                // skip build and dev dependencies
451                if link.dev_only() {
452                    continue;
453                }
454                let from_id = link.from().id();
455
456                if !subtree_package_set.contains(from_id) || nonunique_deps_set.contains(from_id) {
457                    // if the from is from outside the subtree rooted at root_id, we ignore it
458                    if let Some(root_id) = root_id {
459                        if !dep_cache.depends_on(root_id, from_id)? {
460                            continue;
461                        }
462                    }
463
464                    unique = false;
465                    nonunique_deps_set.insert(dep_package_id);
466                    break;
467                }
468            }
469
470            let unique_list = unique_deps.entry(package_id).or_default();
471            if unique {
472                unique_list.insert(dep_package_id);
473            }
474        }
475    }
476
477    let mut sorted_unique_deps = unique_deps.into_iter().collect::<Vec<_>>();
478    sorted_unique_deps.sort_by_key(|a| cmp::Reverse(a.1.len()));
479
480    for (package_id, deps) in sorted_unique_deps.iter() {
481        if !deps.is_empty() {
482            println!("{} {}", deps.len(), package_id);
483        }
484        for dep in deps {
485            println!("    {}", dep);
486        }
487    }
488
489    Ok(())
490}