use std::path::Path;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PluginFlavor {
Gcc,
Llvm,
Unknown,
}
impl PluginFlavor {
#[must_use]
pub fn detect(path: &Path, options: &[String]) -> Self {
let name = path
.file_name()
.map(|name| name.to_string_lossy().to_ascii_lowercase())
.unwrap_or_default();
if name.contains("lto_plugin") || name.contains("lto-plugin") {
return Self::Gcc;
}
if name.contains("llvmgold") {
return Self::Llvm;
}
let gcc = options.iter().any(|option| {
option.starts_with("-fresolution=") || option.starts_with("-pass-through=")
});
if gcc {
return Self::Gcc;
}
let llvm = options.iter().any(|option| {
matches!(
classify(Self::Llvm, option),
PluginOption::OptLevel(_)
| PluginOption::Cpu(_)
| PluginOption::ThinLto
| PluginOption::Jobs(_)
| PluginOption::CacheDir(_)
)
});
if llvm { Self::Llvm } else { Self::Unknown }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PluginOption<'a> {
LtoWrapper(&'a str),
ResolutionFile(&'a str),
PassThrough(&'a str),
LtransObjects(&'a str),
SaveTemps,
OptLevel(u8),
Cpu(&'a str),
ThinLto,
Jobs(&'a str),
CacheDir(&'a str),
CachePolicy(&'a str),
ObjPath(&'a str),
ExtraLibraryPath(&'a str),
ThinLtoIndexOnly(Option<&'a str>),
EmitLlvm,
EmitAsm,
DisableOutput,
Other(&'a str),
}
impl PluginOption<'_> {
#[must_use]
pub const fn exits_process(&self) -> bool {
matches!(
self,
Self::ThinLtoIndexOnly(_) | Self::EmitLlvm | Self::EmitAsm | Self::DisableOutput
)
}
}
#[must_use]
pub fn classify(flavor: PluginFlavor, option: &str) -> PluginOption<'_> {
match flavor {
PluginFlavor::Gcc => classify_gcc(option),
PluginFlavor::Llvm => classify_llvm(option),
PluginFlavor::Unknown if option.starts_with('-') => classify_gcc(option),
PluginFlavor::Unknown => match classify_llvm(option) {
PluginOption::Other(_) => PluginOption::Other(option),
known => known,
},
}
}
fn classify_gcc(option: &str) -> PluginOption<'_> {
if let Some(file) = option.strip_prefix("-fresolution=") {
PluginOption::ResolutionFile(file)
} else if let Some(arg) = option.strip_prefix("-pass-through=") {
PluginOption::PassThrough(arg)
} else if let Some(file) = option.strip_prefix("-ltrans-objects=") {
PluginOption::LtransObjects(file)
} else if option == "-save-temps" {
PluginOption::SaveTemps
} else if !option.starts_with('-') && !option.is_empty() {
PluginOption::LtoWrapper(option)
} else {
PluginOption::Other(option)
}
}
fn classify_llvm(option: &str) -> PluginOption<'_> {
type Make = fn(&str) -> PluginOption<'_>;
let prefixed: [(&str, Make); 7] = [
("mcpu=", |value| PluginOption::Cpu(value)),
("jobs=", |value| PluginOption::Jobs(value)),
("cache-dir=", |value| PluginOption::CacheDir(value)),
("cache-policy=", |value| PluginOption::CachePolicy(value)),
("obj-path=", |value| PluginOption::ObjPath(value)),
("extra-library-path=", |value| {
PluginOption::ExtraLibraryPath(value)
}),
("thinlto-index-only=", |value| {
PluginOption::ThinLtoIndexOnly(Some(value))
}),
];
for (prefix, make) in prefixed {
if let Some(value) = option.strip_prefix(prefix) {
return make(value);
}
}
match option.as_bytes() {
[b'O', level @ b'0'..=b'3'] => PluginOption::OptLevel(level - b'0'),
b"thinlto" => PluginOption::ThinLto,
b"thinlto-index-only" => PluginOption::ThinLtoIndexOnly(None),
b"emit-llvm" => PluginOption::EmitLlvm,
b"emit-asm" => PluginOption::EmitAsm,
b"disable-output" => PluginOption::DisableOutput,
b"save-temps" => PluginOption::SaveTemps,
_ => PluginOption::Other(option),
}
}
#[must_use]
pub fn required_environment(flavor: PluginFlavor) -> &'static [&'static str] {
match flavor {
PluginFlavor::Gcc => &["COLLECT_GCC", "COLLECT_GCC_OPTIONS"],
PluginFlavor::Llvm | PluginFlavor::Unknown => &[],
}
}
pub fn missing_environment(
flavor: PluginFlavor,
options: &[String],
lookup: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> Vec<&'static str> {
if flavor == PluginFlavor::Gcc
&& options.iter().any(|option| {
option == "-nop" || matches!(classify(flavor, option), PluginOption::LtransObjects(_))
})
{
return Vec::new();
}
required_environment(flavor)
.iter()
.copied()
.filter(|name| lookup(name).is_none_or(|value| value.is_empty()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|&v| v.to_owned()).collect()
}
#[test]
fn gcc_forms() {
let flavor = PluginFlavor::Gcc;
assert_eq!(
classify(
flavor,
"/usr/libexec/gcc/x86_64-pc-linux-gnu/15/lto-wrapper"
),
PluginOption::LtoWrapper("/usr/libexec/gcc/x86_64-pc-linux-gnu/15/lto-wrapper")
);
assert_eq!(
classify(flavor, "-fresolution=/tmp/cc1.res"),
PluginOption::ResolutionFile("/tmp/cc1.res")
);
assert_eq!(
classify(flavor, "-pass-through=-lgcc_s"),
PluginOption::PassThrough("-lgcc_s")
);
assert_eq!(
classify(flavor, "-linker-output-known"),
PluginOption::Other("-linker-output-known")
);
assert_eq!(classify(flavor, "-save-temps"), PluginOption::SaveTemps);
}
#[test]
fn llvm_forms() {
let flavor = PluginFlavor::Llvm;
assert_eq!(classify(flavor, "O2"), PluginOption::OptLevel(2));
assert_eq!(classify(flavor, "O9"), PluginOption::Other("O9"));
assert_eq!(classify(flavor, "mcpu=znver3"), PluginOption::Cpu("znver3"));
assert_eq!(classify(flavor, "thinlto"), PluginOption::ThinLto);
assert_eq!(classify(flavor, "jobs=8"), PluginOption::Jobs("8"));
assert_eq!(
classify(flavor, "cache-dir=/tmp/c"),
PluginOption::CacheDir("/tmp/c")
);
assert_eq!(
classify(flavor, "thinlto-index-only=list.txt"),
PluginOption::ThinLtoIndexOnly(Some("list.txt"))
);
assert!(classify(flavor, "thinlto-index-only").exits_process());
assert!(classify(flavor, "emit-llvm").exits_process());
assert!(!classify(flavor, "O3").exits_process());
assert_eq!(
classify(flavor, "-generate-arange-section"),
PluginOption::Other("-generate-arange-section")
);
}
#[test]
fn flavor_detection() {
assert_eq!(
PluginFlavor::detect(Path::new("/usr/lib/llvm/22/lib64/LLVMgold.so"), &[]),
PluginFlavor::Llvm
);
assert_eq!(
PluginFlavor::detect(Path::new("/usr/libexec/gcc/x/15/liblto_plugin.so"), &[]),
PluginFlavor::Gcc
);
assert_eq!(
PluginFlavor::detect(Path::new("p.so"), &strings(&["-fresolution=x.res"])),
PluginFlavor::Gcc
);
assert_eq!(
PluginFlavor::detect(Path::new("p.so"), &strings(&["O2", "thinlto"])),
PluginFlavor::Llvm
);
assert_eq!(
PluginFlavor::detect(Path::new("p.so"), &[]),
PluginFlavor::Unknown
);
}
#[test]
fn gcc_environment() {
let flavor = PluginFlavor::Gcc;
let none = |_: &str| None;
assert_eq!(
missing_environment(flavor, &[], none),
["COLLECT_GCC", "COLLECT_GCC_OPTIONS"]
);
let some = |name: &str| (name == "COLLECT_GCC").then(|| "gcc".into());
assert_eq!(
missing_environment(flavor, &[], some),
["COLLECT_GCC_OPTIONS"]
);
assert!(missing_environment(flavor, &strings(&["-nop"]), none).is_empty());
assert!(missing_environment(PluginFlavor::Llvm, &[], none).is_empty());
}
}