#![deny(missing_docs)]
use mbx_cache_core::{CacheDigest, canonical_json};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::SystemTime;
const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR";
const PROBE_GLOBAL_FLAGS: [&str; 3] = ["-C", "--config", "-Z"];
const PROBE_MANIFEST_TOGGLES: [&str; 3] = ["--offline", "--frozen", "--locked"];
#[derive(Debug)]
#[non_exhaustive]
pub enum ProbeFailure {
Io(std::io::Error),
Exit {
status: std::process::ExitStatus,
stderr: String,
},
Json(serde_json::Error),
Schema(&'static str),
}
impl std::fmt::Display for ProbeFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(error) => write!(f, "cargo metadata I/O error: {error}"),
Self::Exit { status, stderr } => {
write!(f, "cargo metadata failed with {status}")?;
if !stderr.is_empty() {
write!(f, ":\n{stderr}")?;
}
Ok(())
}
Self::Json(error) => write!(f, "invalid cargo metadata JSON: {error}"),
Self::Schema(field) => write!(f, "cargo metadata {field} missing or not a string"),
}
}
}
impl std::error::Error for ProbeFailure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(error) => Some(error),
Self::Json(error) => Some(error),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoInvocation {
pub workspace_root: PathBuf,
pub target_dir: PathBuf,
pub build_dir: Option<PathBuf>,
pub target_dir_requested: bool,
pub build_identity: String,
}
pub fn resolve(
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> CargoInvocation {
resolve_in(
cache_root().as_deref(),
cargo,
arguments,
working_dir,
target_dir_env,
)
}
fn resolve_in(
cache: Option<&Path>,
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> CargoInvocation {
let (reported, arguments) = probed_roots(
cache,
effective_cargo_home().as_deref(),
cargo,
arguments,
working_dir,
target_dir_env.as_deref(),
);
resolve_with_reported(arguments, working_dir, target_dir_env, reported.ok())
}
pub fn resolve_reported(
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> Option<CargoInvocation> {
try_resolve_reported(cargo, arguments, working_dir, target_dir_env).ok()
}
pub fn try_resolve_reported(
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> Result<CargoInvocation, ProbeFailure> {
try_resolve_reported_from_home(
cache_root().as_deref(),
effective_cargo_home().as_deref(),
cargo,
arguments,
working_dir,
target_dir_env,
)
}
#[cfg(test)]
fn resolve_reported_in(
cache: Option<&Path>,
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> Option<CargoInvocation> {
resolve_reported_from_home(
cache,
effective_cargo_home().as_deref(),
cargo,
arguments,
working_dir,
target_dir_env,
)
}
fn effective_cargo_home() -> Option<PathBuf> {
std::env::var_os("CARGO_HOME")
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".cargo")))
}
#[cfg(test)]
fn resolve_reported_from_home(
cache: Option<&Path>,
cargo_home: Option<&Path>,
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> Option<CargoInvocation> {
try_resolve_reported_from_home(
cache,
cargo_home,
cargo,
arguments,
working_dir,
target_dir_env,
)
.ok()
}
fn try_resolve_reported_from_home(
cache: Option<&Path>,
cargo_home: Option<&Path>,
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
) -> Result<CargoInvocation, ProbeFailure> {
let (reported, arguments) = probed_roots(
cache,
cargo_home,
cargo,
arguments,
working_dir,
target_dir_env.as_deref(),
);
Ok(resolve_with_reported(
arguments,
working_dir,
target_dir_env,
Some(reported?),
))
}
type ReportedRoots = (PathBuf, PathBuf, Option<PathBuf>);
fn probed_roots<'a>(
cache: Option<&Path>,
cargo_home: Option<&Path>,
cargo: &OsStr,
arguments: &'a [String],
working_dir: &Path,
target_dir_env: Option<&OsStr>,
) -> (Result<ReportedRoots, ProbeFailure>, &'a [String]) {
let probe = |arguments: &[String]| {
recalled_cargo_roots(
cache,
cargo_home,
cargo,
cargo_arguments(arguments),
working_dir,
target_dir_env,
)
};
let full = probe(arguments);
if full.is_ok() {
return (full, arguments);
}
match owned_arguments(arguments) {
Some(owned) => (probe(owned), owned),
None => (full, arguments),
}
}
fn owned_arguments(arguments: &[String]) -> Option<&[String]> {
let arguments = cargo_arguments(arguments);
let (index, command) = subcommand(arguments)?;
(!is_builtin_command(command) && index + 1 < arguments.len()).then(|| &arguments[..=index])
}
pub fn is_builtin_command(command: &str) -> bool {
matches!(
command,
"build"
| "check"
| "test"
| "run"
| "bench"
| "doc"
| "rustc"
| "rustdoc"
| "fix"
| "install"
| "help"
| "new"
| "init"
| "add"
| "remove"
| "update"
| "fetch"
| "clean"
| "config"
| "tree"
| "search"
| "info"
| "login"
| "logout"
| "owner"
| "package"
| "publish"
| "uninstall"
| "yank"
| "locate-project"
| "metadata"
| "generate-lockfile"
| "pkgid"
| "read-manifest"
| "report"
| "vendor"
| "verify-project"
| "version"
| "git-checkout"
)
}
fn resolve_with_reported(
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<OsString>,
reported: Option<(PathBuf, PathBuf, Option<PathBuf>)>,
) -> CargoInvocation {
let cargo_args = cargo_arguments(arguments);
let install_source = path_install_dir(cargo_args, working_dir);
let invocation_dir = if install_source.is_some() {
directory_option_dir(cargo_args, working_dir)
} else {
invocation_dir(cargo_args, working_dir)
};
let workspace_root = reported
.as_ref()
.map(|roots| roots.0.clone())
.unwrap_or_else(|| workspace_root(&invocation_dir));
let flagged = target_dir_argument(cargo_args);
let target_dir_requested = flagged.is_some()
|| target_dir_env
.as_ref()
.is_some_and(|value| !value.is_empty())
|| cargo_config_may_set_target_dir(
cargo_args,
install_source.as_deref().unwrap_or(&invocation_dir),
);
let build_dir = reported.as_ref().and_then(|roots| roots.2.clone());
let target_dir = flagged
.map(|value| absolute(&invocation_dir, value))
.or_else(|| reported.map(|roots| roots.1))
.or_else(|| {
target_dir_env
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.map(|value| absolute(&invocation_dir, &value.to_string_lossy()))
})
.unwrap_or_else(|| workspace_root.join("target"));
let build_identity = build_identity(&workspace_root, arguments);
CargoInvocation {
workspace_root,
target_dir,
target_dir_requested,
build_dir,
build_identity,
}
}
#[derive(Serialize)]
struct ActionIdentity<'a> {
version: u8,
workspace: &'a str,
os: &'static str,
arch: &'static str,
}
pub fn build_identity(workspace_root: &Path, _command: &[String]) -> String {
identity_for_workspace(&workspace_marker(workspace_root))
}
pub fn previous_build_identities(workspace_root: &Path) -> Vec<String> {
let Ok(lock) = std::fs::read(workspace_root.join("Cargo.lock")) else {
return Vec::new();
};
let current = lockfile_marker(&lock);
let checked_out = CacheDigest::blake3(&lock).hash;
let converts = current != checked_out;
let mut seen = BTreeSet::from([current]);
let mut identities = Vec::new();
if seen.insert(checked_out.clone()) {
identities.push(identity_for_workspace(&checked_out));
}
if git_output(
workspace_root,
&["ls-files", "--error-unmatch", "--", "Cargo.lock"],
)
.is_none()
{
return identities;
}
let limit = PREVIOUS_LOCKFILE_STATES.to_string();
let Some(revisions) = git_output(
workspace_root,
&["rev-list", "-n", &limit, "HEAD", "--", "Cargo.lock"],
) else {
return identities;
};
let revisions = String::from_utf8_lossy(&revisions);
let candidates = ["HEAD", "HEAD^"].into_iter().map(str::to_string).chain(
revisions
.lines()
.map(|revision| format!("{}^", revision.trim())),
);
let mut states = 0;
for revision in candidates {
let Some(lock) = git_output(
workspace_root,
&["show", &format!("{revision}:./Cargo.lock")],
) else {
continue;
};
let normalized = lockfile_marker(&lock);
let new_state = !seen.contains(&normalized);
let mut markers = vec![normalized, CacheDigest::blake3(&lock).hash];
if converts {
markers.push(CacheDigest::blake3(&crlf_checkout(&lock)).hash);
}
for marker in markers {
if seen.insert(marker.clone()) {
identities.push(identity_for_workspace(&marker));
}
}
states += usize::from(new_state);
if states == PREVIOUS_LOCKFILE_STATES {
break;
}
}
identities
}
const PREVIOUS_LOCKFILE_STATES: usize = 8;
fn git_output(workspace_root: &Path, arguments: &[&str]) -> Option<Vec<u8>> {
let output = Command::new("git")
.arg("-C")
.arg(workspace_root)
.args(arguments)
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
output.status.success().then_some(output.stdout)
}
fn identity_for_workspace(workspace: &str) -> String {
let bytes = canonical_json(&ActionIdentity {
version: 3,
workspace,
os: std::env::consts::OS,
arch: std::env::consts::ARCH,
})
.expect("Cargo build identity must serialize");
CacheDigest::blake3(&bytes).hash
}
fn workspace_marker(workspace_root: &Path) -> String {
std::fs::read(workspace_root.join("Cargo.lock")).map_or_else(
|_| {
workspace_root
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned()
},
|lock| lockfile_marker(&lock),
)
}
fn lockfile_marker(lock: &[u8]) -> String {
let mut normalized = Vec::with_capacity(lock.len());
let mut bytes = lock.iter().copied().peekable();
while let Some(byte) = bytes.next() {
if byte != b'\r' || bytes.peek() != Some(&b'\n') {
normalized.push(byte);
}
}
CacheDigest::blake3(&normalized).hash
}
fn crlf_checkout(lock: &[u8]) -> Vec<u8> {
let mut converted = Vec::with_capacity(lock.len() + lock.len() / 32);
let mut previous = None;
for &byte in lock {
if byte == b'\n' && previous != Some(b'\r') {
converted.push(b'\r');
}
converted.push(byte);
previous = Some(byte);
}
converted
}
pub fn cache_root() -> Option<PathBuf> {
if let Some(root) = std::env::var_os("MBX_CACHE_DIR").filter(|value| value.to_str().is_some()) {
return (!root.is_empty()).then(|| PathBuf::from(root));
}
if let Some(config) = dirs::config_dir().map(|root| root.join("mbx/config.toml"))
&& let Ok(contents) = std::fs::read_to_string(config)
&& let Ok(document) = toml::from_str::<toml::Value>(&contents)
&& let Some(root) = document.get("cache_dir").and_then(toml::Value::as_str)
{
return Some(PathBuf::from(root));
}
dirs::cache_dir().map(|root| root.join("mbx"))
}
fn cargo_arguments(arguments: &[String]) -> &[String] {
&arguments[..arguments
.iter()
.position(|argument| argument == "--")
.unwrap_or(arguments.len())]
}
fn absolute(working_dir: &Path, value: &str) -> PathBuf {
let path = PathBuf::from(value);
if path.is_absolute() {
path
} else {
working_dir.join(path)
}
}
fn forwarded_flags(arguments: &[String], flags: &[&str]) -> Vec<String> {
let mut forwarded = Vec::new();
let mut remaining = arguments.iter();
while let Some(argument) = remaining.next() {
if let Some((flag, value)) = argument
.split_once('=')
.filter(|(flag, _)| flags.contains(flag))
{
forwarded.extend([flag.to_string(), value.to_string()]);
} else if flags.contains(&argument.as_str()) {
if let Some(value) = remaining.next() {
forwarded.extend([argument.clone(), value.clone()]);
}
} else if flags
.iter()
.any(|flag| flag.len() == 2 && argument.len() > 2 && argument.starts_with(flag))
{
forwarded.push(argument.clone());
}
}
forwarded
}
fn invocation_dir(arguments: &[String], working_dir: &Path) -> PathBuf {
flag_value(arguments, "-C")
.map(|value| absolute(working_dir, value))
.unwrap_or_else(|| working_dir.to_path_buf())
}
fn directory_option_dir(arguments: &[String], working_dir: &Path) -> PathBuf {
flag_value(arguments, "-C")
.or_else(|| flag_value(arguments, "--directory"))
.or_else(|| {
arguments
.iter()
.find_map(|arg| arg.strip_prefix("-C").filter(|value| !value.is_empty()))
})
.map(|value| absolute(working_dir, value))
.unwrap_or_else(|| working_dir.to_path_buf())
}
pub fn manifest_in_scope(arguments: &[String], working_dir: &Path) -> bool {
let arguments = cargo_arguments(arguments);
let invocation = directory_option_dir(arguments, working_dir);
if let Some(source) = path_install_dir(arguments, working_dir) {
return source.join("Cargo.toml").is_file();
}
match flag_value(arguments, "--manifest-path") {
Some(manifest) => absolute(&invocation, manifest).is_file(),
None => invocation
.ancestors()
.any(|directory| directory.join("Cargo.toml").is_file()),
}
}
fn path_install_dir(arguments: &[String], working_dir: &Path) -> Option<PathBuf> {
let arguments = cargo_arguments(arguments);
let (_, command) = subcommand(arguments)?;
(command == "install")
.then(|| {
flag_value(arguments, "--path")
.map(|path| absolute(&directory_option_dir(arguments, working_dir), path))
})
.flatten()
}
fn subcommand(arguments: &[String]) -> Option<(usize, &str)> {
let mut remaining = arguments.iter().enumerate();
while let Some((index, argument)) = remaining.next() {
match argument.as_str() {
"--color" | "--config" | "-Z" | "-C" | "--directory" => {
remaining.next()?;
}
value if !value.starts_with('-') && !value.starts_with('+') => {
return Some((index, value));
}
_ => {}
}
}
None
}
pub fn is_remote_install(arguments: &[String]) -> bool {
let arguments = cargo_arguments(arguments);
subcommand(arguments).is_some_and(|(_, command)| command == "install")
&& flag_value(arguments, "--path").is_none()
&& !arguments.iter().any(|argument| argument == "--list")
}
pub fn remote_install_target_dir(
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<&OsStr>,
) -> Option<PathBuf> {
let invocation = directory_option_dir(cargo_arguments(arguments), working_dir);
target_dir_argument(cargo_arguments(arguments))
.map(|value| absolute(&invocation, value))
.or_else(|| {
target_dir_env
.filter(|value| !value.is_empty())
.map(|value| absolute(&invocation, &value.to_string_lossy()))
})
}
pub fn remote_install_name(arguments: &[String]) -> String {
const VALUE_FLAGS: [&str; 24] = [
"--version",
"--git",
"--branch",
"--tag",
"--rev",
"--root",
"--registry",
"--index",
"--features",
"-F",
"--bin",
"--example",
"--profile",
"--target",
"--target-dir",
"-j",
"--jobs",
"--config",
"-Z",
"--color",
"--message-format",
"--directory",
"-C",
"--path",
];
let arguments = cargo_arguments(arguments);
let start = subcommand(arguments).map_or(arguments.len(), |(index, _)| index + 1);
let mut packages = Vec::new();
let mut remaining = arguments[start..].iter();
while let Some(argument) = remaining.next() {
if VALUE_FLAGS.contains(&argument.as_str()) {
remaining.next();
} else if !argument.starts_with('-') {
packages.push(argument.as_str());
}
}
let identity = canonical_json(&(
&packages,
flag_value(arguments, "--git"),
flag_value(arguments, "--version"),
))
.unwrap_or_default();
let label: String = packages
.first()
.copied()
.unwrap_or("git")
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' || c == '_' {
c
} else {
'_'
}
})
.take(64)
.collect();
let hash = CacheDigest::blake3(&identity).hash;
format!("{label}-{}", &hash[..12.min(hash.len())])
}
fn rebase_cli_include(value: &str, caller: &Path) -> String {
fn rebase(value: &mut toml::Value, caller: &Path) {
match value {
toml::Value::String(path) => {
*path = absolute(caller, path).to_string_lossy().into_owned();
}
toml::Value::Array(entries) => {
for entry in entries {
rebase(entry, caller);
}
}
toml::Value::Table(entry) => {
if let Some(toml::Value::String(path)) = entry.get_mut("path") {
*path = absolute(caller, path).to_string_lossy().into_owned();
}
}
_ => {}
}
}
if let Ok(mut config) = toml::from_str::<toml::Value>(value)
&& config.as_table().is_some_and(|table| table.len() == 1)
&& let Some(include) = config.get_mut("include")
{
rebase(include, caller);
return format!("include = {include}");
}
value.to_owned()
}
fn recalled_cargo_roots(
cache: Option<&Path>,
cargo_home: Option<&Path>,
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<&OsStr>,
) -> Result<(PathBuf, PathBuf, Option<PathBuf>), ProbeFailure> {
if let Some(source) = path_install_dir(arguments, working_dir) {
let caller = directory_option_dir(arguments, working_dir);
let mut probe_args = Vec::new();
if let Some(target) =
std::env::var_os("CARGO_BUILD_TARGET_DIR").filter(|value| !value.is_empty())
{
probe_args.extend([
"--config".into(),
format!(
"build.target-dir = {}",
toml::Value::String(
absolute(&caller, &target.to_string_lossy())
.to_string_lossy()
.into_owned()
)
),
]);
}
for value in config_arguments(arguments) {
let value = if value.contains('=') {
rebase_cli_include(value, &caller)
} else {
absolute(&caller, value).to_string_lossy().into_owned()
};
probe_args.extend(["--config".into(), value.clone()]);
if let Ok(config) = toml::from_str::<toml::Value>(&value)
&& let Some(target) = config
.get("build")
.and_then(|build| build.get("target-dir"))
.and_then(toml::Value::as_str)
{
probe_args.extend([
"--config".into(),
format!(
"build.target-dir = {}",
toml::Value::String(
absolute(&caller, target).to_string_lossy().into_owned()
)
),
]);
}
}
probe_args.extend(forwarded_flags(arguments, &["-Z"]));
probe_args.push("build".into());
probe_args.extend(
arguments
.iter()
.filter(|arg| PROBE_MANIFEST_TOGGLES.contains(&arg.as_str()))
.cloned(),
);
let target = target_dir_env
.filter(|value| !value.is_empty())
.map(|value| absolute(&caller, &value.to_string_lossy()).into_os_string());
return recalled_cargo_roots(
cache,
cargo_home,
cargo,
&probe_args,
&source,
target.as_deref(),
);
}
let probe = cache.and_then(|cache| {
ProbeRecord::describe(
cache,
cargo_home,
cargo,
arguments,
working_dir,
target_dir_env,
)
});
if let Some(recalled) = probe.as_ref().and_then(ProbeRecord::recall) {
return Ok(recalled);
}
let roots = cargo_roots(cargo, arguments, working_dir, target_dir_env)?;
if let Some(probe) = probe {
probe.remember(&roots);
}
Ok(roots)
}
const PROBE_RECORD_VERSION: u8 = 3;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct ProbeRecord {
version: u8,
key: ProbeKey,
pins: Vec<Pin>,
#[serde(skip)]
path: PathBuf,
#[serde(skip)]
observed_at: Option<SystemTime>,
workspace_root: PathBuf,
target_dir: PathBuf,
build_dir: Option<PathBuf>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct ProbeKey {
cargo: PathBuf,
arguments: Vec<String>,
working_dir: PathBuf,
target_dir_env: Option<String>,
build_target_dir_env: Option<String>,
build_dir_env: Option<String>,
target_dir_argument: Option<String>,
cargo_home: Option<String>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Pin {
path: PathBuf,
state: Option<(u64, u64, u32)>,
}
impl Pin {
fn describe(path: PathBuf) -> Option<Self> {
let state = match std::fs::metadata(&path) {
Ok(metadata) => {
let modified = metadata
.modified()
.ok()?
.duration_since(SystemTime::UNIX_EPOCH)
.ok()?;
Some((metadata.len(), modified.as_secs(), modified.subsec_nanos()))
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(_) => return None,
};
Some(Self { path, state })
}
fn holds(&self) -> bool {
Pin::describe(self.path.clone()).as_ref() == Some(self)
}
fn is_racy(&self, observed_at: SystemTime) -> bool {
let Some((_, seconds, nanos)) = self.state else {
return false;
};
let window = if nanos == 0 {
COARSE_TIMESTAMP_RACE_WINDOW
} else {
FINE_TIMESTAMP_RACE_WINDOW
};
let modified = SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds, nanos);
observed_at
.duration_since(modified)
.map_or(true, |age| age < window)
}
}
const FINE_TIMESTAMP_RACE_WINDOW: std::time::Duration = std::time::Duration::from_millis(20);
const COARSE_TIMESTAMP_RACE_WINDOW: std::time::Duration = std::time::Duration::from_secs(2);
impl ProbeRecord {
fn describe(
cache: &Path,
cargo_home: Option<&Path>,
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<&OsStr>,
) -> Option<Self> {
let cargo = resolve_program(cargo)?;
let env =
|name: &str| std::env::var_os(name).map(|value| value.to_string_lossy().into_owned());
let key = ProbeKey {
cargo: cargo.clone(),
arguments: probe_arguments(arguments),
working_dir: working_dir.to_path_buf(),
target_dir_env: target_dir_env.map(|value| value.to_string_lossy().into_owned()),
build_target_dir_env: env("CARGO_BUILD_TARGET_DIR"),
build_dir_env: env("CARGO_BUILD_BUILD_DIR"),
target_dir_argument: target_dir_argument(arguments).map(str::to_owned),
cargo_home: cargo_home.map(|home| home.to_string_lossy().into_owned()),
};
let invocation_dir = invocation_dir(arguments, working_dir);
let manifest_start = flag_value(arguments, "--manifest-path")
.map(|manifest| absolute(&invocation_dir, manifest))
.and_then(|manifest| manifest.parent().map(Path::to_path_buf))
.unwrap_or_else(|| invocation_dir.clone());
let mut watched = vec![cargo];
watched.extend(
manifest_start
.ancestors()
.map(|directory| directory.join("Cargo.toml")),
);
for directory in invocation_dir.ancestors() {
let dot_cargo = directory.join(".cargo");
watched.push(dot_cargo.join("config.toml"));
watched.push(dot_cargo.join("config"));
}
if let Some(home) = cargo_home {
watched.push(home.join("config.toml"));
watched.push(home.join("config"));
}
watched.extend(
config_arguments(arguments)
.map(|value| absolute(&invocation_dir, value))
.filter(|path| path.is_file()),
);
if config_arguments(arguments).any(|value| {
value
.split_once('=')
.is_some_and(|(key, _)| key.trim() == "include")
}) || watched
.iter()
.skip(1)
.filter(|path| path.file_name().is_some_and(|name| name != "Cargo.toml"))
.any(|path| config_includes_files(path))
{
return None;
}
let observed_at = SystemTime::now();
let pins = watched
.into_iter()
.map(Pin::describe)
.collect::<Option<Vec<_>>>()?;
let selector = canonical_json(&key).ok()?;
let path = cache
.join("cargo-roots")
.join("v1")
.join(format!("{}.json", CacheDigest::blake3(&selector).hash));
Some(Self {
version: PROBE_RECORD_VERSION,
key,
pins,
path,
observed_at: Some(observed_at),
workspace_root: PathBuf::new(),
target_dir: PathBuf::new(),
build_dir: None,
})
}
fn recall(&self) -> Option<(PathBuf, PathBuf, Option<PathBuf>)> {
let bytes = std::fs::read(&self.path).ok()?;
let recorded: ProbeRecord = serde_json::from_slice(&bytes).ok()?;
if recorded.version != PROBE_RECORD_VERSION
|| recorded.key != self.key
|| !recorded.pins.iter().all(Pin::holds)
{
return None;
}
Some((
recorded.workspace_root,
recorded.target_dir,
recorded.build_dir,
))
}
fn remember(mut self, roots: &(PathBuf, PathBuf, Option<PathBuf>)) {
let root_manifest = roots.0.join("Cargo.toml");
if !self.pins.iter().any(|pin| pin.path == root_manifest) {
let Some(pin) = Pin::describe(root_manifest) else {
return;
};
self.pins.push(pin);
}
if let Some(observed_at) = self.observed_at
&& self.pins.iter().any(|pin| pin.is_racy(observed_at))
{
return;
}
self.workspace_root = roots.0.clone();
self.target_dir = roots.1.clone();
self.build_dir = roots.2.clone();
let Ok(bytes) = serde_json::to_vec(&self) else {
return;
};
let Some(directory) = self.path.parent() else {
return;
};
if std::fs::create_dir_all(directory).is_err() {
return;
}
let staged = directory.join(format!(
".{}.{}",
self.path.file_name().unwrap_or_default().to_string_lossy(),
std::process::id()
));
if std::fs::write(&staged, bytes).is_ok() && std::fs::rename(&staged, &self.path).is_err() {
let _ = std::fs::remove_file(&staged);
}
}
}
fn config_includes_files(path: &Path) -> bool {
let contents = match std::fs::read_to_string(path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
Err(_) => return true,
};
toml::from_str::<toml::Value>(&contents).is_ok_and(|config| config.get("include").is_some())
|| toml::from_str::<toml::Value>(&contents).is_err()
}
fn probe_arguments(arguments: &[String]) -> Vec<String> {
let mut probe = forwarded_flags(arguments, &PROBE_GLOBAL_FLAGS);
probe.extend(
["metadata", "--no-deps", "--format-version", "1"]
.iter()
.map(|argument| (*argument).to_string()),
);
if let Some(manifest) = flag_value(arguments, "--manifest-path") {
probe.push("--manifest-path".into());
probe.push(manifest.into());
}
probe.extend(
arguments
.iter()
.filter(|argument| PROBE_MANIFEST_TOGGLES.contains(&argument.as_str()))
.cloned(),
);
probe
}
fn resolve_program(program: &OsStr) -> Option<PathBuf> {
let candidate = Path::new(program);
if candidate.components().count() > 1 {
return candidate.is_file().then(|| candidate.to_path_buf());
}
let names = if cfg!(windows) {
vec![candidate.as_os_str().to_os_string(), {
let mut exe = candidate.as_os_str().to_os_string();
exe.push(".exe");
exe
}]
} else {
vec![candidate.as_os_str().to_os_string()]
};
std::env::split_paths(&std::env::var_os("PATH")?)
.flat_map(|directory| names.iter().map(move |name| directory.join(name)))
.find(|path| path.is_file())
}
fn cargo_roots(
cargo: &OsStr,
arguments: &[String],
working_dir: &Path,
target_dir_env: Option<&OsStr>,
) -> Result<(PathBuf, PathBuf, Option<PathBuf>), ProbeFailure> {
let mut command = Command::new(cargo);
match target_dir_argument(arguments)
.map(OsStr::new)
.or(target_dir_env)
{
Some(value) => command.env(CARGO_TARGET_DIR_ENV, value),
None => command.env_remove(CARGO_TARGET_DIR_ENV),
};
command
.current_dir(working_dir)
.args(probe_arguments(arguments));
let output = command.output().map_err(ProbeFailure::Io)?;
log::debug!(
"cargo metadata stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
if !output.status.success() {
return Err(ProbeFailure::Exit {
status: output.status,
stderr: stderr_tail(&output.stderr),
});
}
parse_metadata(&output.stdout)
}
fn parse_metadata(bytes: &[u8]) -> Result<(PathBuf, PathBuf, Option<PathBuf>), ProbeFailure> {
let metadata: serde_json::Value = serde_json::from_slice(bytes).map_err(ProbeFailure::Json)?;
let required = |field: &'static str| {
metadata
.get(field)
.and_then(|value| value.as_str())
.map(PathBuf::from)
.ok_or(ProbeFailure::Schema(field))
};
Ok((
required("workspace_root")?,
required("target_directory")?,
metadata
.get("build_directory")
.and_then(|value| value.as_str())
.map(PathBuf::from),
))
}
fn stderr_tail(bytes: &[u8]) -> String {
let stderr = String::from_utf8_lossy(bytes);
let lines: Vec<_> = stderr
.lines()
.filter(|line| !line.trim().is_empty())
.rev()
.take(5)
.collect();
let tail = lines.into_iter().rev().collect::<Vec<_>>().join("\n");
let mut start = tail.len().saturating_sub(1024);
while !tail.is_char_boundary(start) {
start += 1;
}
tail[start..].to_owned()
}
fn target_dir_argument(arguments: &[String]) -> Option<&str> {
flag_value(arguments, "--target-dir")
}
fn cargo_config_may_set_target_dir(arguments: &[String], invocation_dir: &Path) -> bool {
if std::env::var_os("CARGO_BUILD_TARGET_DIR").is_some_and(|value| !value.is_empty()) {
return true;
}
if config_arguments(arguments).any(|value| {
value
.split_once('=')
.is_none_or(|(key, _)| matches!(key.trim(), "build.target-dir" | "include"))
}) {
return true;
}
let project = invocation_dir.ancestors().flat_map(|directory| {
let cargo = directory.join(".cargo");
[cargo.join("config.toml"), cargo.join("config")]
});
let home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|root| root.join(".cargo")))
.into_iter()
.flat_map(|cargo| [cargo.join("config.toml"), cargo.join("config")]);
project.chain(home).any(|path| config_may_set_target(&path))
}
fn config_arguments(arguments: &[String]) -> impl Iterator<Item = &str> {
let mut values = Vec::new();
let mut remaining = arguments.iter();
while let Some(argument) = remaining.next() {
if let Some(value) = argument.strip_prefix("--config=") {
values.push(value);
} else if argument == "--config"
&& let Some(value) = remaining.next()
{
values.push(value);
}
}
values.into_iter()
}
fn config_may_set_target(path: &Path) -> bool {
let contents = match std::fs::read_to_string(path) {
Ok(contents) => contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return false,
Err(_) => return true,
};
let Ok(config) = toml::from_str::<toml::Value>(&contents) else {
return true;
};
config
.get("build")
.and_then(|build| build.get("target-dir"))
.is_some()
|| config.get("include").is_some()
}
fn flag_value<'a>(arguments: &'a [String], flag: &str) -> Option<&'a str> {
let joined = format!("{flag}=");
let mut arguments = arguments.iter();
while let Some(argument) = arguments.next() {
if let Some(value) = argument.strip_prefix(&joined) {
return Some(value);
}
if argument == flag {
return arguments.next().map(String::as_str);
}
}
None
}
fn workspace_root(start: &Path) -> PathBuf {
start
.ancestors()
.find(|directory| directory.join("Cargo.toml").is_file())
.unwrap_or(start)
.to_path_buf()
}
#[cfg(test)]
mod tests {
use super::*;
fn cargo_fixture() -> tempfile::TempDir {
let directory = tempfile::tempdir().unwrap();
std::fs::write(
directory.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
std::fs::create_dir_all(directory.path().join("src")).unwrap();
std::fs::write(directory.path().join("src/lib.rs"), "").unwrap();
directory
}
fn fixture_root(path: &Path) -> PathBuf {
if cfg!(windows) {
path.to_path_buf()
} else {
path.canonicalize().unwrap()
}
}
#[test]
fn a_manifest_is_in_scope_only_where_cargo_would_read_one() {
let outside = tempfile::tempdir().unwrap();
let outside = outside.path();
let arguments = |value: &str| {
value
.split_whitespace()
.map(str::to_owned)
.collect::<Vec<_>>()
};
assert!(!manifest_in_scope(&arguments("binstall"), outside));
assert!(!manifest_in_scope(&arguments("build"), outside));
let project = cargo_fixture();
let project = project.path();
let nested = project.join("src");
assert!(manifest_in_scope(&arguments("binstall"), project));
assert!(manifest_in_scope(&arguments("binstall"), &nested));
assert!(manifest_in_scope(
&[
"-C".into(),
project.to_string_lossy().into_owned(),
"binstall".into()
],
outside
));
assert!(!manifest_in_scope(
&[
"--directory".into(),
outside.to_string_lossy().into_owned(),
"binstall".into()
],
project
));
assert!(manifest_in_scope(
&[
"binstall".into(),
"--manifest-path".into(),
"Cargo.toml".into()
],
project
));
assert!(!manifest_in_scope(
&["binstall".into(), "--manifest-path=src/Cargo.toml".into()],
project
));
assert!(manifest_in_scope(
&[
"install".into(),
"--path".into(),
project.to_string_lossy().into_owned()
],
outside
));
assert!(!manifest_in_scope(
&[
"generate".into(),
"--path".into(),
project.to_string_lossy().into_owned()
],
outside
));
assert!(!manifest_in_scope(
&[
"install".into(),
"--path".into(),
outside.join("absent").to_string_lossy().into_owned()
],
outside
));
assert!(!manifest_in_scope(
&[
"run".into(),
"--".into(),
"--path".into(),
project.to_string_lossy().into_owned()
],
outside
));
}
#[test]
fn lockfile_makes_identity_independent_of_checkout_path() {
let left = tempfile::tempdir().unwrap();
let right = tempfile::tempdir().unwrap();
std::fs::write(left.path().join("Cargo.lock"), "same").unwrap();
std::fs::write(right.path().join("Cargo.lock"), "same").unwrap();
let command = vec!["build".to_string(), "--workspace".to_string()];
assert_eq!(
build_identity(left.path(), &command),
build_identity(right.path(), &command)
);
}
#[test]
fn cargo_commands_and_toolchain_selectors_share_predictions() {
let project = tempfile::tempdir().unwrap();
std::fs::write(project.path().join("Cargo.lock"), "same").unwrap();
let build = vec!["build".to_string()];
let test = vec!["test".to_string(), "--workspace".to_string()];
let msrv = vec!["+1.91".to_string(), "check".to_string()];
assert_eq!(
build_identity(project.path(), &build),
build_identity(project.path(), &test),
);
assert_eq!(
build_identity(project.path(), &build),
build_identity(project.path(), &msrv),
);
}
#[test]
fn rustc_flags_after_separator_are_not_cargo_globals() {
let args = vec![
"rustc".into(),
"--".into(),
"-C".into(),
"opt-level=3".into(),
];
assert_eq!(
invocation_dir(cargo_arguments(&args), Path::new("/work")),
PathBuf::from("/work")
);
}
#[test]
fn target_directory_flags_and_environment_are_recorded_as_explicit() {
let directory = cargo_fixture();
let root = directory.path();
let cargo = OsStr::new("cargo-that-does-not-exist");
let plain = ["build".to_string()];
let default = resolve(cargo, &plain, root, None);
assert_eq!(default.target_dir, root.join("target"));
assert!(!default.target_dir_requested);
for arguments in [
vec!["build".into(), "--target-dir=target".into()],
vec!["build".into(), "--target-dir".into(), "target".into()],
] {
let resolved = resolve(cargo, &arguments, root, None);
assert_eq!(resolved.target_dir, root.join("target"));
assert!(resolved.target_dir_requested);
}
let from_environment = resolve(cargo, &plain, root, Some("elsewhere".into()));
assert_eq!(from_environment.target_dir, root.join("elsewhere"));
assert!(from_environment.target_dir_requested);
let empty_environment = resolve(cargo, &plain, root, Some("".into()));
assert!(!empty_environment.target_dir_requested);
let dangling = ["build".into(), "--target-dir".into()];
assert!(!resolve(cargo, &dangling, root, None).target_dir_requested);
}
#[test]
fn project_config_that_names_the_default_target_is_still_explicit() {
let directory = cargo_fixture();
let root = directory.path();
std::fs::create_dir_all(root.join(".cargo")).unwrap();
std::fs::write(
root.join(".cargo/config.toml"),
"[build]\ntarget-dir = \"target\"\n",
)
.unwrap();
let resolved = resolve(
OsStr::new("cargo-that-does-not-exist"),
&["build".into()],
root,
None,
);
assert_eq!(resolved.target_dir, root.join("target"));
assert!(resolved.target_dir_requested);
}
#[test]
fn command_line_config_include_may_set_the_target_directory() {
let directory = cargo_fixture();
let arguments = [
"build".into(),
"--config".into(),
"include='target-config.toml'".into(),
];
let resolved = resolve(
OsStr::new("cargo-that-does-not-exist"),
&arguments,
directory.path(),
None,
);
assert!(resolved.target_dir_requested);
}
#[test]
fn command_line_target_config_reaches_the_cargo_probe() {
let directory = cargo_fixture();
let root = directory.path();
let configured = root.join("configured-target");
let arguments = [
"build".into(),
"--offline".into(),
"--manifest-path".into(),
root.join("Cargo.toml").display().to_string(),
"--config".into(),
format!("build.target-dir='{}'", configured.display()),
];
let resolved =
resolve_reported_in(None, OsStr::new("cargo"), &arguments, root, None).unwrap();
assert_eq!(resolved.target_dir, configured);
assert!(resolved.target_dir_requested);
}
#[test]
fn remote_installs_are_installs_without_a_path_or_listing() {
let remote = |words: &[&str]| {
is_remote_install(
&words
.iter()
.map(|word| word.to_string())
.collect::<Vec<_>>(),
)
};
assert!(remote(&["install", "ripgrep"]));
assert!(remote(&[
"+stable",
"install",
"--git",
"https://example.com/r"
]));
assert!(remote(&[
"--color",
"always",
"install",
"--version",
"1.0.0",
"x"
]));
assert!(!remote(&["install", "--path", "."]));
assert!(!remote(&["install", "--path=."]));
assert!(!remote(&["install", "--list"]));
assert!(!remote(&["build"]));
assert!(!remote(&["-C", "install", "build"]));
assert!(!remote(&["run", "--", "install", "x"]));
}
#[test]
fn remote_install_target_is_the_one_the_caller_named() {
let words = |words: &[&str]| {
words
.iter()
.map(|word| word.to_string())
.collect::<Vec<_>>()
};
let cwd = Path::new("/work");
assert_eq!(
remote_install_target_dir(&words(&["install", "x"]), cwd, None),
None
);
assert_eq!(
remote_install_target_dir(&words(&["install", "x", "--target-dir", "out"]), cwd, None),
Some(PathBuf::from("/work/out"))
);
assert_eq!(
remote_install_target_dir(&words(&["install", "x"]), cwd, Some(OsStr::new("/env"))),
Some(PathBuf::from("/env"))
);
assert_eq!(
remote_install_target_dir(
&words(&["install", "x", "--target-dir=flag"]),
cwd,
Some(OsStr::new("/env"))
),
Some(PathBuf::from("/work/flag"))
);
assert_eq!(
remote_install_target_dir(&words(&["install", "x"]), cwd, Some(OsStr::new(""))),
None
);
}
#[test]
fn remote_install_name_ignores_options_that_do_not_change_the_package() {
let name = |words: &[&str]| {
remote_install_name(
&words
.iter()
.map(|word| word.to_string())
.collect::<Vec<_>>(),
)
};
let plain = name(&["install", "helloworld-rs"]);
assert!(plain.starts_with("helloworld-rs-"));
assert_eq!(
plain,
name(&[
"install",
"--root",
"out",
"--force",
"helloworld-rs",
"--locked"
])
);
assert_ne!(plain, name(&["install", "helloworld-rs", "--version", "2"]));
assert_ne!(plain, name(&["install", "other"]));
assert!(name(&["install", "--git", "https://example.com/r"]).starts_with("git-"));
assert_ne!(
name(&["install", "--git", "https://example.com/a"]),
name(&["install", "--git", "https://example.com/b"])
);
assert!(!name(&["install", "../evil/x"]).contains('/'));
}
#[test]
fn path_install_probes_source_config_and_preserves_caller_relative_targets() {
let source = cargo_fixture();
let caller = cargo_fixture();
let source_root = fixture_root(source.path());
let caller_root = fixture_root(caller.path());
for (root, target) in [
(source_root.as_path(), "source-target"),
(caller_root.as_path(), "caller-target"),
] {
std::fs::create_dir_all(root.join(".cargo")).unwrap();
std::fs::write(
root.join(".cargo/config.toml"),
format!("[build]\ntarget-dir = '{target}'\n"),
)
.unwrap();
}
let arguments = vec![
"install".into(),
format!("--path={}", source_root.as_path().display()),
"--offline".into(),
];
let cache = tempfile::tempdir().unwrap();
let resolve = |args: &[String], target| {
resolve_reported_in(
Some(cache.path()),
OsStr::new("cargo"),
args,
caller_root.as_path(),
target,
)
.unwrap()
};
let roots = resolve(&arguments, None);
assert_eq!(roots.workspace_root, source_root.as_path());
assert_eq!(
roots.target_dir,
source_root.as_path().join("source-target")
);
assert!(roots.target_dir_requested);
let mut flagged = arguments.clone();
flagged.extend(["--target-dir".into(), "flag-target".into()]);
assert_eq!(
resolve(&flagged, None).target_dir,
caller_root.as_path().join("flag-target")
);
assert_eq!(
resolve(&arguments, Some("env-target".into())).target_dir,
caller_root.as_path().join("env-target")
);
let mut configured = arguments.clone();
configured.extend(["--config".into(), "build.target-dir='cli-target'".into()]);
assert_eq!(
resolve(&configured, None).target_dir,
caller_root.as_path().join("cli-target")
);
std::fs::write(
source_root.as_path().join(".cargo/config.toml"),
"[build]\ntarget-dir = 'changed-source-target'\n",
)
.unwrap();
assert_eq!(
resolve(&arguments, None).target_dir,
source_root.as_path().join("changed-source-target")
);
}
#[test]
fn path_install_directory_flags_rebase_the_source_and_target() {
let source = cargo_fixture();
let source_root = fixture_root(source.path());
let caller = source_root.parent().unwrap();
let directory = source_root.file_name().unwrap().to_str().unwrap();
for flags in [
vec!["--directory".into(), directory.into()],
vec![format!("--directory={directory}")],
vec!["-C".into(), directory.into()],
vec![format!("-C{directory}")],
] {
let mut arguments = flags;
arguments.extend(
[
"install",
"--path",
".",
"--target-dir",
"shared",
"--offline",
]
.map(str::to_owned),
);
let roots =
resolve_reported_in(None, OsStr::new("cargo"), &arguments, caller, None).unwrap();
assert_eq!(roots.workspace_root, source_root);
assert_eq!(roots.target_dir, source_root.join("shared"));
assert!(roots.target_dir_requested);
}
}
#[test]
fn path_install_cli_includes_keep_caller_paths_and_override_order() {
let source = cargo_fixture();
let caller = cargo_fixture();
let source_root = fixture_root(source.path());
let caller_root = fixture_root(caller.path());
for (root, target) in [
(&caller_root, "caller-target"),
(&source_root, "wrong-target"),
] {
std::fs::create_dir_all(root.join(".config")).unwrap();
std::fs::write(
root.join(".config/extra.toml"),
format!("[build]\ntarget-dir = '{target}'\n"),
)
.unwrap();
}
let resolve = |overrides: &[&str]| {
let mut args = vec![
"install".into(),
"--path".into(),
source_root.display().to_string(),
"--offline".into(),
];
for value in overrides {
args.extend(["--config".into(), (*value).into()]);
}
resolve_reported_in(None, OsStr::new("cargo"), &args, &caller_root, None)
};
for include in [
"include=['.config/extra.toml']",
"include=[{path='.config/extra.toml'}, {path='.config/missing.toml', optional=true}]",
] {
assert_eq!(
resolve(&[include]).unwrap().target_dir,
caller_root.join("caller-target")
);
assert_eq!(
resolve(&[include, "build.target-dir='override'"])
.unwrap()
.target_dir,
caller_root.join("override")
);
}
let absolute_include = format!(
"include=[{}]",
toml::Value::String(caller_root.join(".config/extra.toml").display().to_string())
);
assert_eq!(
resolve(&[&absolute_include]).unwrap().target_dir,
caller_root.join("caller-target")
);
assert!(resolve(&["include=['.config/missing.toml']"]).is_none());
assert!(resolve(&["include=42"]).is_none());
}
#[cfg(unix)]
fn logging_cargo(directory: &Path, project: &Path, log: &Path) -> PathBuf {
use std::os::unix::fs::PermissionsExt as _;
let script = directory.join("cargo");
std::fs::write(
&script,
format!(
"#!/bin/sh\nprintf '%s\\n' \"$*\" >> {log}\ncase \" $* \" in *' metadata '*) printf '{{\"workspace_root\":\"{root}\",\"target_directory\":\"{root}/target\",\"packages\":[]}}';; esac\n",
log = log.display(),
root = project.display(),
),
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
script
}
#[cfg(unix)]
fn build_dir_cargo(directory: &Path, project: &Path, log: &Path) -> PathBuf {
use std::os::unix::fs::PermissionsExt as _;
let script = directory.join("cargo");
std::fs::write(
&script,
format!(
"#!/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",
log = log.display(),
root = project.display(),
),
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
script
}
#[cfg(unix)]
fn probes(log: &Path) -> usize {
std::fs::read_to_string(log)
.unwrap_or_default()
.lines()
.filter(|line| line.contains("metadata"))
.count()
}
fn settle() {
let probe = tempfile::NamedTempFile::new().unwrap();
let coarse = probe
.as_file()
.metadata()
.and_then(|metadata| metadata.modified())
.ok()
.and_then(|modified| modified.duration_since(SystemTime::UNIX_EPOCH).ok())
.is_none_or(|since| since.subsec_nanos() == 0);
std::thread::sleep(if coarse {
COARSE_TIMESTAMP_RACE_WINDOW + FINE_TIMESTAMP_RACE_WINDOW
} else {
FINE_TIMESTAMP_RACE_WINDOW * 2
});
}
#[cfg(unix)]
fn stable_cargo(directory: &Path, project: &Path, log: &Path) -> PathBuf {
use std::os::unix::fs::PermissionsExt as _;
let script = directory.join("cargo");
std::fs::write(
&script,
format!(
"#!/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",
log = log.display(),
root = project.display(),
),
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
script
}
#[test]
#[cfg(unix)]
fn flags_after_an_external_subcommand_stay_out_of_the_probe() {
let directory = cargo_fixture();
let root = directory.path();
let scratch = tempfile::tempdir().unwrap();
let log = scratch.path().join("log");
let cargo = stable_cargo(scratch.path(), root, &log);
let resolve = |words: &[&str]| {
let arguments = words
.iter()
.map(|word| (*word).to_owned())
.collect::<Vec<_>>();
resolve_reported_in(None, cargo.as_os_str(), &arguments, root, None)
};
for words in [
&["foo", "-Z", "x"][..],
&["foo", "-Zx"],
&["foo", "-C", "dir"],
&["foo", "autoharness", "-Z", "autoharness"],
&["+stable", "foo", "-Z", "x"],
] {
let resolved = resolve(words).unwrap_or_else(|| panic!("{words:?} was refused"));
assert_eq!(resolved.workspace_root, root, "{words:?}");
assert_eq!(resolved.target_dir, root.join("target"), "{words:?}");
}
assert!(resolve(&["-Z", "x", "foo"]).is_none());
assert!(resolve(&["build", "-Z", "x"]).is_none());
assert!(resolve(&["build", "-C", "dir"]).is_none());
}
#[test]
#[cfg(unix)]
fn flags_after_an_alias_or_external_subcommand_still_reach_a_probe_that_accepts_them() {
let directory = cargo_fixture();
let root = directory.path();
let scratch = tempfile::tempdir().unwrap();
let log = scratch.path().join("log");
let cargo = logging_cargo(scratch.path(), root, &log);
let arguments = ["b", "--config", "net.offline=true"].map(str::to_owned);
resolve_reported_in(None, cargo.as_os_str(), &arguments, root, None).unwrap();
let logged = std::fs::read_to_string(&log).unwrap();
assert!(
logged.contains("--config net.offline=true"),
"an alias is Cargo's to parse: {logged}"
);
assert_eq!(probes(&log), 1, "{logged}");
}
#[test]
fn reported_intermediates_survive_resolution_and_cached_records() {
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let arguments = ["build".to_owned()];
let roots = (
root.to_path_buf(),
root.join("target"),
Some(root.join("intermediates")),
);
let resolved = resolve_with_reported(&arguments, root, None, Some(roots.clone()));
assert_eq!(resolved.build_dir, roots.2);
let describe = || {
ProbeRecord::describe(
cache.path(),
None,
OsStr::new("cargo"),
&arguments,
root,
None,
)
.unwrap()
};
settle();
describe().remember(&roots);
assert_eq!(describe().recall(), Some(roots.clone()));
let old_cargo =
resolve_with_reported(&arguments, root, None, Some((roots.0, roots.1, None)));
assert_eq!(old_cargo.build_dir, None);
}
#[cfg(unix)]
const BUILD_DIR_PROBE_CHILD: &str = "MBX_CARGO_BUILD_DIR_PROBE_CHILD";
#[test]
#[cfg(unix)]
fn a_probed_build_directory_is_parsed_recalled_and_rekeyed() {
if std::env::var_os(BUILD_DIR_PROBE_CHILD).is_none() {
let status = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"--nocapture",
"--test-threads=1",
"tests::a_probed_build_directory_is_parsed_recalled_and_rekeyed",
])
.env(BUILD_DIR_PROBE_CHILD, "1")
.env_remove("CARGO_BUILD_BUILD_DIR")
.env_remove("CARGO_TARGET_DIR")
.env_remove("CARGO_BUILD_TARGET_DIR")
.status()
.unwrap();
assert!(status.success(), "the child run of this test failed");
return;
}
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let log = root.join("cargo.log");
let cargo = build_dir_cargo(root, root, &log);
let home = root.join("cargo-home");
let resolve = |arguments: &[String]| {
resolve_reported_from_home(
Some(cache.path()),
Some(&home),
cargo.as_os_str(),
arguments,
root,
None,
)
.unwrap()
};
let build = ["build".to_string()];
settle();
let first = resolve(&build);
assert_eq!(first.build_dir, Some(root.join("target/build-dir")));
assert_eq!(probes(&log), 1);
assert_eq!(resolve(&build).build_dir, first.build_dir);
assert_eq!(probes(&log), 1);
let elsewhere = root.join("elsewhere");
unsafe { std::env::set_var("CARGO_BUILD_BUILD_DIR", &elsewhere) };
assert_eq!(resolve(&build).build_dir, Some(elsewhere));
assert_eq!(probes(&log), 2);
unsafe { std::env::remove_var("CARGO_BUILD_BUILD_DIR") };
assert_eq!(resolve(&build).build_dir, first.build_dir);
assert_eq!(probes(&log), 2);
let other_target = root.join("other-target");
let targeted = [
"build".to_string(),
"--target-dir".to_string(),
other_target.display().to_string(),
];
let with_target = resolve(&targeted);
assert_eq!(with_target.build_dir, Some(other_target.join("build-dir")));
assert_eq!(probes(&log), 3);
assert_eq!(resolve(&targeted).build_dir, with_target.build_dir);
assert_eq!(resolve(&build).build_dir, first.build_dir);
assert_eq!(probes(&log), 3);
}
#[test]
#[cfg(unix)]
fn a_probe_is_remembered_while_what_cargo_read_stands() {
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let log = root.join("cargo.log");
let cargo = logging_cargo(root, root, &log);
let arguments = ["build".to_string(), "--locked".to_string()];
let home = root.join("cargo-home");
let resolve = |target_dir_env: Option<&str>| {
resolve_reported_from_home(
Some(cache.path()),
Some(&home),
cargo.as_os_str(),
&arguments,
root,
target_dir_env.map(OsString::from),
)
.unwrap()
};
settle();
let first = resolve(None);
assert_eq!(first.workspace_root, root);
assert_eq!(probes(&log), 1);
assert_eq!(resolve(None), first);
assert_eq!(probes(&log), 1);
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.2.0\"\nedition = \"2021\"\n",
)
.unwrap();
settle();
assert_eq!(resolve(None), first);
assert_eq!(probes(&log), 2);
assert_eq!(resolve(None), first);
assert_eq!(probes(&log), 2);
std::fs::create_dir_all(root.join(".cargo")).unwrap();
std::fs::write(root.join(".cargo/config.toml"), "[build]\njobs = 2\n").unwrap();
settle();
assert_eq!(resolve(None), first);
assert_eq!(probes(&log), 3);
assert!(resolve(Some("elsewhere")).target_dir_requested);
assert_eq!(probes(&log), 4);
assert_eq!(resolve(None), first);
assert_eq!(probes(&log), 4);
let other_home = root.join("other-home");
let from_other_home = resolve_reported_from_home(
Some(cache.path()),
Some(&other_home),
cargo.as_os_str(),
&arguments,
root,
None,
)
.unwrap();
assert_eq!(from_other_home.workspace_root, first.workspace_root);
assert_eq!(probes(&log), 5);
std::fs::create_dir_all(&other_home).unwrap();
std::fs::write(other_home.join("config.toml"), "[build]\njobs = 4\n").unwrap();
resolve_reported_from_home(
Some(cache.path()),
Some(&other_home),
cargo.as_os_str(),
&arguments,
root,
None,
)
.unwrap();
assert_eq!(probes(&log), 6);
assert_eq!(resolve(None), first);
assert_eq!(probes(&log), 6);
std::fs::write(
root.join(".cargo/config.toml"),
"include = \"other.toml\"\n",
)
.unwrap();
settle();
assert_eq!(resolve(None).workspace_root, first.workspace_root);
assert_eq!(resolve(None).workspace_root, first.workspace_root);
assert_eq!(probes(&log), 8);
}
#[test]
#[cfg(unix)]
fn a_config_file_named_on_the_command_line_is_pinned() {
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let log = root.join("cargo.log");
let cargo = logging_cargo(root, root, &log);
let extra = root.join("extra.toml");
std::fs::write(&extra, "[build]\njobs = 2\n").unwrap();
let arguments = [
"build".to_string(),
"--config".to_string(),
extra.display().to_string(),
];
let resolve = || {
resolve_reported_in(
Some(cache.path()),
cargo.as_os_str(),
&arguments,
root,
None,
)
.unwrap()
};
settle();
let first = resolve();
assert_eq!(resolve(), first);
assert_eq!(probes(&log), 1);
std::fs::write(&extra, "[build]\njobs = 3\n").unwrap();
settle();
assert_eq!(resolve(), first);
assert_eq!(resolve(), first);
assert_eq!(probes(&log), 2);
std::fs::write(&extra, "include = \"more.toml\"\n").unwrap();
settle();
resolve();
resolve();
assert_eq!(probes(&log), 4);
}
#[test]
#[cfg(unix)]
fn a_probe_taken_within_a_timestamp_tick_of_a_write_is_not_remembered() {
use std::os::unix::fs::PermissionsExt as _;
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let log = root.join("cargo.log");
let extra = root.join("extra.toml");
let cargo = root.join("cargo");
std::fs::write(
&cargo,
format!(
"#!/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",
log = log.display(),
extra = extra.display(),
root = root.display(),
),
)
.unwrap();
std::fs::set_permissions(&cargo, std::fs::Permissions::from_mode(0o755)).unwrap();
let config = |target: &Path| format!("[build]\ntarget-dir = \"{}\"\n", target.display());
let (before, after) = (root.join("aaa"), root.join("bbb"));
let arguments = [
"build".to_string(),
"--config".to_string(),
extra.display().to_string(),
];
let resolve = || {
resolve_reported_in(
Some(cache.path()),
cargo.as_os_str(),
&arguments,
root,
None,
)
.unwrap()
.target_dir
};
let stamp = |at: SystemTime| {
std::fs::File::options()
.write(true)
.open(&extra)
.unwrap()
.set_modified(at)
.unwrap();
};
let ahead = SystemTime::now() + std::time::Duration::from_secs(3600);
settle();
std::fs::write(&extra, config(&before)).unwrap();
stamp(ahead);
assert_eq!(resolve(), before);
std::fs::write(&extra, config(&after)).unwrap();
stamp(ahead);
assert_eq!(resolve(), after);
assert_eq!(probes(&log), 2);
stamp(SystemTime::now() - std::time::Duration::from_secs(3600));
assert_eq!(resolve(), after);
assert_eq!(resolve(), after);
assert_eq!(probes(&log), 3);
}
#[test]
fn a_pin_is_racy_within_its_filesystems_timestamp_tick() {
let at = |seconds, nanos| SystemTime::UNIX_EPOCH + std::time::Duration::new(seconds, nanos);
let pin = |seconds, nanos| Pin {
path: PathBuf::new(),
state: Some((0, seconds, nanos)),
};
let now = at(1_000, 500_000_000);
assert!(pin(1_000, 495_000_000).is_racy(now));
assert!(!pin(1_000, 470_000_000).is_racy(now));
assert!(pin(1_000, 600_000_000).is_racy(now));
assert!(pin(999, 0).is_racy(now));
assert!(!pin(998, 0).is_racy(now));
assert!(
!Pin {
path: PathBuf::new(),
state: None,
}
.is_racy(now)
);
}
#[test]
#[cfg(unix)]
fn a_failed_probe_is_not_remembered() {
use std::os::unix::fs::PermissionsExt as _;
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let script = root.join("cargo");
std::fs::write(&script, "#!/bin/sh\nexit 1\n").unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let arguments = ["build".to_string()];
assert!(
resolve_reported_in(
Some(cache.path()),
script.as_os_str(),
&arguments,
root,
None
)
.is_none()
);
assert!(!cache.path().join("cargo-roots").exists());
}
#[test]
fn reported_resolution_requires_a_successful_metadata_probe() {
let directory = cargo_fixture();
assert!(
resolve_reported(
OsStr::new("cargo-that-does-not-exist"),
&["build".into()],
directory.path(),
None,
)
.is_none()
);
}
#[test]
fn missing_cargo_reports_the_io_error() {
let directory = tempfile::tempdir().unwrap();
let missing = directory.path().join("missing-cargo");
let error = try_resolve_reported(
missing.as_os_str(),
&["build".into()],
directory.path(),
None,
)
.unwrap_err();
assert!(matches!(error, ProbeFailure::Io(_)));
assert!(error.to_string().contains("I/O error"));
assert!(std::error::Error::source(&error).is_some());
}
#[test]
fn invalid_metadata_reports_the_json_error() {
let error = parse_metadata(b"not json").unwrap_err();
assert!(matches!(error, ProbeFailure::Json(_)));
assert!(error.to_string().contains("invalid cargo metadata JSON"));
assert!(std::error::Error::source(&error).is_some());
}
#[test]
fn metadata_requires_string_roots() {
for (metadata, field) in [
(r#"{}"#, "workspace_root"),
(r#"{"workspace_root":3}"#, "workspace_root"),
(r#"{"workspace_root":"/project"}"#, "target_directory"),
(
r#"{"workspace_root":"/project","target_directory":null}"#,
"target_directory",
),
] {
let error = parse_metadata(metadata.as_bytes()).unwrap_err();
assert!(matches!(error, ProbeFailure::Schema(name) if name == field));
assert!(
error
.to_string()
.contains(&format!("{field} missing or not a string"))
);
}
}
#[test]
fn stderr_tail_is_bounded_and_skips_empty_lines() {
assert_eq!(
stderr_tail(b"one\n\ntwo\nthree\nfour\n \nfive\nsix\n"),
"two\nthree\nfour\nfive\nsix"
);
let text = "é".repeat(2000);
let tail = stderr_tail(text.as_bytes());
assert!(tail.len() <= 1024);
assert!(text.ends_with(&tail));
assert_eq!(stderr_tail(b"\n \n"), "");
assert!(stderr_tail(b"invalid \xff").contains('�'));
}
#[test]
#[cfg(unix)]
fn unsuccessful_cargo_reports_status_and_stderr_without_caching_failure() {
use std::os::unix::fs::PermissionsExt as _;
let directory = cargo_fixture();
let root = directory.path();
let cache = tempfile::tempdir().unwrap();
let script = root.join("cargo");
let log = root.join("probes");
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();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
for _ in 0..2 {
let error = try_resolve_reported_from_home(
Some(cache.path()),
None,
script.as_os_str(),
&["build".into()],
root,
None,
)
.unwrap_err();
assert!(matches!(error, ProbeFailure::Exit { .. }));
let message = error.to_string();
assert!(message.contains("exit status: 17"), "{message}");
assert!(message.contains("component download failed"), "{message}");
assert!(!message.contains("old"), "{message}");
}
assert_eq!(probes(&log), 2);
assert!(!cache.path().join("cargo-roots").exists());
}
fn git(directory: &Path, arguments: &[&str]) {
let status = Command::new("git")
.arg("-C")
.arg(directory)
.args(["-c", "commit.gpgsign=false", "-c", "user.name=t"])
.args(["-c", "user.email=t@example.com"])
.args(arguments)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
assert!(status.success(), "git {arguments:?} failed");
}
#[test]
fn earlier_lockfile_states_are_offered_newest_first() {
let directory = tempfile::tempdir().unwrap();
let root = directory.path();
let command = Vec::new();
git(root, &["init", "-q"]);
let mut committed = Vec::new();
for state in ["one", "two", "three"] {
std::fs::write(root.join("Cargo.lock"), state).unwrap();
committed.push(build_identity(root, &command));
git(root, &["add", "Cargo.lock"]);
git(root, &["commit", "-q", "-m", state]);
}
committed.reverse();
std::fs::write(root.join("Cargo.lock"), "four").unwrap();
assert!(!committed.contains(&build_identity(root, &command)));
assert_eq!(previous_build_identities(root), committed);
git(root, &["commit", "-q", "-am", "four"]);
assert_eq!(previous_build_identities(root), committed);
}
#[test]
fn a_crlf_checkout_matches_the_lf_states_in_history() {
let directory = tempfile::tempdir().unwrap();
let root = directory.path();
let command = Vec::new();
git(root, &["init", "-q"]);
let mut committed = Vec::new();
for state in ["one\n", "two\n"] {
std::fs::write(root.join("Cargo.lock"), state).unwrap();
committed.push(build_identity(root, &command));
git(root, &["add", "Cargo.lock"]);
git(root, &["commit", "-q", "-m", state]);
}
std::fs::write(root.join("Cargo.lock"), "two\r\n").unwrap();
assert_eq!(build_identity(root, &command), committed[1]);
let raw = |state: &[u8]| identity_for_workspace(&CacheDigest::blake3(state).hash);
assert_eq!(
previous_build_identities(root),
vec![raw(b"two\r\n"), committed[0].clone(), raw(b"one\r\n")]
);
}
#[test]
fn crlf_states_in_history_also_offer_their_earlier_identity() {
let directory = tempfile::tempdir().unwrap();
let root = directory.path();
let command = Vec::new();
git(root, &["init", "-q"]);
git(root, &["config", "core.autocrlf", "false"]);
std::fs::write(root.join(".gitattributes"), "Cargo.lock -text\n").unwrap();
for state in ["one\r\n", "two\r\n"] {
std::fs::write(root.join("Cargo.lock"), state).unwrap();
git(root, &["add", "Cargo.lock"]);
git(root, &["commit", "-q", "-m", state]);
}
let raw = |state: &[u8]| identity_for_workspace(&CacheDigest::blake3(state).hash);
std::fs::write(root.join("Cargo.lock"), "one\n").unwrap();
let one = build_identity(root, &command);
std::fs::write(root.join("Cargo.lock"), "two\r\n").unwrap();
assert_eq!(
previous_build_identities(root),
vec![raw(b"two\r\n"), one, raw(b"one\r\n")]
);
}
#[test]
fn lockfile_markers_ignore_only_crlf_line_endings() {
assert_eq!(lockfile_marker(b"a\r\nb\r\n"), lockfile_marker(b"a\nb\n"));
assert_ne!(lockfile_marker(b"a\rb"), lockfile_marker(b"ab"));
assert_eq!(lockfile_marker(b"a\nb"), CacheDigest::blake3(b"a\nb").hash);
assert_eq!(crlf_checkout(b"a\nb\r\nc\n"), b"a\r\nb\r\nc\r\n");
}
#[test]
fn a_shallow_clone_offers_the_states_it_can_reach() {
let origin = tempfile::tempdir().unwrap();
let root = origin.path();
let command = Vec::new();
git(root, &["init", "-q"]);
let mut committed = Vec::new();
for state in ["one", "two", "three", "four"] {
std::fs::write(root.join("Cargo.lock"), state).unwrap();
committed.push(build_identity(root, &command));
git(root, &["add", "Cargo.lock"]);
git(root, &["commit", "-q", "-m", state]);
}
let clones = tempfile::tempdir().unwrap();
let shallow = clones.path().join("shallow");
git(
clones.path(),
&[
"clone",
"-q",
"--depth",
"2",
&format!("file://{}", root.display()),
shallow.to_str().unwrap(),
],
);
assert_eq!(
previous_build_identities(&shallow),
vec![committed[2].clone()]
);
}
#[test]
fn an_untracked_lockfile_has_no_history_to_borrow_from() {
let directory = tempfile::tempdir().unwrap();
let root = directory.path();
git(root, &["init", "-q"]);
std::fs::write(root.join("Cargo.lock"), "tracked").unwrap();
git(root, &["add", "Cargo.lock"]);
git(root, &["commit", "-q", "-m", "tracked"]);
git(root, &["rm", "-q", "Cargo.lock"]);
git(root, &["commit", "-q", "-m", "removed"]);
std::fs::write(root.join("Cargo.lock"), "generated").unwrap();
assert!(previous_build_identities(root).is_empty());
}
#[test]
fn a_workspace_outside_version_control_has_no_earlier_states() {
let directory = cargo_fixture();
std::fs::write(directory.path().join("Cargo.lock"), "one").unwrap();
assert!(previous_build_identities(directory.path()).is_empty());
std::fs::write(directory.path().join("Cargo.lock"), "one\r\n").unwrap();
assert_eq!(
previous_build_identities(directory.path()),
vec![identity_for_workspace(
&CacheDigest::blake3(b"one\r\n").hash
)]
);
}
}