Skip to main content

cargo_libc/
cli.rs

1use crate::{
2    report::{self, ScanOptions},
3    simulate::{self, Simulation},
4    toolchain::{self, Toolchain},
5    version::Version,
6};
7use libc_abi_tools::provider::cache::Mode;
8use std::{ffi::OsString, path::PathBuf};
9
10const HELP: &str = r#"cargo-libc — builds for an older glibc, and native ELF inspection
11
12USAGE
13  cargo libc build  --target x86_64-unknown-linux-gnu.2.28 --artifact FILE
14                    [OPTIONS] -- [CARGO BUILD ARGUMENTS]
15  cargo libc doctor --sysroot DIR --gcc-runtime DIR --glibc VERSION [OPTIONS]
16  cargo libc doctor (--distro ID | --kernel VERSION) [--runner R] [--image IMAGE]
17  cargo libc build  --sysroot DIR --gcc-runtime DIR --glibc VERSION
18                    --artifact FILE [OPTIONS] -- [CARGO BUILD ARGUMENTS]
19  cargo libc check  --sysroot DIR --gcc-runtime DIR --glibc VERSION
20                    [OPTIONS] [-- CARGO CHECK ARGUMENTS]
21  cargo libc test   --sysroot DIR --gcc-runtime DIR --glibc VERSION
22                    [OPTIONS] [-- CARGO TEST ARGUMENTS]
23  cargo libc run    (same as test) [-- CARGO RUN ARGUMENTS]
24  cargo libc clippy|doc|rustc  (same as check) [-- CARGO ARGUMENTS [-- TOOL ARGUMENTS]]
25  cargo libc install (same as build, without --artifact) [-- CARGO INSTALL ARGUMENTS]
26  cargo libc env    (same selection as build) [--format sh|cargo]
27  cargo libc inspect FILE [--max-glibc VERSION | --distro ID] [--kernel VERSION]
28                     [--scan] [--strict] [--verbose] [--json]
29  cargo libc provider prepare|list|path|verify|export|import|remove ...
30
31PROVIDERS
32Without --provider or --sysroot, build/check/test/run/doctor use a glibc link
33provider for the selected release (2.17 through 2.44), prepared on first use and
34cached (see `cargo libc provider`). The default mode, source, compiles glibc's
35startup objects from the recorded sources with the host's GCC (or
36$CARGO_LIBC_PROVIDER_GCC), offline, and verifies them against the qualified build;
37it refuses rather than use objects that differ. container runs the pinned
38container build (podman or docker).
39
40BUILD / DOCTOR OPTIONS
41  --provider-mode M  source (default) or container, for the automatic provider
42                     (also CARGO_LIBC_PROVIDER_MODE); prebuilt awaits a legal review
43  --provider FILE    glibc provider descriptor (see docs/PROVIDER-CONTRACT.md) instead of the automatic provider
44  --sysroot DIR      Target development filesystem, including glibc and GCC runtime
45  --gcc-runtime DIR  Absolute GCC runtime directory inside that sysroot
46  --glibc VERSION    Maximum permitted glibc version, at least 2.17 (e.g. 2.22)
47  --target TARGET    x86_64-unknown-linux-gnu[.GLIBC]; suffix supplies --glibc
48  --cc FILE          Host Clang driver (default: /usr/bin/clang)
49  --ld FILE          Host native linker (default: /usr/bin/ld.bfd)
50  --artifact FILE    Exact executable from this Cargo build to inspect (build only)
51  --distro ID        Target preset (e.g. sles-12-sp5): supplies --glibc and the
52                     kernel floor (build: tier 1 scan; test/run: simulation)
53  --kernel VERSION   Kernel floor without a preset
54  --strict           Scan warnings also fail (build only)
55  --runner R         local or container (test/run/doctor; default: container
56                     when an image is known, else local)
57  --image IMAGE      Container image (test/run/doctor; default: the preset's)
58  --dry-run          Probe target inputs and print the command without building
59
60test/run use the libc-runner helper as Cargo's target runner. It refuses a
61binary that needs a newer glibc than --glibc, then runs it under the kernel
62simulation (tier 2): the local seccomp filter keeps the host userland and needs
63no root; the container uses the image's userland (podman or docker, chosen by
64CONTAINER_ENGINE). Neither replaces testing on a real target host (tier 3).
65
66check type-checks with the same target selection and flags as build, so host
67build scripts and procedural macros stay native. It does not link, so it
68proves nothing about the final executable's glibc or kernel requirements.
69clippy and doc work the same way (doc also gives rustdoc the flags); rustc
70builds and may link, but checks nothing: inspect its output. Arguments after
71Cargo's own -- (clippy, rustc) must not replace the target or the linker.
72
73install runs cargo install with the build flags, then checks every executable
74it built like build does (--distro/--kernel add the tier 1 scan, --strict).
75The check runs after Cargo installed them; a failure exits with 2.
76
77env prints the configuration build/test apply, for plain Cargo with
78`--target x86_64-unknown-linux-gnu`: shell exports (--format sh, default) or a
79Cargo configuration file (--format cargo, for --config FILE). It includes the
80libc-runner target runner (with --distro/--kernel/--runner/--image) and the C
81compiler wrappers. Plain Cargo does not check the output; use inspect.
82
83doctor with --distro or --kernel also checks both runners: it installs the
84filter and runs /bin/sh under it, and starts `true` in the image under the
85profile (which may pull the image). It exits with 2 when the runner test/run
86would choose is not usable. Without --sysroot/--provider it checks runners only.
87
88INSPECT OPTIONS
89  --max-glibc VERSION  Fail when a mandatory GLIBC_* requirement is newer
90  --distro ID          Target preset (e.g. sles-12-sp5): sets the glibc and
91                       kernel floors and runs the tier 1 scan
92  --kernel VERSION     Kernel floor (e.g. 3.10); runs the tier 1 scan
93  --scan               Run the tier 1 scan with only the --max-glibc floor
94  --strict             Scan warnings (heuristic or optional use) also fail
95  --verbose            Show the scan's informational findings
96  --json               Machine-readable report
97
98GENERAL
99  -h, --help         Show this help
100  -V, --version      Show the tool version
101
102This prototype supports x86-64 Linux dynamically linked Rust executables.
103With --sysroot it uses that existing target sysroot; --glibc does not create or
104downgrade one. It uses a Rust linker helper, a host Clang driver, and the selected
105native linker. It does not use Zig, Python, or third-party Rust crates.
106
107The ELF check covers direct version requirements in the selected file.
108The tier 1 scan (linux-abi-scan) adds kernel checks: imported syscall
109wrappers and DT_RELR exactly, constant syscall()/madvise() call sites
110heuristically. Neither resolves dependent libraries or replaces running the
111program under a kernel simulation or on the target OS.
112
113C DEPENDENCIES
114build/check/test/run point the cc crate (and CMake through it) at libc-cc for
115the target: Clang with the selected provider's or sysroot's glibc headers, and
116for links its startup objects and stubs. Build scripts and procedural macros
117keep the host compiler (HOST_CC, CC, else cc). C++ for the target is refused
118(no target C++ runtime), and so is -static. pkg-config and other system
119libraries found on the host are not configured for the target.
120
121Use separate values for options, e.g. --glibc 2.22 (not --glibc=2.22).
122Direct invocation as `cargo-libc ...` is also supported.
123"#;
124
125#[derive(Debug)]
126struct RawConfig {
127    provider: Option<PathBuf>,
128    /// Prepare (or reuse) a provider in this mode: no provider or sysroot given.
129    auto: Option<Mode>,
130    sysroot: PathBuf,
131    gcc_runtime: PathBuf,
132    cc: PathBuf,
133    ld: PathBuf,
134    ceiling: Version,
135}
136
137impl RawConfig {
138    fn resolve(self) -> Result<Toolchain, String> {
139        if let Some(mode) = self.auto {
140            let release = self.ceiling.to_string();
141            let gcc = std::env::var_os("CARGO_LIBC_PROVIDER_GCC").map(PathBuf::from);
142            let prepared = libc_abi_tools::provider::cli::ensure(&release, mode, gcc)
143                .map_err(|e| format!("cannot prepare the glibc {release} provider: {e}"))?;
144            let descriptor = prepared.entry.descriptor();
145            eprintln!(
146                "glibc {release} provider ({mode} mode, {}): {}",
147                if prepared.reused {
148                    "cached"
149                } else {
150                    "prepared"
151                },
152                descriptor.display()
153            );
154            for note in &prepared.notes {
155                eprintln!("  {note}");
156            }
157            return Toolchain::from_provider(&descriptor, self.cc, self.ld, self.ceiling);
158        }
159        if let Some(path) = self.provider {
160            return Toolchain::from_provider(&path, self.cc, self.ld, self.ceiling);
161        }
162        Toolchain::new(
163            self.sysroot,
164            self.gcc_runtime,
165            self.cc,
166            self.ld,
167            self.ceiling,
168        )
169    }
170}
171
172#[derive(Debug)]
173enum ParsedCommand {
174    Help,
175    Version,
176    Doctor {
177        config: Option<RawConfig>,
178        simulation: Simulation,
179    },
180    Build {
181        config: RawConfig,
182        artifact: PathBuf,
183        scan: Option<ScanOptions>,
184        dry_run: bool,
185        cargo_args: Vec<OsString>,
186    },
187    /// check, clippy, doc, rustc.
188    Passthrough {
189        config: RawConfig,
190        subcommand: &'static str,
191        dry_run: bool,
192        cargo_args: Vec<OsString>,
193    },
194    Install {
195        config: RawConfig,
196        scan: Option<ScanOptions>,
197        dry_run: bool,
198        cargo_args: Vec<OsString>,
199    },
200    Env {
201        config: RawConfig,
202        simulation: Simulation,
203        format: toolchain::EnvFormat,
204    },
205    Test {
206        config: RawConfig,
207        subcommand: &'static str,
208        simulation: Simulation,
209        dry_run: bool,
210        cargo_args: Vec<OsString>,
211    },
212    Inspect {
213        file: PathBuf,
214        ceiling: Option<Version>,
215        scan: Option<ScanOptions>,
216        json: bool,
217    },
218    /// `cargo libc provider ...`: the arguments after `provider`.
219    Provider(Vec<OsString>),
220}
221
222fn next_value(args: &[OsString], i: &mut usize, flag: &str) -> Result<OsString, String> {
223    *i += 1;
224    let value = args
225        .get(*i)
226        .ok_or_else(|| format!("{flag} needs a value"))?;
227    if value.is_empty() || value == "--" {
228        return Err(format!("{flag} needs a nonempty value"));
229    }
230    Ok(value.clone())
231}
232
233fn parse_version(value: OsString, flag: &str) -> Result<Version, String> {
234    value
235        .to_str()
236        .ok_or_else(|| format!("{flag} must be UTF-8"))?
237        .parse::<Version>()?
238        .validate_glibc_target()
239}
240
241fn parse(mut args: Vec<OsString>) -> Result<ParsedCommand, String> {
242    // Cargo includes the external subcommand's name in argv.
243    if args.first().is_some_and(|arg| arg == "libc") {
244        args.remove(0);
245    }
246    let Some(command) = args.first() else {
247        return Ok(ParsedCommand::Help);
248    };
249    match command.to_str() {
250        Some("-h" | "--help" | "help") => return Ok(ParsedCommand::Help),
251        Some("-V" | "--version") => return Ok(ParsedCommand::Version),
252        Some("inspect") => return parse_inspect(&args[1..]),
253        Some("provider") => return Ok(ParsedCommand::Provider(args[1..].to_vec())),
254        Some(
255            "doctor" | "build" | "check" | "test" | "run" | "clippy" | "doc" | "rustc" | "install"
256            | "env",
257        ) => {}
258        _ => {
259            return Err(format!(
260                "unknown command {command:?}; run cargo libc --help"
261            ))
262        }
263    }
264    let subcommand: &'static str = match command.to_str() {
265        Some("build") => "build",
266        Some("check") => "check",
267        Some("test") => "test",
268        Some("run") => "run",
269        Some("clippy") => "clippy",
270        Some("doc") => "doc",
271        Some("rustc") => "rustc",
272        Some("install") => "install",
273        Some("env") => "env",
274        _ => "doctor",
275    };
276    let build = subcommand == "build";
277    // build and install check the executables they build.
278    let checks = build || subcommand == "install";
279    let cargo_command = !matches!(subcommand, "doctor" | "env");
280    let runs = matches!(subcommand, "test" | "run");
281    let configures_runner = runs || !cargo_command;
282    let mut format = None;
283    let mut simulation = Simulation::default();
284    let mut strict = false;
285    let mut provider = None;
286    let mut mode = None;
287    let mut sysroot = None;
288    let mut gcc_runtime = None;
289    let mut ceiling = None;
290    let mut target = None;
291    let mut cc = PathBuf::from("/usr/bin/clang");
292    let mut ld = PathBuf::from("/usr/bin/ld.bfd");
293    let mut artifact = None;
294    let mut dry_run = false;
295    let mut cargo_args = Vec::new();
296    let mut i = 1;
297    while i < args.len() {
298        match args[i].to_str() {
299            Some("-h" | "--help") => return Ok(ParsedCommand::Help),
300            Some("--provider") => {
301                if provider.is_some() {
302                    return Err("duplicate --provider".into());
303                }
304                provider = Some(PathBuf::from(next_value(&args, &mut i, "--provider")?));
305            }
306            Some("--provider-mode") => {
307                let value = next_value(&args, &mut i, "--provider-mode")?;
308                let parsed = value
309                    .to_str()
310                    .ok_or("--provider-mode must be UTF-8")?
311                    .parse::<Mode>()?;
312                if mode.replace(parsed).is_some() {
313                    return Err("duplicate --provider-mode".into());
314                }
315            }
316            Some("--sysroot") => {
317                sysroot = Some(PathBuf::from(next_value(&args, &mut i, "--sysroot")?))
318            }
319            Some("--gcc-runtime") => {
320                gcc_runtime = Some(PathBuf::from(next_value(&args, &mut i, "--gcc-runtime")?))
321            }
322            Some("--cc") => cc = PathBuf::from(next_value(&args, &mut i, "--cc")?),
323            Some("--ld") => ld = PathBuf::from(next_value(&args, &mut i, "--ld")?),
324            Some("--target") => {
325                if target.is_some() {
326                    return Err("duplicate --target".into());
327                }
328                let value = next_value(&args, &mut i, "--target")?;
329                let text = value.to_str().ok_or("--target must be UTF-8")?;
330                let (triple, suffix) = text
331                    .split_once('.')
332                    .map_or((text, None), |(t, v)| (t, Some(v)));
333                if triple != toolchain::TARGET {
334                    return Err(format!(
335                        "unsupported target {triple:?}; expected {}",
336                        toolchain::TARGET
337                    ));
338                }
339                target = Some(
340                    suffix
341                        .map(|v| parse_version(v.into(), "--target"))
342                        .transpose()?,
343                );
344            }
345            Some("--glibc") => {
346                if ceiling.is_some() {
347                    return Err("duplicate --glibc".into());
348                }
349                ceiling = Some(parse_version(
350                    next_value(&args, &mut i, "--glibc")?,
351                    "--glibc",
352                )?)
353            }
354            Some("--artifact") if build => {
355                artifact = Some(PathBuf::from(next_value(&args, &mut i, "--artifact")?))
356            }
357            Some(flag @ ("--distro" | "--kernel")) => {
358                let value = next_value(&args, &mut i, flag)?
359                    .into_string()
360                    .map_err(|_| format!("{flag} must be UTF-8"))?;
361                let slot = if flag == "--distro" {
362                    &mut simulation.distro
363                } else {
364                    &mut simulation.kernel
365                };
366                if slot.replace(value).is_some() {
367                    return Err(format!("duplicate {flag}"));
368                }
369            }
370            Some("--strict") if checks => strict = true,
371            Some("--format") if subcommand == "env" => {
372                let value = next_value(&args, &mut i, "--format")?;
373                let parsed = match value.to_str() {
374                    Some("sh") => toolchain::EnvFormat::Shell,
375                    Some("cargo") => toolchain::EnvFormat::Cargo,
376                    _ => return Err(format!("--format must be sh or cargo, not {value:?}")),
377                };
378                if format.replace(parsed).is_some() {
379                    return Err("duplicate --format".into());
380                }
381            }
382            Some("--runner") if configures_runner => {
383                let value = next_value(&args, &mut i, "--runner")?;
384                let runner = kernel_abi_tools::Runner::parse(
385                    value.to_str().ok_or("--runner must be UTF-8")?,
386                )?;
387                if simulation.runner.replace(runner).is_some() {
388                    return Err("duplicate --runner".into());
389                }
390            }
391            Some("--image") if configures_runner => {
392                let value = next_value(&args, &mut i, "--image")?
393                    .into_string()
394                    .map_err(|_| "--image must be UTF-8".to_string())?;
395                if simulation.image.replace(value).is_some() {
396                    return Err("duplicate --image".into());
397                }
398            }
399            Some("--dry-run") if cargo_command => dry_run = true,
400            Some("--") if cargo_command => {
401                cargo_args.extend_from_slice(&args[i + 1..]);
402                break;
403            }
404            _ => {
405                return Err(format!(
406                    "unexpected option {:?}; Cargo arguments belong after --",
407                    args[i]
408                ))
409            }
410        }
411        i += 1;
412    }
413    if provider.is_some() && (sysroot.is_some() || gcc_runtime.is_some()) {
414        return Err("--provider conflicts with --sysroot and --gcc-runtime".into());
415    }
416    let explicit = provider.is_some() || sysroot.is_some() || gcc_runtime.is_some();
417    if mode.is_some() && explicit {
418        return Err(
419            "--provider-mode selects the automatic provider; it conflicts with --provider, --sysroot and --gcc-runtime"
420                .into(),
421        );
422    }
423    if let Some(Some(version)) = target {
424        if ceiling.is_some_and(|explicit| explicit != version) {
425            return Err("--target version conflicts with --glibc".into());
426        }
427        ceiling = Some(version);
428    }
429    if simulation.distro.is_some() && simulation.kernel.is_some() {
430        return Err("--distro sets the kernel floor; do not combine it with --kernel".into());
431    }
432    if let Some(id) = &simulation.distro {
433        if ceiling.is_some() {
434            return Err(format!(
435                "--distro {id} sets the glibc version; do not combine it with --glibc or a versioned --target"
436            ));
437        }
438        let preset = linux_targets::distro(id).ok_or_else(|| format!("unknown distro {id:?}"))?;
439        ceiling = Some(parse_version(preset.glibc.to_string().into(), "--distro")?);
440    }
441    // doctor --distro/--kernel without toolchain inputs checks the runners only.
442    if subcommand == "doctor"
443        && !explicit
444        && mode.is_none()
445        && (simulation.distro.is_some() || simulation.kernel.is_some())
446    {
447        simulation.plan()?;
448        return Ok(ParsedCommand::Doctor {
449            config: None,
450            simulation,
451        });
452    }
453    let auto = if explicit {
454        None
455    } else {
456        Some(match mode {
457            Some(mode) => mode,
458            None => match std::env::var("CARGO_LIBC_PROVIDER_MODE") {
459                Ok(value) => value
460                    .parse()
461                    .map_err(|e| format!("CARGO_LIBC_PROVIDER_MODE: {e}"))?,
462                Err(_) => Mode::Source,
463            },
464        })
465    };
466    let sysroot_mode = provider.is_none() && auto.is_none();
467    let config = RawConfig {
468        provider: provider.clone(),
469        auto,
470        sysroot: if sysroot_mode {
471            sysroot.ok_or("--sysroot is required")?
472        } else {
473            PathBuf::new()
474        },
475        gcc_runtime: if sysroot_mode {
476            gcc_runtime.ok_or("--gcc-runtime is required")?
477        } else {
478            PathBuf::new()
479        },
480        cc,
481        ld,
482        ceiling: ceiling.ok_or("--glibc is required")?,
483    };
484    if checks {
485        toolchain::validate_cargo_args(subcommand, &cargo_args)?;
486        let scan =
487            (simulation.distro.is_some() || simulation.kernel.is_some()).then(|| ScanOptions {
488                distro: simulation.distro.clone(),
489                kernel: simulation.kernel.clone(),
490                strict,
491                verbose: false,
492            });
493        if strict && scan.is_none() {
494            return Err("--strict applies to the scan; add --distro or --kernel".into());
495        }
496        if subcommand == "install" {
497            return Ok(ParsedCommand::Install {
498                config,
499                scan,
500                dry_run,
501                cargo_args,
502            });
503        }
504        Ok(ParsedCommand::Build {
505            config,
506            artifact: artifact.ok_or(
507                "--artifact is required; name the exact executable produced by this build",
508            )?,
509            scan,
510            dry_run,
511            cargo_args,
512        })
513    } else if matches!(subcommand, "check" | "clippy" | "doc" | "rustc") {
514        if simulation.kernel.is_some() {
515            return Err(format!(
516                "{subcommand} does not check or run anything; --kernel applies to build, install, test, run, env and doctor"
517            ));
518        }
519        toolchain::validate_cargo_args(subcommand, &cargo_args)?;
520        Ok(ParsedCommand::Passthrough {
521            config,
522            subcommand,
523            dry_run,
524            cargo_args,
525        })
526    } else if subcommand == "env" {
527        simulation.plan()?;
528        Ok(ParsedCommand::Env {
529            config,
530            simulation,
531            format: format.unwrap_or(toolchain::EnvFormat::Shell),
532        })
533    } else if runs {
534        toolchain::validate_cargo_args(subcommand, &cargo_args)?;
535        simulation.plan()?;
536        Ok(ParsedCommand::Test {
537            config,
538            subcommand,
539            simulation,
540            dry_run,
541            cargo_args,
542        })
543    } else {
544        simulation.plan()?;
545        Ok(ParsedCommand::Doctor {
546            config: Some(config),
547            simulation,
548        })
549    }
550}
551
552fn parse_inspect(args: &[OsString]) -> Result<ParsedCommand, String> {
553    let mut file = None;
554    let mut ceiling = None;
555    let mut json = false;
556    let mut scan = ScanOptions::default();
557    let mut scan_requested = false;
558    let mut positional_only = false;
559    let mut i = 0;
560    while i < args.len() {
561        if !positional_only {
562            match args[i].to_str() {
563                Some("-h" | "--help") => return Ok(ParsedCommand::Help),
564                Some("--max-glibc") => {
565                    ceiling = Some(parse_version(
566                        next_value(args, &mut i, "--max-glibc")?,
567                        "--max-glibc",
568                    )?);
569                    i += 1;
570                    continue;
571                }
572                Some("--json") => {
573                    json = true;
574                    i += 1;
575                    continue;
576                }
577                Some(flag @ ("--distro" | "--kernel")) => {
578                    let value = next_value(args, &mut i, flag)?
579                        .into_string()
580                        .map_err(|_| format!("{flag} must be UTF-8"))?;
581                    let slot = if flag == "--distro" {
582                        &mut scan.distro
583                    } else {
584                        &mut scan.kernel
585                    };
586                    if slot.replace(value).is_some() {
587                        return Err(format!("duplicate {flag}"));
588                    }
589                    i += 1;
590                    continue;
591                }
592                Some(flag @ ("--scan" | "--strict" | "--verbose")) => {
593                    match flag {
594                        "--scan" => scan_requested = true,
595                        "--strict" => scan.strict = true,
596                        _ => scan.verbose = true,
597                    }
598                    i += 1;
599                    continue;
600                }
601                Some("--") => {
602                    positional_only = true;
603                    i += 1;
604                    continue;
605                }
606                Some(s) if s.starts_with('-') => {
607                    return Err(format!("unknown inspect option {s:?}"))
608                }
609                _ => {}
610            }
611        }
612        if file.replace(PathBuf::from(&args[i])).is_some() {
613            return Err("inspect accepts exactly one ELF file".into());
614        }
615        i += 1;
616    }
617    if scan.distro.is_some() && ceiling.is_some() {
618        return Err("--distro sets the glibc floor; do not combine it with --max-glibc".into());
619    }
620    let scan = if scan_requested || scan.distro.is_some() || scan.kernel.is_some() {
621        Some(scan)
622    } else if scan.strict || scan.verbose {
623        return Err(
624            "--strict and --verbose apply to the scan; add --distro, --kernel, or --scan".into(),
625        );
626    } else {
627        None
628    };
629    Ok(ParsedCommand::Inspect {
630        file: file.ok_or("inspect requires an ELF file path")?,
631        ceiling,
632        scan,
633        json,
634    })
635}
636
637pub fn run(args: impl IntoIterator<Item = OsString>) -> Result<i32, String> {
638    match parse(args.into_iter().collect())? {
639        ParsedCommand::Help => {
640            print!("{HELP}");
641            Ok(0)
642        }
643        ParsedCommand::Version => {
644            println!("cargo-libc {}", env!("CARGO_PKG_VERSION"));
645            Ok(0)
646        }
647        ParsedCommand::Doctor { config, simulation } => {
648            if let Some(raw) = config {
649                let config = raw.resolve()?;
650                config.print_diagnostics(&config.diagnose()?);
651            }
652            let runner_ok = simulate::doctor(&simulation)?;
653            Ok(if runner_ok { 0 } else { 2 })
654        }
655        ParsedCommand::Build {
656            config,
657            artifact,
658            scan,
659            dry_run,
660            cargo_args,
661        } => toolchain::build(
662            &config.resolve()?,
663            &cargo_args,
664            &artifact,
665            scan.as_ref(),
666            dry_run,
667        ),
668        ParsedCommand::Passthrough {
669            config,
670            subcommand,
671            dry_run,
672            cargo_args,
673        } => toolchain::cargo_passthrough(&config.resolve()?, subcommand, &cargo_args, dry_run),
674        ParsedCommand::Install {
675            config,
676            scan,
677            dry_run,
678            cargo_args,
679        } => toolchain::install(&config.resolve()?, &cargo_args, scan.as_ref(), dry_run),
680        ParsedCommand::Env {
681            config,
682            simulation,
683            format,
684        } => {
685            print!(
686                "{}",
687                toolchain::plain_cargo(&config.resolve()?, &simulation, format)?
688            );
689            Ok(0)
690        }
691        ParsedCommand::Test {
692            config,
693            subcommand,
694            simulation,
695            dry_run,
696            cargo_args,
697        } => toolchain::test_or_run(
698            &config.resolve()?,
699            subcommand,
700            &cargo_args,
701            &simulation,
702            dry_run,
703        ),
704        ParsedCommand::Inspect {
705            file,
706            ceiling,
707            scan,
708            json,
709        } => report::inspect_file(&file, ceiling, scan.as_ref(), json),
710        ParsedCommand::Provider(args) => {
711            if args.is_empty() || matches!(args[0].to_str(), Some("-h" | "--help" | "help")) {
712                print!("{}", libc_abi_tools::provider::cli::help("cargo libc"));
713                return Ok(0);
714            }
715            libc_abi_tools::provider::cli::run(&args, "cargo libc")?;
716            Ok(0)
717        }
718    }
719}
720
721#[cfg(test)]
722mod tests {
723    use super::*;
724    #[test]
725    fn provider_selection_is_explicit_and_excludes_sysroot_options() {
726        let base = [
727            "doctor",
728            "--provider",
729            "/sdk/provider.txt",
730            "--glibc",
731            "2.17",
732        ];
733        let ParsedCommand::Doctor {
734            config: Some(config),
735            ..
736        } = parse(args(&base)).unwrap()
737        else {
738            panic!("wrong command")
739        };
740        assert_eq!(config.provider, Some(PathBuf::from("/sdk/provider.txt")));
741        for extra in [
742            ["--sysroot", "/sdk"],
743            ["--gcc-runtime", "/sdk/gcc"],
744            ["--provider", "/other"],
745        ] {
746            let mut values = base.to_vec();
747            values.extend(extra);
748            assert!(parse(args(&values)).is_err());
749        }
750        assert!(parse(args(&["doctor", "--provider", "/sdk/provider.txt"])).is_err());
751    }
752    #[test]
753    fn versioned_target_selects_ceiling_and_preserves_cargo_arguments() {
754        for &version in crate::provider::RELEASES {
755            let target = format!("{}.{}", toolchain::TARGET, version);
756            let ParsedCommand::Build {
757                config, cargo_args, ..
758            } = parse(args(&[
759                "build",
760                "--provider",
761                "/sdk/provider.txt",
762                "--target",
763                &target,
764                "--artifact",
765                "target/app",
766                "--",
767                "--release",
768            ]))
769            .unwrap()
770            else {
771                panic!("wrong command")
772            };
773            assert_eq!(config.ceiling, version.parse().unwrap());
774            assert_eq!(cargo_args, args(&["--release"]));
775        }
776    }
777
778    #[test]
779    fn target_selection_rejects_conflicts_and_malformed_requests() {
780        for extra in [
781            vec!["--target", "x86_64-unknown-linux-gnu.2.18"],
782            vec!["--target", "aarch64-unknown-linux-gnu.2.17"],
783            vec!["--target", "x86_64-unknown-linux-gnu."],
784            vec!["--target", "x86_64-unknown-linux-gnu.2.16"],
785            vec!["--target", "x86_64-unknown-linux-gnu.2.17.extra"],
786            vec![
787                "--target",
788                "x86_64-unknown-linux-gnu",
789                "--target",
790                "x86_64-unknown-linux-gnu",
791            ],
792            vec!["--glibc", "2.17"],
793        ] {
794            let mut argv = vec![
795                "doctor",
796                "--provider",
797                "/sdk/provider.txt",
798                "--glibc",
799                "2.17",
800            ];
801            argv.extend(extra);
802            assert!(parse(args(&argv)).is_err(), "{argv:?}");
803        }
804        for target in ["x86_64-unknown-linux-gnu", "x86_64-unknown-linux-gnu.2.17"] {
805            assert!(parse(args(&[
806                "doctor",
807                "--provider",
808                "/sdk/provider.txt",
809                "--glibc",
810                "2.17",
811                "--target",
812                target
813            ]))
814            .is_ok());
815        }
816        assert!(parse(args(&[
817            "doctor",
818            "--provider",
819            "/sdk/provider.txt",
820            "--target",
821            toolchain::TARGET
822        ]))
823        .is_err());
824    }
825
826    #[test]
827    fn without_provider_or_sysroot_a_provider_is_prepared_automatically() {
828        let build = |extra: &[&str]| {
829            let mut argv = vec![
830                "build",
831                "--target",
832                "x86_64-unknown-linux-gnu.2.28",
833                "--artifact",
834                "app",
835            ];
836            argv.extend(extra);
837            parse(args(&argv))
838        };
839        let ParsedCommand::Build { config, .. } = build(&[]).unwrap() else {
840            panic!("wrong command")
841        };
842        assert_eq!(config.auto, Some(Mode::Source));
843        assert_eq!(config.ceiling, "2.28".parse().unwrap());
844        let ParsedCommand::Build { config, .. } = build(&["--provider-mode", "container"]).unwrap()
845        else {
846            panic!("wrong command")
847        };
848        assert_eq!(config.auto, Some(Mode::Container));
849        let ParsedCommand::Build { config, .. } = build(&["--provider", "/p.txt"]).unwrap() else {
850            panic!("wrong command")
851        };
852        assert_eq!(config.auto, None);
853        for extra in [
854            &["--provider-mode", "binary"][..],
855            &["--provider-mode", "source", "--provider-mode", "source"],
856            &["--provider-mode", "source", "--provider", "/p.txt"],
857            &["--provider-mode", "source", "--sysroot", "/sdk"],
858            &["--sysroot", "/sdk"],
859        ] {
860            assert!(build(extra).is_err(), "{extra:?}");
861        }
862        let ParsedCommand::Provider(rest) = parse(args(&["libc", "provider", "list"])).unwrap()
863        else {
864            panic!("wrong command")
865        };
866        assert_eq!(rest, args(&["list"]));
867    }
868
869    fn args(values: &[&str]) -> Vec<OsString> {
870        values.iter().map(OsString::from).collect()
871    }
872
873    #[test]
874    fn cargo_subcommand_preserves_paths_and_passthrough() {
875        let parsed = parse(args(&[
876            "libc",
877            "build",
878            "--sysroot",
879            "/sdk root",
880            "--gcc-runtime",
881            "/sdk root/gcc",
882            "--glibc",
883            "2.22",
884            "--artifact",
885            "target/app name",
886            "--",
887            "--release",
888            "--offline",
889        ]))
890        .unwrap();
891        let ParsedCommand::Build {
892            config,
893            artifact,
894            cargo_args,
895            ..
896        } = parsed
897        else {
898            panic!("wrong command")
899        };
900        assert_eq!(config.sysroot, PathBuf::from("/sdk root"));
901        assert_eq!(artifact, PathBuf::from("target/app name"));
902        assert_eq!(cargo_args, args(&["--release", "--offline"]));
903    }
904
905    #[test]
906    fn incomplete_and_ambiguous_commands_are_errors() {
907        for argv in [
908            vec!["build", "--sysroot"],
909            vec!["inspect"],
910            vec!["inspect", "a", "b"],
911            vec!["install"],
912            vec!["doctor", "--dry-run"],
913        ] {
914            assert!(parse(args(&argv)).is_err(), "{argv:?}");
915        }
916    }
917
918    #[test]
919    fn inspect_supports_literal_dash_filename_and_numeric_ceiling() {
920        let parsed = parse(args(&[
921            "inspect",
922            "--max-glibc",
923            "2.22",
924            "--json",
925            "--",
926            "-app",
927        ]))
928        .unwrap();
929        let ParsedCommand::Inspect {
930            file,
931            ceiling,
932            scan,
933            json,
934        } = parsed
935        else {
936            panic!("wrong command")
937        };
938        assert_eq!(file, PathBuf::from("-app"));
939        assert_eq!(ceiling, Some("2.22".parse().unwrap()));
940        assert!(scan.is_none(), "the scan runs only when asked for");
941        assert!(json);
942    }
943
944    const SDK: [&str; 4] = ["--sysroot", "/sdk", "--gcc-runtime", "/sdk/gcc"];
945
946    fn with_sdk(command: &str, extra: &[&str]) -> Vec<OsString> {
947        let mut argv = vec![command];
948        argv.extend(SDK);
949        argv.extend(extra);
950        args(&argv)
951    }
952
953    #[test]
954    fn distro_supplies_the_glibc_version_and_excludes_explicit_ones() {
955        let ParsedCommand::Build { config, scan, .. } = parse(with_sdk(
956            "build",
957            &["--distro", "sles-12-sp5", "--strict", "--artifact", "app"],
958        ))
959        .unwrap() else {
960            panic!("wrong command")
961        };
962        assert_eq!(config.ceiling, "2.22".parse().unwrap());
963        let scan = scan.unwrap();
964        assert!(scan.strict && scan.distro.as_deref() == Some("sles-12-sp5"));
965        let ParsedCommand::Build { scan, .. } =
966            parse(with_sdk("build", &["--glibc", "2.17", "--artifact", "app"])).unwrap()
967        else {
968            panic!("wrong command")
969        };
970        assert!(scan.is_none(), "no scan without a target");
971        for extra in [
972            vec!["--distro", "rhel-7", "--glibc", "2.17", "--artifact", "app"],
973            vec![
974                "--distro",
975                "rhel-7",
976                "--target",
977                "x86_64-unknown-linux-gnu.2.17",
978                "--artifact",
979                "app",
980            ],
981            vec![
982                "--distro",
983                "rhel-7",
984                "--kernel",
985                "3.10",
986                "--artifact",
987                "app",
988            ],
989            vec!["--distro", "no-such", "--artifact", "app"],
990            vec!["--glibc", "2.17", "--strict", "--artifact", "app"],
991            vec!["--glibc", "2.17", "--runner", "local", "--artifact", "app"],
992        ] {
993            assert!(parse(with_sdk("build", &extra)).is_err(), "{extra:?}");
994        }
995    }
996
997    #[test]
998    fn test_and_run_carry_the_simulation_and_cargo_arguments() {
999        let ParsedCommand::Test {
1000            config,
1001            subcommand,
1002            simulation,
1003            cargo_args,
1004            ..
1005        } = parse(with_sdk(
1006            "test",
1007            &[
1008                "--distro",
1009                "rhel-7",
1010                "--runner",
1011                "local",
1012                "--",
1013                "--release",
1014                "--lib",
1015            ],
1016        ))
1017        .unwrap()
1018        else {
1019            panic!("wrong command")
1020        };
1021        assert_eq!(subcommand, "test");
1022        assert_eq!(config.ceiling, "2.17".parse().unwrap());
1023        assert_eq!(simulation.runner, Some(kernel_abi_tools::Runner::Local));
1024        assert_eq!(cargo_args, args(&["--release", "--lib"]));
1025        let ParsedCommand::Test {
1026            subcommand,
1027            simulation,
1028            ..
1029        } = parse(with_sdk(
1030            "run",
1031            &["--glibc", "2.17", "--kernel", "3.10", "--image", "centos:7"],
1032        ))
1033        .unwrap()
1034        else {
1035            panic!("wrong command")
1036        };
1037        assert_eq!(subcommand, "run");
1038        assert_eq!(simulation.image.as_deref(), Some("centos:7"));
1039        for extra in [
1040            vec!["--glibc", "2.17", "--runner", "local"],
1041            vec![
1042                "--glibc",
1043                "2.17",
1044                "--kernel",
1045                "3.10",
1046                "--runner",
1047                "container",
1048            ],
1049            vec!["--glibc", "2.17", "--kernel", "3.10", "--runner", "vm"],
1050            vec!["--glibc", "2.17", "--strict"],
1051            vec!["--glibc", "2.17", "--artifact", "app"],
1052            vec!["--glibc", "2.17", "--", "--config", "x=1"],
1053        ] {
1054            assert!(parse(with_sdk("test", &extra)).is_err(), "{extra:?}");
1055        }
1056    }
1057
1058    #[test]
1059    fn check_uses_the_build_selection_without_an_artifact() {
1060        let ParsedCommand::Passthrough {
1061            config,
1062            dry_run,
1063            cargo_args,
1064            ..
1065        } = parse(with_sdk(
1066            "check",
1067            &[
1068                "--target",
1069                "x86_64-unknown-linux-gnu.2.17",
1070                "--dry-run",
1071                "--",
1072                "--workspace",
1073                "--all-targets",
1074            ],
1075        ))
1076        .unwrap()
1077        else {
1078            panic!("wrong command")
1079        };
1080        assert_eq!(config.ceiling, "2.17".parse().unwrap());
1081        assert!(dry_run);
1082        assert_eq!(cargo_args, args(&["--workspace", "--all-targets"]));
1083        let ParsedCommand::Passthrough { config, .. } =
1084            parse(with_sdk("check", &["--distro", "sles-12-sp5"])).unwrap()
1085        else {
1086            panic!("wrong command")
1087        };
1088        assert_eq!(config.ceiling, "2.22".parse().unwrap());
1089        for extra in [
1090            vec!["--glibc", "2.17", "--artifact", "app"],
1091            vec!["--glibc", "2.17", "--kernel", "3.10"],
1092            vec!["--glibc", "2.17", "--strict"],
1093            vec!["--glibc", "2.17", "--runner", "local"],
1094            vec!["--glibc", "2.17", "--image", "centos:7"],
1095            vec!["--glibc", "2.17", "--target", "aarch64-unknown-linux-gnu"],
1096            vec![
1097                "--glibc",
1098                "2.17",
1099                "--target",
1100                "x86_64-unknown-linux-gnu.2.18",
1101            ],
1102            vec![
1103                "--glibc",
1104                "2.17",
1105                "--",
1106                "--target",
1107                "x86_64-unknown-linux-gnu",
1108            ],
1109            vec!["--glibc", "2.17", "--", "--message-format=json"],
1110            vec!["--glibc", "2.16"],
1111            vec![],
1112        ] {
1113            assert!(parse(with_sdk("check", &extra)).is_err(), "{extra:?}");
1114        }
1115    }
1116
1117    #[test]
1118    fn doctor_checks_runners_with_or_without_toolchain_inputs() {
1119        let ParsedCommand::Doctor { config, simulation } =
1120            parse(args(&["doctor", "--kernel", "3.10", "--runner", "local"])).unwrap()
1121        else {
1122            panic!("wrong command")
1123        };
1124        assert!(config.is_none(), "runner checks only");
1125        assert_eq!(simulation.kernel.as_deref(), Some("3.10"));
1126        let ParsedCommand::Doctor { config, simulation } =
1127            parse(with_sdk("doctor", &["--distro", "sles-12-sp5"])).unwrap()
1128        else {
1129            panic!("wrong command")
1130        };
1131        assert_eq!(config.unwrap().ceiling, "2.22".parse().unwrap());
1132        assert_eq!(simulation.distro.as_deref(), Some("sles-12-sp5"));
1133        for argv in [
1134            vec!["doctor", "--runner", "local"],
1135            vec!["doctor", "--kernel", "3.10", "--runner", "container"],
1136            vec!["doctor", "--image", "centos:7"],
1137            vec!["doctor"],
1138        ] {
1139            assert!(parse(args(&argv)).is_err(), "{argv:?}");
1140        }
1141    }
1142
1143    fn inspect_scan(argv: &[&str]) -> Option<ScanOptions> {
1144        let ParsedCommand::Inspect { scan, .. } = parse(args(argv)).unwrap() else {
1145            panic!("wrong command")
1146        };
1147        scan
1148    }
1149
1150    #[test]
1151    fn inspect_target_options_select_the_scan() {
1152        let scan =
1153            inspect_scan(&["inspect", "app", "--distro", "sles-12-sp5", "--strict"]).unwrap();
1154        assert_eq!(scan.distro.as_deref(), Some("sles-12-sp5"));
1155        assert!(scan.strict && !scan.verbose);
1156        let scan =
1157            inspect_scan(&["inspect", "app", "--max-glibc", "2.17", "--kernel", "3.10"]).unwrap();
1158        assert_eq!(scan.kernel.as_deref(), Some("3.10"));
1159        assert!(inspect_scan(&["inspect", "app", "--max-glibc", "2.17", "--scan"]).is_some());
1160        for argv in [
1161            vec![
1162                "inspect",
1163                "app",
1164                "--distro",
1165                "rhel-7",
1166                "--max-glibc",
1167                "2.17",
1168            ],
1169            vec!["inspect", "app", "--distro", "rhel-7", "--distro", "rhel-7"],
1170            vec!["inspect", "app", "--strict"],
1171            vec!["inspect", "app", "--max-glibc", "2.17", "--verbose"],
1172            vec!["inspect", "app", "--kernel"],
1173        ] {
1174            assert!(parse(args(&argv)).is_err(), "{argv:?}");
1175        }
1176    }
1177
1178    #[test]
1179    fn clippy_doc_and_rustc_pass_through_with_the_build_selection() {
1180        for command in ["clippy", "doc", "rustc"] {
1181            let ParsedCommand::Passthrough {
1182                subcommand,
1183                cargo_args,
1184                ..
1185            } = parse(with_sdk(command, &["--glibc", "2.17", "--", "--workspace"])).unwrap()
1186            else {
1187                panic!("wrong command")
1188            };
1189            assert_eq!(subcommand, command);
1190            assert_eq!(cargo_args, args(&["--workspace"]));
1191            assert!(parse(with_sdk(command, &["--glibc", "2.17", "--kernel", "3.10"])).is_err());
1192        }
1193        // clippy and rustc take tool arguments after their own --.
1194        assert!(parse(with_sdk(
1195            "clippy",
1196            &["--glibc", "2.17", "--", "--", "-D", "warnings"]
1197        ))
1198        .is_ok());
1199        assert!(parse(with_sdk(
1200            "rustc",
1201            &["--glibc", "2.17", "--", "--", "-C", "opt-level=1"]
1202        ))
1203        .is_ok());
1204        assert!(parse(with_sdk(
1205            "rustc",
1206            &["--glibc", "2.17", "--", "--", "-C", "linker=cc"]
1207        ))
1208        .is_err());
1209        assert!(parse(with_sdk(
1210            "rustc",
1211            &["--glibc", "2.17", "--", "--", "--target=x"]
1212        ))
1213        .is_err());
1214        assert!(parse(with_sdk("doc", &["--glibc", "2.17", "--", "--", "-D"])).is_err());
1215    }
1216
1217    #[test]
1218    fn install_checks_like_build_without_an_artifact() {
1219        let ParsedCommand::Install {
1220            scan, cargo_args, ..
1221        } = parse(with_sdk(
1222            "install",
1223            &[
1224                "--glibc", "2.17", "--kernel", "3.10", "--strict", "--", "--path", "app",
1225            ],
1226        ))
1227        .unwrap()
1228        else {
1229            panic!("wrong command")
1230        };
1231        assert!(scan.unwrap().strict);
1232        assert_eq!(cargo_args, args(&["--path", "app"]));
1233        assert!(parse(with_sdk("install", &["--glibc", "2.17", "--artifact", "x"])).is_err());
1234        assert!(parse(with_sdk("install", &["--glibc", "2.17", "--strict"])).is_err());
1235    }
1236
1237    #[test]
1238    fn env_selects_a_format_and_takes_no_cargo_arguments() {
1239        let ParsedCommand::Env {
1240            format, simulation, ..
1241        } = parse(with_sdk(
1242            "env",
1243            &["--glibc", "2.17", "--kernel", "3.10", "--format", "cargo"],
1244        ))
1245        .unwrap()
1246        else {
1247            panic!("wrong command")
1248        };
1249        assert_eq!(format, toolchain::EnvFormat::Cargo);
1250        assert_eq!(simulation.kernel.as_deref(), Some("3.10"));
1251        let ParsedCommand::Env { format, .. } =
1252            parse(args(&["env", "--target", "x86_64-unknown-linux-gnu.2.28"])).unwrap()
1253        else {
1254            panic!("wrong command")
1255        };
1256        assert_eq!(format, toolchain::EnvFormat::Shell);
1257        for extra in [
1258            &["--glibc", "2.17", "--format", "json"][..],
1259            &["--glibc", "2.17", "--", "--release"],
1260            &["--glibc", "2.17", "--dry-run"],
1261            &["--glibc", "2.17", "--format", "sh", "--format", "sh"],
1262        ] {
1263            assert!(parse(with_sdk("env", extra)).is_err(), "{extra:?}");
1264        }
1265    }
1266}