1mod 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 #[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(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 target_platform: Option<String>,
227
228 #[clap(long = "host-platform")]
229 host_platform: Option<String>,
231
232 #[clap(long, arg_enum, default_value = "all")]
233 build_kind: BuildKind,
235
236 #[clap(long, parse(from_os_str))]
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", parse(from_flag = parse_direction))]
345 output_direction: DependencyDirection,
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
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 #[clap(rename_all = "screaming_snake_case")]
400 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 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 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 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}