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 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 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 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 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 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 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 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}