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