Skip to main content

cargo_libc/
toolchain.rs

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