1use 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 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 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 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 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 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 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
639pub 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 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}