Skip to main content

cargo_libc/
toolchain.rs

1//! Toolchain selection and the Cargo commands that use it, and `libc-linker`, the
2//! linker Cargo calls for target units.
3
4use crate::{
5    cargo_output,
6    report::{self, ScanOptions},
7    simulate::{self, Simulation},
8    version::Version,
9};
10use std::{
11    env,
12    ffi::OsString,
13    fs,
14    io::{BufRead, BufReader, Read},
15    path::{Path, PathBuf},
16    process::{Command, ExitStatus, Stdio},
17};
18
19pub(crate) const TARGET: &str = "x86_64-unknown-linux-gnu";
20pub(crate) const LOADER: &str = "/lib64/ld-linux-x86-64.so.2";
21const SYSROOT_ENV: &str = "CARGO_LIBC_SYSROOT";
22const GCC_ENV: &str = "CARGO_LIBC_GCC_RUNTIME";
23const CC_ENV: &str = "CARGO_LIBC_CC";
24const LD_ENV: &str = "CARGO_LIBC_LD";
25const DESCRIPTOR_ENV: &str = "CARGO_LIBC_PROVIDER_DESCRIPTOR";
26
27#[derive(Clone, Debug)]
28pub(crate) struct Toolchain {
29    pub(crate) provider: Option<crate::provider::Provider>,
30    pub(crate) sysroot: PathBuf,
31    pub(crate) gcc_runtime: PathBuf,
32    pub(crate) cc: PathBuf,
33    pub(crate) ld: PathBuf,
34    pub(crate) ceiling: Version,
35}
36
37#[derive(Debug)]
38pub(crate) struct Diagnostics {
39    pub(crate) header_version: Version,
40    pub(crate) compiler_version: String,
41    pub(crate) linker_version: String,
42    pub(crate) files: Vec<(String, PathBuf)>,
43}
44
45fn canonical(path: &Path, label: &str) -> Result<PathBuf, String> {
46    fs::canonicalize(path).map_err(|e| format!("cannot resolve {label} {}: {e}", path.display()))
47}
48
49fn inside_file(path: &Path, root: &Path) -> Result<PathBuf, String> {
50    let actual = canonical(path, "target file")?;
51    if !actual.starts_with(root) {
52        return Err(format!(
53            "target file {} resolves outside sysroot {}",
54            path.display(),
55            root.display()
56        ));
57    }
58    if !actual.is_file() {
59        return Err(format!(
60            "target input is not a regular file: {}",
61            actual.display()
62        ));
63    }
64    Ok(actual)
65}
66
67pub(crate) fn text_path(path: &Path) -> Result<&str, String> {
68    let text = path
69        .to_str()
70        .ok_or("toolchain paths must be UTF-8 for Cargo's encoded flags")?;
71    if text.contains('\u{1f}') {
72        return Err("toolchain paths cannot contain the Cargo flag separator U+001F".into());
73    }
74    Ok(text)
75}
76
77impl Toolchain {
78    pub(crate) fn from_provider(
79        path: &Path,
80        cc: PathBuf,
81        ld: PathBuf,
82        ceiling: Version,
83    ) -> Result<Self, String> {
84        if !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
85            return Err("provider builds require x86-64 Linux".into());
86        }
87
88        let provider = crate::provider::Provider::load(path)?;
89        provider.validate_selection(ceiling)?;
90        provider.validate_files()?;
91        let sysroot = canonical(&provider.sysroot, "provider sysroot")?;
92        let gcc_runtime = canonical(&provider.gcc_startup, "provider GCC startup")?;
93        let cc = canonical(&cc, "Clang driver")?;
94        let ld = canonical(&ld, "linker")?;
95        for path in [
96            &sysroot,
97            &gcc_runtime,
98            &provider.stubs,
99            &provider.compiler_runtime,
100            &cc,
101            &ld,
102        ] {
103            text_path(path)?;
104        }
105        Ok(Self {
106            provider: Some(provider),
107            sysroot,
108            gcc_runtime,
109            cc,
110            ld,
111            ceiling,
112        })
113    }
114    pub(crate) fn new(
115        sysroot: PathBuf,
116        gcc_runtime: PathBuf,
117        cc: PathBuf,
118        ld: PathBuf,
119        ceiling: Version,
120    ) -> Result<Self, String> {
121        if !cfg!(all(target_os = "linux", target_arch = "x86_64")) {
122            return Err("the build coordinator currently supports x86-64 Linux hosts only".into());
123        }
124        ceiling.validate_glibc_target()?;
125        let sysroot = canonical(&sysroot, "sysroot")?;
126        let gcc_runtime = canonical(&gcc_runtime, "GCC runtime directory")?;
127        if !sysroot.is_dir() || !gcc_runtime.is_dir() {
128            return Err("sysroot and GCC runtime must be directories".into());
129        }
130        if !gcc_runtime.starts_with(&sysroot) {
131            return Err("the GCC runtime directory must be inside the supplied sysroot".into());
132        }
133        let cc = canonical(&cc, "Clang driver")?;
134        let ld = canonical(&ld, "linker")?;
135        if !cc.is_file() || !ld.is_file() {
136            return Err("compiler and linker paths must identify files".into());
137        }
138        for path in [&sysroot, &gcc_runtime, &cc, &ld] {
139            text_path(path)?;
140        }
141        Ok(Self {
142            provider: None,
143            sysroot,
144            gcc_runtime,
145            cc,
146            ld,
147            ceiling,
148        })
149    }
150
151    pub(crate) fn driver_args(&self) -> Vec<OsString> {
152        vec![
153            format!("--target={TARGET}").into(),
154            prefixed_path("--sysroot=", &self.sysroot),
155            prefixed_path("--gcc-install-dir=", &self.gcc_runtime),
156            "--rtlib=libgcc".into(),
157            prefixed_path("-fuse-ld=", &self.ld),
158        ]
159    }
160
161    fn command(&self) -> Command {
162        let mut cmd = Command::new(&self.cc);
163        cmd.args(self.driver_args());
164        remove_search_overrides(&mut cmd);
165        cmd
166    }
167
168    pub(crate) fn diagnose(&self) -> Result<Diagnostics, String> {
169        let header = inside_file(&self.sysroot.join("usr/include/features.h"), &self.sysroot)?;
170        let text =
171            fs::read_to_string(&header).map_err(|e| format!("cannot read features.h: {e}"))?;
172        let header_version = header_version(&text)?;
173        if header_version > self.ceiling {
174            return Err(format!("sysroot headers are glibc {header_version}, newer than requested ceiling {}; supply older target development files", self.ceiling));
175        }
176        let compiler_version = tool_version(&self.cc)?;
177        if !compiler_version.to_ascii_lowercase().contains("clang") {
178            return Err(format!("--cc must be a Clang driver supporting --gcc-install-dir; got {compiler_version:?}"));
179        }
180        let linker_version = tool_version(&self.ld)?;
181        let mut files = Vec::new();
182        if let Some(provider) = &self.provider {
183            provider.validate_files()?;
184            if header_version != self.ceiling {
185                return Err(format!(
186                    "provider headers must match glibc {} exactly",
187                    self.ceiling
188                ));
189            }
190            for name in ["crt1.o", "crti.o", "crtn.o"] {
191                let path = inside_file(&self.sysroot.join("usr/lib64").join(name), &self.sysroot)?;
192                validate_x86_object(&path)?;
193                files.push((name.into(), path));
194            }
195            for name in ["crtbegin.o", "crtend.o"] {
196                let path = inside_file(&self.gcc_runtime.join(name), &self.gcc_runtime)?;
197                validate_x86_object(&path)?;
198                files.push((name.into(), path));
199            }
200            for name in crate::provider::SONAMES {
201                let path = provider.stubs.join(name);
202                validate_x86_object(&path)?;
203                files.push((name.into(), path));
204            }
205            return Ok(Diagnostics {
206                header_version,
207                compiler_version,
208                linker_version,
209                files,
210            });
211        }
212        for name in [
213            "Scrt1.o",
214            "crt1.o",
215            "crti.o",
216            "crtn.o",
217            "crtbeginS.o",
218            "crtendS.o",
219            "libc.so",
220            "libc_nonshared.a",
221            "libgcc_s.so",
222        ] {
223            let output = self
224                .command()
225                .arg(format!("-print-file-name={name}"))
226                .output()
227                .map_err(|e| format!("cannot probe target input {name}: {e}"))?;
228            if !output.status.success() {
229                return Err(format!(
230                    "Clang failed while finding {name}: {}",
231                    String::from_utf8_lossy(&output.stderr).trim()
232                ));
233            }
234            let value = std::str::from_utf8(&output.stdout)
235                .map_err(|_| "Clang returned a non-UTF-8 input path")?
236                .trim();
237            if value == name || value.is_empty() || !Path::new(value).is_absolute() {
238                return Err(format!(
239                    "Clang did not locate target input {name}; returned {value:?}"
240                ));
241            }
242            let path = inside_file(Path::new(value), &self.sysroot)?;
243            if name.ends_with(".o") {
244                validate_x86_object(&path)?;
245            }
246            files.push((name.into(), path));
247        }
248        // Explicit runtime selection must really select the supplied GCC directory.
249        for name in ["crtbeginS.o", "crtendS.o"] {
250            let expected = inside_file(&self.gcc_runtime.join(name), &self.sysroot)?;
251            let selected = files.iter().find(|(n, _)| n == name).unwrap();
252            if selected.1 != expected {
253                return Err(format!("Clang selected an unexpected {name}"));
254            }
255        }
256        inside_file(
257            &self.sysroot.join(LOADER.trim_start_matches('/')),
258            &self.sysroot,
259        )?;
260        Ok(Diagnostics {
261            header_version,
262            compiler_version,
263            linker_version,
264            files,
265        })
266    }
267
268    pub(crate) fn print_diagnostics(&self, info: &Diagnostics) {
269        println!(
270            "Target: {TARGET}; requested glibc ceiling: {}",
271            self.ceiling
272        );
273        println!(
274            "Sysroot: {}; headers: glibc {}",
275            self.sysroot.display(),
276            info.header_version
277        );
278        println!("Compiler: {}", info.compiler_version);
279        println!("Linker: {}", info.linker_version);
280        for (name, path) in &info.files {
281            println!("  {name}: {}", path.display());
282        }
283        println!("Target input probes passed. Full dependency resolution and target runtime behavior remain separate checks.");
284    }
285
286    pub(crate) fn rustflags(&self, driver: &Path) -> Result<OsString, String> {
287        Ok(self.rustflag_list(driver)?.join("\u{1f}").into())
288    }
289
290    pub(crate) fn rustflag_list(&self, driver: &Path) -> Result<Vec<String>, String> {
291        // These input paths deliberately also appear in rustc flags: Cargo then sees
292        // profile-path changes in its fingerprints rather than only hidden env vars.
293        let flags = vec![
294            "-C".to_string(),
295            format!("linker={}", text_path(driver)?),
296            "-C".into(),
297            "linker-flavor=gcc".into(),
298            "-C".to_string(),
299            "linker-features=-lld".into(),
300            "-C".into(),
301            "target-cpu=x86-64".into(),
302            "-C".into(),
303            format!("link-arg=--sysroot={}", text_path(&self.sysroot)?),
304            "-C".into(),
305            format!(
306                "link-arg=--gcc-install-dir={}",
307                text_path(&self.gcc_runtime)?
308            ),
309            "-C".into(),
310            format!("link-arg=-fuse-ld={}", text_path(&self.ld)?),
311            "--cfg".into(),
312            format!("cargo_libc_profile=\"{}\"", self.profile_fingerprint()?),
313        ];
314        Ok(flags)
315    }
316
317    fn profile_fingerprint(&self) -> Result<String, String> {
318        // FNV-1a is only a stable cache discriminator, not an integrity check.
319        // SDK/tool contents changed in place still require a clean rebuild.
320        let mut hash = 0xcbf29ce484222325u64;
321        let ceiling = self.ceiling.to_string();
322        for item in [
323            text_path(&self.sysroot)?,
324            text_path(&self.gcc_runtime)?,
325            text_path(&self.cc)?,
326            text_path(&self.ld)?,
327            &ceiling,
328        ] {
329            for byte in item.bytes().chain(std::iter::once(0)) {
330                hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3);
331            }
332        }
333        if let Some(provider) = &self.provider {
334            for byte in provider.descriptor.bytes() {
335                hash = (hash ^ u64::from(byte)).wrapping_mul(0x100000001b3);
336            }
337        }
338        Ok(format!("{hash:016x}"))
339    }
340
341    /// The selection the helpers read (libc-linker, libc-cc).
342    pub(crate) fn variables(&self) -> Vec<(&'static str, OsString)> {
343        let mut variables = vec![
344            (SYSROOT_ENV, self.sysroot.clone().into_os_string()),
345            (GCC_ENV, self.gcc_runtime.clone().into_os_string()),
346            (CC_ENV, self.cc.clone().into_os_string()),
347            (LD_ENV, self.ld.clone().into_os_string()),
348        ];
349        if let Some(provider) = &self.provider {
350            variables.push((DESCRIPTOR_ENV, provider.descriptor.clone().into()));
351        }
352        variables
353    }
354
355    pub(crate) fn set_environment(&self, cmd: &mut Command) {
356        cmd.env_remove(DESCRIPTOR_ENV);
357        cmd.envs(self.variables());
358    }
359}
360
361/// Cargo subcommands whose own `--` passes the rest to the compiler or linter.
362fn has_tool_arguments(subcommand: &str) -> bool {
363    matches!(subcommand, "clippy" | "rustc")
364}
365
366pub(crate) fn validate_cargo_args(subcommand: &str, args: &[OsString]) -> Result<(), String> {
367    for (i, arg) in args.iter().enumerate() {
368        let text = arg.to_string_lossy();
369        if text == "--" && has_tool_arguments(subcommand) {
370            return validate_tool_args(&args[i + 1..]);
371        }
372        if text == "--target"
373            || text.starts_with("--target=")
374            || text == "--config"
375            || text.starts_with("--config=")
376            || text == "--message-format"
377            || text.starts_with("--message-format=")
378            || text == "--"
379            || text.starts_with('+')
380        {
381            return Err(format!("Cargo argument {text:?} conflicts with this prototype's controlled target configuration"));
382        }
383    }
384    Ok(())
385}
386
387/// Arguments after `cargo clippy --` or `cargo rustc --` reach rustc directly:
388/// they must not replace the target, linker or relocation model.
389fn validate_tool_args(args: &[OsString]) -> Result<(), String> {
390    for arg in args {
391        let text = arg.to_string_lossy();
392        if text.starts_with("--target")
393            || text.contains("linker")
394            || text.contains("relocation-model")
395            || text.contains("link-arg")
396        {
397            return Err(format!(
398                "compiler argument {text:?} conflicts with the managed target configuration"
399            ));
400        }
401    }
402    Ok(())
403}
404
405pub(crate) fn sibling(name: &str) -> Result<PathBuf, String> {
406    let executable = env::current_exe().map_err(|e| e.to_string())?;
407    let path = executable
408        .parent()
409        .ok_or_else(|| format!("cannot locate sibling helper {name}"))?
410        .join(name);
411    if !path.is_file() {
412        return Err(format!(
413            "missing Rust helper {}; install/build all cargo-libc binaries",
414            path.display()
415        ));
416    }
417    Ok(path)
418}
419
420/// A TOML basic string, for Cargo's `--config KEY=VALUE`.
421fn toml_string(text: &str) -> String {
422    let mut out = String::from("\"");
423    for c in text.chars() {
424        match c {
425            '"' => out.push_str("\\\""),
426            '\\' => out.push_str("\\\\"),
427            c if c.is_control() => out.push_str(&format!("\\u{:04X}", c as u32)),
428            c => out.push(c),
429        }
430    }
431    out.push('"');
432    out
433}
434
435/// Probes the inputs and prepares `cargo SUBCOMMAND --target ...` with the
436/// managed target flags; returns the command and the linker driver path.
437fn cargo_command(
438    config: &Toolchain,
439    subcommand: &str,
440    cargo_args: &[OsString],
441) -> Result<(Command, PathBuf), String> {
442    validate_cargo_args(subcommand, cargo_args)?;
443    // `test` compiles doctests and `doc` documents with rustdoc, which needs the
444    // same flags (the cfg, and the linker for doctests).
445    let doctests = matches!(subcommand, "test" | "doc");
446    let mut managed = vec!["RUSTFLAGS", "CARGO_ENCODED_RUSTFLAGS"];
447    if doctests {
448        managed.extend(["RUSTDOCFLAGS", "CARGO_ENCODED_RUSTDOCFLAGS"]);
449    }
450    for key in managed {
451        if env::var_os(key).is_some_and(|value| !value.is_empty()) {
452            return Err(format!("{key} is already set; use a shell without that override so the target flags are explicit"));
453        }
454    }
455    let diagnostic = config.diagnose()?;
456    config.print_diagnostics(&diagnostic);
457    let driver = sibling("libc-linker")?;
458    let (cc, cxx) = crate::cc::wrappers()?;
459    let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
460    let mut cmd = Command::new(cargo);
461    cmd.arg(subcommand).arg("--target").arg(TARGET);
462    config.set_environment(&mut cmd);
463    // C dependencies (the `cc` crate, CMake through it) compile for the target
464    // with libc-cc; build scripts and proc macros keep the host compiler.
465    crate::cc::set_environment(&mut cmd, &cc, &cxx)?;
466    // Target linker config also affects host build units on same-triple builds.
467    // Explicit --target plus encoded rustflags keeps this linker target-only.
468    let flags = config.rustflags(&driver)?;
469    println!(
470        "Target C compiler (cc crate, CMake): {} (C only; host units keep the host compiler)",
471        cc.display()
472    );
473    if doctests {
474        cmd.env("CARGO_ENCODED_RUSTDOCFLAGS", &flags);
475    }
476    cmd.env("CARGO_ENCODED_RUSTFLAGS", flags);
477    cmd.env_remove("CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS");
478    Ok((cmd, driver))
479}
480
481fn print_command(cmd: &Command) {
482    println!(
483        "Cargo command: {:?} {:?}",
484        cmd.get_program(),
485        cmd.get_args().collect::<Vec<_>>()
486    );
487}
488
489pub(crate) fn build(
490    config: &Toolchain,
491    cargo_args: &[OsString],
492    artifact: &Path,
493    scan: Option<&ScanOptions>,
494    dry_run: bool,
495) -> Result<i32, String> {
496    let (mut cmd, driver) = cargo_command(config, "build", cargo_args)?;
497    cmd.arg("--message-format=json-render-diagnostics")
498        .args(cargo_args);
499    print_command(&cmd);
500    println!("Rust linker driver: {}", driver.display());
501    println!(
502        "Artifact selected for direct ELF verification: {}",
503        artifact.display()
504    );
505    if dry_run {
506        return Ok(0);
507    }
508    let (status, artifacts) = run_cargo(&mut cmd)?;
509    if !status.success() {
510        return Ok(status.code().unwrap_or(1));
511    }
512    let selected = canonical(artifact, "selected executable")?;
513    if !artifacts
514        .iter()
515        .any(|path| fs::canonicalize(path).is_ok_and(|actual| actual == selected))
516    {
517        return Err(format!("{} was not reported as an executable by this Cargo invocation; select an executable it actually built or found fresh", artifact.display()));
518    }
519    let passed = verify_executable(config, artifact, scan)?;
520    Ok(if passed { 0 } else { 2 })
521}
522
523/// The direct ELF check (and the tier 1 scan when requested) of one executable
524/// from this build; prints the report and returns whether it passed.
525fn verify_executable(
526    config: &Toolchain,
527    artifact: &Path,
528    scan: Option<&ScanOptions>,
529) -> Result<bool, String> {
530    let bytes = report::read_bytes(artifact)?;
531    let info = crate::elf::inspect(&bytes)?;
532    let mut assessment = report::assess(&info, Some(config.ceiling));
533    if info.interpreter.as_deref() != Some(LOADER) {
534        assessment.errors.push(format!(
535            "expected dynamically linked executable with interpreter {LOADER:?}; got {:?}",
536            info.interpreter
537        ));
538    }
539    if config.sysroot != Path::new("/") {
540        for paths in info.rpath.iter().chain(&info.runpath) {
541            for entry in paths.split(':') {
542                if Path::new(entry).starts_with(&config.sysroot) {
543                    assessment.errors.push(format!(
544                        "build-time sysroot leaked into runtime library search path {entry:?}"
545                    ));
546                }
547            }
548        }
549    }
550    // A preset already set the ceiling; otherwise the scan uses --glibc.
551    let scan = scan
552        .map(|options| {
553            let ceiling = options.distro.is_none().then_some(config.ceiling);
554            report::scan(&bytes, ceiling, options).map(|(_, scan)| scan)
555        })
556        .transpose()?;
557    println!(
558        "{}",
559        report::render(
560            artifact,
561            &info,
562            &assessment,
563            Some(config.ceiling),
564            scan.as_ref(),
565            false
566        )
567    );
568    Ok(assessment.errors.is_empty() && scan.as_ref().is_none_or(report::Scan::passed))
569}
570
571/// `cargo libc check|clippy|doc|rustc`: Cargo with the same managed target
572/// flags as `build`. Cargo's diagnostics and exit status pass through
573/// unchanged; there is no artifact to verify (`rustc` may link one: inspect it).
574pub(crate) fn cargo_passthrough(
575    config: &Toolchain,
576    subcommand: &str,
577    cargo_args: &[OsString],
578    dry_run: bool,
579) -> Result<i32, String> {
580    let (mut cmd, driver) = cargo_command(config, subcommand, cargo_args)?;
581    cmd.args(cargo_args);
582    print_command(&cmd);
583    let links = subcommand == "rustc";
584    println!(
585        "Rust linker driver: {}{}",
586        driver.display(),
587        if links {
588            ""
589        } else {
590            " (configured; this command does not link)"
591        }
592    );
593    if dry_run {
594        return Ok(0);
595    }
596    let status = cmd.status().map_err(|e| format!("cannot run Cargo: {e}"))?;
597    if status.success() {
598        if subcommand == "check" {
599            println!(
600                "Type check passed for glibc {} target flags. Nothing was linked: build and inspect the executable to check its requirements.",
601                config.ceiling
602            );
603        } else if links {
604            println!(
605                "Built with glibc {} target flags. The output was not checked: run cargo libc inspect on it.",
606                config.ceiling
607            );
608        }
609    }
610    Ok(status.code().unwrap_or(1))
611}
612
613/// `cargo libc install`: `cargo install` with the managed target flags, then
614/// the same direct ELF check (and scan) as `build` for every executable built.
615/// The check runs after Cargo installed them; a failure exits with 2.
616pub(crate) fn install(
617    config: &Toolchain,
618    cargo_args: &[OsString],
619    scan: Option<&ScanOptions>,
620    dry_run: bool,
621) -> Result<i32, String> {
622    let (mut cmd, driver) = cargo_command(config, "install", cargo_args)?;
623    cmd.arg("--message-format=json-render-diagnostics");
624    // cargo install deletes its temporary target directory; keep the
625    // executables until they are checked.
626    let explicit = env::var_os("CARGO_TARGET_DIR").is_some_and(|v| !v.is_empty())
627        || cargo_args.iter().any(|arg| {
628            let text = arg.to_string_lossy();
629            text == "--target-dir" || text.starts_with("--target-dir=")
630        });
631    let temporary = (!explicit).then(|| {
632        let nanos = std::time::SystemTime::now()
633            .duration_since(std::time::UNIX_EPOCH)
634            .map_or(0, |d| d.as_nanos());
635        env::temp_dir().join(format!("cargo-libc-install-{}-{nanos}", std::process::id()))
636    });
637    if let Some(dir) = &temporary {
638        cmd.arg("--target-dir").arg(dir);
639    }
640    cmd.args(cargo_args);
641    print_command(&cmd);
642    println!("Rust linker driver: {}", driver.display());
643    println!(
644        "Every executable this install builds is checked against glibc {}.",
645        config.ceiling
646    );
647    if dry_run {
648        return Ok(0);
649    }
650    let result = (|| {
651        let (status, artifacts) = run_cargo(&mut cmd)?;
652        if !status.success() {
653            return Ok(status.code().unwrap_or(1));
654        }
655        if artifacts.is_empty() {
656            println!("Cargo built no executable (already installed?); nothing was checked.");
657            return Ok(0);
658        }
659        let mut passed = true;
660        for artifact in &artifacts {
661            passed &= verify_executable(config, artifact, scan)?;
662        }
663        if !passed {
664            println!("An installed executable failed the check above; remove it or rebuild for a newer target.");
665        }
666        Ok(if passed { 0 } else { 2 })
667    })();
668    if let Some(dir) = &temporary {
669        let _ = fs::remove_dir_all(dir);
670    }
671    result
672}
673
674/// Output format of `cargo libc env`.
675#[derive(Clone, Copy, Debug, PartialEq, Eq)]
676pub(crate) enum EnvFormat {
677    /// POSIX shell `export` lines.
678    Shell,
679    /// Cargo configuration (TOML) for `--config FILE` or `.cargo/config.toml`.
680    Cargo,
681}
682
683fn shell_quote(text: &str) -> String {
684    format!("'{}'", text.replace('\'', "'\\''"))
685}
686
687/// `cargo libc env`: the configuration `build`/`test` apply, for plain Cargo
688/// with `--target x86_64-unknown-linux-gnu`.
689pub(crate) fn plain_cargo(
690    config: &Toolchain,
691    simulation: &Simulation,
692    format: EnvFormat,
693) -> Result<String, String> {
694    simulation.plan()?;
695    config.diagnose()?;
696    let driver = sibling("libc-linker")?;
697    let runner = sibling("libc-runner")?;
698    let (cc, cxx) = crate::cc::wrappers()?;
699    let flags = config.rustflag_list(&driver)?;
700    let runner = text_path(&runner)?.to_string();
701    let mut variables: Vec<(String, String)> = Vec::new();
702    let utf8 = |key: &str, value: OsString| {
703        value
704            .into_string()
705            .map_err(|_| format!("{key} must be UTF-8 for plain Cargo configuration"))
706    };
707    for (key, value) in config.variables() {
708        variables.push((key.to_string(), utf8(key, value)?));
709    }
710    for (key, value) in crate::cc::variables(&cc, &cxx, format == EnvFormat::Shell)? {
711        let value = utf8(&key, value)?;
712        variables.push((key, value));
713    }
714    for (key, value) in simulation.variables(config.ceiling) {
715        if let Some(value) = value {
716            variables.push((key.to_string(), value));
717        }
718    }
719    let triple_key = TARGET.to_uppercase().replace('-', "_");
720    let mut out = format!(
721        "# cargo-libc {}: glibc {} for {TARGET}, generated for this machine.\n\
722         # Use with: cargo build --target {TARGET} (always pass --target, so build\n\
723         # scripts and proc macros stay native). RUSTFLAGS or CARGO_ENCODED_RUSTFLAGS in\n\
724         # the environment replace these flags. Nothing checks the output: run\n\
725         # cargo libc inspect FILE --max-glibc {}. Regenerate after updating\n\
726         # cargo-libc or the provider.\n",
727        env!("CARGO_PKG_VERSION"),
728        config.ceiling,
729        config.ceiling
730    );
731    match format {
732        EnvFormat::Shell => {
733            if let Some(flag) = flags
734                .iter()
735                .chain([&runner])
736                .find(|f| f.chars().any(char::is_whitespace))
737            {
738                return Err(format!("{flag:?} contains whitespace, which Cargo's flag variables cannot hold; use --format cargo"));
739            }
740            let joined = flags.join(" ");
741            out.push_str(&format!(
742                "export CARGO_TARGET_{triple_key}_RUSTFLAGS={}\n",
743                shell_quote(&joined)
744            ));
745            out.push_str(&format!("export RUSTDOCFLAGS={}\n", shell_quote(&joined)));
746            out.push_str(&format!(
747                "export CARGO_TARGET_{triple_key}_RUNNER={}\n",
748                shell_quote(&runner)
749            ));
750            for (key, value) in variables {
751                out.push_str(&format!("export {key}={}\n", shell_quote(&value)));
752            }
753        }
754        EnvFormat::Cargo => {
755            let list = |items: &[String]| {
756                items
757                    .iter()
758                    .map(|f| toml_string(f))
759                    .collect::<Vec<_>>()
760                    .join(", ")
761            };
762            out.push_str(&format!(
763                "\n[target.{TARGET}]\nrustflags = [{}]\nrunner = [{}]\n\n[build]\nrustdocflags = [{}]\n\n[env]\n",
764                list(&flags),
765                toml_string(&runner),
766                list(&flags)
767            ));
768            for (key, value) in variables {
769                out.push_str(&format!("{key} = {}\n", toml_string(&value)));
770            }
771        }
772    }
773    Ok(out)
774}
775
776/// `cargo libc test` / `cargo libc run`: Cargo builds with the managed target
777/// flags and runs every target binary through `libc-runner` (tier 2).
778pub(crate) fn test_or_run(
779    config: &Toolchain,
780    subcommand: &str,
781    cargo_args: &[OsString],
782    simulation: &Simulation,
783    dry_run: bool,
784) -> Result<i32, String> {
785    let (plan, _) = simulation.plan()?;
786    let (mut cmd, driver) = cargo_command(config, subcommand, cargo_args)?;
787    let runner = sibling("libc-runner")?;
788    // An array keeps a runner path with spaces as one program name.
789    cmd.arg("--config").arg(format!(
790        "target.{TARGET}.runner=[{}]",
791        toml_string(text_path(&runner)?)
792    ));
793    cmd.args(cargo_args);
794    simulation.set_environment(&mut cmd, config.ceiling);
795    print_command(&cmd);
796    println!("Rust linker driver: {}", driver.display());
797    println!("Target runner: {}", runner.display());
798    println!("Kernel simulation: {}", simulate::describe(&plan));
799    println!(
800        "Each binary is checked against glibc {} before it runs.",
801        config.ceiling
802    );
803    if dry_run {
804        return Ok(0);
805    }
806    let status = cmd.status().map_err(|e| format!("cannot run Cargo: {e}"))?;
807    Ok(status.code().unwrap_or(1))
808}
809
810fn run_cargo(cmd: &mut Command) -> Result<(ExitStatus, Vec<PathBuf>), String> {
811    let mut child = cmd
812        .stdout(Stdio::piped())
813        .spawn()
814        .map_err(|e| format!("cannot run Cargo: {e}"))?;
815    let stdout = child.stdout.take().expect("piped Cargo stdout");
816    let parsed = (|| {
817        const LINE_LIMIT: u64 = 16 * 1024 * 1024;
818        let mut reader = BufReader::new(stdout);
819        let mut artifacts = Vec::new();
820        loop {
821            let mut line = Vec::new();
822            let count = (&mut reader)
823                .take(LINE_LIMIT + 1)
824                .read_until(b'\n', &mut line)
825                .map_err(|e| format!("cannot read Cargo messages: {e}"))?;
826            if count == 0 {
827                break;
828            }
829            if count as u64 > LINE_LIMIT {
830                return Err("Cargo output line exceeds the 16 MiB limit".to_string());
831            }
832            let line =
833                std::str::from_utf8(&line).map_err(|_| "Cargo output is not UTF-8".to_string())?;
834            if let Some(path) = cargo_output::executable_from_message(line)? {
835                if artifacts.len() >= 100_000 {
836                    return Err("Cargo executable count exceeds the prototype limit".into());
837                }
838                artifacts.push(path);
839            } else if !line.trim_start().starts_with('{') {
840                print!("{line}");
841            }
842        }
843        Ok(artifacts)
844    })();
845    match parsed {
846        Ok(artifacts) => Ok((
847            child
848                .wait()
849                .map_err(|e| format!("cannot wait for Cargo: {e}"))?,
850            artifacts,
851        )),
852        Err(error) => {
853            let _ = child.kill();
854            let _ = child.wait();
855            Err(error)
856        }
857    }
858}
859
860fn prefixed_path(prefix: &str, path: &Path) -> OsString {
861    let mut arg = OsString::from(prefix);
862    arg.push(path.as_os_str());
863    arg
864}
865
866fn tool_version(path: &Path) -> Result<String, String> {
867    let output = Command::new(path)
868        .arg("--version")
869        .output()
870        .map_err(|e| format!("cannot run {}: {e}", path.display()))?;
871    if !output.status.success() {
872        return Err(format!("{} --version failed", path.display()));
873    }
874    Ok(String::from_utf8_lossy(&output.stdout)
875        .lines()
876        .next()
877        .unwrap_or("")
878        .into())
879}
880
881fn remove_search_overrides(cmd: &mut Command) {
882    for key in [
883        "LIBRARY_PATH",
884        "CPATH",
885        "C_INCLUDE_PATH",
886        "CPLUS_INCLUDE_PATH",
887        "OBJC_INCLUDE_PATH",
888        "GCC_EXEC_PREFIX",
889        "COMPILER_PATH",
890    ] {
891        cmd.env_remove(key);
892    }
893}
894
895/// The selection `cargo libc` hands its helpers through the environment.
896struct Selection {
897    cc: OsString,
898    sysroot: OsString,
899    gcc: OsString,
900    ld: OsString,
901    provider: Option<crate::provider::Provider>,
902}
903
904impl Selection {
905    fn from_environment(helper: &str) -> Result<Self, String> {
906        let required = |key: &str| {
907            env::var_os(key).ok_or_else(|| {
908                format!("{key} is missing; {helper} is launched by cargo libc build")
909            })
910        };
911        let provider = env::var_os("CARGO_LIBC_PROVIDER_DESCRIPTOR")
912            .map(|descriptor| {
913                let provider = crate::provider::Provider::parse(
914                    descriptor
915                        .to_str()
916                        .ok_or("provider descriptor must be UTF-8")?,
917                )?;
918                provider.validate_files()?;
919                Ok::<_, String>(provider)
920            })
921            .transpose()?;
922        Ok(Self {
923            cc: required(CC_ENV)?,
924            sysroot: required(SYSROOT_ENV)?,
925            gcc: required(GCC_ENV)?,
926            ld: required(LD_ENV)?,
927            provider,
928        })
929    }
930
931    /// Fixed profile selections; they come last, after the caller's arguments.
932    fn finish(&self, cmd: &mut Command, link: bool) {
933        cmd.arg(format!("--target={TARGET}"));
934        cmd.arg(prefixed_path("--sysroot=", Path::new(&self.sysroot)));
935        cmd.arg(prefixed_path("--gcc-install-dir=", Path::new(&self.gcc)));
936        if link {
937            if self.provider.is_none() {
938                cmd.arg("--rtlib=libgcc");
939            }
940            cmd.arg(prefixed_path("-fuse-ld=", Path::new(&self.ld)));
941        }
942        remove_search_overrides(cmd);
943    }
944}
945
946/// A provider link: only the provider's startup objects, stubs and archives,
947/// around `args`. `shared` links a shared library instead of an executable.
948fn provider_link(
949    cmd: &mut Command,
950    provider: &crate::provider::Provider,
951    args: impl IntoIterator<Item = OsString>,
952    default_libraries: bool,
953    shared: bool,
954) {
955    let (begin, end) = if shared {
956        ("crtbeginS.o", "crtendS.o")
957    } else {
958        ("crtbegin.o", "crtend.o")
959    };
960    cmd.arg("-nostdlib");
961    if !shared {
962        cmd.arg("-no-pie");
963    }
964    if let Some(path) = env::var_os("CARGO_LIBC_PROVIDER_LINK_MAP") {
965        cmd.arg(prefixed_path("-Wl,-Map=", Path::new(&path)));
966    }
967    cmd.arg(prefixed_path("-L", &provider.stubs));
968    cmd.arg(prefixed_path("-L", &provider.compiler_runtime));
969    if !shared {
970        cmd.arg(provider.sysroot.join("usr/lib64/crt1.o"));
971    }
972    cmd.arg(provider.sysroot.join("usr/lib64/crti.o"));
973    cmd.arg(provider.gcc_startup.join(begin));
974    cmd.args(args);
975    // GCC's default libraries for a dynamic link; rustc names its own.
976    let as_needed_gcc_s = ["-Wl,--push-state,--as-needed", "-lgcc_s", "-Wl,--pop-state"];
977    if default_libraries {
978        cmd.arg("-lgcc").args(as_needed_gcc_s).arg("-lc");
979    }
980    for archive in provider.nonshared_archives() {
981        cmd.arg(provider.sysroot.join(archive));
982    }
983    if default_libraries {
984        cmd.arg("-lgcc").args(as_needed_gcc_s);
985    }
986    cmd.arg(provider.gcc_startup.join(end));
987    cmd.arg(provider.sysroot.join("usr/lib64/crtn.o"));
988    if !shared {
989        cmd.arg(format!("-Wl,--dynamic-linker={LOADER}"));
990    }
991}
992
993/// rustc's arguments for a provider link: whether it links a shared library
994/// (a `cdylib` or `dylib`), and the arguments without `-pie`. Rust code is
995/// position-independent, so executables still link as non-PIE with the
996/// provider's `crt1.o`.
997fn provider_link_args(args: impl Iterator<Item = OsString>) -> (bool, Vec<OsString>) {
998    let mut shared = false;
999    let mut kept = Vec::new();
1000    for arg in args {
1001        if arg == "-shared" {
1002            shared = true;
1003        }
1004        if arg != "-pie" {
1005            kept.push(arg);
1006        }
1007    }
1008    (shared, kept)
1009}
1010
1011/// The `libc-linker` entry point: links a target unit with Clang and the selected
1012/// sysroot or provider. `args` are rustc's linker arguments. With a provider it adds
1013/// the release's startup objects, stubs and loader; for shared libraries
1014/// (`-shared`, such as Python extensions) it uses the shared startup objects and no
1015/// loader, and it drops rustc's `-pie`. Returns the linker's exit code.
1016///
1017/// The selection comes from the `CARGO_LIBC_*` environment variables that
1018/// `cargo libc` or `cargo libc env` sets in the target rustflags.
1019///
1020/// # Errors
1021///
1022/// Returns a message when the selection is missing or invalid, or when Clang cannot
1023/// be started.
1024pub fn run_linker(args: impl Iterator<Item = OsString>) -> Result<i32, String> {
1025    let selection = Selection::from_environment("this helper")?;
1026    let mut cmd = Command::new(&selection.cc);
1027    match &selection.provider {
1028        Some(provider) => {
1029            let (shared, args) = provider_link_args(args);
1030            provider_link(&mut cmd, provider, args, false, shared)
1031        }
1032        None => {
1033            cmd.args(args);
1034        }
1035    }
1036    selection.finish(&mut cmd, true);
1037    let status = cmd
1038        .status()
1039        .map_err(|e| format!("cannot launch Clang driver: {e}"))?;
1040    Ok(status.code().unwrap_or(1))
1041}
1042
1043/// The target C compiler command for `libc-cc`: Clang with the selected
1044/// sysroot or provider. Links get the provider's startup objects and stubs.
1045pub(crate) fn target_compiler(
1046    action: crate::cc::Action,
1047    args: Vec<OsString>,
1048) -> Result<Command, String> {
1049    use crate::cc::Action;
1050    let selection = Selection::from_environment("this compiler wrapper")?;
1051    let mut cmd = Command::new(&selection.cc);
1052    let link = matches!(action, Action::Link { .. } | Action::Relocatable);
1053    match (&selection.provider, action) {
1054        (Some(provider), Action::Link { shared }) => {
1055            provider_link(&mut cmd, provider, args, true, shared)
1056        }
1057        _ => {
1058            cmd.args(args);
1059        }
1060    }
1061    selection.finish(&mut cmd, link);
1062    Ok(cmd)
1063}
1064
1065fn validate_x86_object(path: &Path) -> Result<(), String> {
1066    use std::io::Read;
1067    let mut bytes = [0u8; 20];
1068    fs::File::open(path)
1069        .and_then(|mut f| f.read_exact(&mut bytes))
1070        .map_err(|e| format!("cannot read object {}: {e}", path.display()))?;
1071    if &bytes[0..4] != b"\x7fELF"
1072        || bytes[4] != 2
1073        || bytes[5] != 1
1074        || u16::from_le_bytes([bytes[18], bytes[19]]) != 62
1075    {
1076        return Err(format!(
1077            "{} is not an x86-64 little-endian ELF object",
1078            path.display()
1079        ));
1080    }
1081    Ok(())
1082}
1083
1084pub(crate) fn header_version(text: &str) -> Result<Version, String> {
1085    fn number(text: &str, name: &str) -> Result<u32, String> {
1086        for line in text.lines() {
1087            let Some(rest) = line.trim_start().strip_prefix('#') else {
1088                continue;
1089            };
1090            let mut words = rest.split_whitespace();
1091            if words.next() == Some("define") && words.next() == Some(name) {
1092                return words
1093                    .next()
1094                    .ok_or_else(|| format!("missing {name} value"))?
1095                    .parse()
1096                    .map_err(|_| format!("invalid {name} value"));
1097            }
1098        }
1099        Err(format!("features.h does not define {name}"))
1100    }
1101    Ok(Version {
1102        major: number(text, "__GLIBC__")?,
1103        minor: number(text, "__GLIBC_MINOR__")?,
1104        patch: 0,
1105    })
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use super::*;
1111
1112    #[test]
1113    fn parses_glibc_headers_without_running_preprocessor() {
1114        assert_eq!(
1115            header_version("#define __GLIBC__ 2\n# define __GLIBC_MINOR__ 22\n").unwrap(),
1116            "2.22".parse().unwrap()
1117        );
1118        assert!(header_version("#define __GLIBC__ 2").is_err());
1119    }
1120
1121    #[test]
1122    fn provider_links_tell_shared_libraries_and_drop_pie() {
1123        let args = |list: &[&str]| list.iter().map(OsString::from).collect::<Vec<_>>();
1124        let (shared, kept) = provider_link_args(args(&["a.o", "-pie", "-o", "app"]).into_iter());
1125        assert!(!shared);
1126        assert_eq!(kept, args(&["a.o", "-o", "app"]));
1127        let (shared, kept) =
1128            provider_link_args(args(&["a.o", "-shared", "-o", "lib.so"]).into_iter());
1129        assert!(shared);
1130        assert_eq!(kept, args(&["a.o", "-shared", "-o", "lib.so"]));
1131    }
1132
1133    #[test]
1134    fn cargo_cannot_override_managed_target() {
1135        for value in [
1136            "--target",
1137            "--target=aarch64-unknown-linux-gnu",
1138            "--config=target.x.linker=other",
1139            "--message-format=json",
1140            "--",
1141            "+nightly",
1142        ] {
1143            assert!(validate_cargo_args("build", &[value.into()]).is_err());
1144        }
1145        assert!(validate_cargo_args(
1146            "build",
1147            &[
1148                "--release".into(),
1149                "--target-dir".into(),
1150                "target with spaces".into()
1151            ]
1152        )
1153        .is_ok());
1154    }
1155
1156    #[test]
1157    fn runner_path_is_one_toml_string() {
1158        assert_eq!(
1159            toml_string("/tool path/libc-runner"),
1160            "\"/tool path/libc-runner\""
1161        );
1162        assert_eq!(toml_string("a\"b\\c\n"), "\"a\\\"b\\\\c\\u000A\"");
1163    }
1164
1165    #[test]
1166    fn path_with_spaces_is_one_driver_argument() {
1167        let cfg = Toolchain {
1168            provider: None,
1169            sysroot: "/sdk root".into(),
1170            gcc_runtime: "/sdk root/gcc".into(),
1171            cc: "/usr/bin/clang".into(),
1172            ld: "/tools/ld.bfd".into(),
1173            ceiling: "2.22".parse().unwrap(),
1174        };
1175        assert!(cfg
1176            .driver_args()
1177            .contains(&OsString::from("--sysroot=/sdk root")));
1178        let flags = cfg.rustflags(Path::new("/tool path/libc-linker")).unwrap();
1179        assert!(flags
1180            .to_str()
1181            .unwrap()
1182            .split('\u{1f}')
1183            .any(|s| s == "link-arg=--sysroot=/sdk root"));
1184    }
1185
1186    #[test]
1187    fn shell_values_are_single_quoted() {
1188        assert_eq!(shell_quote("/a b/c"), "'/a b/c'");
1189        assert_eq!(shell_quote("it's"), "'it'\\''s'");
1190    }
1191}