use std::ffi::OsString;
use std::path::Path;
pub(crate) fn rustc_unit(
arguments: &[OsString],
out_dir: Option<&Path>,
) -> (Option<String>, Vec<String>) {
let mut unit = None;
let mut out_directory = None;
let mut dependencies = Vec::new();
let mut arguments = arguments.iter().filter_map(|argument| argument.to_str());
while let Some(argument) = arguments.next() {
let output = match argument {
"--out-dir" => arguments.next(),
_ => argument.strip_prefix("--out-dir="),
};
if let Some(output) = output {
out_directory = unit_directory_hash(Path::new(output));
continue;
}
let codegen = match argument {
"-C" | "--codegen" => arguments.next(),
_ => argument
.strip_prefix("-C")
.or_else(|| argument.strip_prefix("--codegen="))
.filter(|option| !option.is_empty()),
};
if let Some(hash) = codegen
.and_then(|option| option.strip_prefix("extra-filename="))
.map(|value| value.trim_start_matches('-'))
.filter(|hash| is_hash(hash))
{
unit = Some(hash.to_string());
continue;
}
let external = match argument {
"--extern" => arguments.next(),
_ => argument.strip_prefix("--extern="),
};
if let Some(hash) = external
.and_then(|value| value.split_once('=').map(|(_, path)| path))
.and_then(|path| artifact_hash(Path::new(path)))
{
dependencies.push(hash);
}
}
dependencies.extend(out_dir.and_then(build_script_run));
dependencies.sort();
dependencies.dedup();
(unit.or(out_directory), dependencies)
}
pub(crate) fn build_script_run_unit(
out_dir: Option<&Path>,
script: &Path,
) -> (Option<String>, Vec<String>) {
let unit = out_dir.and_then(build_script_run);
let compilation = script.parent().and_then(unit_directory_hash);
(unit, compilation.into_iter().collect())
}
fn build_script_run(out_dir: &Path) -> Option<String> {
if out_dir.file_name()? != "out" {
return None;
}
unit_directory_hash(out_dir).map(|hash| format!("run-{hash}"))
}
fn unit_directory_hash(directory: &Path) -> Option<String> {
let directory = if directory.file_name()? == "out" {
directory.parent()?
} else {
directory
};
let name = directory.file_name()?.to_str()?;
if is_hash(name)
&& directory
.parent()
.and_then(Path::parent)
.and_then(Path::file_name)
.is_some_and(|build| build == "build")
{
return Some(name.to_string());
}
directory_hash(name)
}
fn directory_hash(name: &str) -> Option<String> {
let (_, hash) = name.rsplit_once('-')?;
is_hash(hash).then(|| hash.to_string())
}
fn artifact_hash(path: &Path) -> Option<String> {
let extension = path.extension()?.to_str()?;
if !matches!(extension, "rlib" | "rmeta" | "so" | "dylib" | "dll") {
return None;
}
directory_hash(path.file_stem()?.to_str()?)
}
fn is_hash(value: &str) -> bool {
!value.is_empty() && value.bytes().all(|byte| byte.is_ascii_hexdigit())
}
#[cfg(test)]
mod tests {
use super::*;
fn arguments(values: &[&str]) -> Vec<OsString> {
values.iter().map(OsString::from).collect()
}
#[test]
fn a_rustc_unit_is_named_by_its_extra_filename_and_reads_its_externs() {
let (unit, dependencies) = rustc_unit(
&arguments(&[
"--crate-name",
"app",
"-C",
"metadata=0123",
"-C",
"extra-filename=-5d4c3b2a",
"--extern",
"engine=/t/debug/deps/libengine-aa11.rmeta",
"--extern=serde_derive=/t/debug/deps/libserde_derive-bb22.so",
"--extern",
"noprelude:alloc=/t/debug/deps/liballoc-cc33.rlib",
"--extern",
"proc_macro",
]),
Some(Path::new("/t/debug/build/app-dd44/out")),
);
assert_eq!(unit.as_deref(), Some("5d4c3b2a"));
assert_eq!(dependencies, ["aa11", "bb22", "cc33", "run-dd44"]);
}
#[test]
fn joined_codegen_spellings_are_read_too() {
assert_eq!(
rustc_unit(&arguments(&["-Cextra-filename=-ab12"]), None).0,
Some("ab12".into())
);
assert_eq!(
rustc_unit(&arguments(&["--codegen=extra-filename=-cd34"]), None).0,
Some("cd34".into())
);
}
#[test]
fn a_name_that_is_not_cargo_s_yields_no_identity() {
let (unit, dependencies) = rustc_unit(
&arguments(&[
"-C",
"extra-filename=custom",
"--extern",
"engine=/elsewhere/engine.rlib",
]),
Some(Path::new("/generated")),
);
assert_eq!(unit, None);
assert!(dependencies.is_empty());
}
#[test]
fn the_cargo_1_100_layout_is_read_too() {
let (unit, dependencies) = rustc_unit(
&arguments(&[
"--crate-name",
"build_script_build",
"--out-dir",
"/t/debug/build/api/b08ebe71fd39a343/out",
]),
None,
);
assert_eq!(unit.as_deref(), Some("b08ebe71fd39a343"));
assert!(dependencies.is_empty());
let (unit, dependencies) = rustc_unit(
&arguments(&[
"-C",
"extra-filename=-9d3b46aad32ac5c9",
"--out-dir",
"/t/debug/build/api/9d3b46aad32ac5c9/out",
"--extern",
"engine=/t/debug/build/engine/4bf0fb4dd59f0179/out/libengine-4bf0fb4dd59f0179.rmeta",
]),
Some(Path::new("/t/debug/build/api/ba26dbedf7b267d9/out")),
);
assert_eq!(unit.as_deref(), Some("9d3b46aad32ac5c9"));
assert_eq!(dependencies, ["4bf0fb4dd59f0179", "run-ba26dbedf7b267d9"]);
let (unit, dependencies) = build_script_run_unit(
Some(Path::new("/t/debug/build/api/ba26dbedf7b267d9/out")),
Path::new("/t/debug/build/api/b08ebe71fd39a343/out/build-script-build"),
);
assert_eq!(unit.as_deref(), Some("run-ba26dbedf7b267d9"));
assert_eq!(dependencies, ["b08ebe71fd39a343"]);
}
#[test]
fn a_shared_deps_directory_names_no_unit() {
assert_eq!(
rustc_unit(&arguments(&["--out-dir", "/t/debug/deps"]), None).0,
None
);
}
#[test]
fn a_build_script_run_depends_on_its_compilation() {
let (unit, dependencies) = build_script_run_unit(
Some(Path::new("/t/debug/build/ring-77ee/out")),
Path::new("/t/debug/build/ring-66ff/build-script-build"),
);
assert_eq!(unit.as_deref(), Some("run-77ee"));
assert_eq!(dependencies, ["66ff"]);
}
}