Skip to main content

cargo_libc/
cli.rs

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