use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
pub struct Plan {
pub key: String,
pub store: PathBuf,
pub target_dir: PathBuf,
pub nightly: bool,
pub profile_dir: String,
pub toolchain: String,
pub lock_contents: String,
pub lineage: String,
}
impl Plan {
pub fn resolve(args: &[String]) -> Result<Self, String> {
let manifest_dir = workspace_root()?;
let lock = manifest_dir.join("Cargo.lock");
let rustc = tool_version("rustc")?;
let cargo = tool_version("cargo")?;
let mut shared = String::new();
let _ = writeln!(shared, "rustc:{rustc}\ncargo:{cargo}");
let _ = writeln!(shared, "root:{}", manifest_dir.display());
for arg in args.iter().filter(|a| !is_irrelevant(a)) {
let _ = writeln!(shared, "arg:{arg}");
}
for var in [
"RUSTFLAGS",
"RUSTDOCFLAGS",
"CARGO_ENCODED_RUSTFLAGS",
"CARGO_PROFILE",
] {
if let Some(v) = env::var_os(var) {
let _ = writeln!(shared, "{var}:{}", v.to_string_lossy());
}
}
let lock_contents = fs::read_to_string(&lock)
.map_err(|e| format!("cannot read {}: {e}", lock.display()))?;
let lineage = format!("{:016x}", hash(shared.as_bytes()));
let key = format!(
"{:016x}",
hash(format!("{lineage}\nlock:{:016x}", hash(lock_contents.as_bytes())).as_bytes())
);
Ok(Self {
key,
lineage,
profile_dir: profile_dir(args),
toolchain: format!(
"{rustc}|{cargo}|{}",
["RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS"]
.iter()
.filter_map(|v| env::var(v).ok())
.collect::<Vec<_>>()
.join(" ")
),
lock_contents,
store: store_dir(),
target_dir: manifest_dir.join("target"),
nightly: rustc.contains("nightly") || rustc.contains("dev"),
})
}
pub fn snapshot(&self) -> PathBuf {
self.store.join(&self.key[..2]).join(&self.key)
}
}
fn profile_dir(args: &[String]) -> String {
let mut args = args.iter();
while let Some(arg) = args.next() {
if arg == "--release" || arg == "-r" {
return "release".into();
}
let named = if arg == "--profile" {
args.next().cloned()
} else {
arg.strip_prefix("--profile=").map(str::to_owned)
};
if let Some(profile) = named {
return match profile.as_str() {
"dev" | "test" => "debug".into(),
"bench" => "release".into(),
other => other.into(),
};
}
}
"debug".into()
}
fn is_irrelevant(arg: &str) -> bool {
const OUTPUT_ONLY: &[&str] = &[
"-q",
"--quiet",
"-v",
"--verbose",
"-vv",
"--color",
"--message-format",
"--timings",
];
arg.contains('/')
|| arg.starts_with("--target-dir")
|| arg.starts_with("--manifest-path")
|| OUTPUT_ONLY
.iter()
.any(|f| arg == *f || arg.starts_with(&format!("{f}=")))
}
fn workspace_root() -> Result<PathBuf, String> {
let mut dir = env::current_dir().map_err(|e| e.to_string())?;
loop {
if dir.join("Cargo.lock").is_file() {
return Ok(dir);
}
if !dir.pop() {
return Err("no Cargo.lock in this directory or any parent".into());
}
}
}
fn tool_version(tool: &str) -> Result<String, String> {
let out = Command::new(tool)
.arg("-vV")
.output()
.map_err(|e| format!("cannot run {tool}: {e}"))?;
if !out.status.success() {
return Err(format!("{tool} -vV failed"));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
}
pub fn store_dir() -> PathBuf {
if let Some(dir) = env::var_os("CARGO_TURBO_DIR") {
return PathBuf::from(dir);
}
let home = env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(env::temp_dir);
home.join(".cache").join("cargo-turbo")
}
pub fn hash(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in bytes {
h ^= *b as u64;
h = h.wrapping_mul(0x1000_0000_01b3);
}
h
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn locations_do_not_enter_the_key() {
assert!(is_irrelevant("--target-dir=/tmp/x"));
assert!(is_irrelevant("/abs/path"));
assert!(is_irrelevant("--manifest-path=Cargo.toml"));
}
#[test]
fn verbosity_does_not_enter_the_key() {
for flag in [
"-q",
"--quiet",
"-v",
"--verbose",
"--color=never",
"--message-format=json",
] {
assert!(is_irrelevant(flag), "{flag} should not affect the key");
}
}
#[test]
fn anything_that_changes_the_build_does_enter_the_key() {
for flag in [
"check",
"build",
"--workspace",
"--release",
"--all-features",
"-p",
"test",
] {
assert!(!is_irrelevant(flag), "{flag} must affect the key");
}
}
#[test]
fn the_profile_directory_matches_where_cargo_writes() {
assert_eq!(profile_dir(&["check".into()]), "debug");
assert_eq!(
profile_dir(&["build".into(), "--release".into()]),
"release"
);
assert_eq!(profile_dir(&["build".into(), "-r".into()]), "release");
assert_eq!(
profile_dir(&["build".into(), "--profile=fast".into()]),
"fast"
);
assert_eq!(
profile_dir(&["build".into(), "--profile".into(), "fast".into()]),
"fast"
);
assert_eq!(
profile_dir(&["test".into(), "--profile".into(), "test".into()]),
"debug"
);
assert_eq!(
profile_dir(&["bench".into(), "--profile".into(), "bench".into()]),
"release"
);
}
#[test]
fn hashing_is_stable_and_distinguishes_inputs() {
assert_eq!(hash(b"cargo turbo"), hash(b"cargo turbo"));
assert_ne!(hash(b"check --workspace"), hash(b"check"));
assert_ne!(hash(b"rustc 1.99.0"), hash(b"rustc 1.99.1"));
}
#[test]
fn snapshot_paths_fan_out_by_key_prefix() {
let plan = Plan {
key: "abcdef0123456789".into(),
lineage: "0000000000000000".into(),
profile_dir: "debug".into(),
toolchain: "test".into(),
lock_contents: String::new(),
store: PathBuf::from("/store"),
target_dir: PathBuf::from("/t"),
nightly: true,
};
assert_eq!(plan.snapshot(), PathBuf::from("/store/ab/abcdef0123456789"));
}
}