Skip to main content

cargo_libc/
cc.rs

1//! Compiler wrappers for C dependencies (`libc-cc`, and `libc-cxx` to refuse C++).
2//!
3//! `cargo libc` points the `cc` crate (and CMake through it) at these wrappers.
4//! Cargo builds host and target units for the same triple, so the triple cannot
5//! tell them apart; the wrapper uses the build script's `CARGO_CFG_CARGO_LIBC_PROFILE`,
6//! which only target units have (it comes from the target rustflags). Target
7//! compiles get the selected sysroot or provider; host compiles run the host
8//! compiler unchanged.
9use crate::toolchain;
10use std::{
11    env,
12    ffi::OsString,
13    path::{Path, PathBuf},
14    process::Command,
15};
16
17/// Set in the build script environment of target units only.
18pub(crate) const TARGET_MARKER: &str = "CARGO_CFG_CARGO_LIBC_PROFILE";
19pub(crate) const HOST_CC_ENV: &str = "CARGO_LIBC_HOST_CC";
20pub(crate) const HOST_CXX_ENV: &str = "CARGO_LIBC_HOST_CXX";
21
22/// The language a wrapper compiles: which executable is running.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Language {
25    /// C, as `libc-cc`.
26    C,
27    /// C++, as `libc-cxx`: host units only; target units are refused.
28    Cxx,
29}
30
31/// The `cc` crate's compiler variables, most specific first; with the host
32/// triple equal to the target triple it reads `HOST_CC`, never `TARGET_CC`.
33fn compiler_variables(base: &str) -> [String; 4] {
34    let triple = toolchain::TARGET;
35    [
36        format!("{base}_{triple}"),
37        format!("{base}_{}", triple.replace('-', "_")),
38        format!("HOST_{base}"),
39        base.to_string(),
40    ]
41}
42
43/// The variables pointing `CC`/`CXX` for the target triple at the wrappers.
44/// The host's own choice (`HOST_CC`, `CC`, or the `cc` crate's default) is kept
45/// for host units. `shell_names` uses `CC_x86_64_unknown_linux_gnu`, which a
46/// shell can export, instead of the spelling the `cc` crate reads first.
47pub(crate) fn variables(
48    cc: &Path,
49    cxx: &Path,
50    shell_names: bool,
51) -> Result<Vec<(String, OsString)>, String> {
52    let mut variables = Vec::new();
53    for (base, wrapper, host_env, default) in [
54        ("CC", cc, HOST_CC_ENV, "cc"),
55        ("CXX", cxx, HOST_CXX_ENV, "c++"),
56    ] {
57        let [hyphen, underscore, host, plain] = compiler_variables(base);
58        for key in [&hyphen, &underscore] {
59            if env::var_os(key).is_some_and(|value| !value.is_empty()) {
60                return Err(format!("{key} is already set; cargo-libc selects the target C compiler itself, so use a shell without that override"));
61            }
62        }
63        let host_compiler = [&host, &plain]
64            .into_iter()
65            .find_map(|key| env::var_os(key).filter(|value| !value.is_empty()))
66            .unwrap_or_else(|| default.into());
67        variables.push((host_env.to_string(), host_compiler));
68        let key = if shell_names { underscore } else { hyphen };
69        variables.push((key, wrapper.as_os_str().to_owned()));
70    }
71    Ok(variables)
72}
73
74pub(crate) fn set_environment(cmd: &mut Command, cc: &Path, cxx: &Path) -> Result<(), String> {
75    cmd.envs(variables(cc, cxx, false)?);
76    Ok(())
77}
78
79/// What a driver invocation does with these arguments.
80#[derive(Debug, PartialEq, Eq)]
81pub(crate) enum Action {
82    /// Preprocess or query (`-E`, `--version`, `-print-*`, no inputs).
83    Query,
84    /// Compile only (`-c`, `-S`, `-fsyntax-only`): no target link inputs are added.
85    Compile,
86    Link {
87        shared: bool,
88    },
89    /// `-r` partial links take no startup files or libraries.
90    Relocatable,
91}
92
93/// Driver options whose value is the next argument.
94const SEPARATE_VALUE: &[&str] = &[
95    "-o",
96    "-I",
97    "-L",
98    "-D",
99    "-U",
100    "-include",
101    "-imacros",
102    "-isystem",
103    "-iquote",
104    "-idirafter",
105    "-iprefix",
106    "-isysroot",
107    "-x",
108    "-MF",
109    "-MT",
110    "-MQ",
111    "-Xlinker",
112    "-Xassembler",
113    "-Xpreprocessor",
114    "-Xclang",
115    "-target",
116    "-T",
117    "-z",
118    "-u",
119    "-e",
120    "-arch",
121];
122
123pub(crate) fn classify(args: &[OsString]) -> Result<Action, String> {
124    let mut inputs = 0;
125    let mut shared = false;
126    let mut relocatable = false;
127    let mut compile = false;
128    let mut i = 0;
129    while i < args.len() {
130        let arg = args[i].to_string_lossy();
131        match arg.as_ref() {
132            "-E" | "-M" | "-MM" | "--version" | "-dumpversion" | "-dumpfullversion"
133            | "-dumpmachine" | "-###" => return Ok(Action::Query),
134            "-c" | "-S" | "-fsyntax-only" => compile = true,
135            "-static" | "-static-pie" => {
136                return Err(
137                    "static linking is not supported: providers have no static glibc".into(),
138                )
139            }
140            "-shared" => shared = true,
141            "-r" => relocatable = true,
142            _ if arg.starts_with("-print-") || arg.starts_with("--print-") => {
143                return Ok(Action::Query)
144            }
145            _ if SEPARATE_VALUE.contains(&arg.as_ref()) => i += 1,
146            // `-` is standard input; `@file` may hold inputs (CMake response files).
147            _ if arg == "-" || !arg.starts_with('-') => inputs += 1,
148            _ => {}
149        }
150        i += 1;
151    }
152    Ok(if inputs == 0 {
153        Action::Query
154    } else if compile {
155        Action::Compile
156    } else if relocatable {
157        Action::Relocatable
158    } else {
159        Action::Link { shared }
160    })
161}
162
163/// C++ needs a target C++ runtime, which providers do not have.
164fn is_cxx(args: &[OsString]) -> bool {
165    let mut language_cxx = false;
166    let mut i = 0;
167    while i < args.len() {
168        let arg = args[i].to_string_lossy();
169        i += 1;
170        let language = if arg == "-x" {
171            i += 1;
172            Some(
173                args.get(i - 1)
174                    .map_or(String::new(), |v| v.to_string_lossy().into_owned()),
175            )
176        } else {
177            arg.strip_prefix("-x").map(str::to_string)
178        };
179        if let Some(language) = language {
180            language_cxx = language.starts_with("c++");
181            continue;
182        }
183        if SEPARATE_VALUE.contains(&arg.as_ref()) {
184            i += 1;
185            continue;
186        }
187        if arg.starts_with('-') && arg != "-" {
188            continue;
189        }
190        let ext = Path::new(arg.as_ref())
191            .extension()
192            .map(|e| e.to_string_lossy().into_owned());
193        if language_cxx
194            || matches!(
195                ext.as_deref(),
196                Some("cc" | "cp" | "cpp" | "cxx" | "CPP" | "c++" | "C" | "ii" | "cppm" | "ixx")
197            )
198        {
199            return true;
200        }
201    }
202    false
203}
204
205/// Builds the compiler command for one wrapper invocation.
206pub(crate) fn command(language: Language, args: Vec<OsString>) -> Result<Command, String> {
207    if env::var_os(TARGET_MARKER).is_none() {
208        let key = match language {
209            Language::C => HOST_CC_ENV,
210            Language::Cxx => HOST_CXX_ENV,
211        };
212        let host = env::var_os(key)
213            .ok_or_else(|| format!("{key} is missing; this wrapper is launched by cargo libc"))?;
214        // A host compiler variable may hold a wrapper and its compiler ("ccache cc").
215        let text = host.to_string_lossy().into_owned();
216        let mut words = text.split_whitespace();
217        let program = words.next().ok_or(format!("{key} is empty"))?;
218        let mut cmd = Command::new(program);
219        cmd.args(words).args(args);
220        return Ok(cmd);
221    }
222    let action = classify(&args)?;
223    // Queries (the `cc` crate's compiler detection) still answer for libc-cxx.
224    if action != Action::Query && (language == Language::Cxx || is_cxx(&args)) {
225        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());
226    }
227    toolchain::target_compiler(action, args)
228}
229
230/// The `libc-cc` and `libc-cxx` entry point. Compiles with the arguments the
231/// process was started with: target units with Clang and the selected sysroot or
232/// provider, host units with the host compiler unchanged. Returns the compiler's
233/// exit code, or 1 after printing why the compile could not run (for example C++
234/// for the target).
235pub fn run(language: Language) -> i32 {
236    let name = match language {
237        Language::C => "libc-cc",
238        Language::Cxx => "libc-cxx",
239    };
240    let result = command(language, env::args_os().skip(1).collect()).and_then(|mut cmd| {
241        cmd.status()
242            .map_err(|e| format!("cannot run {:?}: {e}", cmd.get_program()))
243    });
244    match result {
245        Ok(status) => status.code().unwrap_or(1),
246        Err(error) => {
247            eprintln!("{name}: {error}");
248            1
249        }
250    }
251}
252
253/// The wrapper paths next to the running executable.
254pub(crate) fn wrappers() -> Result<(PathBuf, PathBuf), String> {
255    Ok((
256        toolchain::sibling("libc-cc")?,
257        toolchain::sibling("libc-cxx")?,
258    ))
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    fn args(values: &[&str]) -> Vec<OsString> {
266        values.iter().map(OsString::from).collect()
267    }
268
269    #[test]
270    fn compiles_queries_and_links_are_told_apart() {
271        assert_eq!(
272            classify(&args(&["-c", "a.c", "-o", "a.o"])).unwrap(),
273            Action::Compile
274        );
275        for query in [
276            &["-E", "probe.c"][..],
277            &["--version"],
278            &["-v"],
279            &["-print-file-name=crt1.o"],
280            &["-o", "out", "-I", "include"],
281        ] {
282            assert_eq!(classify(&args(query)).unwrap(), Action::Query, "{query:?}");
283        }
284        assert_eq!(
285            classify(&args(&["a.o", "-o", "cmTC_1"])).unwrap(),
286            Action::Link { shared: false }
287        );
288        assert_eq!(
289            classify(&args(&["-shared", "a.o", "-o", "liba.so"])).unwrap(),
290            Action::Link { shared: true }
291        );
292        assert_eq!(
293            classify(&args(&["@objects.rsp", "-o", "app"])).unwrap(),
294            Action::Link { shared: false }
295        );
296        assert_eq!(
297            classify(&args(&["-r", "a.o", "-o", "b.o"])).unwrap(),
298            Action::Relocatable
299        );
300        assert!(classify(&args(&["-static", "a.o"])).is_err());
301    }
302
303    #[test]
304    fn cxx_sources_are_recognized() {
305        assert!(is_cxx(&args(&["-c", "a.cpp"])));
306        assert!(is_cxx(&args(&["-x", "c++", "-c", "a.h"])));
307        assert!(is_cxx(&args(&["-xc++", "-"])));
308        assert!(!is_cxx(&args(&["-c", "a.c", "-o", "a.o"])));
309        assert!(!is_cxx(&args(&["-x", "c++", "-x", "c", "a.c"])));
310    }
311}