1mod core;
40mod diff;
41mod mv;
42
43pub use crate::{core::*, mv::*};
44
45use ahash::AHashMap;
46use camino::Utf8PathBuf;
47use clap::{Parser, ValueEnum};
48use color_eyre::eyre::{Result, WrapErr, bail};
49use guppy::{
50 PackageId,
51 graph::{
52 DependencyDirection, DotWrite, PackageDotVisitor, PackageGraph, PackageLink,
53 PackageMetadata,
54 cargo::{CargoOptions, CargoSet},
55 feature::{FeatureSet, StandardFeatures},
56 summaries::Summary,
57 },
58};
59use guppy_cmdlib::{
60 CargoMetadataOptions, CargoResolverOpts, PackagesAndFeatures, string_to_platform_spec,
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 #[clap(name = "OLD")]
136 pub old: Utf8PathBuf,
137
138 #[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 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(ValueEnum, 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 target_platform: Option<String>,
227
228 #[clap(long = "host-platform")]
229 host_platform: Option<String>,
231
232 #[clap(long, value_enum, default_value = "all")]
233 build_kind: BuildKind,
235
236 #[clap(long, value_parser)]
237 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 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", action = clap::ArgAction::SetTrue)]
345 output_reverse: bool,
347
348 #[clap(long, rename_all = "kebab-case")]
349 output_dot: Option<String>,
351
352 #[clap(flatten)]
353 query_opts: QueryOptions,
354
355 #[clap(flatten)]
356 metadata_opts: CargoMetadataOptions,
357}
358
359impl CmdSelectOptions {
360 fn output_direction(&self) -> DependencyDirection {
361 if self.output_reverse {
362 DependencyDirection::Reverse
363 } else {
364 DependencyDirection::Forward
365 }
366 }
367}
368
369pub fn cmd_select(options: &CmdSelectOptions) -> Result<()> {
370 let command = options.metadata_opts.make_command();
371 let pkg_graph = command.build_graph()?;
372
373 let query = options.query_opts.apply(&pkg_graph)?;
374 let resolver = options.filter_opts.make_resolver(&pkg_graph)?;
375 let package_set = query.resolve_with_fn(resolver);
376
377 for package_id in package_set.package_ids(options.output_direction()) {
378 let package = pkg_graph.metadata(package_id).unwrap();
379 let in_workspace = package.in_workspace();
380 let direct_dep = package
381 .reverse_direct_links()
382 .any(|link| link.from().in_workspace() && !link.to().in_workspace());
383 let show_package = match options.filter_opts.base_opts.kind {
384 Kind::All => true,
385 Kind::Workspace => in_workspace,
386 Kind::DirectThirdParty => direct_dep,
387 Kind::ThirdParty => !in_workspace,
388 };
389 if show_package {
390 println!("{package_id}");
391 }
392 }
393
394 if let Some(ref output_file) = options.output_dot {
395 let dot = package_set.display_dot(NameVisitor);
396 let mut f = fs::File::create(output_file)?;
397 write!(f, "{dot}")?;
398 }
399
400 Ok(())
401}
402
403#[derive(Debug, Parser)]
404pub struct SubtreeSizeOptions {
405 #[clap(flatten)]
406 filter_opts: FilterOptions,
407
408 #[clap(rename_all = "screaming_snake_case")]
410 root: Option<String>,
412
413 #[clap(flatten)]
414 metadata_opts: CargoMetadataOptions,
415}
416
417pub fn cmd_subtree_size(options: &SubtreeSizeOptions) -> Result<()> {
418 let command = options.metadata_opts.make_command();
419 let pkg_graph = command.build_graph()?;
420
421 let resolver = options.filter_opts.make_resolver(&pkg_graph)?;
422
423 let mut dep_cache = pkg_graph.new_depends_cache();
424
425 let root_id = options
426 .root
427 .as_ref()
428 .and_then(|root_name| {
429 pkg_graph
430 .packages()
431 .find(|metadata| root_name == metadata.name())
432 })
433 .map(|metadata| metadata.id());
434 let selection = if options.root.is_some() {
435 pkg_graph.query_forward(iter::once(root_id.unwrap()))?
436 } else {
437 pkg_graph.query_workspace()
438 };
439
440 let mut unique_deps: AHashMap<&PackageId, HashSet<&PackageId>> = AHashMap::new();
441 for package_id in selection
442 .resolve_with_fn(&resolver)
443 .package_ids(DependencyDirection::Forward)
444 {
445 let subtree_package_set: HashSet<&PackageId> = pkg_graph
446 .query_forward(iter::once(package_id))?
447 .resolve_with_fn(&resolver)
448 .package_ids(DependencyDirection::Forward)
449 .collect();
450 let mut nonunique_deps_set: HashSet<&PackageId> = HashSet::new();
451 for dep_package_id in &subtree_package_set {
452 if *dep_package_id == package_id {
454 continue;
455 }
456
457 let mut unique = true;
458 let dep_package = pkg_graph.metadata(dep_package_id).unwrap();
459 for link in dep_package.reverse_direct_links() {
460 if link.dev_only() {
462 continue;
463 }
464 let from_id = link.from().id();
465
466 if !subtree_package_set.contains(from_id) || nonunique_deps_set.contains(from_id) {
467 if let Some(root_id) = root_id {
469 if !dep_cache.depends_on(root_id, from_id)? {
470 continue;
471 }
472 }
473
474 unique = false;
475 nonunique_deps_set.insert(dep_package_id);
476 break;
477 }
478 }
479
480 let unique_list = unique_deps.entry(package_id).or_default();
481 if unique {
482 unique_list.insert(dep_package_id);
483 }
484 }
485 }
486
487 let mut sorted_unique_deps = unique_deps.into_iter().collect::<Vec<_>>();
488 sorted_unique_deps.sort_by_key(|a| cmp::Reverse(a.1.len()));
489
490 for (package_id, deps) in sorted_unique_deps.iter() {
491 if !deps.is_empty() {
492 println!("{} {}", deps.len(), package_id);
493 }
494 for dep in deps {
495 println!(" {dep}");
496 }
497 }
498
499 Ok(())
500}