1use std::collections::BTreeMap;
2use std::collections::BTreeSet;
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::env;
6use std::io::BufRead;
7use std::io::BufReader;
8use std::io::Cursor;
9use std::path::Path;
10use std::process::Command;
11use std::process::Stdio;
12
13use anyhow::Context;
14use cargo_metadata::Metadata;
15use cargo_metadata::MetadataCommand;
16use cargo_util::paths;
17use cargo_util_schemas::core::PackageIdSpec;
18use clap::ArgAction;
19use clap::Parser;
20use indexmap::{IndexMap, IndexSet};
21use rustfix::{collect_suggestions, CodeFix, Suggestion};
22use tracing::{trace, warn};
23
24use crate::util::cli::PackageSelection;
25use crate::{
26 core::{shell, sysroot::get_sysroot},
27 ops::check::{
28 BuildUnit, CheckOutput, CrateType, DiagnosticLevel, Message, MessageDiagnostic, TargetKind,
29 },
30 util::{
31 cli::CheckFlags, messages::gen_please_report_this_bug_text, package::format_package_id,
32 vcs::VcsOpts,
33 },
34 CargoResult,
35};
36
37#[derive(Debug, Parser)]
38pub struct FixitArgs {
39 #[arg(long)]
41 clippy: bool,
42
43 #[arg(long)]
45 broken_code: bool,
46
47 #[arg(long = "Zdangerous-parallel-fixes")]
49 dangerous_parallel_fixes: bool,
50
51 #[command(flatten)]
52 color: colorchoice_clap::Color,
53
54 #[command(flatten)]
55 vcs_opts: VcsOpts,
56
57 #[command(flatten)]
58 check_flags: CheckFlags,
59
60 #[arg(long, action = ArgAction::Count)]
61 verbose: u8,
62}
63
64impl FixitArgs {
65 pub fn exec(self) -> CargoResult<()> {
66 exec(self)
67 }
68
69 fn to_command(&self) -> Command {
70 let cmd = if self.clippy { "clippy" } else { "check" };
71 let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
72 let mut command = Command::new(cargo);
73 command.arg(cmd).args(self.check_flags.to_flags());
74 command
75 }
76}
77
78#[derive(Debug, Default)]
79struct ActiveState {
80 snapshots: IndexMap<String, File>,
81 iterations: usize,
82}
83
84#[derive(Debug, Default)]
85struct File {
86 fixes: u32,
87 original_source: String,
88}
89
90type BuildUnitErrors = IndexMap<UnitId, IndexSet<String>>;
91type BuildUnitSuggestions =
92 IndexMap<UnitId, IndexMap<String, IndexSet<(Suggestion, Option<String>)>>>;
93
94#[tracing::instrument(skip_all)]
95fn exec(args: FixitArgs) -> CargoResult<()> {
96 args.color.write_global();
97
98 args.vcs_opts.valid_vcs()?;
99
100 let mut active_units = IndexMap::new();
101 match fix(&args, &mut active_units) {
102 Ok(()) => Ok(()),
103 Err(error) => {
104 for (file, original) in active_units
105 .values()
106 .flat_map(|state| state.snapshots.iter())
107 {
108 paths::write(file, &original.original_source)?;
109 }
110 Err(error)
111 }
112 }
113}
114
115fn fix(args: &FixitArgs, active_units: &mut IndexMap<UnitId, ActiveState>) -> CargoResult<()> {
116 let max_iterations: usize = env::var("CARGO_FIX_MAX_RETRIES")
117 .ok()
118 .and_then(|i| i.parse().ok())
119 .unwrap_or(4);
120 let package_metadata = package_metadata(&args.check_flags)?;
121 let primary_packages = PrimaryPackages::from_metadata(&package_metadata, &args.check_flags)?;
122 let mut plan = if args.dangerous_parallel_fixes {
123 UnitGraph::flat(&package_metadata)
124 } else {
125 UnitGraph::new(&package_metadata)
126 };
127 trace!("plan `{plan:#?}`");
128
129 let mut lint_cap = false;
130 let mut seen = BTreeSet::new();
131 let mut first = true;
132 let mut claimed_files: HashMap<same_file::Handle, UnitId> = HashMap::new();
133 loop {
134 trace!("check ({active_units:?})");
135 let (mut messages, exit_code) = check(args, &mut lint_cap)?;
136 messages.sort_unstable_by_key(|m| m.build_unit().cloned());
137 print_built(args, &messages)?;
138
139 if messages.is_empty() && exit_code != Some(0) {
140 let mut command = args.to_command();
141 command.status()?;
142 anyhow::bail!("could not compile");
143 } else if !args.broken_code && exit_code != Some(0) {
144 let mut out = String::new();
145
146 if !active_units.is_empty() {
147 out.push_str(
148 "failed to automatically apply fixes suggested by rustc\n\n\
149 after fixes were automatically applied the \
150 compiler reported errors within these files:\n\n",
151 );
152
153 for (
154 file,
155 File {
156 fixes: _,
157 original_source,
158 },
159 ) in active_units
160 .values()
161 .flat_map(|state| state.snapshots.iter())
162 {
163 out.push_str(&format!(" * {file}\n"));
164 shell::note(format!("reverting `{file}` to its original state"))?;
165 paths::write(file, original_source)?;
166 }
167 active_units.clear();
168 out.push('\n');
169
170 out.push_str(&gen_please_report_this_bug_text(args.clippy));
171
172 let mut errors = messages
173 .into_iter()
174 .filter_map(|e| match e {
175 CheckOutput::Message(m) => m.message.diagnostic.rendered,
176 _ => None,
177 })
178 .peekable();
179 if errors.peek().is_some() {
180 out.push_str("The errors reported are:\n");
181 }
182
183 for e in errors {
184 out.push_str(&format!("{}\n\n", e.trim_end()));
185 }
186
187 let (messages, _) = check(args, &mut lint_cap)?;
188 print_built(args, &messages)?;
189 let mut errors = messages
190 .into_iter()
191 .filter_map(|e| match e {
192 CheckOutput::Message(m) => m.message.diagnostic.rendered,
193 _ => None,
194 })
195 .peekable();
196
197 if errors.peek().is_some() {
198 out.push_str("The original errors are:\n");
199 }
200
201 for e in errors {
202 out.push_str(&format!("{}\n\n", e.trim_end()));
203 }
204
205 shell::warn(out)?;
206 } else {
207 for e in messages.into_iter().filter_map(|e| match e {
208 CheckOutput::Message(m) => m.message.diagnostic.rendered,
209 _ => None,
210 }) {
211 shell::print_ansi_stderr(format!("{}\n\n", e.trim_end()).as_bytes())?;
212 }
213 }
214
215 shell::note("try using `--broken-code` to fix errors")?;
216 anyhow::bail!("could not compile");
217 }
218 if first {
219 first = false;
220 let mut errors = IndexMap::new();
221 for message in &messages {
222 match message {
223 CheckOutput::Message(Message {
224 build_unit,
225 message: MessageDiagnostic { diagnostic, .. },
226 }) => {
227 let unit_id = UnitId::from_message(build_unit);
228 if let Some(rendered) = diagnostic.rendered.clone() {
229 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
230 errors.insert(rendered);
231 }
232 }
233 CheckOutput::Artifact(a) => {
234 let package_id = &a.build_unit.package_id;
235 let unit_id = UnitId::from_message(&a.build_unit);
236 if !is_local(package_id) || !plan.dependencies.contains_key(&unit_id) {
237 for error in errors.get(&unit_id).into_iter().flatten() {
238 shell::print_ansi_stderr(
239 format!("{}\n\n", error.trim_end()).as_bytes(),
240 )?;
241 }
242 if !a.fresh && seen.insert(package_id.to_owned()) {
243 shell::status("Checking", format_package_id(package_id)?)?;
244 }
245 }
246 }
247 }
248 }
249 }
250
251 let observed_packages: HashSet<String> = messages
252 .iter()
253 .filter_map(CheckOutput::build_unit)
254 .map(|unit| unit.package_id.clone())
255 .collect();
256 let (mut errors, suggestions) = collect_diagnostics(
257 messages.into_iter(),
258 &plan.finished,
259 &primary_packages,
260 active_units,
261 max_iterations,
262 );
263
264 let mut finishing = true;
265 while finishing {
266 let mut finished = BTreeSet::new();
267 for unit_id in active_units.keys() {
268 if suggestions.contains_key(unit_id) {
269 continue;
270 }
271 let errors = errors.shift_remove(unit_id);
272 finish_unit(unit_id, active_units, errors.as_ref())?;
273 finished.insert(unit_id.clone());
274 }
275 active_units.retain(|k, _v| !finished.contains(k));
276 claimed_files.retain(|_k, v| !finished.contains(v));
277 plan.mark_finished(finished);
278 finishing = false;
279 for unit_id in plan.take_ready() {
280 finishing = true;
281 trace!("scheduling `{unit_id:?}`");
282 let package_id = unit_id.package_id();
283 if observed_packages.contains(package_id) && seen.insert(package_id.to_owned()) {
284 shell::status("Checking", format_package_id(package_id)?)?;
285 }
286 active_units.insert(unit_id, Default::default());
287 }
288 }
289 if active_units.is_empty() {
290 assert!(plan.is_empty(), "{plan:#?}");
291 break;
292 }
293
294 'units: for (unit_id, state) in active_units.iter_mut() {
295 let unit_suggestions = suggestions
296 .get(unit_id)
297 .expect("finished all active_units without suggestions");
298 for path in state.snapshots.keys().chain(unit_suggestions.keys()) {
299 let Ok(handle) = same_file::Handle::from_path(path) else {
300 continue;
301 };
302 match claimed_files.entry(handle) {
303 std::collections::hash_map::Entry::Occupied(entry)
304 if entry.get() != unit_id =>
305 {
306 trace!("deferring `{unit_id:?}` due to contention over {path}");
307 claimed_files.retain(|_k, v| v != unit_id);
308 continue 'units;
309 }
310 std::collections::hash_map::Entry::Occupied(_) => {}
311 std::collections::hash_map::Entry::Vacant(entry) => {
312 entry.insert(unit_id.clone());
313 }
314 }
315 }
316 trace!("fixing `{unit_id:?}` {state:?}");
317 state.iterations += 1;
318 let _made_changes = fix_suggestions(unit_suggestions, state)?;
319 }
320 }
321 Ok(())
322}
323
324#[derive(Debug)]
326struct PrimaryPackages {
327 package_ids: HashSet<String>,
328}
329
330impl PrimaryPackages {
331 fn from_metadata(metadata: &Metadata, flags: &CheckFlags) -> CargoResult<Self> {
333 let package_ids = match flags.package_selection() {
334 PackageSelection::Default => metadata
335 .workspace_default_members
336 .iter()
337 .map(|package_id| package_id.repr.clone())
338 .collect(),
339 PackageSelection::Workspace { exclude } => {
340 let matcher = PackageSpecMatcher::new(exclude)?;
341 let mut package_ids = HashSet::new();
342 for package in metadata.workspace_packages() {
343 if !matcher.matches(package)? {
344 package_ids.insert(package.id.repr.clone());
345 }
346 }
347 package_ids
348 }
349 PackageSelection::Packages(packages) => {
350 let matcher = PackageSpecMatcher::new(packages)?;
351 let mut package_ids = HashSet::new();
352 for package in metadata.workspace_packages() {
353 if matcher.matches(package)? {
354 package_ids.insert(package.id.repr.clone());
355 }
356 }
357 package_ids
358 }
359 };
360 Ok(Self { package_ids })
361 }
362
363 fn contains(&self, package_id: &str) -> bool {
364 self.package_ids.contains(package_id)
365 }
366}
367
368#[derive(Debug)]
370struct PackageSpecMatcher {
371 specs: Vec<PackageIdSpec>,
372 patterns: Vec<glob::Pattern>,
373}
374
375impl PackageSpecMatcher {
376 fn new(raw_specs: &[String]) -> CargoResult<Self> {
377 let mut specs = Vec::new();
378 let mut patterns = Vec::new();
379
380 for raw_spec in raw_specs {
381 match PackageIdSpec::parse(raw_spec) {
382 Ok(spec) => specs.push(spec),
383 Err(_) if raw_spec.contains(&['*', '?', '[', ']'][..]) => {
384 let pattern = glob::Pattern::new(raw_spec)
385 .with_context(|| format!("failed to parse package pattern `{raw_spec}`"))?;
386 patterns.push(pattern);
387 }
388 Err(error) => {
389 return Err(error).with_context(|| {
390 format!("failed to parse package specification `{raw_spec}`")
391 });
392 }
393 }
394 }
395
396 Ok(Self { specs, patterns })
397 }
398
399 fn matches(&self, package: &cargo_metadata::Package) -> CargoResult<bool> {
400 if self
401 .patterns
402 .iter()
403 .any(|pattern| pattern.matches(package.name.as_ref()))
404 {
405 return Ok(true);
406 }
407
408 let package_id = PackageIdSpec::parse(&package.id.repr)
409 .with_context(|| format!("failed to parse package ID `{}`", package.id))?;
410 Ok(self
411 .specs
412 .iter()
413 .any(|spec| package_id_matches(spec, &package_id)))
414 }
415}
416
417fn package_id_matches(spec: &PackageIdSpec, package_id: &PackageIdSpec) -> bool {
419 spec.name() == package_id.name()
420 && spec.partial_version().is_none_or(|version| {
421 package_id
422 .version()
423 .is_some_and(|package_version| version.matches(&package_version))
424 })
425 && spec.url().is_none_or(|url| package_id.url() == Some(url))
426 && spec
427 .kind()
428 .is_none_or(|kind| package_id.kind() == Some(kind))
429}
430
431fn package_metadata(flags: &CheckFlags) -> CargoResult<Metadata> {
433 let mut command = MetadataCommand::new();
434 command.no_deps();
435 command.other_options(flags.to_metadata_flags());
436 let metadata = command.exec().context("failed to run `cargo metadata`")?;
437 Ok(metadata)
438}
439
440fn finish_unit(
441 unit_id: &UnitId,
442 active_units: &IndexMap<UnitId, ActiveState>,
443 errors: Option<&IndexSet<String>>,
444) -> CargoResult<()> {
445 trace!("finishing build unit `{unit_id:?}`");
446 if let Some(state) = active_units.get(unit_id) {
447 for (name, file) in &state.snapshots {
448 shell::fixed(name, file.fixes)?;
449 }
450 }
451
452 for error in errors.into_iter().flatten() {
453 shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
454 }
455
456 Ok(())
457}
458
459fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
460 let mut command = args.to_command();
461 command
462 .args(["--message-format", "json-diagnostic-rendered-ansi"])
463 .stderr(Stdio::piped())
464 .stdout(Stdio::piped());
465 if *lint_cap {
466 cap_lints(&mut command);
467 }
468 let output = command.output()?;
469 let mut output = to_check_output(output);
470
471 if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
472 *lint_cap = true;
473 cap_lints(&mut command);
474 output = to_check_output(command.output()?);
475 }
476
477 Ok(output)
478}
479
480fn print_built(args: &FixitArgs, messages: &[CheckOutput]) -> CargoResult<()> {
481 if args.verbose == 0 {
482 return Ok(());
483 }
484
485 for message in messages {
486 match message {
487 CheckOutput::Message(_) => {}
488 CheckOutput::Artifact(a) => {
489 if !a.fresh {
490 let pkg_id = format_package_id(&a.build_unit.package_id)?;
491 let name = &a.build_unit.target.name;
492 let kind = &a.build_unit.target.kind;
493 let kind = if 1 < kind.len() {
494 "lib" } else {
496 match &kind[0] {
497 TargetKind::Bin => "bin",
498 TargetKind::Test => "test",
499 TargetKind::Bench => "bench",
500 TargetKind::Example => "example",
501 TargetKind::CustomBuild => "custom-build",
502 TargetKind::Lib(_) => "lib",
503 }
504 };
505 shell::status("Checked", format!("{pkg_id} - {name} ({kind})"))?;
506 }
507 }
508 }
509 }
510
511 Ok(())
512}
513
514fn cap_lints(command: &mut Command) {
516 if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
517 let separator = if flags.is_empty() { "" } else { "\u{1f}" };
518 command.env(
519 "CARGO_ENCODED_RUSTFLAGS",
520 format!("{flags}{separator}--cap-lints=warn"),
521 );
522 } else {
523 command.env(
524 "RUSTFLAGS",
525 format!(
526 "--cap-lints=warn {}",
527 env::var("RUSTFLAGS").unwrap_or("".to_owned())
528 ),
529 );
530 }
531}
532
533fn denied_lint(messages: &[CheckOutput]) -> bool {
534 messages.iter().any(|message| {
535 matches!(&message, CheckOutput::Message(message)
536 if message.message.level == DiagnosticLevel::Error
537 && message.message.diagnostic.code.is_some())
538 })
539}
540
541fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
542 let buf = BufReader::new(Cursor::new(output.stdout));
543 (
544 buf.lines()
545 .map_while(|l| l.ok())
546 .filter_map(|l| serde_json::from_str(&l).ok())
547 .collect(),
548 output.status.code(),
549 )
550}
551
552#[tracing::instrument(skip_all)]
553fn collect_diagnostics(
554 messages: impl Iterator<Item = CheckOutput>,
555 finished: &BTreeSet<UnitId>,
556 primary_packages: &PrimaryPackages,
557 active_units: &mut IndexMap<UnitId, ActiveState>,
558 max_iterations: usize,
559) -> (BuildUnitErrors, BuildUnitSuggestions) {
560 let only = HashSet::new();
561
562 let mut suggestions = IndexMap::new();
563 let mut errors = IndexMap::new();
564
565 for message in messages {
566 let Message {
567 build_unit,
568 message: MessageDiagnostic { diagnostic, .. },
569 } = match message {
570 CheckOutput::Message(m) => m,
571 CheckOutput::Artifact(a) => {
572 let unit_id = UnitId::from_message(&a.build_unit);
573 errors.entry(unit_id).or_insert_with(IndexSet::new);
574 continue;
575 }
576 };
577
578 let unit_id = UnitId::from_message(&build_unit);
579 if finished.contains(&unit_id) {
580 trace!("rejecting build unit `{:?}` already finished", build_unit);
581 continue;
582 }
583
584 if let Some(state) = active_units.get_mut(&unit_id) {
585 if state.iterations >= max_iterations {
586 trace!(
587 "rejecting build unit `{:?}` exceeded max iteration count",
588 build_unit
589 );
590 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
591 if let Some(rendered) = diagnostic.rendered {
592 errors.insert(rendered);
593 }
594 continue;
595 }
596 }
597
598 if !primary_packages.contains(&build_unit.package_id) {
599 trace!(
600 "rejecting build unit `{:?}` not selected by the user",
601 build_unit
602 );
603 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
604 if let Some(rendered) = diagnostic.rendered {
605 errors.insert(rendered);
606 }
607 continue;
608 }
609
610 let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
611 rustfix::Filter::Everything
612 } else {
613 rustfix::Filter::MachineApplicableOnly
614 };
615 let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
616 trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
617 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
618 if let Some(rendered) = diagnostic.rendered {
619 errors.insert(rendered);
620 }
621 continue;
622 };
623
624 let mut file_names = suggestion
625 .solutions
626 .iter()
627 .flat_map(|s| s.replacements.iter())
628 .map(|r| &r.snippet.file_name);
629
630 let Some(file_name) = file_names.next() else {
631 trace!("rejecting as it has no solutions {:?}", suggestion);
632 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
633 if let Some(rendered) = diagnostic.rendered {
634 errors.insert(rendered);
635 }
636 continue;
637 };
638
639 if !file_names.all(|f| f == file_name) {
640 trace!("rejecting as it changes multiple files: {:?}", suggestion);
641 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
642 if let Some(rendered) = diagnostic.rendered {
643 errors.insert(rendered);
644 }
645 continue;
646 }
647
648 let file_path = Path::new(&file_name);
649 if let Ok(home) = env::var("CARGO_HOME") {
651 if file_path.starts_with(home) {
652 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
653 if let Some(rendered) = diagnostic.rendered {
654 errors.insert(rendered);
655 }
656 continue;
657 }
658 }
659
660 if file_path.is_absolute() {
661 if let Some(sysroot) = get_sysroot() {
662 if file_path.starts_with(sysroot) {
663 let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
664 if let Some(rendered) = diagnostic.rendered {
665 errors.insert(rendered);
666 }
667 continue;
668 }
669 }
670 }
671
672 let unit_suggestions = suggestions
673 .entry(unit_id.clone())
674 .or_insert(IndexMap::new());
675 unit_suggestions
676 .entry(file_name.to_owned())
677 .or_insert_with(IndexSet::new)
678 .insert((suggestion, diagnostic.rendered));
679 }
680
681 (errors, suggestions)
682}
683
684#[tracing::instrument(skip_all)]
685fn fix_suggestions(
686 unit_suggestions: &IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
687 state: &mut ActiveState,
688) -> CargoResult<bool> {
689 let mut made_changes = false;
690 for (file, suggestions) in unit_suggestions {
691 let source = match paths::read(file.as_ref()) {
692 Ok(s) => s,
693 Err(e) => {
694 warn!("failed to read `{}`: {}", file, e);
695 continue;
696 }
697 };
698
699 let mut fixed = CodeFix::new(&source);
700 let mut num_fixes = 0;
701
702 for (suggestion, _rendered) in suggestions.iter().rev() {
703 match fixed.apply(suggestion) {
704 Ok(()) => num_fixes += 1,
705 Err(rustfix::Error::AlreadyReplaced {
706 is_identical: true, ..
707 }) => {}
708 Err(e) => {
709 warn!("{e:?}");
710 }
711 }
712 }
713 if fixed.modified() {
714 let new_source = fixed.finish()?;
715 let file_state = state.snapshots.entry(file.clone()).or_insert(File {
716 fixes: 0,
717 original_source: source,
718 });
719 paths::write(file, new_source)?;
720 made_changes = true;
721 file_state.fixes += num_fixes;
722 }
723 }
724
725 Ok(made_changes)
726}
727
728#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
729struct UnitId {
730 inner: std::sync::Arc<UnitIdInner>,
731}
732
733#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
734struct UnitIdInner {
735 package_id: String,
740 target_kind: TargetKind,
741}
742
743impl UnitId {
744 fn from_message(build_unit: &BuildUnit) -> Self {
745 let target_kind = build_unit
747 .target
748 .kind
749 .first()
750 .expect("build unit targets have at least one kind");
751 let target_kind = match target_kind {
752 TargetKind::Lib(_) => TargetKind::Lib(CrateType::Lib),
753 target_kind => target_kind.clone(),
754 };
755
756 Self {
757 inner: std::sync::Arc::new(UnitIdInner {
758 package_id: build_unit.package_id.clone(),
759 target_kind,
760 }),
761 }
762 }
763
764 fn from_metadata(
765 package: &cargo_metadata::Package,
766 target_kind: &cargo_metadata::TargetKind,
767 ) -> Self {
768 let target_kind = match target_kind {
769 cargo_metadata::TargetKind::Bin => TargetKind::Bin,
770 cargo_metadata::TargetKind::Test => TargetKind::Test,
771 cargo_metadata::TargetKind::Bench => TargetKind::Bench,
772 cargo_metadata::TargetKind::Example => TargetKind::Example,
773 cargo_metadata::TargetKind::CustomBuild => TargetKind::CustomBuild,
774 cargo_metadata::TargetKind::Lib
776 | cargo_metadata::TargetKind::RLib
777 | cargo_metadata::TargetKind::DyLib
778 | cargo_metadata::TargetKind::CDyLib
779 | cargo_metadata::TargetKind::StaticLib
780 | cargo_metadata::TargetKind::ProcMacro => TargetKind::Lib(CrateType::Lib),
781 target_kind => TargetKind::Lib(CrateType::Other(target_kind.to_string())),
782 };
783
784 Self {
785 inner: std::sync::Arc::new(UnitIdInner {
786 package_id: package.id.repr.clone(),
787 target_kind,
788 }),
789 }
790 }
791
792 fn package_id(&self) -> &str {
793 &self.inner.package_id
794 }
795
796 fn target_kind(&self) -> &TargetKind {
797 &self.inner.target_kind
798 }
799}
800
801#[derive(Debug)]
802struct UnitGraph {
803 dependencies: BTreeMap<UnitId, BTreeSet<UnitId>>,
804 finished: BTreeSet<UnitId>,
805}
806
807impl UnitGraph {
808 fn flat(metadata: &Metadata) -> Self {
809 let mut dependencies = BTreeMap::default();
810 for package in &metadata.packages {
811 for target in &package.targets {
812 for kind in &target.kind {
813 let unit_id = UnitId::from_metadata(package, kind);
814 dependencies.insert(unit_id, Default::default());
815 }
816 }
817 }
818
819 Self {
820 dependencies,
821 finished: Default::default(),
822 }
823 }
824
825 fn new(metadata: &Metadata) -> Self {
826 let mut dependencies = BTreeMap::default();
827 let mut path_to_lib_unit_ids = BTreeMap::default();
828 for package in &metadata.packages {
829 let mut build_script_unit_id = None;
830 let mut lib_unit_ids = BTreeSet::new();
831 let mut other_unit_ids = BTreeSet::new();
832 for target in &package.targets {
833 for kind in &target.kind {
834 let unit_id = UnitId::from_metadata(package, kind);
835 if matches!(unit_id.target_kind(), TargetKind::CustomBuild) {
836 build_script_unit_id = Some(unit_id);
837 } else if matches!(unit_id.target_kind(), TargetKind::Lib(_)) {
838 lib_unit_ids.insert(unit_id);
839 } else {
840 other_unit_ids.insert(unit_id);
841 }
842 }
843 }
844
845 for unit_id in other_unit_ids {
846 let deps = if !lib_unit_ids.is_empty() {
847 lib_unit_ids.clone()
848 } else {
849 build_script_unit_id.clone().into_iter().collect()
850 };
851 dependencies.insert(unit_id, deps);
852 }
853 if !lib_unit_ids.is_empty() {
854 let path_source = manifest_path_to_dep_path(&package.manifest_path);
855 path_to_lib_unit_ids.insert(path_source.to_owned(), lib_unit_ids.clone());
856 for unit_id in lib_unit_ids {
857 let deps = build_script_unit_id.clone().into_iter().collect();
858 dependencies.insert(unit_id, deps);
859 }
860 }
861 if let Some(unit_id) = build_script_unit_id {
862 dependencies.insert(unit_id, Default::default());
863 }
864 }
865
866 for package in &metadata.packages {
867 for dependency in &package.dependencies {
868 let Some(dep_path) = &dependency.path else {
869 continue;
870 };
871 let Some(dep_unit_ids) = path_to_lib_unit_ids.get(dep_path) else {
872 continue;
873 };
874 for target in &package.targets {
875 for kind in &target.kind {
876 let unit_id = UnitId::from_metadata(package, kind);
877 let applies = match (&unit_id.target_kind(), &dependency.kind) {
878 (TargetKind::CustomBuild, cargo_metadata::DependencyKind::Build) => {
879 true
880 }
881 (TargetKind::Lib(_), cargo_metadata::DependencyKind::Normal) => true,
882 (TargetKind::Lib(_), cargo_metadata::DependencyKind::Development) => {
883 false
886 }
887 (TargetKind::Bin, cargo_metadata::DependencyKind::Normal) => true,
888 (TargetKind::Bin, cargo_metadata::DependencyKind::Development) => {
889 true
891 }
892 (TargetKind::Test, cargo_metadata::DependencyKind::Normal) => true,
893 (TargetKind::Test, cargo_metadata::DependencyKind::Development) => true,
894 (TargetKind::Bench, cargo_metadata::DependencyKind::Normal) => true,
895 (TargetKind::Bench, cargo_metadata::DependencyKind::Development) => {
896 true
897 }
898 (TargetKind::Example, cargo_metadata::DependencyKind::Normal) => true,
899 (TargetKind::Example, cargo_metadata::DependencyKind::Development) => {
900 true
901 }
902 _ => false,
903 };
904 if applies {
905 dependencies
906 .entry(unit_id)
907 .or_default()
908 .extend(dep_unit_ids.clone());
909 }
910 }
911 }
912 }
913 }
914
915 Self {
916 dependencies,
917 finished: Default::default(),
918 }
919 }
920
921 fn is_empty(&self) -> bool {
922 self.dependencies.is_empty()
923 }
924
925 fn take_ready(&mut self) -> BTreeSet<UnitId> {
926 self.dependencies
927 .extract_if(.., |_k, v| v.is_empty())
928 .map(|(k, _v)| k)
929 .collect()
930 }
931
932 fn mark_finished(&mut self, finished: BTreeSet<UnitId>) {
933 for dependencies in self.dependencies.values_mut() {
934 dependencies.retain(|id| !finished.contains(id));
935 }
936 self.finished.extend(finished);
937 }
938}
939
940fn manifest_path_to_dep_path(manifest_path: &camino::Utf8Path) -> &camino::Utf8Path {
941 if manifest_path.ends_with("Cargo.toml") {
942 manifest_path.parent().unwrap()
943 } else {
944 manifest_path
945 }
946}
947
948fn is_local(package_id: &str) -> bool {
949 package_id.starts_with("path+")
950}