Skip to main content

cargo_fixit/ops/
fixit.rs

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    /// Run `clippy` instead of `check`
40    #[arg(long)]
41    clippy: bool,
42
43    /// Fix code even if it already has compiler errors
44    #[arg(long)]
45    broken_code: bool,
46
47    /// Fix all targets together, risking stale suggestions
48    #[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/// Packages that Cargo treats as primary for the current invocation.
325#[derive(Debug)]
326struct PrimaryPackages {
327    package_ids: HashSet<String>,
328}
329
330impl PrimaryPackages {
331    /// Reconstructs Cargo's primary package set from its package and target selectors.
332    fn from_metadata(metadata: &Metadata, flags: &CheckFlags) -> CargoResult<Self> {
333        let mut 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
361        for package in metadata.workspace_packages() {
362            if package_ids.contains(&package.id.repr) && !flags.selects_package_targets(package)? {
363                package_ids.remove(&package.id.repr);
364            }
365        }
366
367        Ok(Self { package_ids })
368    }
369
370    fn contains(&self, package_id: &str) -> bool {
371        self.package_ids.contains(package_id)
372    }
373}
374
375/// Matches Cargo package specifications and package-name glob patterns.
376#[derive(Debug)]
377struct PackageSpecMatcher {
378    specs: Vec<PackageIdSpec>,
379    patterns: Vec<glob::Pattern>,
380}
381
382impl PackageSpecMatcher {
383    fn new(raw_specs: &[String]) -> CargoResult<Self> {
384        let mut specs = Vec::new();
385        let mut patterns = Vec::new();
386
387        for raw_spec in raw_specs {
388            match PackageIdSpec::parse(raw_spec) {
389                Ok(spec) => specs.push(spec),
390                Err(_) if raw_spec.contains(&['*', '?', '[', ']'][..]) => {
391                    let pattern = glob::Pattern::new(raw_spec)
392                        .with_context(|| format!("failed to parse package pattern `{raw_spec}`"))?;
393                    patterns.push(pattern);
394                }
395                Err(error) => {
396                    return Err(error).with_context(|| {
397                        format!("failed to parse package specification `{raw_spec}`")
398                    });
399                }
400            }
401        }
402
403        Ok(Self { specs, patterns })
404    }
405
406    fn matches(&self, package: &cargo_metadata::Package) -> CargoResult<bool> {
407        if self
408            .patterns
409            .iter()
410            .any(|pattern| pattern.matches(package.name.as_ref()))
411        {
412            return Ok(true);
413        }
414
415        let package_id = PackageIdSpec::parse(&package.id.repr)
416            .with_context(|| format!("failed to parse package ID `{}`", package.id))?;
417        Ok(self
418            .specs
419            .iter()
420            .any(|spec| package_id_matches(spec, &package_id)))
421    }
422}
423
424/// Mirrors Cargo's internal package-ID matching rules.
425fn package_id_matches(spec: &PackageIdSpec, package_id: &PackageIdSpec) -> bool {
426    spec.name() == package_id.name()
427        && spec.partial_version().is_none_or(|version| {
428            package_id
429                .version()
430                .is_some_and(|package_version| version.matches(&package_version))
431        })
432        && spec.url().is_none_or(|url| package_id.url() == Some(url))
433        && spec
434            .kind()
435            .is_none_or(|kind| package_id.kind() == Some(kind))
436}
437
438/// Loads unresolved package metadata once and reuses it for selection and ordering.
439fn package_metadata(flags: &CheckFlags) -> CargoResult<Metadata> {
440    let mut command = MetadataCommand::new();
441    command.no_deps();
442    command.other_options(flags.to_metadata_flags());
443    let metadata = command.exec().context("failed to run `cargo metadata`")?;
444    Ok(metadata)
445}
446
447fn finish_unit(
448    unit_id: &UnitId,
449    active_units: &IndexMap<UnitId, ActiveState>,
450    errors: Option<&IndexSet<String>>,
451) -> CargoResult<()> {
452    trace!("finishing build unit `{unit_id:?}`");
453    if let Some(state) = active_units.get(unit_id) {
454        for (name, file) in &state.snapshots {
455            shell::fixed(name, file.fixes)?;
456        }
457    }
458
459    for error in errors.into_iter().flatten() {
460        shell::print_ansi_stderr(format!("{}\n\n", error.trim_end()).as_bytes())?;
461    }
462
463    Ok(())
464}
465
466fn check(args: &FixitArgs, lint_cap: &mut bool) -> CargoResult<(Vec<CheckOutput>, Option<i32>)> {
467    let mut command = args.to_command();
468    command
469        .args(["--message-format", "json-diagnostic-rendered-ansi"])
470        .stderr(Stdio::piped())
471        .stdout(Stdio::piped());
472    if *lint_cap {
473        cap_lints(&mut command);
474    }
475    let output = command.output()?;
476    let mut output = to_check_output(output);
477
478    if output.1 != Some(0) && !*lint_cap && denied_lint(&output.0) {
479        *lint_cap = true;
480        cap_lints(&mut command);
481        output = to_check_output(command.output()?);
482    }
483
484    Ok(output)
485}
486
487fn print_built(args: &FixitArgs, messages: &[CheckOutput]) -> CargoResult<()> {
488    if args.verbose == 0 {
489        return Ok(());
490    }
491
492    for message in messages {
493        match message {
494            CheckOutput::Message(_) => {}
495            CheckOutput::Artifact(a) => {
496                if !a.fresh {
497                    let pkg_id = format_package_id(&a.build_unit.package_id)?;
498                    let name = &a.build_unit.target.name;
499                    let kind = &a.build_unit.target.kind;
500                    let kind = if 1 < kind.len() {
501                        "lib" // HACK: if its multiple, it is only a lib
502                    } else {
503                        match &kind[0] {
504                            TargetKind::Bin => "bin",
505                            TargetKind::Test => "test",
506                            TargetKind::Bench => "bench",
507                            TargetKind::Example => "example",
508                            TargetKind::CustomBuild => "custom-build",
509                            TargetKind::Lib(_) => "lib",
510                        }
511                    };
512                    shell::status("Checked", format!("{pkg_id} - {name} ({kind})"))?;
513                }
514            }
515        }
516    }
517
518    Ok(())
519}
520
521/// Applies the original lint cap while preserving existing compiler flags.
522fn cap_lints(command: &mut Command) {
523    if let Ok(flags) = env::var("CARGO_ENCODED_RUSTFLAGS") {
524        let separator = if flags.is_empty() { "" } else { "\u{1f}" };
525        command.env(
526            "CARGO_ENCODED_RUSTFLAGS",
527            format!("{flags}{separator}--cap-lints=warn"),
528        );
529    } else {
530        command.env(
531            "RUSTFLAGS",
532            format!(
533                "--cap-lints=warn {}",
534                env::var("RUSTFLAGS").unwrap_or("".to_owned())
535            ),
536        );
537    }
538}
539
540fn denied_lint(messages: &[CheckOutput]) -> bool {
541    messages.iter().any(|message| {
542        matches!(&message, CheckOutput::Message(message)
543                if message.message.level == DiagnosticLevel::Error
544                    && message.message.diagnostic.code.is_some())
545    })
546}
547
548fn to_check_output(output: std::process::Output) -> (Vec<CheckOutput>, Option<i32>) {
549    let buf = BufReader::new(Cursor::new(output.stdout));
550    (
551        buf.lines()
552            .map_while(|l| l.ok())
553            .filter_map(|l| serde_json::from_str(&l).ok())
554            .collect(),
555        output.status.code(),
556    )
557}
558
559#[tracing::instrument(skip_all)]
560fn collect_diagnostics(
561    messages: impl Iterator<Item = CheckOutput>,
562    finished: &BTreeSet<UnitId>,
563    primary_packages: &PrimaryPackages,
564    active_units: &mut IndexMap<UnitId, ActiveState>,
565    max_iterations: usize,
566) -> (BuildUnitErrors, BuildUnitSuggestions) {
567    let only = HashSet::new();
568
569    let mut suggestions = IndexMap::new();
570    let mut errors = IndexMap::new();
571
572    for message in messages {
573        let Message {
574            build_unit,
575            message: MessageDiagnostic { diagnostic, .. },
576        } = match message {
577            CheckOutput::Message(m) => m,
578            CheckOutput::Artifact(a) => {
579                let unit_id = UnitId::from_message(&a.build_unit);
580                errors.entry(unit_id).or_insert_with(IndexSet::new);
581                continue;
582            }
583        };
584
585        let unit_id = UnitId::from_message(&build_unit);
586        if finished.contains(&unit_id) {
587            trace!("rejecting build unit `{:?}` already finished", build_unit);
588            continue;
589        }
590
591        if let Some(state) = active_units.get_mut(&unit_id) {
592            if state.iterations >= max_iterations {
593                trace!(
594                    "rejecting build unit `{:?}` exceeded max iteration count",
595                    build_unit
596                );
597                let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
598                if let Some(rendered) = diagnostic.rendered {
599                    errors.insert(rendered);
600                }
601                continue;
602            }
603        }
604
605        if !primary_packages.contains(&build_unit.package_id) {
606            trace!(
607                "rejecting build unit `{:?}` not selected by the user",
608                build_unit
609            );
610            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
611            if let Some(rendered) = diagnostic.rendered {
612                errors.insert(rendered);
613            }
614            continue;
615        }
616
617        let filter = if env::var("__CARGO_FIX_YOLO").is_ok() {
618            rustfix::Filter::Everything
619        } else {
620            rustfix::Filter::MachineApplicableOnly
621        };
622        let Some(suggestion) = collect_suggestions(&diagnostic, &only, filter) else {
623            trace!("rejecting as not a MachineApplicable diagnosis: {diagnostic:?}");
624            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
625            if let Some(rendered) = diagnostic.rendered {
626                errors.insert(rendered);
627            }
628            continue;
629        };
630
631        let mut file_names = suggestion
632            .solutions
633            .iter()
634            .flat_map(|s| s.replacements.iter())
635            .map(|r| &r.snippet.file_name);
636
637        let Some(file_name) = file_names.next() else {
638            trace!("rejecting as it has no solutions {:?}", suggestion);
639            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
640            if let Some(rendered) = diagnostic.rendered {
641                errors.insert(rendered);
642            }
643            continue;
644        };
645
646        if !file_names.all(|f| f == file_name) {
647            trace!("rejecting as it changes multiple files: {:?}", suggestion);
648            let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
649            if let Some(rendered) = diagnostic.rendered {
650                errors.insert(rendered);
651            }
652            continue;
653        }
654
655        let file_path = Path::new(&file_name);
656        // Do not write into registry cache. See rust-lang/cargo#9857.
657        if let Ok(home) = env::var("CARGO_HOME") {
658            if file_path.starts_with(home) {
659                let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
660                if let Some(rendered) = diagnostic.rendered {
661                    errors.insert(rendered);
662                }
663                continue;
664            }
665        }
666
667        if file_path.is_absolute() {
668            if let Some(sysroot) = get_sysroot() {
669                if file_path.starts_with(sysroot) {
670                    let errors = errors.entry(unit_id).or_insert_with(IndexSet::new);
671                    if let Some(rendered) = diagnostic.rendered {
672                        errors.insert(rendered);
673                    }
674                    continue;
675                }
676            }
677        }
678
679        let unit_suggestions = suggestions
680            .entry(unit_id.clone())
681            .or_insert(IndexMap::new());
682        unit_suggestions
683            .entry(file_name.to_owned())
684            .or_insert_with(IndexSet::new)
685            .insert((suggestion, diagnostic.rendered));
686    }
687
688    (errors, suggestions)
689}
690
691#[tracing::instrument(skip_all)]
692fn fix_suggestions(
693    unit_suggestions: &IndexMap<String, IndexSet<(Suggestion, Option<String>)>>,
694    state: &mut ActiveState,
695) -> CargoResult<bool> {
696    let mut made_changes = false;
697    for (file, suggestions) in unit_suggestions {
698        let source = match paths::read(file.as_ref()) {
699            Ok(s) => s,
700            Err(e) => {
701                warn!("failed to read `{}`: {}", file, e);
702                continue;
703            }
704        };
705
706        let mut fixed = CodeFix::new(&source);
707        let mut num_fixes = 0;
708
709        for (suggestion, _rendered) in suggestions.iter().rev() {
710            match fixed.apply(suggestion) {
711                Ok(()) => num_fixes += 1,
712                Err(rustfix::Error::AlreadyReplaced {
713                    is_identical: true, ..
714                }) => {}
715                Err(e) => {
716                    warn!("{e:?}");
717                }
718            }
719        }
720        if fixed.modified() {
721            let new_source = fixed.finish()?;
722            let file_state = state.snapshots.entry(file.clone()).or_insert(File {
723                fixes: 0,
724                original_source: source,
725            });
726            paths::write(file, new_source)?;
727            made_changes = true;
728            file_state.fixes += num_fixes;
729        }
730    }
731
732    Ok(made_changes)
733}
734
735#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
736struct UnitId {
737    inner: std::sync::Arc<UnitIdInner>,
738}
739
740#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
741struct UnitIdInner {
742    // HACK: this should also track
743    // - test-mode or not
744    // - platform
745    // - host vs target mode
746    package_id: String,
747    target_kind: TargetKind,
748}
749
750impl UnitId {
751    fn from_message(build_unit: &BuildUnit) -> Self {
752        // HACK: just collapse all libs to one kind since we can't distinguish them
753        let target_kind = build_unit
754            .target
755            .kind
756            .first()
757            .expect("build unit targets have at least one kind");
758        let target_kind = match target_kind {
759            TargetKind::Lib(_) => TargetKind::Lib(CrateType::Lib),
760            target_kind => target_kind.clone(),
761        };
762
763        Self {
764            inner: std::sync::Arc::new(UnitIdInner {
765                package_id: build_unit.package_id.clone(),
766                target_kind,
767            }),
768        }
769    }
770
771    fn from_metadata(
772        package: &cargo_metadata::Package,
773        target_kind: &cargo_metadata::TargetKind,
774    ) -> Self {
775        let target_kind = match target_kind {
776            cargo_metadata::TargetKind::Bin => TargetKind::Bin,
777            cargo_metadata::TargetKind::Test => TargetKind::Test,
778            cargo_metadata::TargetKind::Bench => TargetKind::Bench,
779            cargo_metadata::TargetKind::Example => TargetKind::Example,
780            cargo_metadata::TargetKind::CustomBuild => TargetKind::CustomBuild,
781            // HACK: just collapse all libs to one kind since we can't distinguish them
782            cargo_metadata::TargetKind::Lib
783            | cargo_metadata::TargetKind::RLib
784            | cargo_metadata::TargetKind::DyLib
785            | cargo_metadata::TargetKind::CDyLib
786            | cargo_metadata::TargetKind::StaticLib
787            | cargo_metadata::TargetKind::ProcMacro => TargetKind::Lib(CrateType::Lib),
788            target_kind => TargetKind::Lib(CrateType::Other(target_kind.to_string())),
789        };
790
791        Self {
792            inner: std::sync::Arc::new(UnitIdInner {
793                package_id: package.id.repr.clone(),
794                target_kind,
795            }),
796        }
797    }
798
799    fn package_id(&self) -> &str {
800        &self.inner.package_id
801    }
802
803    fn target_kind(&self) -> &TargetKind {
804        &self.inner.target_kind
805    }
806}
807
808#[derive(Debug)]
809struct UnitGraph {
810    dependencies: BTreeMap<UnitId, BTreeSet<UnitId>>,
811    finished: BTreeSet<UnitId>,
812}
813
814impl UnitGraph {
815    fn flat(metadata: &Metadata) -> Self {
816        let mut dependencies = BTreeMap::default();
817        for package in &metadata.packages {
818            for target in &package.targets {
819                for kind in &target.kind {
820                    let unit_id = UnitId::from_metadata(package, kind);
821                    dependencies.insert(unit_id, Default::default());
822                }
823            }
824        }
825
826        Self {
827            dependencies,
828            finished: Default::default(),
829        }
830    }
831
832    fn new(metadata: &Metadata) -> Self {
833        let mut dependencies = BTreeMap::default();
834        let mut path_to_lib_unit_ids = BTreeMap::default();
835        for package in &metadata.packages {
836            let mut build_script_unit_id = None;
837            let mut lib_unit_ids = BTreeSet::new();
838            let mut other_unit_ids = BTreeSet::new();
839            for target in &package.targets {
840                for kind in &target.kind {
841                    let unit_id = UnitId::from_metadata(package, kind);
842                    if matches!(unit_id.target_kind(), TargetKind::CustomBuild) {
843                        build_script_unit_id = Some(unit_id);
844                    } else if matches!(unit_id.target_kind(), TargetKind::Lib(_)) {
845                        lib_unit_ids.insert(unit_id);
846                    } else {
847                        other_unit_ids.insert(unit_id);
848                    }
849                }
850            }
851
852            for unit_id in other_unit_ids {
853                let deps = if !lib_unit_ids.is_empty() {
854                    lib_unit_ids.clone()
855                } else {
856                    build_script_unit_id.clone().into_iter().collect()
857                };
858                dependencies.insert(unit_id, deps);
859            }
860            if !lib_unit_ids.is_empty() {
861                let path_source = manifest_path_to_dep_path(&package.manifest_path);
862                path_to_lib_unit_ids.insert(path_source.to_owned(), lib_unit_ids.clone());
863                for unit_id in lib_unit_ids {
864                    let deps = build_script_unit_id.clone().into_iter().collect();
865                    dependencies.insert(unit_id, deps);
866                }
867            }
868            if let Some(unit_id) = build_script_unit_id {
869                dependencies.insert(unit_id, Default::default());
870            }
871        }
872
873        for package in &metadata.packages {
874            for dependency in &package.dependencies {
875                let Some(dep_path) = &dependency.path else {
876                    continue;
877                };
878                let Some(dep_unit_ids) = path_to_lib_unit_ids.get(dep_path) else {
879                    continue;
880                };
881                for target in &package.targets {
882                    for kind in &target.kind {
883                        let unit_id = UnitId::from_metadata(package, kind);
884                        let applies = match (&unit_id.target_kind(), &dependency.kind) {
885                            (TargetKind::CustomBuild, cargo_metadata::DependencyKind::Build) => {
886                                true
887                            }
888                            (TargetKind::Lib(_), cargo_metadata::DependencyKind::Normal) => true,
889                            (TargetKind::Lib(_), cargo_metadata::DependencyKind::Development) => {
890                                // HACK: should include for unit test variant except that would
891                                // cause cycles
892                                false
893                            }
894                            (TargetKind::Bin, cargo_metadata::DependencyKind::Normal) => true,
895                            (TargetKind::Bin, cargo_metadata::DependencyKind::Development) => {
896                                // HACK: for the unit test variant
897                                true
898                            }
899                            (TargetKind::Test, cargo_metadata::DependencyKind::Normal) => true,
900                            (TargetKind::Test, cargo_metadata::DependencyKind::Development) => true,
901                            (TargetKind::Bench, cargo_metadata::DependencyKind::Normal) => true,
902                            (TargetKind::Bench, cargo_metadata::DependencyKind::Development) => {
903                                true
904                            }
905                            (TargetKind::Example, cargo_metadata::DependencyKind::Normal) => true,
906                            (TargetKind::Example, cargo_metadata::DependencyKind::Development) => {
907                                true
908                            }
909                            _ => false,
910                        };
911                        if applies {
912                            dependencies
913                                .entry(unit_id)
914                                .or_default()
915                                .extend(dep_unit_ids.clone());
916                        }
917                    }
918                }
919            }
920        }
921
922        Self {
923            dependencies,
924            finished: Default::default(),
925        }
926    }
927
928    fn is_empty(&self) -> bool {
929        self.dependencies.is_empty()
930    }
931
932    fn take_ready(&mut self) -> BTreeSet<UnitId> {
933        self.dependencies
934            .extract_if(.., |_k, v| v.is_empty())
935            .map(|(k, _v)| k)
936            .collect()
937    }
938
939    fn mark_finished(&mut self, finished: BTreeSet<UnitId>) {
940        for dependencies in self.dependencies.values_mut() {
941            dependencies.retain(|id| !finished.contains(id));
942        }
943        self.finished.extend(finished);
944    }
945}
946
947fn manifest_path_to_dep_path(manifest_path: &camino::Utf8Path) -> &camino::Utf8Path {
948    if manifest_path.ends_with("Cargo.toml") {
949        manifest_path.parent().unwrap()
950    } else {
951        manifest_path
952    }
953}
954
955fn is_local(package_id: &str) -> bool {
956    package_id.starts_with("path+")
957}