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-selection flags.
332    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/// Matches Cargo package specifications and package-name glob patterns.
369#[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
417/// Mirrors Cargo's internal package-ID matching rules.
418fn 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
431/// Loads unresolved package metadata once and reuses it for selection and ordering.
432fn 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" // HACK: if its multiple, it is only a lib
495                    } 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
514/// Applies the original lint cap while preserving existing compiler flags.
515fn 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        // Do not write into registry cache. See rust-lang/cargo#9857.
650        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    // HACK: this should also track
736    // - test-mode or not
737    // - platform
738    // - host vs target mode
739    package_id: String,
740    target_kind: TargetKind,
741}
742
743impl UnitId {
744    fn from_message(build_unit: &BuildUnit) -> Self {
745        // HACK: just collapse all libs to one kind since we can't distinguish them
746        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            // HACK: just collapse all libs to one kind since we can't distinguish them
775            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                                // HACK: should include for unit test variant except that would
884                                // cause cycles
885                                false
886                            }
887                            (TargetKind::Bin, cargo_metadata::DependencyKind::Normal) => true,
888                            (TargetKind::Bin, cargo_metadata::DependencyKind::Development) => {
889                                // HACK: for the unit test variant
890                                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}