use zccache_core::NormalizedPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiverFamily {
Ar,
LlvmAr,
MsvcLib,
}
#[derive(Debug, Clone)]
pub enum ParsedArchiveInvocation {
Cacheable(CacheableArchive),
NonCacheable {
reason: String,
},
}
#[derive(Debug, Clone)]
pub struct CacheableArchive {
pub tool: NormalizedPath,
pub family: ArchiverFamily,
pub input_files: Vec<NormalizedPath>,
pub output_file: NormalizedPath,
pub cache_relevant_flags: Vec<String>,
pub original_args: Vec<String>,
pub non_deterministic: bool,
}
#[must_use]
pub fn is_archiver(tool: &str) -> bool {
detect_family(tool).is_some()
}
fn detect_family(tool: &str) -> Option<ArchiverFamily> {
let name = std::path::Path::new(tool)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or(tool);
if name.eq_ignore_ascii_case("lib") {
return Some(ArchiverFamily::MsvcLib);
}
if name.starts_with("llvm-ar") || name.starts_with("llvm_ar") {
return Some(ArchiverFamily::LlvmAr);
}
if name == "ar" || name.ends_with("-ar") {
return Some(ArchiverFamily::Ar);
}
None
}
#[must_use]
pub fn parse_archive_invocation(tool: &str, args: &[String]) -> ParsedArchiveInvocation {
let family = match detect_family(tool) {
Some(f) => f,
None => {
return ParsedArchiveInvocation::NonCacheable {
reason: format!("not a recognized archiver: {tool}"),
};
}
};
match family {
ArchiverFamily::MsvcLib => parse_msvc_lib(tool, args),
ArchiverFamily::Ar | ArchiverFamily::LlvmAr => parse_gnu_ar(tool, family, args),
}
}
fn parse_gnu_ar(tool: &str, family: ArchiverFamily, args: &[String]) -> ParsedArchiveInvocation {
if args.is_empty() {
return ParsedArchiveInvocation::NonCacheable {
reason: "no arguments".to_string(),
};
}
let mut op_idx = 0;
let mut long_flags = Vec::new();
while op_idx < args.len() && args[op_idx].starts_with("--") {
long_flags.push(args[op_idx].clone());
op_idx += 1;
if op_idx < args.len()
&& !args[op_idx].starts_with('-')
&& matches!(
long_flags.last().map(|s| s.as_str()),
Some("--plugin" | "--target")
)
{
long_flags.push(args[op_idx].clone());
op_idx += 1;
}
}
if op_idx >= args.len() {
return ParsedArchiveInvocation::NonCacheable {
reason: "no operation specified".to_string(),
};
}
let op_str = args[op_idx].strip_prefix('-').unwrap_or(&args[op_idx]);
if op_str.contains('x') {
return ParsedArchiveInvocation::NonCacheable {
reason: "extract operation (x) not cacheable".to_string(),
};
}
if op_str.contains('t') {
return ParsedArchiveInvocation::NonCacheable {
reason: "list operation (t) not cacheable".to_string(),
};
}
if op_str.contains('d') {
return ParsedArchiveInvocation::NonCacheable {
reason: "delete operation (d) not cacheable".to_string(),
};
}
if op_str.contains('p') {
return ParsedArchiveInvocation::NonCacheable {
reason: "print operation (p) not cacheable".to_string(),
};
}
if !op_str.contains('r') && !op_str.contains('q') {
return ParsedArchiveInvocation::NonCacheable {
reason: format!("unsupported operation: {op_str}"),
};
}
let non_deterministic = !op_str.contains('D');
let has_relpos = op_str.contains('a') || op_str.contains('b') || op_str.contains('i');
let has_count = op_str.contains('N');
let mut pos = op_idx + 1;
if has_relpos {
pos += 1;
}
if has_count {
pos += 1;
}
if pos >= args.len() {
return ParsedArchiveInvocation::NonCacheable {
reason: "no archive file specified".to_string(),
};
}
let output_file = NormalizedPath::new(&args[pos]);
pos += 1;
let input_files: Vec<NormalizedPath> = args[pos..].iter().map(NormalizedPath::from).collect();
if input_files.is_empty() {
return ParsedArchiveInvocation::NonCacheable {
reason: "no input files specified".to_string(),
};
}
let mut cache_relevant_flags = vec![op_str.to_string()];
cache_relevant_flags.extend(long_flags);
ParsedArchiveInvocation::Cacheable(CacheableArchive {
tool: NormalizedPath::new(tool),
family,
input_files,
output_file,
cache_relevant_flags,
original_args: args.to_vec(),
non_deterministic,
})
}
fn parse_msvc_lib(tool: &str, args: &[String]) -> ParsedArchiveInvocation {
if args.is_empty() {
return ParsedArchiveInvocation::NonCacheable {
reason: "no arguments".to_string(),
};
}
let mut output_file: Option<NormalizedPath> = None;
let mut input_files: Vec<NormalizedPath> = Vec::new();
let mut cache_relevant_flags: Vec<String> = Vec::new();
let mut is_extract = false;
let mut has_brepro = false;
let mut has_list = false;
for arg in args {
let upper = arg.to_uppercase();
if upper.starts_with("/EXTRACT:") || upper.starts_with("-EXTRACT:") {
is_extract = true;
break;
}
if upper == "/LIST" || upper == "-LIST" {
has_list = true;
}
if upper.starts_with("/OUT:") || upper.starts_with("-OUT:") {
output_file = Some(NormalizedPath::new(&arg[5..]));
continue;
}
if upper == "/BREPRO" || upper == "-BREPRO" {
has_brepro = true;
cache_relevant_flags.push(arg.clone());
continue;
}
if arg.starts_with('/') || arg.starts_with('-') {
cache_relevant_flags.push(arg.clone());
continue;
}
input_files.push(NormalizedPath::new(arg));
}
if is_extract {
return ParsedArchiveInvocation::NonCacheable {
reason: "extract operation (/EXTRACT) not cacheable".to_string(),
};
}
if has_list {
return ParsedArchiveInvocation::NonCacheable {
reason: "list operation (/LIST) not cacheable".to_string(),
};
}
if input_files.is_empty() {
return ParsedArchiveInvocation::NonCacheable {
reason: "no input files specified".to_string(),
};
}
let output_file = output_file.unwrap_or_else(|| {
let first = &input_files[0];
NormalizedPath::new(first.with_extension("lib"))
});
ParsedArchiveInvocation::Cacheable(CacheableArchive {
tool: NormalizedPath::new(tool),
family: ArchiverFamily::MsvcLib,
input_files,
output_file,
cache_relevant_flags,
original_args: args.to_vec(),
non_deterministic: !has_brepro,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn args(s: &[&str]) -> Vec<String> {
s.iter().map(|x| x.to_string()).collect()
}
#[test]
fn detect_gnu_ar() {
assert_eq!(detect_family("ar"), Some(ArchiverFamily::Ar));
assert_eq!(detect_family("/usr/bin/ar"), Some(ArchiverFamily::Ar));
assert_eq!(
detect_family("x86_64-linux-gnu-ar"),
Some(ArchiverFamily::Ar)
);
assert_eq!(
detect_family("aarch64-linux-gnu-ar"),
Some(ArchiverFamily::Ar)
);
}
#[test]
fn detect_llvm_ar() {
assert_eq!(detect_family("llvm-ar"), Some(ArchiverFamily::LlvmAr));
assert_eq!(detect_family("llvm-ar-15"), Some(ArchiverFamily::LlvmAr));
assert_eq!(
detect_family("/usr/bin/llvm-ar"),
Some(ArchiverFamily::LlvmAr)
);
}
#[test]
fn detect_msvc_lib() {
assert_eq!(detect_family("lib"), Some(ArchiverFamily::MsvcLib));
assert_eq!(detect_family("lib.exe"), Some(ArchiverFamily::MsvcLib));
assert_eq!(detect_family("LIB"), Some(ArchiverFamily::MsvcLib));
assert_eq!(detect_family("LIB.EXE"), Some(ArchiverFamily::MsvcLib));
}
#[test]
fn detect_unknown_tool() {
assert_eq!(detect_family("gcc"), None);
assert_eq!(detect_family("clang"), None);
assert_eq!(detect_family("ld"), None);
assert_eq!(detect_family("lzma"), None);
}
#[test]
fn is_archiver_works() {
assert!(is_archiver("ar"));
assert!(is_archiver("llvm-ar"));
assert!(is_archiver("lib.exe"));
assert!(!is_archiver("gcc"));
assert!(!is_archiver("ld"));
}
#[test]
fn basic_ar_rcs() {
let result = parse_archive_invocation("ar", &args(&["rcs", "libfoo.a", "a.o", "b.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.family, ArchiverFamily::Ar);
assert_eq!(c.output_file, NormalizedPath::new("libfoo.a"));
assert_eq!(c.input_files.len(), 2);
assert_eq!(c.input_files[0], NormalizedPath::new("a.o"));
assert_eq!(c.input_files[1], NormalizedPath::new("b.o"));
assert!(c.non_deterministic); }
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ar_with_dash_prefix() {
let result = parse_archive_invocation("ar", &args(&["-rcs", "libfoo.a", "a.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("libfoo.a"));
assert_eq!(c.input_files, vec![NormalizedPath::new("a.o")]);
assert_eq!(c.cache_relevant_flags, vec!["rcs"]);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ar_deterministic_flag() {
let result = parse_archive_invocation("ar", &args(&["rcsD", "libfoo.a", "a.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert!(!c.non_deterministic); }
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ar_extract_non_cacheable() {
let result = parse_archive_invocation("ar", &args(&["x", "libfoo.a"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn ar_list_non_cacheable() {
let result = parse_archive_invocation("ar", &args(&["t", "libfoo.a"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn ar_delete_non_cacheable() {
let result = parse_archive_invocation("ar", &args(&["d", "libfoo.a", "old.o"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn ar_print_non_cacheable() {
let result = parse_archive_invocation("ar", &args(&["p", "libfoo.a"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn ar_no_args() {
let result = parse_archive_invocation("ar", &args(&[]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn ar_no_inputs() {
let result = parse_archive_invocation("ar", &args(&["rcs", "libfoo.a"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn ar_quick_append() {
let result =
parse_archive_invocation("ar", &args(&["qcs", "libfoo.a", "a.o", "b.o", "c.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.input_files.len(), 3);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ar_preserves_input_order() {
let result =
parse_archive_invocation("ar", &args(&["rcs", "libfoo.a", "z.o", "a.o", "m.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.input_files[0], NormalizedPath::new("z.o"));
assert_eq!(c.input_files[1], NormalizedPath::new("a.o"));
assert_eq!(c.input_files[2], NormalizedPath::new("m.o"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ar_with_relpos_modifier() {
let result =
parse_archive_invocation("ar", &args(&["rcsb", "existing.o", "libfoo.a", "new.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("libfoo.a"));
assert_eq!(c.input_files, vec![NormalizedPath::new("new.o")]);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn ar_with_long_options() {
let result = parse_archive_invocation(
"ar",
&args(&["--plugin", "liblto_plugin.so", "rcs", "libfoo.a", "a.o"]),
);
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("libfoo.a"));
assert_eq!(c.input_files, vec![NormalizedPath::new("a.o")]);
assert!(c.cache_relevant_flags.contains(&"--plugin".to_string()));
assert!(c
.cache_relevant_flags
.contains(&"liblto_plugin.so".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn llvm_ar_basic() {
let result = parse_archive_invocation("llvm-ar", &args(&["rcs", "libfoo.a", "a.o", "b.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.family, ArchiverFamily::LlvmAr);
assert_eq!(c.output_file, NormalizedPath::new("libfoo.a"));
assert_eq!(c.input_files.len(), 2);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn cross_compile_ar() {
let result =
parse_archive_invocation("x86_64-linux-gnu-ar", &args(&["rcs", "libfoo.a", "a.o"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.family, ArchiverFamily::Ar);
assert_eq!(c.tool, NormalizedPath::new("x86_64-linux-gnu-ar"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn basic_msvc_lib() {
let result =
parse_archive_invocation("lib.exe", &args(&["/OUT:foo.lib", "a.obj", "b.obj"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.family, ArchiverFamily::MsvcLib);
assert_eq!(c.output_file, NormalizedPath::new("foo.lib"));
assert_eq!(c.input_files.len(), 2);
assert_eq!(c.input_files[0], NormalizedPath::new("a.obj"));
assert_eq!(c.input_files[1], NormalizedPath::new("b.obj"));
assert!(c.non_deterministic); }
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn msvc_lib_with_brepro() {
let result =
parse_archive_invocation("lib.exe", &args(&["/BREPRO", "/OUT:foo.lib", "a.obj"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert!(!c.non_deterministic); }
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn msvc_lib_extract_non_cacheable() {
let result =
parse_archive_invocation("lib.exe", &args(&["/EXTRACT:member.obj", "foo.lib"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn msvc_lib_list_non_cacheable() {
let result = parse_archive_invocation("lib.exe", &args(&["/LIST", "foo.lib"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn msvc_lib_default_output_name() {
let result = parse_archive_invocation("lib.exe", &args(&["a.obj", "b.obj"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("a.lib"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn msvc_lib_no_inputs() {
let result = parse_archive_invocation("lib.exe", &args(&["/OUT:foo.lib"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
#[test]
fn msvc_lib_with_flags() {
let result = parse_archive_invocation(
"lib.exe",
&args(&["/NOLOGO", "/MACHINE:X64", "/OUT:foo.lib", "a.obj"]),
);
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert!(c.cache_relevant_flags.contains(&"/NOLOGO".to_string()));
assert!(c.cache_relevant_flags.contains(&"/MACHINE:X64".to_string()));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn msvc_lib_preserves_input_order() {
let result = parse_archive_invocation(
"lib.exe",
&args(&["/OUT:foo.lib", "z.obj", "a.obj", "m.obj"]),
);
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.input_files[0], NormalizedPath::new("z.obj"));
assert_eq!(c.input_files[1], NormalizedPath::new("a.obj"));
assert_eq!(c.input_files[2], NormalizedPath::new("m.obj"));
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn msvc_lib_dash_syntax() {
let result =
parse_archive_invocation("lib.exe", &args(&["-OUT:foo.lib", "-BREPRO", "a.obj"]));
match result {
ParsedArchiveInvocation::Cacheable(c) => {
assert_eq!(c.output_file, NormalizedPath::new("foo.lib"));
assert!(!c.non_deterministic);
}
other => panic!("expected cacheable, got: {other:?}"),
}
}
#[test]
fn unknown_tool_non_cacheable() {
let result = parse_archive_invocation("gcc", &args(&["-c", "foo.c"]));
assert!(matches!(
result,
ParsedArchiveInvocation::NonCacheable { .. }
));
}
}