use crate::toolchain;
use std::{
env,
ffi::OsString,
path::{Path, PathBuf},
process::Command,
};
pub(crate) const TARGET_MARKER: &str = "CARGO_CFG_CARGO_LIBC_PROFILE";
pub(crate) const HOST_CC_ENV: &str = "CARGO_LIBC_HOST_CC";
pub(crate) const HOST_CXX_ENV: &str = "CARGO_LIBC_HOST_CXX";
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Language {
C,
Cxx,
}
fn compiler_variables(base: &str) -> [String; 4] {
let triple = toolchain::TARGET;
[
format!("{base}_{triple}"),
format!("{base}_{}", triple.replace('-', "_")),
format!("HOST_{base}"),
base.to_string(),
]
}
pub(crate) fn variables(
cc: &Path,
cxx: &Path,
shell_names: bool,
) -> Result<Vec<(String, OsString)>, String> {
let mut variables = Vec::new();
for (base, wrapper, host_env, default) in [
("CC", cc, HOST_CC_ENV, "cc"),
("CXX", cxx, HOST_CXX_ENV, "c++"),
] {
let [hyphen, underscore, host, plain] = compiler_variables(base);
for key in [&hyphen, &underscore] {
if env::var_os(key).is_some_and(|value| !value.is_empty()) {
return Err(format!("{key} is already set; cargo-libc selects the target C compiler itself, so use a shell without that override"));
}
}
let host_compiler = [&host, &plain]
.into_iter()
.find_map(|key| env::var_os(key).filter(|value| !value.is_empty()))
.unwrap_or_else(|| default.into());
variables.push((host_env.to_string(), host_compiler));
let key = if shell_names { underscore } else { hyphen };
variables.push((key, wrapper.as_os_str().to_owned()));
}
Ok(variables)
}
pub(crate) fn set_environment(cmd: &mut Command, cc: &Path, cxx: &Path) -> Result<(), String> {
cmd.envs(variables(cc, cxx, false)?);
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum Action {
Query,
Compile,
Link {
shared: bool,
},
Relocatable,
}
const SEPARATE_VALUE: &[&str] = &[
"-o",
"-I",
"-L",
"-D",
"-U",
"-include",
"-imacros",
"-isystem",
"-iquote",
"-idirafter",
"-iprefix",
"-isysroot",
"-x",
"-MF",
"-MT",
"-MQ",
"-Xlinker",
"-Xassembler",
"-Xpreprocessor",
"-Xclang",
"-target",
"-T",
"-z",
"-u",
"-e",
"-arch",
];
pub(crate) fn classify(args: &[OsString]) -> Result<Action, String> {
let mut inputs = 0;
let mut shared = false;
let mut relocatable = false;
let mut compile = false;
let mut i = 0;
while i < args.len() {
let arg = args[i].to_string_lossy();
match arg.as_ref() {
"-E" | "-M" | "-MM" | "--version" | "-dumpversion" | "-dumpfullversion"
| "-dumpmachine" | "-###" => return Ok(Action::Query),
"-c" | "-S" | "-fsyntax-only" => compile = true,
"-static" | "-static-pie" => {
return Err(
"static linking is not supported: providers have no static glibc".into(),
)
}
"-shared" => shared = true,
"-r" => relocatable = true,
_ if arg.starts_with("-print-") || arg.starts_with("--print-") => {
return Ok(Action::Query)
}
_ if SEPARATE_VALUE.contains(&arg.as_ref()) => i += 1,
_ if arg == "-" || !arg.starts_with('-') => inputs += 1,
_ => {}
}
i += 1;
}
Ok(if inputs == 0 {
Action::Query
} else if compile {
Action::Compile
} else if relocatable {
Action::Relocatable
} else {
Action::Link { shared }
})
}
fn is_cxx(args: &[OsString]) -> bool {
let mut language_cxx = false;
let mut i = 0;
while i < args.len() {
let arg = args[i].to_string_lossy();
i += 1;
let language = if arg == "-x" {
i += 1;
Some(
args.get(i - 1)
.map_or(String::new(), |v| v.to_string_lossy().into_owned()),
)
} else {
arg.strip_prefix("-x").map(str::to_string)
};
if let Some(language) = language {
language_cxx = language.starts_with("c++");
continue;
}
if SEPARATE_VALUE.contains(&arg.as_ref()) {
i += 1;
continue;
}
if arg.starts_with('-') && arg != "-" {
continue;
}
let ext = Path::new(arg.as_ref())
.extension()
.map(|e| e.to_string_lossy().into_owned());
if language_cxx
|| matches!(
ext.as_deref(),
Some("cc" | "cp" | "cpp" | "cxx" | "CPP" | "c++" | "C" | "ii" | "cppm" | "ixx")
)
{
return true;
}
}
false
}
pub(crate) fn command(language: Language, args: Vec<OsString>) -> Result<Command, String> {
if env::var_os(TARGET_MARKER).is_none() {
let key = match language {
Language::C => HOST_CC_ENV,
Language::Cxx => HOST_CXX_ENV,
};
let host = env::var_os(key)
.ok_or_else(|| format!("{key} is missing; this wrapper is launched by cargo libc"))?;
let text = host.to_string_lossy().into_owned();
let mut words = text.split_whitespace();
let program = words.next().ok_or(format!("{key} is empty"))?;
let mut cmd = Command::new(program);
cmd.args(words).args(args);
return Ok(cmd);
}
let action = classify(&args)?;
if action != Action::Query && (language == Language::Cxx || is_cxx(&args)) {
return Err("C++ for the target is not supported yet: providers have no target C++ runtime. Build only C for the target (CMake: project(NAME C))".into());
}
toolchain::target_compiler(action, args)
}
pub fn run(language: Language) -> i32 {
let name = match language {
Language::C => "libc-cc",
Language::Cxx => "libc-cxx",
};
let result = command(language, env::args_os().skip(1).collect()).and_then(|mut cmd| {
cmd.status()
.map_err(|e| format!("cannot run {:?}: {e}", cmd.get_program()))
});
match result {
Ok(status) => status.code().unwrap_or(1),
Err(error) => {
eprintln!("{name}: {error}");
1
}
}
}
pub(crate) fn wrappers() -> Result<(PathBuf, PathBuf), String> {
Ok((
toolchain::sibling("libc-cc")?,
toolchain::sibling("libc-cxx")?,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn args(values: &[&str]) -> Vec<OsString> {
values.iter().map(OsString::from).collect()
}
#[test]
fn compiles_queries_and_links_are_told_apart() {
assert_eq!(
classify(&args(&["-c", "a.c", "-o", "a.o"])).unwrap(),
Action::Compile
);
for query in [
&["-E", "probe.c"][..],
&["--version"],
&["-v"],
&["-print-file-name=crt1.o"],
&["-o", "out", "-I", "include"],
] {
assert_eq!(classify(&args(query)).unwrap(), Action::Query, "{query:?}");
}
assert_eq!(
classify(&args(&["a.o", "-o", "cmTC_1"])).unwrap(),
Action::Link { shared: false }
);
assert_eq!(
classify(&args(&["-shared", "a.o", "-o", "liba.so"])).unwrap(),
Action::Link { shared: true }
);
assert_eq!(
classify(&args(&["@objects.rsp", "-o", "app"])).unwrap(),
Action::Link { shared: false }
);
assert_eq!(
classify(&args(&["-r", "a.o", "-o", "b.o"])).unwrap(),
Action::Relocatable
);
assert!(classify(&args(&["-static", "a.o"])).is_err());
}
#[test]
fn cxx_sources_are_recognized() {
assert!(is_cxx(&args(&["-c", "a.cpp"])));
assert!(is_cxx(&args(&["-x", "c++", "-c", "a.h"])));
assert!(is_cxx(&args(&["-xc++", "-"])));
assert!(!is_cxx(&args(&["-c", "a.c", "-o", "a.o"])));
assert!(!is_cxx(&args(&["-x", "c++", "-x", "c", "a.c"])));
}
}