Skip to main content

mbx_cache_cargo/
lib.rs

1//! Cargo invocation resolution shared by mbx and embedded cache clients.
2//!
3//! This crate is intentionally unstable while the first embedders converge.
4//! Breaking changes are made in a new pre-1.0 minor release.
5#![deny(missing_docs)]
6
7use mbx_cache_core::{CacheDigest, canonical_json};
8use serde::{Deserialize, Serialize};
9use std::collections::BTreeSet;
10use std::ffi::{OsStr, OsString};
11use std::path::{Path, PathBuf};
12use std::process::Command;
13use std::time::SystemTime;
14
15const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR";
16const PROBE_GLOBAL_FLAGS: [&str; 3] = ["-C", "--config", "-Z"];
17const PROBE_MANIFEST_TOGGLES: [&str; 3] = ["--offline", "--frozen", "--locked"];
18
19/// Why Cargo could not report the storage roots for an invocation.
20#[derive(Debug)]
21#[non_exhaustive]
22pub enum ProbeFailure {
23    /// Cargo could not be started or its output could not be read.
24    Io(std::io::Error),
25    /// Cargo exited unsuccessfully; stderr contains a bounded tail of its output.
26    Exit {
27        /// Cargo's process exit status.
28        status: std::process::ExitStatus,
29        /// The last five non-empty stderr lines, limited to 1 KiB.
30        stderr: String,
31    },
32    /// Cargo's stdout was not valid metadata JSON.
33    Json(serde_json::Error),
34    /// A required metadata field was missing or was not a string.
35    Schema(&'static str),
36}
37
38impl std::fmt::Display for ProbeFailure {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            Self::Io(error) => write!(f, "cargo metadata I/O error: {error}"),
42            Self::Exit { status, stderr } => {
43                write!(f, "cargo metadata failed with {status}")?;
44                if !stderr.is_empty() {
45                    write!(f, ":\n{stderr}")?;
46                }
47                Ok(())
48            }
49            Self::Json(error) => write!(f, "invalid cargo metadata JSON: {error}"),
50            Self::Schema(field) => write!(f, "cargo metadata {field} missing or not a string"),
51        }
52    }
53}
54
55impl std::error::Error for ProbeFailure {
56    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57        match self {
58            Self::Io(error) => Some(error),
59            Self::Json(error) => Some(error),
60            _ => None,
61        }
62    }
63}
64
65/// Cargo-resolved roots and the stable prediction-manifest identity for one invocation.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct CargoInvocation {
68    /// Cargo's resolved workspace root.
69    pub workspace_root: PathBuf,
70    /// Cargo's resolved output directory.
71    pub target_dir: PathBuf,
72    /// Cargo's intermediate build directory, when reported by this Cargo version.
73    pub build_dir: Option<PathBuf>,
74    /// Whether a flag, environment value, or Cargo configuration explicitly selected the target.
75    pub target_dir_requested: bool,
76    /// Cross-checkout identity used to select the action-prediction manifest.
77    pub build_identity: String,
78}
79
80/// Resolve one Cargo invocation exactly enough for a cache session.
81pub fn resolve(
82    cargo: &OsStr,
83    arguments: &[String],
84    working_dir: &Path,
85    target_dir_env: Option<OsString>,
86) -> CargoInvocation {
87    resolve_in(
88        cache_root().as_deref(),
89        cargo,
90        arguments,
91        working_dir,
92        target_dir_env,
93    )
94}
95
96fn resolve_in(
97    cache: Option<&Path>,
98    cargo: &OsStr,
99    arguments: &[String],
100    working_dir: &Path,
101    target_dir_env: Option<OsString>,
102) -> CargoInvocation {
103    let (reported, arguments) = probed_roots(
104        cache,
105        effective_cargo_home().as_deref(),
106        cargo,
107        arguments,
108        working_dir,
109        target_dir_env.as_deref(),
110    );
111    resolve_with_reported(arguments, working_dir, target_dir_env, reported.ok())
112}
113
114/// Resolve one Cargo invocation only when Cargo successfully reports its roots.
115///
116/// Persistent Cargo shims use this form to distinguish a usable build from an
117/// invocation they should pass through, while retaining the successful
118/// metadata result for the session instead of probing twice.
119pub fn resolve_reported(
120    cargo: &OsStr,
121    arguments: &[String],
122    working_dir: &Path,
123    target_dir_env: Option<OsString>,
124) -> Option<CargoInvocation> {
125    try_resolve_reported(cargo, arguments, working_dir, target_dir_env).ok()
126}
127
128/// Resolve Cargo's reported roots, preserving the reason a metadata probe failed.
129///
130/// Like [`resolve_reported`], this reuses successful metadata records while
131/// their inputs remain valid. Failures are never cached. An unsuccessful exit
132/// includes Cargo's status and the last five non-empty stderr lines, limited
133/// to 1 KiB; the full stderr is also logged at debug level.
134///
135/// # Errors
136///
137/// Returns an error if Cargo cannot be run, exits unsuccessfully, or reports
138/// invalid JSON or missing/non-string required metadata fields.
139pub fn try_resolve_reported(
140    cargo: &OsStr,
141    arguments: &[String],
142    working_dir: &Path,
143    target_dir_env: Option<OsString>,
144) -> Result<CargoInvocation, ProbeFailure> {
145    try_resolve_reported_from_home(
146        cache_root().as_deref(),
147        effective_cargo_home().as_deref(),
148        cargo,
149        arguments,
150        working_dir,
151        target_dir_env,
152    )
153}
154
155#[cfg(test)]
156fn resolve_reported_in(
157    cache: Option<&Path>,
158    cargo: &OsStr,
159    arguments: &[String],
160    working_dir: &Path,
161    target_dir_env: Option<OsString>,
162) -> Option<CargoInvocation> {
163    resolve_reported_from_home(
164        cache,
165        effective_cargo_home().as_deref(),
166        cargo,
167        arguments,
168        working_dir,
169        target_dir_env,
170    )
171}
172
173/// The directory Cargo reads its home configuration from: `CARGO_HOME`, or
174/// `.cargo` under the home directory when it is unset.
175fn effective_cargo_home() -> Option<PathBuf> {
176    std::env::var_os("CARGO_HOME")
177        .filter(|value| !value.is_empty())
178        .map(PathBuf::from)
179        .or_else(|| dirs::home_dir().map(|home| home.join(".cargo")))
180}
181
182#[cfg(test)]
183fn resolve_reported_from_home(
184    cache: Option<&Path>,
185    cargo_home: Option<&Path>,
186    cargo: &OsStr,
187    arguments: &[String],
188    working_dir: &Path,
189    target_dir_env: Option<OsString>,
190) -> Option<CargoInvocation> {
191    try_resolve_reported_from_home(
192        cache,
193        cargo_home,
194        cargo,
195        arguments,
196        working_dir,
197        target_dir_env,
198    )
199    .ok()
200}
201
202fn try_resolve_reported_from_home(
203    cache: Option<&Path>,
204    cargo_home: Option<&Path>,
205    cargo: &OsStr,
206    arguments: &[String],
207    working_dir: &Path,
208    target_dir_env: Option<OsString>,
209) -> Result<CargoInvocation, ProbeFailure> {
210    let (reported, arguments) = probed_roots(
211        cache,
212        cargo_home,
213        cargo,
214        arguments,
215        working_dir,
216        target_dir_env.as_deref(),
217    );
218    Ok(resolve_with_reported(
219        arguments,
220        working_dir,
221        target_dir_env,
222        Some(reported?),
223    ))
224}
225
226/// The workspace root, target directory, and build directory Cargo reported.
227type ReportedRoots = (PathBuf, PathBuf, Option<PathBuf>);
228
229/// Probe the roots for `arguments`, and report which of them the answer is for.
230///
231/// Cargo hands everything after an external subcommand to it untouched, so
232/// `cargo kani -Z autoharness` gives `-Z` to kani, not to Cargo, and a probe
233/// that forwards it fails. Whether a word is an external subcommand or an alias
234/// for a builtin is not known here, and flags after an alias belong to Cargo,
235/// so the full command line is tried first. Only when that fails are the
236/// arguments cut back to those Cargo reads, and the cut-back ones are the ones
237/// every later step must read too.
238fn probed_roots<'a>(
239    cache: Option<&Path>,
240    cargo_home: Option<&Path>,
241    cargo: &OsStr,
242    arguments: &'a [String],
243    working_dir: &Path,
244    target_dir_env: Option<&OsStr>,
245) -> (Result<ReportedRoots, ProbeFailure>, &'a [String]) {
246    let probe = |arguments: &[String]| {
247        recalled_cargo_roots(
248            cache,
249            cargo_home,
250            cargo,
251            cargo_arguments(arguments),
252            working_dir,
253            target_dir_env,
254        )
255    };
256    let full = probe(arguments);
257    if full.is_ok() {
258        return (full, arguments);
259    }
260    match owned_arguments(arguments) {
261        Some(owned) => (probe(owned), owned),
262        None => (full, arguments),
263    }
264}
265
266/// The arguments up to and including a subcommand that is not a Cargo builtin,
267/// when any follow it. `None` where nothing would be dropped.
268fn owned_arguments(arguments: &[String]) -> Option<&[String]> {
269    let arguments = cargo_arguments(arguments);
270    let (index, command) = subcommand(arguments)?;
271    (!is_builtin_command(command) && index + 1 < arguments.len()).then(|| &arguments[..=index])
272}
273
274/// Whether `command` is one of Cargo's own subcommands, which Cargo parses the
275/// rest of the command line for, as opposed to an alias or an external command.
276pub fn is_builtin_command(command: &str) -> bool {
277    matches!(
278        command,
279        "build"
280            | "check"
281            | "test"
282            | "run"
283            | "bench"
284            | "doc"
285            | "rustc"
286            | "rustdoc"
287            | "fix"
288            | "install"
289            | "help"
290            | "new"
291            | "init"
292            | "add"
293            | "remove"
294            | "update"
295            | "fetch"
296            | "clean"
297            | "config"
298            | "tree"
299            | "search"
300            | "info"
301            | "login"
302            | "logout"
303            | "owner"
304            | "package"
305            | "publish"
306            | "uninstall"
307            | "yank"
308            | "locate-project"
309            | "metadata"
310            | "generate-lockfile"
311            | "pkgid"
312            | "read-manifest"
313            | "report"
314            | "vendor"
315            | "verify-project"
316            | "version"
317            | "git-checkout"
318    )
319}
320
321fn resolve_with_reported(
322    arguments: &[String],
323    working_dir: &Path,
324    target_dir_env: Option<OsString>,
325    reported: Option<(PathBuf, PathBuf, Option<PathBuf>)>,
326) -> CargoInvocation {
327    let cargo_args = cargo_arguments(arguments);
328    let install_source = path_install_dir(cargo_args, working_dir);
329    let invocation_dir = if install_source.is_some() {
330        directory_option_dir(cargo_args, working_dir)
331    } else {
332        invocation_dir(cargo_args, working_dir)
333    };
334    let workspace_root = reported
335        .as_ref()
336        .map(|roots| roots.0.clone())
337        .unwrap_or_else(|| workspace_root(&invocation_dir));
338    let flagged = target_dir_argument(cargo_args);
339    let target_dir_requested = flagged.is_some()
340        || target_dir_env
341            .as_ref()
342            .is_some_and(|value| !value.is_empty())
343        || cargo_config_may_set_target_dir(
344            cargo_args,
345            install_source.as_deref().unwrap_or(&invocation_dir),
346        );
347    let build_dir = reported.as_ref().and_then(|roots| roots.2.clone());
348    let target_dir = flagged
349        .map(|value| absolute(&invocation_dir, value))
350        .or_else(|| reported.map(|roots| roots.1))
351        .or_else(|| {
352            target_dir_env
353                .filter(|value| !value.is_empty())
354                .map(PathBuf::from)
355                .map(|value| absolute(&invocation_dir, &value.to_string_lossy()))
356        })
357        .unwrap_or_else(|| workspace_root.join("target"));
358    let build_identity = build_identity(&workspace_root, arguments);
359    CargoInvocation {
360        workspace_root,
361        target_dir,
362        target_dir_requested,
363        build_dir,
364        build_identity,
365    }
366}
367
368#[derive(Serialize)]
369struct ActionIdentity<'a> {
370    version: u8,
371    workspace: &'a str,
372    os: &'static str,
373    arch: &'static str,
374}
375
376/// Derive the prediction-manifest identity used by mbx for a Cargo command.
377///
378/// The command is accepted for API compatibility but deliberately does not
379/// enter the identity. rustc invocation digests distinguish profiles,
380/// features, targets, and compiler toolchains; keeping the surrounding Cargo
381/// command here would prevent equivalent dependency compilations from sharing
382/// predictions across `build`, `test`, and Clippy.
383pub fn build_identity(workspace_root: &Path, _command: &[String]) -> String {
384    identity_for_workspace(&workspace_marker(workspace_root))
385}
386
387/// The identities recorded for earlier states of this workspace's
388/// `Cargo.lock`, newest first.
389///
390/// The identity is the lockfile's digest, so a dependency bump starts a
391/// manifest with nothing in it although most of the graph is unchanged and
392/// its results are already cached. Version control remembers what the lockfile
393/// was before: the committed copy first, for an edit that has not been
394/// committed yet, then the copy in `HEAD`'s first parent, which on a pull
395/// request's merge commit is the base branch, then the copy each commit that
396/// touched the file replaced. A shallow clone offers what it can reach, which
397/// on a `fetch-depth: 1` checkout is nothing beyond `HEAD` itself. A checkout
398/// that is not under Git, or a lockfile it does not track, has no history to
399/// offer.
400///
401/// Releases before line endings were normalized digested the lockfile bytes
402/// as they were, so a CRLF lockfile, a CRLF copy in history, or on a checkout
403/// that converts line endings any earlier state as it was checked out, was
404/// recorded under an identity of its own. Those identities are offered too,
405/// each after the normalized one for the same state.
406///
407/// Every prediction a manifest holds is rehashed before it is trusted, so an
408/// inherited one can only fail to match, never restore the wrong result.
409pub fn previous_build_identities(workspace_root: &Path) -> Vec<String> {
410    let Ok(lock) = std::fs::read(workspace_root.join("Cargo.lock")) else {
411        return Vec::new();
412    };
413    let current = lockfile_marker(&lock);
414    let checked_out = CacheDigest::blake3(&lock).hash;
415    // A checkout that converted this lockfile converted the earlier ones too.
416    let converts = current != checked_out;
417    let mut seen = BTreeSet::from([current]);
418    let mut identities = Vec::new();
419    if seen.insert(checked_out.clone()) {
420        identities.push(identity_for_workspace(&checked_out));
421    }
422    // History belongs to the tracked file. A lockfile Cargo generated after a
423    // tracked one was deleted has none, whatever the deleted one's was.
424    if git_output(
425        workspace_root,
426        &["ls-files", "--error-unmatch", "--", "Cargo.lock"],
427    )
428    .is_none()
429    {
430        return identities;
431    }
432    let limit = PREVIOUS_LOCKFILE_STATES.to_string();
433    let Some(revisions) = git_output(
434        workspace_root,
435        &["rev-list", "-n", &limit, "HEAD", "--", "Cargo.lock"],
436    ) else {
437        return identities;
438    };
439    // A commit's own copy of the lockfile is what its successor replaced, so
440    // the parent of each commit that touched the file is one state further
441    // back; a root commit has no parent and is skipped.
442    let revisions = String::from_utf8_lossy(&revisions);
443    let candidates = ["HEAD", "HEAD^"].into_iter().map(str::to_string).chain(
444        revisions
445            .lines()
446            .map(|revision| format!("{}^", revision.trim())),
447    );
448    let mut states = 0;
449    for revision in candidates {
450        let Some(lock) = git_output(
451            workspace_root,
452            &["show", &format!("{revision}:./Cargo.lock")],
453        ) else {
454            continue;
455        };
456        let normalized = lockfile_marker(&lock);
457        let new_state = !seen.contains(&normalized);
458        let mut markers = vec![normalized, CacheDigest::blake3(&lock).hash];
459        if converts {
460            markers.push(CacheDigest::blake3(&crlf_checkout(&lock)).hash);
461        }
462        for marker in markers {
463            if seen.insert(marker.clone()) {
464                identities.push(identity_for_workspace(&marker));
465            }
466        }
467        states += usize::from(new_state);
468        if states == PREVIOUS_LOCKFILE_STATES {
469            break;
470        }
471    }
472    identities
473}
474
475/// How many earlier lockfile states are offered as prediction sources.
476///
477/// Each one costs a manifest lookup per identity it offers when it is
478/// consulted, which only happens once nothing was recorded under the current
479/// identity, and a run of dependency bumps rarely goes deeper than this before
480/// a build of the trunk records the newest state.
481const PREVIOUS_LOCKFILE_STATES: usize = 8;
482
483/// Git's standard output for a command run in `workspace_root`, or nothing
484/// when Git is absent or the command fails.
485fn git_output(workspace_root: &Path, arguments: &[&str]) -> Option<Vec<u8>> {
486    let output = Command::new("git")
487        .arg("-C")
488        .arg(workspace_root)
489        .args(arguments)
490        .stdin(std::process::Stdio::null())
491        .stderr(std::process::Stdio::null())
492        .output()
493        .ok()?;
494    output.status.success().then_some(output.stdout)
495}
496
497/// The manifest identity for one workspace marker on this platform.
498fn identity_for_workspace(workspace: &str) -> String {
499    let bytes = canonical_json(&ActionIdentity {
500        version: 3,
501        workspace,
502        os: std::env::consts::OS,
503        arch: std::env::consts::ARCH,
504    })
505    .expect("Cargo build identity must serialize");
506    CacheDigest::blake3(&bytes).hash
507}
508
509fn workspace_marker(workspace_root: &Path) -> String {
510    std::fs::read(workspace_root.join("Cargo.lock")).map_or_else(
511        |_| {
512            workspace_root
513                .file_name()
514                .unwrap_or_default()
515                .to_string_lossy()
516                .into_owned()
517        },
518        |lock| lockfile_marker(&lock),
519    )
520}
521
522/// The digest that names a lockfile's contents, with CRLF line endings read
523/// as LF.
524///
525/// Git for Windows checks text files out with CRLF by default, while history
526/// keeps the LF it was committed with. Digesting the bytes as they are would
527/// give such a checkout an identity no earlier state of its lockfile can
528/// match, so it could never inherit their predictions.
529fn lockfile_marker(lock: &[u8]) -> String {
530    let mut normalized = Vec::with_capacity(lock.len());
531    let mut bytes = lock.iter().copied().peekable();
532    while let Some(byte) = bytes.next() {
533        if byte != b'\r' || bytes.peek() != Some(&b'\n') {
534            normalized.push(byte);
535        }
536    }
537    CacheDigest::blake3(&normalized).hash
538}
539
540/// A lockfile as a checkout that converts line endings writes it.
541fn crlf_checkout(lock: &[u8]) -> Vec<u8> {
542    let mut converted = Vec::with_capacity(lock.len() + lock.len() / 32);
543    let mut previous = None;
544    for &byte in lock {
545        if byte == b'\n' && previous != Some(b'\r') {
546            converted.push(b'\r');
547        }
548        converted.push(byte);
549        previous = Some(byte);
550    }
551    converted
552}
553
554/// Resolve the shared mbx cache root from the environment, machine config, or platform default.
555///
556/// Match `Config`'s environment layer: a Unicode value overrides the file, while a non-Unicode
557/// value is skipped. An empty value overrides the file too, but names no usable directory
558/// (`Config` rejects it once loaded), so it resolves no root rather than a relative path.
559pub fn cache_root() -> Option<PathBuf> {
560    if let Some(root) = std::env::var_os("MBX_CACHE_DIR").filter(|value| value.to_str().is_some()) {
561        return (!root.is_empty()).then(|| PathBuf::from(root));
562    }
563    if let Some(config) = dirs::config_dir().map(|root| root.join("mbx/config.toml"))
564        && let Ok(contents) = std::fs::read_to_string(config)
565        && let Ok(document) = toml::from_str::<toml::Value>(&contents)
566        && let Some(root) = document.get("cache_dir").and_then(toml::Value::as_str)
567    {
568        return Some(PathBuf::from(root));
569    }
570    dirs::cache_dir().map(|root| root.join("mbx"))
571}
572
573fn cargo_arguments(arguments: &[String]) -> &[String] {
574    &arguments[..arguments
575        .iter()
576        .position(|argument| argument == "--")
577        .unwrap_or(arguments.len())]
578}
579
580fn absolute(working_dir: &Path, value: &str) -> PathBuf {
581    let path = PathBuf::from(value);
582    if path.is_absolute() {
583        path
584    } else {
585        working_dir.join(path)
586    }
587}
588
589fn forwarded_flags(arguments: &[String], flags: &[&str]) -> Vec<String> {
590    let mut forwarded = Vec::new();
591    let mut remaining = arguments.iter();
592    while let Some(argument) = remaining.next() {
593        if let Some((flag, value)) = argument
594            .split_once('=')
595            .filter(|(flag, _)| flags.contains(flag))
596        {
597            forwarded.extend([flag.to_string(), value.to_string()]);
598        } else if flags.contains(&argument.as_str()) {
599            if let Some(value) = remaining.next() {
600                forwarded.extend([argument.clone(), value.clone()]);
601            }
602        } else if flags
603            .iter()
604            .any(|flag| flag.len() == 2 && argument.len() > 2 && argument.starts_with(flag))
605        {
606            forwarded.push(argument.clone());
607        }
608    }
609    forwarded
610}
611
612fn invocation_dir(arguments: &[String], working_dir: &Path) -> PathBuf {
613    flag_value(arguments, "-C")
614        .map(|value| absolute(working_dir, value))
615        .unwrap_or_else(|| working_dir.to_path_buf())
616}
617
618// Path installs change the probe's cwd, and manifest discovery starts from
619// wherever Cargo was pointed, so resolve every directory-option spelling.
620fn directory_option_dir(arguments: &[String], working_dir: &Path) -> PathBuf {
621    flag_value(arguments, "-C")
622        .or_else(|| flag_value(arguments, "--directory"))
623        .or_else(|| {
624            arguments
625                .iter()
626                .find_map(|arg| arg.strip_prefix("-C").filter(|value| !value.is_empty()))
627        })
628        .map(|value| absolute(working_dir, value))
629        .unwrap_or_else(|| working_dir.to_path_buf())
630}
631
632/// Whether Cargo has a manifest to work from here.
633///
634/// Without one there is no package to compile, no target directory to place,
635/// and nothing for a probe to report, so a metadata failure means only that
636/// Cargo was run outside a project. Cargo's own diagnostic is then the useful
637/// one, and the caller can hand the invocation straight to it.
638pub fn manifest_in_scope(arguments: &[String], working_dir: &Path) -> bool {
639    let arguments = cargo_arguments(arguments);
640    let invocation = directory_option_dir(arguments, working_dir);
641    // `install --path` compiles the manifest in the directory it names, which
642    // it has even where the invocation directory has none, and does not walk
643    // up to a surrounding workspace for it. Only a real install gives `--path`
644    // that meaning: other subcommands use the same flag for their own
645    // purposes, such as the local template `cargo generate --path` reads.
646    // Callers expand aliases first, so an aliased install arrives spelled out.
647    if let Some(source) = path_install_dir(arguments, working_dir) {
648        return source.join("Cargo.toml").is_file();
649    }
650    match flag_value(arguments, "--manifest-path") {
651        Some(manifest) => absolute(&invocation, manifest).is_file(),
652        None => invocation
653            .ancestors()
654            .any(|directory| directory.join("Cargo.toml").is_file()),
655    }
656}
657
658// Only a real install subcommand gives --path this meaning; option values
659// and arguments after -- must not make an unrelated command look like one.
660fn path_install_dir(arguments: &[String], working_dir: &Path) -> Option<PathBuf> {
661    let arguments = cargo_arguments(arguments);
662    let (_, command) = subcommand(arguments)?;
663    (command == "install")
664        .then(|| {
665            flag_value(arguments, "--path")
666                .map(|path| absolute(&directory_option_dir(arguments, working_dir), path))
667        })
668        .flatten()
669}
670
671// The first word that is neither a toolchain nor the value of a global option.
672fn subcommand(arguments: &[String]) -> Option<(usize, &str)> {
673    let mut remaining = arguments.iter().enumerate();
674    while let Some((index, argument)) = remaining.next() {
675        match argument.as_str() {
676            "--color" | "--config" | "-Z" | "-C" | "--directory" => {
677                remaining.next()?;
678            }
679            value if !value.starts_with('-') && !value.starts_with('+') => {
680                return Some((index, value));
681            }
682            _ => {}
683        }
684    }
685    None
686}
687
688/// Whether the arguments install a package from a registry or Git repository.
689///
690/// Such an install has no local manifest: Cargo downloads the source and
691/// builds it in a scratch directory, so the roots a `cargo metadata` probe
692/// reports describe whatever project the caller happens to stand in, if any.
693/// `--list` only reads the install metadata and compiles nothing.
694pub fn is_remote_install(arguments: &[String]) -> bool {
695    let arguments = cargo_arguments(arguments);
696    subcommand(arguments).is_some_and(|(_, command)| command == "install")
697        && flag_value(arguments, "--path").is_none()
698        && !arguments.iter().any(|argument| argument == "--list")
699}
700
701/// The target directory a remote install was explicitly given, by
702/// `--target-dir` or `CARGO_TARGET_DIR`. Without one Cargo builds in a
703/// directory it creates and deletes, which no cache key can name.
704pub fn remote_install_target_dir(
705    arguments: &[String],
706    working_dir: &Path,
707    target_dir_env: Option<&OsStr>,
708) -> Option<PathBuf> {
709    let invocation = directory_option_dir(cargo_arguments(arguments), working_dir);
710    target_dir_argument(cargo_arguments(arguments))
711        .map(|value| absolute(&invocation, value))
712        .or_else(|| {
713            target_dir_env
714                .filter(|value| !value.is_empty())
715                .map(|value| absolute(&invocation, &value.to_string_lossy()))
716        })
717}
718
719/// A directory name for the package a remote install builds, stable across
720/// repeated installs of it, so their prediction manifest is found again. The
721/// install root, features, and other options do not enter it: predictions are
722/// verified before they are trusted, so sharing one only costs a miss.
723pub fn remote_install_name(arguments: &[String]) -> String {
724    const VALUE_FLAGS: [&str; 24] = [
725        "--version",
726        "--git",
727        "--branch",
728        "--tag",
729        "--rev",
730        "--root",
731        "--registry",
732        "--index",
733        "--features",
734        "-F",
735        "--bin",
736        "--example",
737        "--profile",
738        "--target",
739        "--target-dir",
740        "-j",
741        "--jobs",
742        "--config",
743        "-Z",
744        "--color",
745        "--message-format",
746        "--directory",
747        "-C",
748        "--path",
749    ];
750    let arguments = cargo_arguments(arguments);
751    let start = subcommand(arguments).map_or(arguments.len(), |(index, _)| index + 1);
752    let mut packages = Vec::new();
753    let mut remaining = arguments[start..].iter();
754    while let Some(argument) = remaining.next() {
755        if VALUE_FLAGS.contains(&argument.as_str()) {
756            remaining.next();
757        } else if !argument.starts_with('-') {
758            packages.push(argument.as_str());
759        }
760    }
761    let identity = canonical_json(&(
762        &packages,
763        flag_value(arguments, "--git"),
764        flag_value(arguments, "--version"),
765    ))
766    .unwrap_or_default();
767    let label: String = packages
768        .first()
769        .copied()
770        .unwrap_or("git")
771        .chars()
772        .map(|c| {
773            if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
774                c
775            } else {
776                '_'
777            }
778        })
779        .take(64)
780        .collect();
781    let hash = CacheDigest::blake3(&identity).hash;
782    format!("{label}-{}", &hash[..12.min(hash.len())])
783}
784
785// Inline includes use cwd, unlike includes inside a config file. Rewrite the
786// override itself: appending another include would concatenate array entries.
787fn rebase_cli_include(value: &str, caller: &Path) -> String {
788    fn rebase(value: &mut toml::Value, caller: &Path) {
789        match value {
790            toml::Value::String(path) => {
791                *path = absolute(caller, path).to_string_lossy().into_owned();
792            }
793            toml::Value::Array(entries) => {
794                for entry in entries {
795                    rebase(entry, caller);
796                }
797            }
798            toml::Value::Table(entry) => {
799                if let Some(toml::Value::String(path)) = entry.get_mut("path") {
800                    *path = absolute(caller, path).to_string_lossy().into_owned();
801                }
802            }
803            _ => {}
804        }
805    }
806    if let Ok(mut config) = toml::from_str::<toml::Value>(value)
807        && config.as_table().is_some_and(|table| table.len() == 1)
808        && let Some(include) = config.get_mut("include")
809    {
810        rebase(include, caller);
811        return format!("include = {include}");
812    }
813    value.to_owned()
814}
815
816/// The roots a `cargo metadata` probe reports, remembered under `cache`.
817///
818/// The probe is a Cargo process per build, and it costs more than the shim
819/// work around a hot compile once the rest has been trimmed. Its answer is a
820/// function of things this crate can watch: the Cargo binary, the manifests
821/// and configuration files Cargo reads on the way from the invocation
822/// directory to the root, and the environment that selects a target
823/// directory. A record stands while every one of those is as the probing
824/// run saw it, including the ones that were absent; anything else, or no
825/// cache to remember in, runs the probe.
826fn recalled_cargo_roots(
827    cache: Option<&Path>,
828    cargo_home: Option<&Path>,
829    cargo: &OsStr,
830    arguments: &[String],
831    working_dir: &Path,
832    target_dir_env: Option<&OsStr>,
833) -> Result<(PathBuf, PathBuf, Option<PathBuf>), ProbeFailure> {
834    if let Some(source) = path_install_dir(arguments, working_dir) {
835        // `install --path` reads configuration from the source directory,
836        // whereas metadata normally reads it from the caller's directory.
837        // Probe there, retaining caller-relative CLI paths and target overrides.
838        let caller = directory_option_dir(arguments, working_dir);
839        let mut probe_args = Vec::new();
840        if let Some(target) =
841            std::env::var_os("CARGO_BUILD_TARGET_DIR").filter(|value| !value.is_empty())
842        {
843            // This environment setting outranks files but is itself overridden
844            // by --config; insert it before the caller's explicit overrides.
845            probe_args.extend([
846                "--config".into(),
847                format!(
848                    "build.target-dir = {}",
849                    toml::Value::String(
850                        absolute(&caller, &target.to_string_lossy())
851                            .to_string_lossy()
852                            .into_owned()
853                    )
854                ),
855            ]);
856        }
857        for value in config_arguments(arguments) {
858            let value = if value.contains('=') {
859                rebase_cli_include(value, &caller)
860            } else {
861                absolute(&caller, value).to_string_lossy().into_owned()
862            };
863            probe_args.extend(["--config".into(), value.clone()]);
864            // Inline target paths also stay relative to the caller. Preserve
865            // any other settings in this override and the order of overrides.
866            if let Ok(config) = toml::from_str::<toml::Value>(&value)
867                && let Some(target) = config
868                    .get("build")
869                    .and_then(|build| build.get("target-dir"))
870                    .and_then(toml::Value::as_str)
871            {
872                probe_args.extend([
873                    "--config".into(),
874                    format!(
875                        "build.target-dir = {}",
876                        toml::Value::String(
877                            absolute(&caller, target).to_string_lossy().into_owned()
878                        )
879                    ),
880                ]);
881            }
882        }
883        probe_args.extend(forwarded_flags(arguments, &["-Z"]));
884        probe_args.push("build".into());
885        probe_args.extend(
886            arguments
887                .iter()
888                .filter(|arg| PROBE_MANIFEST_TOGGLES.contains(&arg.as_str()))
889                .cloned(),
890        );
891        let target = target_dir_env
892            .filter(|value| !value.is_empty())
893            .map(|value| absolute(&caller, &value.to_string_lossy()).into_os_string());
894        return recalled_cargo_roots(
895            cache,
896            cargo_home,
897            cargo,
898            &probe_args,
899            &source,
900            target.as_deref(),
901        );
902    }
903    let probe = cache.and_then(|cache| {
904        ProbeRecord::describe(
905            cache,
906            cargo_home,
907            cargo,
908            arguments,
909            working_dir,
910            target_dir_env,
911        )
912    });
913    if let Some(recalled) = probe.as_ref().and_then(ProbeRecord::recall) {
914        return Ok(recalled);
915    }
916    let roots = cargo_roots(cargo, arguments, working_dir, target_dir_env)?;
917    if let Some(probe) = probe {
918        probe.remember(&roots);
919    }
920    Ok(roots)
921}
922
923/// Version 3 stopped remembering probes whose pins were taken within a
924/// timestamp tick of a write; records from before may rest on such pins.
925const PROBE_RECORD_VERSION: u8 = 3;
926
927/// Everything a probe's answer was a function of, and the answer.
928#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
929struct ProbeRecord {
930    version: u8,
931    key: ProbeKey,
932    /// The files Cargo consulted, present or absent, as they were when the
933    /// probe ran. The root manifest joins them once the probe has named it.
934    pins: Vec<Pin>,
935    #[serde(skip)]
936    path: PathBuf,
937    /// When the pins were taken, against which a recent write is judged.
938    #[serde(skip)]
939    observed_at: Option<SystemTime>,
940    workspace_root: PathBuf,
941    target_dir: PathBuf,
942    build_dir: Option<PathBuf>,
943}
944
945/// The inputs that select a record: an identical key with intact pins is
946/// the same question, so it gets the same answer.
947#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
948struct ProbeKey {
949    cargo: PathBuf,
950    arguments: Vec<String>,
951    working_dir: PathBuf,
952    target_dir_env: Option<String>,
953    build_target_dir_env: Option<String>,
954    build_dir_env: Option<String>,
955    target_dir_argument: Option<String>,
956    cargo_home: Option<String>,
957}
958
959/// A file as the probe found it: absent, or present with a length and
960/// modification time. Length alone would miss an edit that kept the size.
961#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
962struct Pin {
963    path: PathBuf,
964    state: Option<(u64, u64, u32)>,
965}
966
967impl Pin {
968    /// Describe `path`, or nothing when the filesystem cannot say enough
969    /// about it to notice a change.
970    fn describe(path: PathBuf) -> Option<Self> {
971        let state = match std::fs::metadata(&path) {
972            Ok(metadata) => {
973                let modified = metadata
974                    .modified()
975                    .ok()?
976                    .duration_since(SystemTime::UNIX_EPOCH)
977                    .ok()?;
978                Some((metadata.len(), modified.as_secs(), modified.subsec_nanos()))
979            }
980            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
981            Err(_) => return None,
982        };
983        Some(Self { path, state })
984    }
985
986    fn holds(&self) -> bool {
987        Pin::describe(self.path.clone()).as_ref() == Some(self)
988    }
989
990    /// Whether the file was written so shortly before `observed_at` that a
991    /// second write could still land in the same timestamp tick, keeping its
992    /// length and modification time while changing what Cargo reads. A
993    /// modification time after `observed_at` counts as recent too. An absent
994    /// file is never racy: creating it changes the pin.
995    fn is_racy(&self, observed_at: SystemTime) -> bool {
996        let Some((_, seconds, nanos)) = self.state else {
997            return false;
998        };
999        let window = if nanos == 0 {
1000            COARSE_TIMESTAMP_RACE_WINDOW
1001        } else {
1002            FINE_TIMESTAMP_RACE_WINDOW
1003        };
1004        let modified = SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds, nanos);
1005        observed_at
1006            .duration_since(modified)
1007            .map_or(true, |age| age < window)
1008    }
1009}
1010
1011/// Timestamp tick of a filesystem that keeps sub-second times: Linux stamps
1012/// files from a clock that advances once per scheduler tick, at most 10 ms,
1013/// and Windows from its 15.6 ms timer tick, both inside this window. Matches
1014/// the window `mbx-cache-core` applies to file digests.
1015const FINE_TIMESTAMP_RACE_WINDOW: std::time::Duration = std::time::Duration::from_millis(20);
1016
1017/// Timestamp tick of a filesystem that keeps whole seconds (ext3, HFS+) or two
1018/// (FAT).
1019const COARSE_TIMESTAMP_RACE_WINDOW: std::time::Duration = std::time::Duration::from_secs(2);
1020
1021impl ProbeRecord {
1022    /// Describe the probe about to run, or nothing when some input cannot be
1023    /// pinned, in which case the probe runs and is not remembered.
1024    fn describe(
1025        cache: &Path,
1026        cargo_home: Option<&Path>,
1027        cargo: &OsStr,
1028        arguments: &[String],
1029        working_dir: &Path,
1030        target_dir_env: Option<&OsStr>,
1031    ) -> Option<Self> {
1032        let cargo = resolve_program(cargo)?;
1033        let env =
1034            |name: &str| std::env::var_os(name).map(|value| value.to_string_lossy().into_owned());
1035        let key = ProbeKey {
1036            cargo: cargo.clone(),
1037            arguments: probe_arguments(arguments),
1038            working_dir: working_dir.to_path_buf(),
1039            target_dir_env: target_dir_env.map(|value| value.to_string_lossy().into_owned()),
1040            build_target_dir_env: env("CARGO_BUILD_TARGET_DIR"),
1041            build_dir_env: env("CARGO_BUILD_BUILD_DIR"),
1042            target_dir_argument: target_dir_argument(arguments).map(str::to_owned),
1043            // The directory itself rather than the variable: with the
1044            // variable unset it follows the home directory, and a record
1045            // made under one home must not answer under another.
1046            cargo_home: cargo_home.map(|home| home.to_string_lossy().into_owned()),
1047        };
1048        let invocation_dir = invocation_dir(arguments, working_dir);
1049        // Cargo finds the manifest nearest the invocation directory, or the
1050        // one named outright, and walks up from there for the workspace. Its
1051        // configuration it reads from every `.cargo` above the invocation
1052        // directory itself, wherever the manifest is, and from home last.
1053        let manifest_start = flag_value(arguments, "--manifest-path")
1054            .map(|manifest| absolute(&invocation_dir, manifest))
1055            .and_then(|manifest| manifest.parent().map(Path::to_path_buf))
1056            .unwrap_or_else(|| invocation_dir.clone());
1057        let mut watched = vec![cargo];
1058        watched.extend(
1059            manifest_start
1060                .ancestors()
1061                .map(|directory| directory.join("Cargo.toml")),
1062        );
1063        for directory in invocation_dir.ancestors() {
1064            let dot_cargo = directory.join(".cargo");
1065            watched.push(dot_cargo.join("config.toml"));
1066            watched.push(dot_cargo.join("config"));
1067        }
1068        if let Some(home) = cargo_home {
1069            watched.push(home.join("config.toml"));
1070            watched.push(home.join("config"));
1071        }
1072        watched.extend(
1073            config_arguments(arguments)
1074                .map(|value| absolute(&invocation_dir, value))
1075                .filter(|path| path.is_file()),
1076        );
1077        // A configuration file may `include` others this list does not
1078        // name, and a command-line `include=` does the same. Their target
1079        // directory cannot be pinned, so it is not remembered. A `--config`
1080        // naming a file is on the list above and read like the rest.
1081        if config_arguments(arguments).any(|value| {
1082            value
1083                .split_once('=')
1084                .is_some_and(|(key, _)| key.trim() == "include")
1085        }) || watched
1086            .iter()
1087            .skip(1)
1088            .filter(|path| path.file_name().is_some_and(|name| name != "Cargo.toml"))
1089            .any(|path| config_includes_files(path))
1090        {
1091            return None;
1092        }
1093        let observed_at = SystemTime::now();
1094        let pins = watched
1095            .into_iter()
1096            .map(Pin::describe)
1097            .collect::<Option<Vec<_>>>()?;
1098        let selector = canonical_json(&key).ok()?;
1099        let path = cache
1100            .join("cargo-roots")
1101            .join("v1")
1102            .join(format!("{}.json", CacheDigest::blake3(&selector).hash));
1103        Some(Self {
1104            version: PROBE_RECORD_VERSION,
1105            key,
1106            pins,
1107            path,
1108            observed_at: Some(observed_at),
1109            workspace_root: PathBuf::new(),
1110            target_dir: PathBuf::new(),
1111            build_dir: None,
1112        })
1113    }
1114
1115    /// The answer an earlier probe left for this key, if its pins all hold.
1116    fn recall(&self) -> Option<(PathBuf, PathBuf, Option<PathBuf>)> {
1117        let bytes = std::fs::read(&self.path).ok()?;
1118        let recorded: ProbeRecord = serde_json::from_slice(&bytes).ok()?;
1119        if recorded.version != PROBE_RECORD_VERSION
1120            || recorded.key != self.key
1121            || !recorded.pins.iter().all(Pin::holds)
1122        {
1123            return None;
1124        }
1125        Some((
1126            recorded.workspace_root,
1127            recorded.target_dir,
1128            recorded.build_dir,
1129        ))
1130    }
1131
1132    /// Leave the answer behind for the next invocation. Best-effort: a
1133    /// record that cannot be written costs the next build a probe.
1134    fn remember(mut self, roots: &(PathBuf, PathBuf, Option<PathBuf>)) {
1135        let root_manifest = roots.0.join("Cargo.toml");
1136        if !self.pins.iter().any(|pin| pin.path == root_manifest) {
1137            let Some(pin) = Pin::describe(root_manifest) else {
1138                return;
1139            };
1140            self.pins.push(pin);
1141        }
1142        // A pin taken within a timestamp tick of the file's last write cannot
1143        // show a second write in that tick that kept the length, so a record
1144        // resting on it could keep answering for a file Cargo no longer reads.
1145        // The answer is still used; it just is not remembered.
1146        if let Some(observed_at) = self.observed_at
1147            && self.pins.iter().any(|pin| pin.is_racy(observed_at))
1148        {
1149            return;
1150        }
1151        self.workspace_root = roots.0.clone();
1152        self.target_dir = roots.1.clone();
1153        self.build_dir = roots.2.clone();
1154        let Ok(bytes) = serde_json::to_vec(&self) else {
1155            return;
1156        };
1157        let Some(directory) = self.path.parent() else {
1158            return;
1159        };
1160        if std::fs::create_dir_all(directory).is_err() {
1161            return;
1162        }
1163        let staged = directory.join(format!(
1164            ".{}.{}",
1165            self.path.file_name().unwrap_or_default().to_string_lossy(),
1166            std::process::id()
1167        ));
1168        if std::fs::write(&staged, bytes).is_ok() && std::fs::rename(&staged, &self.path).is_err() {
1169            let _ = std::fs::remove_file(&staged);
1170        }
1171    }
1172}
1173
1174/// Whether a Cargo configuration file names others through `include`.
1175///
1176/// A file that cannot be read or parsed is treated as though it did: the
1177/// question is whether the probe can be pinned, and a file this cannot see
1178/// into is one it cannot pin.
1179fn config_includes_files(path: &Path) -> bool {
1180    let contents = match std::fs::read_to_string(path) {
1181        Ok(contents) => contents,
1182        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
1183        Err(_) => return true,
1184    };
1185    toml::from_str::<toml::Value>(&contents).is_ok_and(|config| config.get("include").is_some())
1186        || toml::from_str::<toml::Value>(&contents).is_err()
1187}
1188
1189/// The probe command's arguments, in the order it runs them: Cargo's global
1190/// flags, then `metadata`, then the options that pick the manifest.
1191fn probe_arguments(arguments: &[String]) -> Vec<String> {
1192    let mut probe = forwarded_flags(arguments, &PROBE_GLOBAL_FLAGS);
1193    probe.extend(
1194        ["metadata", "--no-deps", "--format-version", "1"]
1195            .iter()
1196            .map(|argument| (*argument).to_string()),
1197    );
1198    if let Some(manifest) = flag_value(arguments, "--manifest-path") {
1199        probe.push("--manifest-path".into());
1200        probe.push(manifest.into());
1201    }
1202    probe.extend(
1203        arguments
1204            .iter()
1205            .filter(|argument| PROBE_MANIFEST_TOGGLES.contains(&argument.as_str()))
1206            .cloned(),
1207    );
1208    probe
1209}
1210
1211/// Where `program` is, the way `Command::new` would find it.
1212fn resolve_program(program: &OsStr) -> Option<PathBuf> {
1213    let candidate = Path::new(program);
1214    if candidate.components().count() > 1 {
1215        return candidate.is_file().then(|| candidate.to_path_buf());
1216    }
1217    let names = if cfg!(windows) {
1218        vec![candidate.as_os_str().to_os_string(), {
1219            let mut exe = candidate.as_os_str().to_os_string();
1220            exe.push(".exe");
1221            exe
1222        }]
1223    } else {
1224        vec![candidate.as_os_str().to_os_string()]
1225    };
1226    std::env::split_paths(&std::env::var_os("PATH")?)
1227        .flat_map(|directory| names.iter().map(move |name| directory.join(name)))
1228        .find(|path| path.is_file())
1229}
1230
1231fn cargo_roots(
1232    cargo: &OsStr,
1233    arguments: &[String],
1234    working_dir: &Path,
1235    target_dir_env: Option<&OsStr>,
1236) -> Result<(PathBuf, PathBuf, Option<PathBuf>), ProbeFailure> {
1237    let mut command = Command::new(cargo);
1238    match target_dir_argument(arguments)
1239        .map(OsStr::new)
1240        .or(target_dir_env)
1241    {
1242        Some(value) => command.env(CARGO_TARGET_DIR_ENV, value),
1243        None => command.env_remove(CARGO_TARGET_DIR_ENV),
1244    };
1245    command
1246        .current_dir(working_dir)
1247        .args(probe_arguments(arguments));
1248    let output = command.output().map_err(ProbeFailure::Io)?;
1249    log::debug!(
1250        "cargo metadata stderr: {}",
1251        String::from_utf8_lossy(&output.stderr)
1252    );
1253    if !output.status.success() {
1254        return Err(ProbeFailure::Exit {
1255            status: output.status,
1256            stderr: stderr_tail(&output.stderr),
1257        });
1258    }
1259    parse_metadata(&output.stdout)
1260}
1261
1262fn parse_metadata(bytes: &[u8]) -> Result<(PathBuf, PathBuf, Option<PathBuf>), ProbeFailure> {
1263    let metadata: serde_json::Value = serde_json::from_slice(bytes).map_err(ProbeFailure::Json)?;
1264    let required = |field: &'static str| {
1265        metadata
1266            .get(field)
1267            .and_then(|value| value.as_str())
1268            .map(PathBuf::from)
1269            .ok_or(ProbeFailure::Schema(field))
1270    };
1271    Ok((
1272        required("workspace_root")?,
1273        required("target_directory")?,
1274        metadata
1275            .get("build_directory")
1276            .and_then(|value| value.as_str())
1277            .map(PathBuf::from),
1278    ))
1279}
1280
1281fn stderr_tail(bytes: &[u8]) -> String {
1282    let stderr = String::from_utf8_lossy(bytes);
1283    let lines: Vec<_> = stderr
1284        .lines()
1285        .filter(|line| !line.trim().is_empty())
1286        .rev()
1287        .take(5)
1288        .collect();
1289    let tail = lines.into_iter().rev().collect::<Vec<_>>().join("\n");
1290    let mut start = tail.len().saturating_sub(1024);
1291    while !tail.is_char_boundary(start) {
1292        start += 1;
1293    }
1294    tail[start..].to_owned()
1295}
1296
1297fn target_dir_argument(arguments: &[String]) -> Option<&str> {
1298    flag_value(arguments, "--target-dir")
1299}
1300
1301fn cargo_config_may_set_target_dir(arguments: &[String], invocation_dir: &Path) -> bool {
1302    if std::env::var_os("CARGO_BUILD_TARGET_DIR").is_some_and(|value| !value.is_empty()) {
1303        return true;
1304    }
1305    if config_arguments(arguments).any(|value| {
1306        value
1307            .split_once('=')
1308            .is_none_or(|(key, _)| matches!(key.trim(), "build.target-dir" | "include"))
1309    }) {
1310        return true;
1311    }
1312    let project = invocation_dir.ancestors().flat_map(|directory| {
1313        let cargo = directory.join(".cargo");
1314        [cargo.join("config.toml"), cargo.join("config")]
1315    });
1316    let home = std::env::var_os("CARGO_HOME")
1317        .map(PathBuf::from)
1318        .or_else(|| dirs::home_dir().map(|root| root.join(".cargo")))
1319        .into_iter()
1320        .flat_map(|cargo| [cargo.join("config.toml"), cargo.join("config")]);
1321    project.chain(home).any(|path| config_may_set_target(&path))
1322}
1323
1324fn config_arguments(arguments: &[String]) -> impl Iterator<Item = &str> {
1325    let mut values = Vec::new();
1326    let mut remaining = arguments.iter();
1327    while let Some(argument) = remaining.next() {
1328        if let Some(value) = argument.strip_prefix("--config=") {
1329            values.push(value);
1330        } else if argument == "--config"
1331            && let Some(value) = remaining.next()
1332        {
1333            values.push(value);
1334        }
1335    }
1336    values.into_iter()
1337}
1338
1339fn config_may_set_target(path: &Path) -> bool {
1340    let contents = match std::fs::read_to_string(path) {
1341        Ok(contents) => contents,
1342        Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
1343        Err(_) => return true,
1344    };
1345    let Ok(config) = toml::from_str::<toml::Value>(&contents) else {
1346        return true;
1347    };
1348    config
1349        .get("build")
1350        .and_then(|build| build.get("target-dir"))
1351        .is_some()
1352        || config.get("include").is_some()
1353}
1354
1355fn flag_value<'a>(arguments: &'a [String], flag: &str) -> Option<&'a str> {
1356    let joined = format!("{flag}=");
1357    let mut arguments = arguments.iter();
1358    while let Some(argument) = arguments.next() {
1359        if let Some(value) = argument.strip_prefix(&joined) {
1360            return Some(value);
1361        }
1362        if argument == flag {
1363            return arguments.next().map(String::as_str);
1364        }
1365    }
1366    None
1367}
1368
1369fn workspace_root(start: &Path) -> PathBuf {
1370    start
1371        .ancestors()
1372        .find(|directory| directory.join("Cargo.toml").is_file())
1373        .unwrap_or(start)
1374        .to_path_buf()
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379    use super::*;
1380
1381    fn cargo_fixture() -> tempfile::TempDir {
1382        let directory = tempfile::tempdir().unwrap();
1383        std::fs::write(
1384            directory.path().join("Cargo.toml"),
1385            "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
1386        )
1387        .unwrap();
1388        std::fs::create_dir_all(directory.path().join("src")).unwrap();
1389        std::fs::write(directory.path().join("src/lib.rs"), "").unwrap();
1390        directory
1391    }
1392
1393    fn fixture_root(path: &Path) -> PathBuf {
1394        // Cargo omits Windows verbatim prefixes; Unix may have aliases (/var).
1395        if cfg!(windows) {
1396            path.to_path_buf()
1397        } else {
1398            path.canonicalize().unwrap()
1399        }
1400    }
1401
1402    #[test]
1403    fn a_manifest_is_in_scope_only_where_cargo_would_read_one() {
1404        let outside = tempfile::tempdir().unwrap();
1405        let outside = outside.path();
1406        let arguments = |value: &str| {
1407            value
1408                .split_whitespace()
1409                .map(str::to_owned)
1410                .collect::<Vec<_>>()
1411        };
1412        assert!(!manifest_in_scope(&arguments("binstall"), outside));
1413        assert!(!manifest_in_scope(&arguments("build"), outside));
1414
1415        let project = cargo_fixture();
1416        let project = project.path();
1417        let nested = project.join("src");
1418        assert!(manifest_in_scope(&arguments("binstall"), project));
1419        // Cargo walks up for the nearest manifest, so a subdirectory of a
1420        // package is inside it.
1421        assert!(manifest_in_scope(&arguments("binstall"), &nested));
1422
1423        // A directory option moves the search, either way.
1424        assert!(manifest_in_scope(
1425            &[
1426                "-C".into(),
1427                project.to_string_lossy().into_owned(),
1428                "binstall".into()
1429            ],
1430            outside
1431        ));
1432        assert!(!manifest_in_scope(
1433            &[
1434                "--directory".into(),
1435                outside.to_string_lossy().into_owned(),
1436                "binstall".into()
1437            ],
1438            project
1439        ));
1440
1441        // `--manifest-path` names the manifest outright rather than starting
1442        // a search, so a missing one is out of scope even inside a package.
1443        assert!(manifest_in_scope(
1444            &[
1445                "binstall".into(),
1446                "--manifest-path".into(),
1447                "Cargo.toml".into()
1448            ],
1449            project
1450        ));
1451        assert!(!manifest_in_scope(
1452            &["binstall".into(), "--manifest-path=src/Cargo.toml".into()],
1453            project
1454        ));
1455
1456        // `install --path` names a manifest the invocation directory does not
1457        // have. Callers expand aliases before asking, so only a real install
1458        // gives `--path` that meaning here; other subcommands spend the same
1459        // flag on their own arguments and must not be read as path installs.
1460        assert!(manifest_in_scope(
1461            &[
1462                "install".into(),
1463                "--path".into(),
1464                project.to_string_lossy().into_owned()
1465            ],
1466            outside
1467        ));
1468        assert!(!manifest_in_scope(
1469            &[
1470                "generate".into(),
1471                "--path".into(),
1472                project.to_string_lossy().into_owned()
1473            ],
1474            outside
1475        ));
1476        assert!(!manifest_in_scope(
1477            &[
1478                "install".into(),
1479                "--path".into(),
1480                outside.join("absent").to_string_lossy().into_owned()
1481            ],
1482            outside
1483        ));
1484        // Arguments after `--` belong to the built program, not to Cargo.
1485        assert!(!manifest_in_scope(
1486            &[
1487                "run".into(),
1488                "--".into(),
1489                "--path".into(),
1490                project.to_string_lossy().into_owned()
1491            ],
1492            outside
1493        ));
1494    }
1495
1496    #[test]
1497    fn lockfile_makes_identity_independent_of_checkout_path() {
1498        let left = tempfile::tempdir().unwrap();
1499        let right = tempfile::tempdir().unwrap();
1500        std::fs::write(left.path().join("Cargo.lock"), "same").unwrap();
1501        std::fs::write(right.path().join("Cargo.lock"), "same").unwrap();
1502        let command = vec!["build".to_string(), "--workspace".to_string()];
1503        assert_eq!(
1504            build_identity(left.path(), &command),
1505            build_identity(right.path(), &command)
1506        );
1507    }
1508
1509    #[test]
1510    fn cargo_commands_and_toolchain_selectors_share_predictions() {
1511        let project = tempfile::tempdir().unwrap();
1512        std::fs::write(project.path().join("Cargo.lock"), "same").unwrap();
1513
1514        let build = vec!["build".to_string()];
1515        let test = vec!["test".to_string(), "--workspace".to_string()];
1516        let msrv = vec!["+1.91".to_string(), "check".to_string()];
1517
1518        assert_eq!(
1519            build_identity(project.path(), &build),
1520            build_identity(project.path(), &test),
1521        );
1522        assert_eq!(
1523            build_identity(project.path(), &build),
1524            build_identity(project.path(), &msrv),
1525        );
1526    }
1527
1528    #[test]
1529    fn rustc_flags_after_separator_are_not_cargo_globals() {
1530        let args = vec![
1531            "rustc".into(),
1532            "--".into(),
1533            "-C".into(),
1534            "opt-level=3".into(),
1535        ];
1536        assert_eq!(
1537            invocation_dir(cargo_arguments(&args), Path::new("/work")),
1538            PathBuf::from("/work")
1539        );
1540    }
1541
1542    #[test]
1543    fn target_directory_flags_and_environment_are_recorded_as_explicit() {
1544        let directory = cargo_fixture();
1545        let root = directory.path();
1546        let cargo = OsStr::new("cargo-that-does-not-exist");
1547        let plain = ["build".to_string()];
1548
1549        let default = resolve(cargo, &plain, root, None);
1550        assert_eq!(default.target_dir, root.join("target"));
1551        assert!(!default.target_dir_requested);
1552
1553        for arguments in [
1554            vec!["build".into(), "--target-dir=target".into()],
1555            vec!["build".into(), "--target-dir".into(), "target".into()],
1556        ] {
1557            let resolved = resolve(cargo, &arguments, root, None);
1558            assert_eq!(resolved.target_dir, root.join("target"));
1559            assert!(resolved.target_dir_requested);
1560        }
1561
1562        let from_environment = resolve(cargo, &plain, root, Some("elsewhere".into()));
1563        assert_eq!(from_environment.target_dir, root.join("elsewhere"));
1564        assert!(from_environment.target_dir_requested);
1565
1566        let empty_environment = resolve(cargo, &plain, root, Some("".into()));
1567        assert!(!empty_environment.target_dir_requested);
1568
1569        let dangling = ["build".into(), "--target-dir".into()];
1570        assert!(!resolve(cargo, &dangling, root, None).target_dir_requested);
1571    }
1572
1573    #[test]
1574    fn project_config_that_names_the_default_target_is_still_explicit() {
1575        let directory = cargo_fixture();
1576        let root = directory.path();
1577        std::fs::create_dir_all(root.join(".cargo")).unwrap();
1578        std::fs::write(
1579            root.join(".cargo/config.toml"),
1580            "[build]\ntarget-dir = \"target\"\n",
1581        )
1582        .unwrap();
1583
1584        let resolved = resolve(
1585            OsStr::new("cargo-that-does-not-exist"),
1586            &["build".into()],
1587            root,
1588            None,
1589        );
1590
1591        assert_eq!(resolved.target_dir, root.join("target"));
1592        assert!(resolved.target_dir_requested);
1593    }
1594
1595    #[test]
1596    fn command_line_config_include_may_set_the_target_directory() {
1597        let directory = cargo_fixture();
1598        let arguments = [
1599            "build".into(),
1600            "--config".into(),
1601            "include='target-config.toml'".into(),
1602        ];
1603
1604        let resolved = resolve(
1605            OsStr::new("cargo-that-does-not-exist"),
1606            &arguments,
1607            directory.path(),
1608            None,
1609        );
1610
1611        assert!(resolved.target_dir_requested);
1612    }
1613
1614    #[test]
1615    fn command_line_target_config_reaches_the_cargo_probe() {
1616        let directory = cargo_fixture();
1617        let root = directory.path();
1618        let configured = root.join("configured-target");
1619        let arguments = [
1620            "build".into(),
1621            "--offline".into(),
1622            "--manifest-path".into(),
1623            root.join("Cargo.toml").display().to_string(),
1624            "--config".into(),
1625            format!("build.target-dir='{}'", configured.display()),
1626        ];
1627
1628        let resolved =
1629            resolve_reported_in(None, OsStr::new("cargo"), &arguments, root, None).unwrap();
1630
1631        assert_eq!(resolved.target_dir, configured);
1632        assert!(resolved.target_dir_requested);
1633    }
1634
1635    #[test]
1636    fn remote_installs_are_installs_without_a_path_or_listing() {
1637        let remote = |words: &[&str]| {
1638            is_remote_install(
1639                &words
1640                    .iter()
1641                    .map(|word| word.to_string())
1642                    .collect::<Vec<_>>(),
1643            )
1644        };
1645        assert!(remote(&["install", "ripgrep"]));
1646        assert!(remote(&[
1647            "+stable",
1648            "install",
1649            "--git",
1650            "https://example.com/r"
1651        ]));
1652        assert!(remote(&[
1653            "--color",
1654            "always",
1655            "install",
1656            "--version",
1657            "1.0.0",
1658            "x"
1659        ]));
1660        assert!(!remote(&["install", "--path", "."]));
1661        assert!(!remote(&["install", "--path=."]));
1662        assert!(!remote(&["install", "--list"]));
1663        assert!(!remote(&["build"]));
1664        assert!(!remote(&["-C", "install", "build"]));
1665        assert!(!remote(&["run", "--", "install", "x"]));
1666    }
1667
1668    #[test]
1669    fn remote_install_target_is_the_one_the_caller_named() {
1670        let words = |words: &[&str]| {
1671            words
1672                .iter()
1673                .map(|word| word.to_string())
1674                .collect::<Vec<_>>()
1675        };
1676        let cwd = Path::new("/work");
1677        assert_eq!(
1678            remote_install_target_dir(&words(&["install", "x"]), cwd, None),
1679            None
1680        );
1681        assert_eq!(
1682            remote_install_target_dir(&words(&["install", "x", "--target-dir", "out"]), cwd, None),
1683            Some(PathBuf::from("/work/out"))
1684        );
1685        assert_eq!(
1686            remote_install_target_dir(&words(&["install", "x"]), cwd, Some(OsStr::new("/env"))),
1687            Some(PathBuf::from("/env"))
1688        );
1689        assert_eq!(
1690            remote_install_target_dir(
1691                &words(&["install", "x", "--target-dir=flag"]),
1692                cwd,
1693                Some(OsStr::new("/env"))
1694            ),
1695            Some(PathBuf::from("/work/flag"))
1696        );
1697        assert_eq!(
1698            remote_install_target_dir(&words(&["install", "x"]), cwd, Some(OsStr::new(""))),
1699            None
1700        );
1701    }
1702
1703    #[test]
1704    fn remote_install_name_ignores_options_that_do_not_change_the_package() {
1705        let name = |words: &[&str]| {
1706            remote_install_name(
1707                &words
1708                    .iter()
1709                    .map(|word| word.to_string())
1710                    .collect::<Vec<_>>(),
1711            )
1712        };
1713        let plain = name(&["install", "helloworld-rs"]);
1714        assert!(plain.starts_with("helloworld-rs-"));
1715        assert_eq!(
1716            plain,
1717            name(&[
1718                "install",
1719                "--root",
1720                "out",
1721                "--force",
1722                "helloworld-rs",
1723                "--locked"
1724            ])
1725        );
1726        assert_ne!(plain, name(&["install", "helloworld-rs", "--version", "2"]));
1727        assert_ne!(plain, name(&["install", "other"]));
1728        assert!(name(&["install", "--git", "https://example.com/r"]).starts_with("git-"));
1729        assert_ne!(
1730            name(&["install", "--git", "https://example.com/a"]),
1731            name(&["install", "--git", "https://example.com/b"])
1732        );
1733        assert!(!name(&["install", "../evil/x"]).contains('/'));
1734    }
1735
1736    #[test]
1737    fn path_install_probes_source_config_and_preserves_caller_relative_targets() {
1738        let source = cargo_fixture();
1739        let caller = cargo_fixture();
1740        let source_root = fixture_root(source.path());
1741        let caller_root = fixture_root(caller.path());
1742        for (root, target) in [
1743            (source_root.as_path(), "source-target"),
1744            (caller_root.as_path(), "caller-target"),
1745        ] {
1746            std::fs::create_dir_all(root.join(".cargo")).unwrap();
1747            std::fs::write(
1748                root.join(".cargo/config.toml"),
1749                format!("[build]\ntarget-dir = '{target}'\n"),
1750            )
1751            .unwrap();
1752        }
1753        let arguments = vec![
1754            "install".into(),
1755            format!("--path={}", source_root.as_path().display()),
1756            "--offline".into(),
1757        ];
1758        let cache = tempfile::tempdir().unwrap();
1759        let resolve = |args: &[String], target| {
1760            resolve_reported_in(
1761                Some(cache.path()),
1762                OsStr::new("cargo"),
1763                args,
1764                caller_root.as_path(),
1765                target,
1766            )
1767            .unwrap()
1768        };
1769        let roots = resolve(&arguments, None);
1770        assert_eq!(roots.workspace_root, source_root.as_path());
1771        assert_eq!(
1772            roots.target_dir,
1773            source_root.as_path().join("source-target")
1774        );
1775        assert!(roots.target_dir_requested);
1776
1777        let mut flagged = arguments.clone();
1778        flagged.extend(["--target-dir".into(), "flag-target".into()]);
1779        assert_eq!(
1780            resolve(&flagged, None).target_dir,
1781            caller_root.as_path().join("flag-target")
1782        );
1783        assert_eq!(
1784            resolve(&arguments, Some("env-target".into())).target_dir,
1785            caller_root.as_path().join("env-target")
1786        );
1787        let mut configured = arguments.clone();
1788        configured.extend(["--config".into(), "build.target-dir='cli-target'".into()]);
1789        assert_eq!(
1790            resolve(&configured, None).target_dir,
1791            caller_root.as_path().join("cli-target")
1792        );
1793
1794        // A warm metadata cache must watch the installed source's config.
1795        std::fs::write(
1796            source_root.as_path().join(".cargo/config.toml"),
1797            "[build]\ntarget-dir = 'changed-source-target'\n",
1798        )
1799        .unwrap();
1800        assert_eq!(
1801            resolve(&arguments, None).target_dir,
1802            source_root.as_path().join("changed-source-target")
1803        );
1804    }
1805
1806    #[test]
1807    fn path_install_directory_flags_rebase_the_source_and_target() {
1808        let source = cargo_fixture();
1809        let source_root = fixture_root(source.path());
1810        let caller = source_root.parent().unwrap();
1811        let directory = source_root.file_name().unwrap().to_str().unwrap();
1812        for flags in [
1813            vec!["--directory".into(), directory.into()],
1814            vec![format!("--directory={directory}")],
1815            vec!["-C".into(), directory.into()],
1816            vec![format!("-C{directory}")],
1817        ] {
1818            let mut arguments = flags;
1819            arguments.extend(
1820                [
1821                    "install",
1822                    "--path",
1823                    ".",
1824                    "--target-dir",
1825                    "shared",
1826                    "--offline",
1827                ]
1828                .map(str::to_owned),
1829            );
1830            let roots =
1831                resolve_reported_in(None, OsStr::new("cargo"), &arguments, caller, None).unwrap();
1832            assert_eq!(roots.workspace_root, source_root);
1833            assert_eq!(roots.target_dir, source_root.join("shared"));
1834            assert!(roots.target_dir_requested);
1835        }
1836    }
1837
1838    #[test]
1839    fn path_install_cli_includes_keep_caller_paths_and_override_order() {
1840        let source = cargo_fixture();
1841        let caller = cargo_fixture();
1842        let source_root = fixture_root(source.path());
1843        let caller_root = fixture_root(caller.path());
1844        for (root, target) in [
1845            (&caller_root, "caller-target"),
1846            (&source_root, "wrong-target"),
1847        ] {
1848            std::fs::create_dir_all(root.join(".config")).unwrap();
1849            std::fs::write(
1850                root.join(".config/extra.toml"),
1851                format!("[build]\ntarget-dir = '{target}'\n"),
1852            )
1853            .unwrap();
1854        }
1855        let resolve = |overrides: &[&str]| {
1856            let mut args = vec![
1857                "install".into(),
1858                "--path".into(),
1859                source_root.display().to_string(),
1860                "--offline".into(),
1861            ];
1862            for value in overrides {
1863                args.extend(["--config".into(), (*value).into()]);
1864            }
1865            resolve_reported_in(None, OsStr::new("cargo"), &args, &caller_root, None)
1866        };
1867        for include in [
1868            "include=['.config/extra.toml']",
1869            "include=[{path='.config/extra.toml'}, {path='.config/missing.toml', optional=true}]",
1870        ] {
1871            assert_eq!(
1872                resolve(&[include]).unwrap().target_dir,
1873                caller_root.join("caller-target")
1874            );
1875            assert_eq!(
1876                resolve(&[include, "build.target-dir='override'"])
1877                    .unwrap()
1878                    .target_dir,
1879                caller_root.join("override")
1880            );
1881        }
1882        let absolute_include = format!(
1883            "include=[{}]",
1884            toml::Value::String(caller_root.join(".config/extra.toml").display().to_string())
1885        );
1886        assert_eq!(
1887            resolve(&[&absolute_include]).unwrap().target_dir,
1888            caller_root.join("caller-target")
1889        );
1890        assert!(resolve(&["include=['.config/missing.toml']"]).is_none());
1891
1892        // Keep unsupported Cargo values invalid instead of repairing them.
1893        assert!(resolve(&["include=42"]).is_none());
1894    }
1895
1896    /// A stand-in Cargo that answers `metadata` and logs every call.
1897    #[cfg(unix)]
1898    fn logging_cargo(directory: &Path, project: &Path, log: &Path) -> PathBuf {
1899        use std::os::unix::fs::PermissionsExt as _;
1900        let script = directory.join("cargo");
1901        std::fs::write(
1902            &script,
1903            format!(
1904                "#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\ncase \" $* \" in *' metadata '*) printf '{{\"workspace_root\":\"{root}\",\"target_directory\":\"{root}/target\",\"packages\":[]}}';; esac\n",
1905                log = log.display(),
1906                root = project.display(),
1907            ),
1908        )
1909        .unwrap();
1910        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
1911        script
1912    }
1913
1914    /// A stand-in Cargo whose reported directories follow what it is given,
1915    /// so a record recalled for the wrong inputs is visibly wrong rather than
1916    /// accidentally right.
1917    #[cfg(unix)]
1918    fn build_dir_cargo(directory: &Path, project: &Path, log: &Path) -> PathBuf {
1919        use std::os::unix::fs::PermissionsExt as _;
1920        let script = directory.join("cargo");
1921        std::fs::write(
1922            &script,
1923            format!(
1924                "#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\ncase \" $* \" in *' metadata '*) target=\"${{CARGO_TARGET_DIR:-{root}/target}}\"; build=\"${{CARGO_BUILD_BUILD_DIR:-$target/build-dir}}\"; printf '{{\"workspace_root\":\"{root}\",\"target_directory\":\"%s\",\"build_directory\":\"%s\",\"packages\":[]}}' \"$target\" \"$build\";; esac\n",
1925                log = log.display(),
1926                root = project.display(),
1927            ),
1928        )
1929        .unwrap();
1930        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
1931        script
1932    }
1933
1934    #[cfg(unix)]
1935    fn probes(log: &Path) -> usize {
1936        std::fs::read_to_string(log)
1937            .unwrap_or_default()
1938            .lines()
1939            .filter(|line| line.contains("metadata"))
1940            .count()
1941    }
1942
1943    /// Wait out the timestamp tick of the files just written, so a probe
1944    /// taken next is old enough to be remembered. The fixtures live in the
1945    /// temporary directory, so a file written there shows which window a
1946    /// pin taken from them falls under.
1947    fn settle() {
1948        let probe = tempfile::NamedTempFile::new().unwrap();
1949        let coarse = probe
1950            .as_file()
1951            .metadata()
1952            .and_then(|metadata| metadata.modified())
1953            .ok()
1954            .and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
1955            .is_none_or(|since| since.subsec_nanos() == 0);
1956        std::thread::sleep(if coarse {
1957            COARSE_TIMESTAMP_RACE_WINDOW + FINE_TIMESTAMP_RACE_WINDOW
1958        } else {
1959            FINE_TIMESTAMP_RACE_WINDOW * 2
1960        });
1961    }
1962
1963    /// A stand-in Cargo that, like the real one for `metadata`, refuses the
1964    /// unstable `-Z` and `-C` flags it was not told to expect.
1965    #[cfg(unix)]
1966    fn stable_cargo(directory: &Path, project: &Path, log: &Path) -> PathBuf {
1967        use std::os::unix::fs::PermissionsExt as _;
1968        let script = directory.join("cargo");
1969        std::fs::write(
1970            &script,
1971            format!(
1972                "#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\ncase \" $* \" in *' -Z'*|*' -C'*) exit 101;; esac\ncase \" $* \" in *' metadata '*) printf '{{\"workspace_root\":\"{root}\",\"target_directory\":\"{root}/target\",\"packages\":[]}}';; esac\n",
1973                log = log.display(),
1974                root = project.display(),
1975            ),
1976        )
1977        .unwrap();
1978        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
1979        script
1980    }
1981
1982    #[test]
1983    #[cfg(unix)]
1984    fn flags_after_an_external_subcommand_stay_out_of_the_probe() {
1985        let directory = cargo_fixture();
1986        let root = directory.path();
1987        let scratch = tempfile::tempdir().unwrap();
1988        let log = scratch.path().join("log");
1989        let cargo = stable_cargo(scratch.path(), root, &log);
1990        let resolve = |words: &[&str]| {
1991            let arguments = words
1992                .iter()
1993                .map(|word| (*word).to_owned())
1994                .collect::<Vec<_>>();
1995            resolve_reported_in(None, cargo.as_os_str(), &arguments, root, None)
1996        };
1997
1998        for words in [
1999            &["foo", "-Z", "x"][..],
2000            &["foo", "-Zx"],
2001            &["foo", "-C", "dir"],
2002            &["foo", "autoharness", "-Z", "autoharness"],
2003            &["+stable", "foo", "-Z", "x"],
2004        ] {
2005            let resolved = resolve(words).unwrap_or_else(|| panic!("{words:?} was refused"));
2006            assert_eq!(resolved.workspace_root, root, "{words:?}");
2007            // The directory option is the plugin's, so it moves nothing.
2008            assert_eq!(resolved.target_dir, root.join("target"), "{words:?}");
2009        }
2010
2011        // Cargo's own flags, before the subcommand, still reach the probe, and a
2012        // failure there is not retried into success.
2013        assert!(resolve(&["-Z", "x", "foo"]).is_none());
2014        // A builtin parses its whole command line: a bad flag is Cargo's to report.
2015        assert!(resolve(&["build", "-Z", "x"]).is_none());
2016        assert!(resolve(&["build", "-C", "dir"]).is_none());
2017    }
2018
2019    #[test]
2020    #[cfg(unix)]
2021    fn flags_after_an_alias_or_external_subcommand_still_reach_a_probe_that_accepts_them() {
2022        let directory = cargo_fixture();
2023        let root = directory.path();
2024        let scratch = tempfile::tempdir().unwrap();
2025        let log = scratch.path().join("log");
2026        let cargo = logging_cargo(scratch.path(), root, &log);
2027        let arguments = ["b", "--config", "net.offline=true"].map(str::to_owned);
2028        resolve_reported_in(None, cargo.as_os_str(), &arguments, root, None).unwrap();
2029        let logged = std::fs::read_to_string(&log).unwrap();
2030        assert!(
2031            logged.contains("--config net.offline=true"),
2032            "an alias is Cargo's to parse: {logged}"
2033        );
2034        assert_eq!(probes(&log), 1, "{logged}");
2035    }
2036
2037    #[test]
2038    fn reported_intermediates_survive_resolution_and_cached_records() {
2039        let directory = cargo_fixture();
2040        let root = directory.path();
2041        let cache = tempfile::tempdir().unwrap();
2042        let arguments = ["build".to_owned()];
2043        let roots = (
2044            root.to_path_buf(),
2045            root.join("target"),
2046            Some(root.join("intermediates")),
2047        );
2048        let resolved = resolve_with_reported(&arguments, root, None, Some(roots.clone()));
2049        assert_eq!(resolved.build_dir, roots.2);
2050        let describe = || {
2051            ProbeRecord::describe(
2052                cache.path(),
2053                None,
2054                OsStr::new("cargo"),
2055                &arguments,
2056                root,
2057                None,
2058            )
2059            .unwrap()
2060        };
2061        settle();
2062        describe().remember(&roots);
2063        assert_eq!(describe().recall(), Some(roots.clone()));
2064        let old_cargo =
2065            resolve_with_reported(&arguments, root, None, Some((roots.0, roots.1, None)));
2066        assert_eq!(old_cargo.build_dir, None);
2067    }
2068
2069    /// The environment variable that names the child run of the test below.
2070    #[cfg(unix)]
2071    const BUILD_DIR_PROBE_CHILD: &str = "MBX_CARGO_BUILD_DIR_PROBE_CHILD";
2072
2073    /// Take a reported build directory through the whole probe path: Cargo
2074    /// reports it, resolution carries it, and the record answers for it. The
2075    /// sibling test above hands the roots in by hand, so it cannot tell
2076    /// whether `cargo metadata` output is read at all, nor whether the
2077    /// inputs that select a build directory select a record. Here the
2078    /// stand-in Cargo derives what it reports from `CARGO_BUILD_BUILD_DIR`
2079    /// and `CARGO_TARGET_DIR`, so a record recalled for the wrong inputs
2080    /// reports the wrong directory instead of the right one by chance. The
2081    /// body runs in a child copy of the test binary because it sets process
2082    /// environment, which is not safe beside tests reading it in parallel.
2083    #[test]
2084    #[cfg(unix)]
2085    fn a_probed_build_directory_is_parsed_recalled_and_rekeyed() {
2086        if std::env::var_os(BUILD_DIR_PROBE_CHILD).is_none() {
2087            let status = Command::new(std::env::current_exe().unwrap())
2088                .args([
2089                    "--exact",
2090                    "--nocapture",
2091                    "--test-threads=1",
2092                    "tests::a_probed_build_directory_is_parsed_recalled_and_rekeyed",
2093                ])
2094                .env(BUILD_DIR_PROBE_CHILD, "1")
2095                .env_remove("CARGO_BUILD_BUILD_DIR")
2096                .env_remove("CARGO_TARGET_DIR")
2097                .env_remove("CARGO_BUILD_TARGET_DIR")
2098                .status()
2099                .unwrap();
2100            assert!(status.success(), "the child run of this test failed");
2101            return;
2102        }
2103
2104        let directory = cargo_fixture();
2105        let root = directory.path();
2106        let cache = tempfile::tempdir().unwrap();
2107        let log = root.join("cargo.log");
2108        let cargo = build_dir_cargo(root, root, &log);
2109        let home = root.join("cargo-home");
2110        let resolve = |arguments: &[String]| {
2111            resolve_reported_from_home(
2112                Some(cache.path()),
2113                Some(&home),
2114                cargo.as_os_str(),
2115                arguments,
2116                root,
2117                None,
2118            )
2119            .unwrap()
2120        };
2121        let build = ["build".to_string()];
2122        settle();
2123
2124        // What Cargo reported is what resolution carries.
2125        let first = resolve(&build);
2126        assert_eq!(first.build_dir, Some(root.join("target/build-dir")));
2127        assert_eq!(probes(&log), 1);
2128
2129        // The same question again is answered from the record, not Cargo.
2130        assert_eq!(resolve(&build).build_dir, first.build_dir);
2131        assert_eq!(probes(&log), 1);
2132
2133        // The environment that selects a build directory is part of the key.
2134        let elsewhere = root.join("elsewhere");
2135        // SAFETY: this child process runs this test and no other.
2136        unsafe { std::env::set_var("CARGO_BUILD_BUILD_DIR", &elsewhere) };
2137        assert_eq!(resolve(&build).build_dir, Some(elsewhere));
2138        assert_eq!(probes(&log), 2);
2139        // SAFETY: as above.
2140        unsafe { std::env::remove_var("CARGO_BUILD_BUILD_DIR") };
2141        assert_eq!(resolve(&build).build_dir, first.build_dir);
2142        assert_eq!(probes(&log), 2);
2143
2144        // So is a target directory named on the command line, which the
2145        // probe never sees among its own arguments.
2146        let other_target = root.join("other-target");
2147        let targeted = [
2148            "build".to_string(),
2149            "--target-dir".to_string(),
2150            other_target.display().to_string(),
2151        ];
2152        let with_target = resolve(&targeted);
2153        assert_eq!(with_target.build_dir, Some(other_target.join("build-dir")));
2154        assert_eq!(probes(&log), 3);
2155        assert_eq!(resolve(&targeted).build_dir, with_target.build_dir);
2156        assert_eq!(resolve(&build).build_dir, first.build_dir);
2157        assert_eq!(probes(&log), 3);
2158    }
2159
2160    #[test]
2161    #[cfg(unix)]
2162    fn a_probe_is_remembered_while_what_cargo_read_stands() {
2163        let directory = cargo_fixture();
2164        let root = directory.path();
2165        let cache = tempfile::tempdir().unwrap();
2166        let log = root.join("cargo.log");
2167        let cargo = logging_cargo(root, root, &log);
2168        let arguments = ["build".to_string(), "--locked".to_string()];
2169        let home = root.join("cargo-home");
2170        let resolve = |target_dir_env: Option<&str>| {
2171            resolve_reported_from_home(
2172                Some(cache.path()),
2173                Some(&home),
2174                cargo.as_os_str(),
2175                &arguments,
2176                root,
2177                target_dir_env.map(OsString::from),
2178            )
2179            .unwrap()
2180        };
2181        settle();
2182
2183        let first = resolve(None);
2184        assert_eq!(first.workspace_root, root);
2185        assert_eq!(probes(&log), 1);
2186        // The same question again is answered from the record.
2187        assert_eq!(resolve(None), first);
2188        assert_eq!(probes(&log), 1);
2189
2190        // An edit to a manifest Cargo read runs the probe again.
2191        std::fs::write(
2192            root.join("Cargo.toml"),
2193            "[package]\nname = \"fixture\"\nversion = \"0.2.0\"\nedition = \"2021\"\n",
2194        )
2195        .unwrap();
2196        settle();
2197        assert_eq!(resolve(None), first);
2198        assert_eq!(probes(&log), 2);
2199        assert_eq!(resolve(None), first);
2200        assert_eq!(probes(&log), 2);
2201
2202        // So does a configuration file appearing where Cargo would look.
2203        std::fs::create_dir_all(root.join(".cargo")).unwrap();
2204        std::fs::write(root.join(".cargo/config.toml"), "[build]\njobs = 2\n").unwrap();
2205        settle();
2206        assert_eq!(resolve(None), first);
2207        assert_eq!(probes(&log), 3);
2208
2209        // The environment that selects a target directory is part of the key.
2210        assert!(resolve(Some("elsewhere")).target_dir_requested);
2211        assert_eq!(probes(&log), 4);
2212        assert_eq!(resolve(None), first);
2213        assert_eq!(probes(&log), 4);
2214
2215        // So is the Cargo home, which follows HOME when CARGO_HOME is unset,
2216        // and whose configuration is watched like the project's.
2217        let other_home = root.join("other-home");
2218        let from_other_home = resolve_reported_from_home(
2219            Some(cache.path()),
2220            Some(&other_home),
2221            cargo.as_os_str(),
2222            &arguments,
2223            root,
2224            None,
2225        )
2226        .unwrap();
2227        assert_eq!(from_other_home.workspace_root, first.workspace_root);
2228        assert_eq!(probes(&log), 5);
2229        std::fs::create_dir_all(&other_home).unwrap();
2230        std::fs::write(other_home.join("config.toml"), "[build]\njobs = 4\n").unwrap();
2231        resolve_reported_from_home(
2232            Some(cache.path()),
2233            Some(&other_home),
2234            cargo.as_os_str(),
2235            &arguments,
2236            root,
2237            None,
2238        )
2239        .unwrap();
2240        assert_eq!(probes(&log), 6);
2241        assert_eq!(resolve(None), first);
2242        assert_eq!(probes(&log), 6);
2243
2244        // A configuration that includes files this cannot see is never
2245        // remembered: every build probes.
2246        std::fs::write(
2247            root.join(".cargo/config.toml"),
2248            "include = \"other.toml\"\n",
2249        )
2250        .unwrap();
2251        settle();
2252        assert_eq!(resolve(None).workspace_root, first.workspace_root);
2253        assert_eq!(resolve(None).workspace_root, first.workspace_root);
2254        assert_eq!(probes(&log), 8);
2255    }
2256
2257    #[test]
2258    #[cfg(unix)]
2259    fn a_config_file_named_on_the_command_line_is_pinned() {
2260        let directory = cargo_fixture();
2261        let root = directory.path();
2262        let cache = tempfile::tempdir().unwrap();
2263        let log = root.join("cargo.log");
2264        let cargo = logging_cargo(root, root, &log);
2265        let extra = root.join("extra.toml");
2266        std::fs::write(&extra, "[build]\njobs = 2\n").unwrap();
2267        let arguments = [
2268            "build".to_string(),
2269            "--config".to_string(),
2270            extra.display().to_string(),
2271        ];
2272        let resolve = || {
2273            resolve_reported_in(
2274                Some(cache.path()),
2275                cargo.as_os_str(),
2276                &arguments,
2277                root,
2278                None,
2279            )
2280            .unwrap()
2281        };
2282        settle();
2283
2284        let first = resolve();
2285        assert_eq!(resolve(), first);
2286        assert_eq!(probes(&log), 1);
2287        // The file is watched like any other configuration.
2288        std::fs::write(&extra, "[build]\njobs = 3\n").unwrap();
2289        settle();
2290        assert_eq!(resolve(), first);
2291        assert_eq!(resolve(), first);
2292        assert_eq!(probes(&log), 2);
2293        // Until it includes something that cannot be.
2294        std::fs::write(&extra, "include = \"more.toml\"\n").unwrap();
2295        settle();
2296        resolve();
2297        resolve();
2298        assert_eq!(probes(&log), 4);
2299    }
2300
2301    /// A second write in the timestamp tick of the first keeps the file's
2302    /// length and modification time, so nothing a pin records can show it.
2303    /// The record must not be left behind in the first place. The test
2304    /// stands for that second write by putting the first one's time back.
2305    #[test]
2306    #[cfg(unix)]
2307    fn a_probe_taken_within_a_timestamp_tick_of_a_write_is_not_remembered() {
2308        use std::os::unix::fs::PermissionsExt as _;
2309        let directory = cargo_fixture();
2310        let root = directory.path();
2311        let cache = tempfile::tempdir().unwrap();
2312        let log = root.join("cargo.log");
2313        let extra = root.join("extra.toml");
2314        // A stand-in Cargo whose target directory is the one the file names,
2315        // so a record that outlived an edit reports the old directory.
2316        let cargo = root.join("cargo");
2317        std::fs::write(
2318            &cargo,
2319            format!(
2320                "#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\ncase \" $* \" in *' metadata '*) target=$(sed -n 's/^target-dir = \"\\(.*\\)\"$/\\1/p' {extra}); printf '{{\"workspace_root\":\"{root}\",\"target_directory\":\"%s\",\"packages\":[]}}' \"$target\";; esac\n",
2321                log = log.display(),
2322                extra = extra.display(),
2323                root = root.display(),
2324            ),
2325        )
2326        .unwrap();
2327        std::fs::set_permissions(&cargo, std::fs::Permissions::from_mode(0o755)).unwrap();
2328        let config = |target: &Path| format!("[build]\ntarget-dir = \"{}\"\n", target.display());
2329        let (before, after) = (root.join("aaa"), root.join("bbb"));
2330        let arguments = [
2331            "build".to_string(),
2332            "--config".to_string(),
2333            extra.display().to_string(),
2334        ];
2335        let resolve = || {
2336            resolve_reported_in(
2337                Some(cache.path()),
2338                cargo.as_os_str(),
2339                &arguments,
2340                root,
2341                None,
2342            )
2343            .unwrap()
2344            .target_dir
2345        };
2346        let stamp = |at: SystemTime| {
2347            std::fs::File::options()
2348                .write(true)
2349                .open(&extra)
2350                .unwrap()
2351                .set_modified(at)
2352                .unwrap();
2353        };
2354        // A modification time ahead of the clock is racy however slowly the
2355        // test runs.
2356        let ahead = SystemTime::now() + std::time::Duration::from_secs(3600);
2357        settle();
2358
2359        std::fs::write(&extra, config(&before)).unwrap();
2360        stamp(ahead);
2361        assert_eq!(resolve(), before);
2362        std::fs::write(&extra, config(&after)).unwrap();
2363        stamp(ahead);
2364        assert_eq!(resolve(), after);
2365        assert_eq!(probes(&log), 2);
2366
2367        // Once the write is older than a tick, the probe is remembered.
2368        stamp(SystemTime::now() - std::time::Duration::from_secs(3600));
2369        assert_eq!(resolve(), after);
2370        assert_eq!(resolve(), after);
2371        assert_eq!(probes(&log), 3);
2372    }
2373
2374    #[test]
2375    fn a_pin_is_racy_within_its_filesystems_timestamp_tick() {
2376        let at = |seconds, nanos| SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds, nanos);
2377        let pin = |seconds, nanos| Pin {
2378            path: PathBuf::new(),
2379            state: Some((0, seconds, nanos)),
2380        };
2381        let now = at(1_000, 500_000_000);
2382        assert!(pin(1_000, 495_000_000).is_racy(now));
2383        assert!(!pin(1_000, 470_000_000).is_racy(now));
2384        // A modification time after the observation is racy too.
2385        assert!(pin(1_000, 600_000_000).is_racy(now));
2386        // Whole-second times leave a whole tick of doubt.
2387        assert!(pin(999, 0).is_racy(now));
2388        assert!(!pin(998, 0).is_racy(now));
2389        // An absent file has no timestamp to share.
2390        assert!(
2391            !Pin {
2392                path: PathBuf::new(),
2393                state: None,
2394            }
2395            .is_racy(now)
2396        );
2397    }
2398
2399    #[test]
2400    #[cfg(unix)]
2401    fn a_failed_probe_is_not_remembered() {
2402        use std::os::unix::fs::PermissionsExt as _;
2403        let directory = cargo_fixture();
2404        let root = directory.path();
2405        let cache = tempfile::tempdir().unwrap();
2406        let script = root.join("cargo");
2407        std::fs::write(&script, "#!/bin/sh\nexit 1\n").unwrap();
2408        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
2409        let arguments = ["build".to_string()];
2410
2411        assert!(
2412            resolve_reported_in(
2413                Some(cache.path()),
2414                script.as_os_str(),
2415                &arguments,
2416                root,
2417                None
2418            )
2419            .is_none()
2420        );
2421        assert!(!cache.path().join("cargo-roots").exists());
2422    }
2423
2424    #[test]
2425    fn reported_resolution_requires_a_successful_metadata_probe() {
2426        let directory = cargo_fixture();
2427        assert!(
2428            resolve_reported(
2429                OsStr::new("cargo-that-does-not-exist"),
2430                &["build".into()],
2431                directory.path(),
2432                None,
2433            )
2434            .is_none()
2435        );
2436    }
2437
2438    #[test]
2439    fn missing_cargo_reports_the_io_error() {
2440        let directory = tempfile::tempdir().unwrap();
2441        let missing = directory.path().join("missing-cargo");
2442        let error = try_resolve_reported(
2443            missing.as_os_str(),
2444            &["build".into()],
2445            directory.path(),
2446            None,
2447        )
2448        .unwrap_err();
2449        assert!(matches!(error, ProbeFailure::Io(_)));
2450        assert!(error.to_string().contains("I/O error"));
2451        assert!(std::error::Error::source(&error).is_some());
2452    }
2453
2454    #[test]
2455    fn invalid_metadata_reports_the_json_error() {
2456        let error = parse_metadata(b"not json").unwrap_err();
2457        assert!(matches!(error, ProbeFailure::Json(_)));
2458        assert!(error.to_string().contains("invalid cargo metadata JSON"));
2459        assert!(std::error::Error::source(&error).is_some());
2460    }
2461
2462    #[test]
2463    fn metadata_requires_string_roots() {
2464        for (metadata, field) in [
2465            (r#"{}"#, "workspace_root"),
2466            (r#"{"workspace_root":3}"#, "workspace_root"),
2467            (r#"{"workspace_root":"/project"}"#, "target_directory"),
2468            (
2469                r#"{"workspace_root":"/project","target_directory":null}"#,
2470                "target_directory",
2471            ),
2472        ] {
2473            let error = parse_metadata(metadata.as_bytes()).unwrap_err();
2474            assert!(matches!(error, ProbeFailure::Schema(name) if name == field));
2475            assert!(
2476                error
2477                    .to_string()
2478                    .contains(&format!("{field} missing or not a string"))
2479            );
2480        }
2481    }
2482
2483    #[test]
2484    fn stderr_tail_is_bounded_and_skips_empty_lines() {
2485        assert_eq!(
2486            stderr_tail(b"one\n\ntwo\nthree\nfour\n  \nfive\nsix\n"),
2487            "two\nthree\nfour\nfive\nsix"
2488        );
2489        let text = "é".repeat(2000);
2490        let tail = stderr_tail(text.as_bytes());
2491        assert!(tail.len() <= 1024);
2492        assert!(text.ends_with(&tail));
2493        assert_eq!(stderr_tail(b"\n \n"), "");
2494        assert!(stderr_tail(b"invalid \xff").contains('�'));
2495    }
2496
2497    #[test]
2498    #[cfg(unix)]
2499    fn unsuccessful_cargo_reports_status_and_stderr_without_caching_failure() {
2500        use std::os::unix::fs::PermissionsExt as _;
2501        let directory = cargo_fixture();
2502        let root = directory.path();
2503        let cache = tempfile::tempdir().unwrap();
2504        let script = root.join("cargo");
2505        let log = root.join("probes");
2506        std::fs::write(&script, format!("#!/bin/sh\necho metadata >> '{}'\nprintf 'old\\n1\\n2\\n3\\n4\\n\\ncomponent download failed\\n' >&2\nexit 17\n", log.display())).unwrap();
2507        std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
2508        for _ in 0..2 {
2509            let error = try_resolve_reported_from_home(
2510                Some(cache.path()),
2511                None,
2512                script.as_os_str(),
2513                &["build".into()],
2514                root,
2515                None,
2516            )
2517            .unwrap_err();
2518            assert!(matches!(error, ProbeFailure::Exit { .. }));
2519            let message = error.to_string();
2520            assert!(message.contains("exit status: 17"), "{message}");
2521            assert!(message.contains("component download failed"), "{message}");
2522            assert!(!message.contains("old"), "{message}");
2523        }
2524        assert_eq!(probes(&log), 2);
2525        assert!(!cache.path().join("cargo-roots").exists());
2526    }
2527
2528    fn git(directory: &Path, arguments: &[&str]) {
2529        let status = Command::new("git")
2530            .arg("-C")
2531            .arg(directory)
2532            .args(["-c", "commit.gpgsign=false", "-c", "user.name=t"])
2533            .args(["-c", "user.email=t@example.com"])
2534            .args(arguments)
2535            .stdout(std::process::Stdio::null())
2536            .stderr(std::process::Stdio::null())
2537            .status()
2538            .unwrap();
2539        assert!(status.success(), "git {arguments:?} failed");
2540    }
2541
2542    #[test]
2543    fn earlier_lockfile_states_are_offered_newest_first() {
2544        let directory = tempfile::tempdir().unwrap();
2545        let root = directory.path();
2546        let command = Vec::new();
2547        git(root, &["init", "-q"]);
2548        let mut committed = Vec::new();
2549        for state in ["one", "two", "three"] {
2550            std::fs::write(root.join("Cargo.lock"), state).unwrap();
2551            committed.push(build_identity(root, &command));
2552            git(root, &["add", "Cargo.lock"]);
2553            git(root, &["commit", "-q", "-m", state]);
2554        }
2555        committed.reverse();
2556
2557        // An uncommitted edit: the committed copy is the nearest earlier state.
2558        std::fs::write(root.join("Cargo.lock"), "four").unwrap();
2559        assert!(!committed.contains(&build_identity(root, &command)));
2560        assert_eq!(previous_build_identities(root), committed);
2561
2562        // Once committed, the working copy matches HEAD and is not offered as
2563        // its own fallback.
2564        git(root, &["commit", "-q", "-am", "four"]);
2565        assert_eq!(previous_build_identities(root), committed);
2566    }
2567
2568    #[test]
2569    fn a_crlf_checkout_matches_the_lf_states_in_history() {
2570        let directory = tempfile::tempdir().unwrap();
2571        let root = directory.path();
2572        let command = Vec::new();
2573        git(root, &["init", "-q"]);
2574        let mut committed = Vec::new();
2575        for state in ["one\n", "two\n"] {
2576            std::fs::write(root.join("Cargo.lock"), state).unwrap();
2577            committed.push(build_identity(root, &command));
2578            git(root, &["add", "Cargo.lock"]);
2579            git(root, &["commit", "-q", "-m", state]);
2580        }
2581        // What Git for Windows writes for the committed "two".
2582        std::fs::write(root.join("Cargo.lock"), "two\r\n").unwrap();
2583        assert_eq!(build_identity(root, &command), committed[1]);
2584        // The identity earlier releases gave these bytes comes first, then
2585        // each earlier state, followed by the identity they gave it as this
2586        // checkout wrote it.
2587        let raw = |state: &[u8]| identity_for_workspace(&CacheDigest::blake3(state).hash);
2588        assert_eq!(
2589            previous_build_identities(root),
2590            vec![raw(b"two\r\n"), committed[0].clone(), raw(b"one\r\n")]
2591        );
2592    }
2593
2594    #[test]
2595    fn crlf_states_in_history_also_offer_their_earlier_identity() {
2596        let directory = tempfile::tempdir().unwrap();
2597        let root = directory.path();
2598        let command = Vec::new();
2599        git(root, &["init", "-q"]);
2600        git(root, &["config", "core.autocrlf", "false"]);
2601        // Commit the CRLF bytes even where global attributes say otherwise.
2602        std::fs::write(root.join(".gitattributes"), "Cargo.lock -text\n").unwrap();
2603        for state in ["one\r\n", "two\r\n"] {
2604            std::fs::write(root.join("Cargo.lock"), state).unwrap();
2605            git(root, &["add", "Cargo.lock"]);
2606            git(root, &["commit", "-q", "-m", state]);
2607        }
2608        let raw = |state: &[u8]| identity_for_workspace(&CacheDigest::blake3(state).hash);
2609        // "two" as checked out is the current state, so only its raw identity
2610        // is offered; "one" offers its normalized identity, then its raw one.
2611        std::fs::write(root.join("Cargo.lock"), "one\n").unwrap();
2612        let one = build_identity(root, &command);
2613        std::fs::write(root.join("Cargo.lock"), "two\r\n").unwrap();
2614        assert_eq!(
2615            previous_build_identities(root),
2616            vec![raw(b"two\r\n"), one, raw(b"one\r\n")]
2617        );
2618    }
2619
2620    #[test]
2621    fn lockfile_markers_ignore_only_crlf_line_endings() {
2622        assert_eq!(lockfile_marker(b"a\r\nb\r\n"), lockfile_marker(b"a\nb\n"));
2623        assert_ne!(lockfile_marker(b"a\rb"), lockfile_marker(b"ab"));
2624        assert_eq!(lockfile_marker(b"a\nb"), CacheDigest::blake3(b"a\nb").hash);
2625        assert_eq!(crlf_checkout(b"a\nb\r\nc\n"), b"a\r\nb\r\nc\r\n");
2626    }
2627
2628    #[test]
2629    fn a_shallow_clone_offers_the_states_it_can_reach() {
2630        let origin = tempfile::tempdir().unwrap();
2631        let root = origin.path();
2632        let command = Vec::new();
2633        git(root, &["init", "-q"]);
2634        let mut committed = Vec::new();
2635        for state in ["one", "two", "three", "four"] {
2636            std::fs::write(root.join("Cargo.lock"), state).unwrap();
2637            committed.push(build_identity(root, &command));
2638            git(root, &["add", "Cargo.lock"]);
2639            git(root, &["commit", "-q", "-m", state]);
2640        }
2641        let clones = tempfile::tempdir().unwrap();
2642        let shallow = clones.path().join("shallow");
2643        git(
2644            clones.path(),
2645            &[
2646                "clone",
2647                "-q",
2648                "--depth",
2649                "2",
2650                &format!("file://{}", root.display()),
2651                shallow.to_str().unwrap(),
2652            ],
2653        );
2654        // HEAD holds "four"; its parent "three" was fetched and "two" was not.
2655        assert_eq!(
2656            previous_build_identities(&shallow),
2657            vec![committed[2].clone()]
2658        );
2659    }
2660
2661    #[test]
2662    fn an_untracked_lockfile_has_no_history_to_borrow_from() {
2663        let directory = tempfile::tempdir().unwrap();
2664        let root = directory.path();
2665        git(root, &["init", "-q"]);
2666        std::fs::write(root.join("Cargo.lock"), "tracked").unwrap();
2667        git(root, &["add", "Cargo.lock"]);
2668        git(root, &["commit", "-q", "-m", "tracked"]);
2669        git(root, &["rm", "-q", "Cargo.lock"]);
2670        git(root, &["commit", "-q", "-m", "removed"]);
2671        // Cargo generated a replacement nobody tracks.
2672        std::fs::write(root.join("Cargo.lock"), "generated").unwrap();
2673        assert!(previous_build_identities(root).is_empty());
2674    }
2675
2676    #[test]
2677    fn a_workspace_outside_version_control_has_no_earlier_states() {
2678        let directory = cargo_fixture();
2679        std::fs::write(directory.path().join("Cargo.lock"), "one").unwrap();
2680        assert!(previous_build_identities(directory.path()).is_empty());
2681        // Only the identity earlier releases gave a CRLF lockfile's bytes.
2682        std::fs::write(directory.path().join("Cargo.lock"), "one\r\n").unwrap();
2683        assert_eq!(
2684            previous_build_identities(directory.path()),
2685            vec![identity_for_workspace(
2686                &CacheDigest::blake3(b"one\r\n").hash
2687            )]
2688        );
2689    }
2690}