Skip to main content

cargo_cross/
cli.rs

1//! Command-line argument parsing for cargo-cross using clap
2
3use crate::config::{
4    self, supported_freebsd_versions_str, supported_glibc_versions_str,
5    supported_iphone_sdk_versions_str, supported_macos_sdk_versions_str,
6    DEFAULT_CROSS_MAKE_VERSION, DEFAULT_FREEBSD_VERSION, DEFAULT_GLIBC_VERSION,
7    DEFAULT_IPHONE_SDK_VERSION, DEFAULT_MACOS_SDK_VERSION, DEFAULT_NDK_VERSION,
8    DEFAULT_QEMU_VERSION, SUPPORTED_FREEBSD_VERSIONS, SUPPORTED_GLIBC_VERSIONS,
9    SUPPORTED_IPHONE_SDK_VERSIONS, SUPPORTED_MACOS_SDK_VERSIONS,
10};
11use crate::error::{CrossError, Result};
12use clap::builder::styling::{AnsiColor, Effects, Styles};
13use clap::ArgAction;
14use clap::{Args as ClapArgs, CommandFactory, FromArgMatches, Parser, Subcommand, ValueHint};
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::sync::LazyLock;
18
19/// Binary name from Cargo.toml (e.g., "cargo-cross")
20const BIN_NAME: &str = env!("CARGO_PKG_NAME");
21
22/// Define subcommand-related constants from a single literal
23macro_rules! define_subcommand {
24    ($name:literal) => {
25        const SUBCOMMAND: &str = $name;
26        const CARGO_DISPLAY_NAME: &str = concat!("cargo ", $name);
27    };
28}
29define_subcommand!("cross");
30
31/// Program name - either "cargo cross" or "cargo-cross" based on invocation style
32static PROGRAM_NAME: LazyLock<&'static str> = LazyLock::new(|| {
33    let args: Vec<String> = std::env::args().collect();
34    let is_cargo_subcommand = std::env::var("CARGO").is_ok()
35        && std::env::var("CARGO_HOME").is_ok()
36        && args.get(1).map(String::as_str) == Some(SUBCOMMAND);
37
38    if is_cargo_subcommand {
39        CARGO_DISPLAY_NAME
40    } else {
41        BIN_NAME
42    }
43});
44
45/// Get the program name for display
46#[must_use]
47pub fn program_name() -> &'static str {
48    *PROGRAM_NAME
49}
50
51/// Custom styles for CLI help output
52fn cli_styles() -> Styles {
53    Styles::styled()
54        .header(AnsiColor::BrightCyan.on_default() | Effects::BOLD)
55        .usage(AnsiColor::BrightCyan.on_default() | Effects::BOLD)
56        .literal(AnsiColor::BrightGreen.on_default())
57        .placeholder(AnsiColor::BrightMagenta.on_default())
58        .valid(AnsiColor::BrightGreen.on_default())
59        .invalid(AnsiColor::BrightRed.on_default())
60        .error(AnsiColor::BrightRed.on_default() | Effects::BOLD)
61}
62
63/// Cross-compilation tool for Rust projects
64#[derive(Parser, Debug)]
65#[command(name = "cargo-cross", version)]
66#[command(about = "Cross-compilation tool for Rust projects, no Docker required")]
67#[command(long_about = "\
68Cross-compilation tool for Rust projects.
69
70This tool provides cross-compilation support for Rust projects across multiple
71platforms including Linux (musl/gnu), Windows, macOS, FreeBSD, iOS, and Android.
72It automatically downloads and configures the appropriate cross-compiler toolchains.")]
73#[command(propagate_version = true)]
74#[command(arg_required_else_help = true)]
75#[command(styles = cli_styles())]
76#[command(override_usage = "cargo-cross [+toolchain] <COMMAND> [OPTIONS]")]
77#[command(after_help = "\
78Use 'cargo-cross <COMMAND> --help' for more information about a command.
79
80TOOLCHAIN:
81    If the first argument begins with +, it will be interpreted as a Rust toolchain
82    name (such as +nightly, +stable, or +1.75.0). This follows the same convention
83    as rustup and cargo.
84
85EXAMPLES:
86    cargo-cross build -t x86_64-unknown-linux-musl
87    cargo-cross +nightly build -t aarch64-unknown-linux-gnu --profile release
88    cargo-cross build -t '*-linux-musl' --crt-static true
89    cargo-cross test -t x86_64-unknown-linux-musl -- --nocapture")]
90pub struct Cli {
91    #[command(subcommand)]
92    pub command: CliCommand,
93}
94
95#[derive(Subcommand, Debug)]
96pub enum CliCommand {
97    /// Compile the current package
98    #[command(visible_alias = "b")]
99    #[command(long_about = "\
100Compile the current package and all of its dependencies.
101
102When no target selection options are given, this tool will build all binary
103and library targets of the selected packages.")]
104    Build(BuildArgs),
105
106    /// Analyze the current package and report errors, but don't build object files
107    #[command(visible_alias = "c")]
108    #[command(long_about = "\
109Check the current package and all of its dependencies for errors.
110
111This will essentially compile packages without performing the final step of
112code generation, which is faster than running build.")]
113    Check(BuildArgs),
114
115    /// Run a binary or example of the current package
116    #[command(visible_alias = "r")]
117    #[command(long_about = "\
118Run a binary or example of the local package.
119
120For cross-compilation targets, QEMU user-mode emulation is used to run the binary.")]
121    Run(BuildArgs),
122
123    /// Run the tests
124    #[command(visible_alias = "t")]
125    #[command(long_about = "\
126Execute all unit and integration tests and build examples of a local package.
127
128For cross-compilation targets, QEMU user-mode emulation is used to run tests.")]
129    Test(BuildArgs),
130
131    /// Run the benchmarks
132    #[command(long_about = "\
133Execute all benchmarks of a local package.
134
135For cross-compilation targets, QEMU user-mode emulation is used to run benchmarks.")]
136    Bench(BuildArgs),
137
138    /// Run Clippy lints on the current package
139    #[command(long_about = "\
140Run Clippy on the current package and all of its dependencies.
141
142This forwards to `cargo clippy` with the configured cross-compilation environment.")]
143    Clippy(BuildArgs),
144
145    /// Prepare and print the configured cross-compilation environment
146    #[command(long_about = "\
147Prepare the cross-compilation environment and print environment variables.
148
149This is intended for use with shell evaluation, for example:
150    eval \"$(cargo cross setup -t aarch64-unknown-linux-musl)\"")]
151    Setup(SetupCliArgs),
152
153    /// Execute an arbitrary command inside the configured cross-compilation environment
154    #[command(long_about = "\
155Prepare the cross-compilation environment and execute an arbitrary command.
156
157This is useful for running custom tooling that should inherit the configured
158compiler, linker, PATH, and cargo target environment variables.")]
159    Exec(ExecCliArgs),
160
161    /// Display all supported cross-compilation targets
162    #[command(long_about = "\
163Display all supported cross-compilation targets.
164
165You can also use glob patterns with --target to match multiple targets,
166for example: --target '*-linux-musl' or --target 'aarch64-*'")]
167    Targets(TargetsArgs),
168
169    /// Print version information
170    Version,
171}
172
173/// Output format for targets command
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
175pub enum OutputFormat {
176    /// Human-readable colored text (default)
177    #[default]
178    Text,
179    /// JSON array format
180    Json,
181    /// Plain text, one target per line
182    Plain,
183}
184
185/// Output format for setup command
186#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
187pub enum SetupOutputFormat {
188    /// Detect the current shell from the environment, fallback to bash syntax
189    #[default]
190    Auto,
191    /// Bash-compatible exports
192    Bash,
193    /// Zsh-compatible exports
194    Zsh,
195    /// Fish shell exports
196    Fish,
197    /// PowerShell environment assignments
198    Powershell,
199    /// Windows cmd.exe environment assignments
200    Cmd,
201    /// JSON object
202    Json,
203}
204
205#[derive(ClapArgs, Debug, Clone)]
206pub struct SetupCliArgs {
207    #[command(flatten)]
208    pub build: BuildArgs,
209
210    /// Output format
211    #[arg(
212        short = 'f',
213        long = "format",
214        value_enum,
215        default_value = "auto",
216        help = "Output format (auto, bash, zsh, fish, powershell, cmd, json)"
217    )]
218    pub format: SetupOutputFormat,
219}
220
221#[derive(Debug, Clone)]
222pub struct SetupArgs {
223    pub args: Args,
224    pub format: SetupOutputFormat,
225}
226
227#[derive(ClapArgs, Debug, Clone)]
228pub struct ExecCliArgs {
229    #[command(flatten)]
230    pub build: BuildArgs,
231}
232
233#[derive(Debug, Clone)]
234pub struct ExecArgs {
235    pub args: Args,
236    pub command: Vec<String>,
237}
238
239#[derive(ClapArgs, Debug, Clone, Default)]
240pub struct TargetsArgs {
241    /// Output format
242    #[arg(
243        short = 'f',
244        long = "format",
245        value_enum,
246        default_value = "text",
247        help = "Output format (text, json, plain)"
248    )]
249    pub format: OutputFormat,
250}
251
252#[derive(ClapArgs, Debug, Clone, Default)]
253#[command(next_help_heading = "Target Selection")]
254pub struct BuildArgs {
255    // ===== Target Selection =====
256    /// Build for the target triple(s)
257    #[arg(
258        short = 't',
259        long = "target",
260        visible_alias = "targets",
261        value_delimiter = ',',
262        env = "TARGETS",
263        value_name = "TRIPLE",
264        help = "Build for the target triple(s), comma-separated",
265        long_help = "\
266Build for the specified target architecture. This flag may be specified multiple
267times or with comma-separated values. Supports glob patterns like '*-linux-musl'.
268The general format of the triple is <arch><sub>-<vendor>-<sys>-<abi>.
269Run the 'targets' subcommand to see all supported targets.
270
271Examples: -t x86_64-unknown-linux-musl, -t '*-linux-musl'"
272    )]
273    pub targets: Vec<String>,
274
275    // ===== Feature Selection =====
276    /// Space or comma separated list of features to activate
277    #[arg(
278        short = 'F',
279        long,
280        env = "FEATURES",
281        value_name = "FEATURES",
282        conflicts_with = "all_features",
283        help_heading = "Feature Selection",
284        long_help = "\
285Space or comma separated list of features to activate. Features of workspace members
286may be enabled with package-name/feature-name syntax. May be specified multiple times."
287    )]
288    pub features: Option<String>,
289
290    /// Do not activate the `default` feature of the selected packages
291    #[arg(long, env = "NO_DEFAULT_FEATURES", help_heading = "Feature Selection")]
292    pub no_default_features: bool,
293
294    /// Activate all available features of all selected packages
295    #[arg(
296        long,
297        env = "ALL_FEATURES",
298        conflicts_with = "features",
299        help_heading = "Feature Selection"
300    )]
301    pub all_features: bool,
302
303    // ===== Profile =====
304    /// Build artifacts in release mode, with optimizations
305    #[arg(
306        short = 'r',
307        long = "release",
308        conflicts_with = "profile",
309        help_heading = "Profile",
310        long_help = "\
311Build artifacts in release mode, with optimizations. Equivalent to --profile=release."
312    )]
313    pub release: bool,
314
315    /// Build artifacts with the specified profile
316    #[arg(
317        long,
318        default_value = "dev",
319        env = "PROFILE",
320        value_name = "PROFILE-NAME",
321        conflicts_with = "release",
322        help_heading = "Profile",
323        long_help = "\
324Build artifacts with the specified profile. Built-in: dev, release, test, bench.
325Custom profiles can be defined in Cargo.toml. Default is 'dev'."
326    )]
327    pub profile: String,
328
329    // ===== Package Selection =====
330    /// Package to build (see `cargo help pkgid`)
331    #[arg(
332        short = 'p',
333        long,
334        env = "PACKAGE",
335        value_name = "SPEC",
336        help_heading = "Package Selection",
337        long_help = "\
338Build only the specified packages. This flag may be specified multiple times
339and supports common Unix glob patterns like *, ?, and []."
340    )]
341    pub package: Option<String>,
342
343    /// Build all members in the workspace
344    #[arg(
345        long,
346        visible_alias = "all",
347        env = "BUILD_WORKSPACE",
348        help_heading = "Package Selection"
349    )]
350    pub workspace: bool,
351
352    /// Exclude packages from the build (must be used with --workspace)
353    #[arg(
354        long,
355        env = "EXCLUDE",
356        value_name = "SPEC",
357        requires = "workspace",
358        help_heading = "Package Selection",
359        long_help = "\
360Exclude the specified packages. Must be used in conjunction with the --workspace flag.
361This flag may be specified multiple times and supports common Unix glob patterns."
362    )]
363    pub exclude: Option<String>,
364
365    /// Build only the specified binary
366    #[arg(
367        long = "bin",
368        env = "BIN_TARGET",
369        value_name = "NAME",
370        help_heading = "Package Selection",
371        long_help = "\
372Build the specified binary. This flag may be specified multiple times
373and supports common Unix glob patterns."
374    )]
375    pub bin_target: Option<String>,
376
377    /// Build all binary targets
378    #[arg(long = "bins", env = "BUILD_BINS", help_heading = "Package Selection")]
379    pub build_bins: bool,
380
381    /// Build only this package's library
382    #[arg(long = "lib", env = "BUILD_LIB", help_heading = "Package Selection")]
383    pub build_lib: bool,
384
385    /// Build only the specified example
386    #[arg(
387        long = "example",
388        env = "EXAMPLE_TARGET",
389        value_name = "NAME",
390        help_heading = "Package Selection",
391        long_help = "\
392Build the specified example. This flag may be specified multiple times
393and supports common Unix glob patterns."
394    )]
395    pub example_target: Option<String>,
396
397    /// Build all example targets
398    #[arg(
399        long = "examples",
400        env = "BUILD_EXAMPLES",
401        help_heading = "Package Selection"
402    )]
403    pub build_examples: bool,
404
405    /// Build only the specified test target
406    #[arg(
407        long = "test",
408        env = "TEST_TARGET",
409        value_name = "NAME",
410        help_heading = "Package Selection",
411        long_help = "\
412Build the specified integration test. This flag may be specified multiple times
413and supports common Unix glob patterns."
414    )]
415    pub test_target: Option<String>,
416
417    /// Build all test targets (includes unit tests from lib/bins)
418    #[arg(
419        long = "tests",
420        env = "BUILD_TESTS",
421        help_heading = "Package Selection",
422        long_help = "\
423Build all targets that have the test = true manifest flag set. By default this
424includes the library and binaries built as unittests, and integration tests."
425    )]
426    pub build_tests: bool,
427
428    /// Build only the specified bench target
429    #[arg(
430        long = "bench",
431        env = "BENCH_TARGET",
432        value_name = "NAME",
433        help_heading = "Package Selection",
434        long_help = "\
435Build the specified benchmark. This flag may be specified multiple times
436and supports common Unix glob patterns."
437    )]
438    pub bench_target: Option<String>,
439
440    /// Build all bench targets
441    #[arg(
442        long = "benches",
443        env = "BUILD_BENCHES",
444        help_heading = "Package Selection",
445        long_help = "\
446Build all targets that have the bench = true manifest flag set. By default this
447includes the library and binaries built as benchmarks, and bench targets."
448    )]
449    pub build_benches: bool,
450
451    /// Build all targets (equivalent to --lib --bins --tests --benches --examples)
452    #[arg(
453        long = "all-targets",
454        env = "BUILD_ALL_TARGETS",
455        help_heading = "Package Selection"
456    )]
457    pub build_all_targets: bool,
458
459    /// Path to Cargo.toml
460    #[arg(long, env = "MANIFEST_PATH", value_name = "PATH",
461          value_hint = ValueHint::FilePath, help_heading = "Package Selection",
462          long_help = "\
463Path to Cargo.toml. By default, Cargo searches for the Cargo.toml file
464in the current directory or any parent directory.")]
465    pub manifest_path: Option<PathBuf>,
466
467    // ===== Version Options =====
468    /// Glibc version for Linux GNU targets
469    #[arg(long, default_value = DEFAULT_GLIBC_VERSION, env = "GLIBC_VERSION",
470          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions")]
471    pub glibc_version: String,
472
473    /// iPhone SDK version for iOS targets
474    #[arg(long, default_value = DEFAULT_IPHONE_SDK_VERSION, env = "IPHONE_SDK_VERSION",
475          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions")]
476    pub iphone_sdk_version: String,
477
478    /// Override iPhoneOS SDK path (skips version lookup)
479    #[arg(long, env = "IPHONE_SDK_PATH", value_name = "PATH",
480          value_hint = ValueHint::DirPath, help_heading = "Toolchain Versions",
481          long_help = "\
482Override iPhoneOS SDK path for device targets. Skips version lookup.")]
483    pub iphone_sdk_path: Option<PathBuf>,
484
485    /// Override iPhoneSimulator SDK path
486    #[arg(long, env = "IPHONE_SIMULATOR_SDK_PATH", value_name = "PATH",
487          value_hint = ValueHint::DirPath, help_heading = "Toolchain Versions",
488          long_help = "\
489Override iPhoneSimulator SDK path for simulator targets. Skips version lookup.")]
490    pub iphone_simulator_sdk_path: Option<PathBuf>,
491
492    /// macOS SDK version for Darwin targets
493    #[arg(long, default_value = DEFAULT_MACOS_SDK_VERSION, env = "MACOS_SDK_VERSION",
494          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions")]
495    pub macos_sdk_version: String,
496
497    /// Override macOS SDK path (skips version lookup)
498    #[arg(long, env = "MACOS_SDK_PATH", value_name = "PATH",
499          value_hint = ValueHint::DirPath, help_heading = "Toolchain Versions",
500          long_help = "\
501Override macOS SDK path directly. Skips version lookup.")]
502    pub macos_sdk_path: Option<PathBuf>,
503
504    /// FreeBSD version for FreeBSD targets
505    #[arg(long, default_value = DEFAULT_FREEBSD_VERSION, env = "FREEBSD_VERSION",
506          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions")]
507    pub freebsd_version: String,
508
509    /// Android NDK version
510    #[arg(long, default_value = DEFAULT_NDK_VERSION, env = "NDK_VERSION",
511          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions",
512          long_help = "\
513Specify Android NDK version for Android targets. Auto-downloaded from Google's official repository.")]
514    pub ndk_version: String,
515
516    /// QEMU version for user-mode emulation
517    #[arg(long, default_value = DEFAULT_QEMU_VERSION, env = "QEMU_VERSION",
518          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions",
519          long_help = "\
520Specify QEMU version for user-mode emulation. Used to run cross-compiled binaries during test/run/bench.")]
521    pub qemu_version: String,
522
523    /// Cross-compiler make version
524    #[arg(long, default_value = DEFAULT_CROSS_MAKE_VERSION, env = "CROSS_MAKE_VERSION",
525          value_name = "VERSION", hide_default_value = true, help_heading = "Toolchain Versions",
526          long_help = "\
527Specify cross-compiler make version. This determines which version of cross-compilation \
528toolchains will be downloaded from the upstream repository.")]
529    pub cross_make_version: String,
530
531    // ===== Directories =====
532    /// Directory for cross-compiler toolchains
533    #[arg(long, env = "CROSS_COMPILER_DIR", value_name = "DIR",
534          value_hint = ValueHint::DirPath, help_heading = "Directories",
535          long_help = "\
536Directory where cross-compiler toolchains will be downloaded and stored. Defaults to temp dir.
537Set this to reuse downloaded toolchains across builds.")]
538    pub cross_compiler_dir: Option<PathBuf>,
539
540    /// Directory for all generated artifacts
541    #[arg(long, visible_alias = "target-dir", env = "CARGO_TARGET_DIR", value_name = "DIR",
542          value_hint = ValueHint::DirPath, help_heading = "Directories",
543          long_help = "\
544Directory for all generated artifacts and intermediate files. Defaults to 'target'.")]
545    pub cargo_target_dir: Option<PathBuf>,
546
547    /// Copy final artifacts to this directory (unstable)
548    #[arg(long, env = "ARTIFACT_DIR", value_name = "DIR",
549          value_hint = ValueHint::DirPath, help_heading = "Directories",
550          long_help = "\
551Copy final artifacts to this directory. Unstable, requires nightly toolchain.")]
552    pub artifact_dir: Option<PathBuf>,
553
554    // ===== Compiler Options =====
555    /// Override C compiler path
556    #[arg(long, env = "CC", value_name = "PATH",
557          value_hint = ValueHint::ExecutablePath, help_heading = "Compiler Options",
558          long_help = "\
559Override the C compiler path. By default, the appropriate cross-compiler is auto-configured.")]
560    pub cc: Option<PathBuf>,
561
562    /// Override C++ compiler path
563    #[arg(long, env = "CXX", value_name = "PATH",
564          value_hint = ValueHint::ExecutablePath, help_heading = "Compiler Options",
565          long_help = "\
566Override the C++ compiler path. By default, the appropriate cross-compiler is auto-configured.")]
567    pub cxx: Option<PathBuf>,
568
569    /// Override archiver (ar) path
570    #[arg(long, env = "AR", value_name = "PATH",
571          value_hint = ValueHint::ExecutablePath, help_heading = "Compiler Options",
572          long_help = "\
573Override the archiver (ar) path. By default, the appropriate archiver is auto-configured.")]
574    pub ar: Option<PathBuf>,
575
576    /// Override linker path
577    #[arg(long, env = "LINKER", value_name = "PATH",
578          value_hint = ValueHint::ExecutablePath, help_heading = "Compiler Options",
579          long_help = "\
580Override the linker path. By default, the cross-compiler is used as linker.
581This option takes precedence over auto-configured linker.")]
582    pub linker: Option<PathBuf>,
583
584    /// Additional flags for C compilation
585    #[arg(
586        long,
587        env = "CFLAGS",
588        value_name = "FLAGS",
589        allow_hyphen_values = true,
590        help_heading = "Compiler Options",
591        long_help = "\
592Additional flags to pass to the C compiler. Appended to default CFLAGS.
593Example: --cflags '-O2 -Wall -march=native'"
594    )]
595    pub cflags: Option<String>,
596
597    /// Additional flags for C++ compilation
598    #[arg(
599        long,
600        env = "CXXFLAGS",
601        value_name = "FLAGS",
602        allow_hyphen_values = true,
603        help_heading = "Compiler Options",
604        long_help = "\
605Additional flags to pass to the C++ compiler. Appended to default CXXFLAGS.
606Example: --cxxflags '-O2 -Wall -std=c++17'"
607    )]
608    pub cxxflags: Option<String>,
609
610    /// Additional flags for linking
611    #[arg(
612        long,
613        env = "LDFLAGS",
614        value_name = "FLAGS",
615        allow_hyphen_values = true,
616        help_heading = "Compiler Options",
617        long_help = "\
618Additional flags to pass to the linker. Appended to default LDFLAGS.
619Example: --ldflags '-L/usr/local/lib -static'"
620    )]
621    pub ldflags: Option<String>,
622
623    /// C++ standard library to use
624    #[arg(
625        long,
626        env = "CXXSTDLIB",
627        value_name = "LIB",
628        help_heading = "Compiler Options",
629        long_help = "\
630Specify the C++ standard library to use (libc++, libstdc++, etc)."
631    )]
632    pub cxxstdlib: Option<String>,
633
634    /// `CMake` generator to use (like cmake -G)
635    #[arg(
636        long,
637        short = 'G',
638        env = "CMAKE_GENERATOR",
639        value_name = "GENERATOR",
640        help_heading = "Compiler Options",
641        long_help = "\
642Specify the CMake generator to use. On Windows, this overrides the auto-detection.
643Common generators: Ninja, 'MinGW Makefiles', 'Unix Makefiles', 'NMake Makefiles'.
644If not specified, auto-detects: Ninja > MinGW Makefiles > Unix Makefiles."
645    )]
646    pub cmake_generator: Option<String>,
647
648    /// Additional RUSTFLAGS (can be repeated)
649    #[arg(long = "rustflag", visible_alias = "rustflags", value_name = "FLAG",
650          env = "ADDITIONAL_RUSTFLAGS", allow_hyphen_values = true,
651          action = clap::ArgAction::Append, help_heading = "Compiler Options",
652          long_help = "\
653Additional flags to pass to rustc via RUSTFLAGS. Can be specified multiple times.
654Example: --rustflag '-C target-cpu=native' --rustflag '-C lto=thin'")]
655    pub rustflags: Vec<String>,
656
657    /// Rustc wrapper program (e.g., sccache, cachepot)
658    #[arg(long, env = "RUSTC_WRAPPER", value_name = "PATH",
659          value_hint = ValueHint::ExecutablePath,
660          conflicts_with = "enable_sccache", help_heading = "Compiler Options",
661          long_help = "\
662Specify a rustc wrapper program (sccache, cachepot, etc) for compilation caching.")]
663    pub rustc_wrapper: Option<PathBuf>,
664
665    /// Skip cross-compilation toolchain setup
666    #[arg(
667        long,
668        env = "NO_TOOLCHAIN_SETUP",
669        help_heading = "Compiler Options",
670        long_help = "\
671Skip downloading and configuring cross-compilation toolchain.
672Use this when you have pre-configured system compilers or want to use
673only CLI-provided compiler options (--cc, --cxx, --ar, --linker)."
674    )]
675    pub no_toolchain_setup: bool,
676
677    // ===== Sccache Options =====
678    /// Enable sccache for compilation caching
679    #[arg(
680        long,
681        env = "ENABLE_SCCACHE",
682        conflicts_with = "rustc_wrapper",
683        help_heading = "Sccache Options",
684        long_help = "\
685Enable sccache as the rustc wrapper for compilation caching.
686Speeds up compilation by caching previous compilations."
687    )]
688    pub enable_sccache: bool,
689
690    /// Directory for sccache local disk cache
691    #[arg(long, env = "SCCACHE_DIR", value_name = "DIR",
692          value_hint = ValueHint::DirPath, help_heading = "Sccache Options",
693          long_help = "\
694Directory for sccache's local disk cache. Defaults to $HOME/.cache/sccache.")]
695    pub sccache_dir: Option<PathBuf>,
696
697    /// Maximum cache size (e.g., '10G', '500M')
698    #[arg(
699        long,
700        env = "SCCACHE_CACHE_SIZE",
701        value_name = "SIZE",
702        help_heading = "Sccache Options",
703        long_help = "\
704Maximum size of the local disk cache (e.g., '10G', '500M'). Default is 10GB."
705    )]
706    pub sccache_cache_size: Option<String>,
707
708    /// Idle timeout in seconds for sccache server
709    #[arg(
710        long,
711        env = "SCCACHE_IDLE_TIMEOUT",
712        value_name = "SECONDS",
713        help_heading = "Sccache Options",
714        long_help = "\
715Idle timeout in seconds for the sccache server. Set to 0 to run indefinitely."
716    )]
717    pub sccache_idle_timeout: Option<String>,
718
719    /// Log level for sccache (error, warn, info, debug, trace)
720    #[arg(
721        long,
722        env = "SCCACHE_LOG",
723        value_name = "LEVEL",
724        help_heading = "Sccache Options",
725        long_help = "\
726Log level for sccache. Valid: error, warn, info, debug, trace"
727    )]
728    pub sccache_log: Option<String>,
729
730    /// Run sccache without the daemon (single process mode)
731    #[arg(
732        long,
733        env = "SCCACHE_NO_DAEMON",
734        help_heading = "Sccache Options",
735        long_help = "\
736Run sccache without daemon (single-process mode). May be slower but avoids daemon startup issues."
737    )]
738    pub sccache_no_daemon: bool,
739
740    /// Enable sccache direct mode (bypass preprocessor)
741    #[arg(
742        long,
743        env = "SCCACHE_DIRECT",
744        help_heading = "Sccache Options",
745        long_help = "\
746Enable sccache direct mode. Caches based on source file content directly, bypassing preprocessor."
747    )]
748    pub sccache_direct: bool,
749
750    // ===== CC Crate Options =====
751    /// Disable CC crate default compiler flags
752    #[arg(
753        long,
754        env = "CRATE_CC_NO_DEFAULTS",
755        hide = true,
756        help_heading = "CC Crate Options"
757    )]
758    pub cc_no_defaults: bool,
759
760    /// Use shell-escaped flags for CC crate
761    #[arg(
762        long,
763        env = "CC_SHELL_ESCAPED_FLAGS",
764        hide = true,
765        help_heading = "CC Crate Options"
766    )]
767    pub cc_shell_escaped_flags: bool,
768
769    /// Enable CC crate debug output
770    #[arg(
771        long,
772        env = "CC_ENABLE_DEBUG_OUTPUT",
773        hide = true,
774        help_heading = "CC Crate Options"
775    )]
776    pub cc_enable_debug: bool,
777
778    // ===== Build Options =====
779    /// Link the C runtime statically
780    #[arg(long, value_parser = parse_optional_bool, env = "CRT_STATIC",
781          value_name = "BOOL", num_args = 0..=1, default_missing_value = "true",
782          help_heading = "Build Options",
783          long_help = "\
784Control whether the C runtime is statically linked. true=static (larger, portable),
785false=dynamic (smaller, requires libc). Musl defaults to static, glibc to dynamic.")]
786    pub crt_static: Option<bool>,
787
788    /// Abort immediately on panic (smaller binary, implies --build-std)
789    #[arg(
790        long,
791        env = "PANIC_IMMEDIATE_ABORT",
792        help_heading = "Build Options",
793        long_help = "\
794Use panic=abort and remove panic formatting code for smaller binaries.
795Requires nightly and implies --build-std. Stack traces will not be available."
796    )]
797    pub panic_immediate_abort: bool,
798
799    /// Debug formatting mode (full, shallow, none) - requires nightly
800    #[arg(long, value_name = "MODE", hide = true, help_heading = "Build Options")]
801    pub fmt_debug: Option<String>,
802
803    /// Location detail mode - requires nightly
804    #[arg(long, value_name = "MODE", hide = true, help_heading = "Build Options")]
805    pub location_detail: Option<String>,
806
807    /// Build the standard library from source
808    #[arg(long, value_parser = parse_build_std, env = "BUILD_STD",
809          value_name = "CRATES", help_heading = "Build Options",
810          num_args = 0..=1, default_missing_value = "true",
811          long_help = "\
812Build the standard library from source (requires nightly). Without arguments, builds 'std'.
813Use 'true' for full std or specify crates like 'core,alloc'. Required for unsupported targets or panic=abort.")]
814    pub build_std: Option<String>,
815
816    /// Features to enable when building std
817    #[arg(
818        long,
819        env = "BUILD_STD_FEATURES",
820        value_name = "FEATURES",
821        requires = "build_std",
822        help_heading = "Build Options",
823        long_help = "\
824Space-separated features for std. Common: panic_immediate_abort, optimize_for_size"
825    )]
826    pub build_std_features: Option<String>,
827
828    /// Trim paths in compiler output for reproducible builds
829    #[arg(
830        long,
831        visible_alias = "trim-paths",
832        env = "CARGO_TRIM_PATHS",
833        value_name = "VALUE",
834        num_args = 0..=1,
835        default_missing_value = "true",
836        help_heading = "Build Options",
837        long_help = "\
838Control how paths are trimmed in compiler output for reproducible builds.
839Valid: true, macro, diagnostics, object, all, none (default: false)"
840    )]
841    pub cargo_trim_paths: Option<String>,
842
843    /// Disable metadata embedding (requires nightly)
844    #[arg(
845        long,
846        env = "NO_EMBED_METADATA",
847        hide = true,
848        help_heading = "Build Options"
849    )]
850    pub no_embed_metadata: bool,
851
852    /// Set `RUSTC_BOOTSTRAP` for using nightly features on stable
853    #[arg(
854        long,
855        env = "RUSTC_BOOTSTRAP",
856        value_name = "VALUE",
857        num_args = 0..=1,
858        default_missing_value = "1",
859        hide = true,
860        help_heading = "Build Options"
861    )]
862    pub rustc_bootstrap: Option<String>,
863
864    // ===== Output Options =====
865    /// Use verbose output (-v, -vv for very verbose)
866    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count,
867          env = "VERBOSE_LEVEL", conflicts_with = "quiet",
868          help_heading = "Output Options",
869          long_help = "\
870Use verbose output. -v=commands/warnings, -vv=+deps/build scripts, -vvv=max verbosity")]
871    pub verbose_level: u8,
872
873    /// Do not print cargo log messages
874    #[arg(
875        short = 'q',
876        long,
877        env = "QUIET",
878        conflicts_with = "verbose_level",
879        help_heading = "Output Options",
880        long_help = "\
881Do not print cargo log messages. Shows only errors and warnings."
882    )]
883    pub quiet: bool,
884
885    /// Diagnostic message format
886    #[arg(
887        long,
888        env = "MESSAGE_FORMAT",
889        value_name = "FMT",
890        help_heading = "Output Options",
891        long_help = "\
892Output format for diagnostics. Valid: human (default), short, json"
893    )]
894    pub message_format: Option<String>,
895
896    /// Control when colored output is used
897    #[arg(
898        long,
899        env = "COLOR",
900        value_name = "WHEN",
901        help_heading = "Output Options",
902        long_help = "\
903Control when colored output is used. Valid: auto (default), always, never"
904    )]
905    pub color: Option<String>,
906
907    /// Output the build plan in JSON (requires nightly)
908    #[arg(long, env = "BUILD_PLAN", hide = true, help_heading = "Output Options")]
909    pub build_plan: bool,
910
911    /// Timing output formats (html, json)
912    #[arg(long, env = "TIMINGS", value_name = "FMTS",
913          num_args = 0..=1, default_missing_value = "true",
914          help_heading = "Output Options",
915          long_help = "\
916Output timing information. --timings=HTML report, --timings=json for JSON. Saved to target/cargo-timings/.")]
917    pub timings: Option<String>,
918
919    // ===== Dependency Options =====
920    /// Ignore `rust-version` specification in packages
921    #[arg(
922        long,
923        env = "IGNORE_RUST_VERSION",
924        help_heading = "Dependency Options",
925        long_help = "\
926Ignore rust-version specification in packages. Allows building with older Rust versions."
927    )]
928    pub ignore_rust_version: bool,
929
930    /// Assert that Cargo.lock will remain unchanged
931    #[arg(
932        long,
933        env = "LOCKED",
934        help_heading = "Dependency Options",
935        long_help = "\
936Assert Cargo.lock will remain unchanged. Exits with error if missing or needs updating. Use in CI."
937    )]
938    pub locked: bool,
939
940    /// Run without accessing the network
941    #[arg(
942        long,
943        env = "OFFLINE",
944        help_heading = "Dependency Options",
945        long_help = "\
946Prevent network access. Uses locally cached data. Run 'cargo fetch' first if needed."
947    )]
948    pub offline: bool,
949
950    /// Require Cargo.lock and cache are up to date (implies --locked --offline)
951    #[arg(
952        long,
953        env = "FROZEN",
954        help_heading = "Dependency Options",
955        long_help = "\
956Equivalent to --locked --offline. Requires Cargo.lock and cache are up to date."
957    )]
958    pub frozen: bool,
959
960    /// Path to Cargo.lock (unstable)
961    #[arg(long, env = "LOCKFILE_PATH", value_name = "PATH",
962          value_hint = ValueHint::FilePath, help_heading = "Dependency Options",
963          long_help = "\
964Override lockfile path from default (<workspace_root>/Cargo.lock). Requires nightly.")]
965    pub lockfile_path: Option<PathBuf>,
966
967    // ===== Build Configuration =====
968    /// Number of parallel jobs to run
969    #[arg(
970        short = 'j',
971        long,
972        env = "JOBS",
973        value_name = "N",
974        help_heading = "Build Configuration",
975        long_help = "\
976Number of parallel jobs. Defaults to logical CPUs. Negative=CPUs+N. 'default' to reset."
977    )]
978    pub jobs: Option<String>,
979
980    /// Build as many crates as possible, rather than aborting on first error
981    #[arg(
982        long,
983        env = "KEEP_GOING",
984        help_heading = "Build Configuration",
985        long_help = "\
986Build as many crates in the dependency graph as possible. Rather than aborting on the first
987crate that fails to build, continue with other crates in the dependency graph."
988    )]
989    pub keep_going: bool,
990
991    /// Output a future incompatibility report after the build
992    #[arg(
993        long,
994        env = "FUTURE_INCOMPAT_REPORT",
995        help_heading = "Build Configuration",
996        long_help = "\
997Displays a future-incompat report for any future-incompatible warnings produced during
998execution of this command. See 'cargo report' for more information."
999    )]
1000    pub future_incompat_report: bool,
1001
1002    // ===== Additional Cargo Arguments =====
1003    /// Additional arguments to pass to cargo
1004    /// Note: `CARGO_ARGS` env var is handled manually in cargo.rs to support shell-style parsing
1005    #[arg(
1006        long,
1007        visible_alias = "args",
1008        value_name = "ARGS",
1009        hide = true,
1010        allow_hyphen_values = true,
1011        action = clap::ArgAction::Append,
1012        help_heading = "Additional Options"
1013    )]
1014    pub cargo_args: Vec<String>,
1015
1016    /// Unstable (nightly-only) flags to Cargo
1017    #[arg(short = 'Z', value_name = "FLAG",
1018          action = clap::ArgAction::Append, help_heading = "Additional Options",
1019          long_help = "\
1020Unstable (nightly-only) flags to Cargo. Run 'cargo -Z help' for details on available flags.
1021Common flags: build-std, unstable-options")]
1022    pub cargo_z_flags: Vec<String>,
1023
1024    /// Override a Cargo configuration value
1025    #[arg(long = "config", value_name = "KEY=VALUE",
1026          action = clap::ArgAction::Append, help_heading = "Additional Options",
1027          long_help = "\
1028Override a Cargo configuration value. The argument should be in TOML syntax of KEY=VALUE.
1029This flag may be specified multiple times.
1030Example: --config 'build.jobs=4' --config 'profile.release.lto=true'")]
1031    pub cargo_config: Vec<String>,
1032
1033    /// Change to directory before doing anything
1034    #[arg(short = 'C', long = "directory", env = "CARGO_CWD",
1035          value_name = "DIR", value_hint = ValueHint::DirPath,
1036          help_heading = "Additional Options",
1037          long_help = "\
1038Changes the current working directory before executing any specified operations.
1039This affects where cargo looks for the project manifest (Cargo.toml) and .cargo/config.toml.")]
1040    pub cargo_cwd: Option<PathBuf>,
1041
1042    /// Rust toolchain to use (alternative to +toolchain syntax)
1043    #[arg(
1044        long = "toolchain",
1045        env = "TOOLCHAIN",
1046        value_name = "TOOLCHAIN",
1047        help_heading = "Additional Options",
1048        long_help = "\
1049Specify the Rust toolchain to use for compilation. This is an alternative to the +toolchain
1050syntax (e.g., +nightly). Examples: --toolchain nightly, --toolchain stable, --toolchain 1.75.0"
1051    )]
1052    pub toolchain_option: Option<String>,
1053
1054    /// GitHub mirror URL for downloading toolchains
1055    #[arg(long, visible_alias = "github-proxy-mirror", env = "GH_PROXY", value_name = "URL",
1056          value_hint = ValueHint::Url, hide_env = true,
1057          help_heading = "Additional Options",
1058          long_help = "\
1059Specify a GitHub mirror/proxy URL for downloading cross-compiler toolchains.
1060Useful in regions where GitHub access is slow or restricted.
1061Example: --github-proxy 'https://ghproxy.com/'")]
1062    pub github_proxy: Option<String>,
1063
1064    /// Clean the target directory before building
1065    #[arg(
1066        long,
1067        env = "CLEAN_CACHE",
1068        help_heading = "Additional Options",
1069        long_help = "\
1070Clean the target directory before building. Equivalent to running 'cargo clean' before the build."
1071    )]
1072    pub clean_cache: bool,
1073
1074    /// Disable automatic --target appending for `exec` cargo commands
1075    #[arg(
1076        long,
1077        env = "NO_APPEND_TARGET",
1078        help_heading = "Additional Options",
1079        long_help = "\
1080Disable automatic insertion of '--target <triple>' when using the 'exec' command
1081with a cargo invocation. By default, 'cargo cross exec --target ... -- cargo ...'
1082will add '--target <triple>' to the cargo command unless it is already present."
1083    )]
1084    pub no_append_target: bool,
1085
1086    /// Arguments passed through to cargo (after --)
1087    /// Note: `CARGO_PASSTHROUGH_ARGS` env var is handled manually in cargo.rs to support shell-style parsing
1088    #[arg(
1089        last = true,
1090        allow_hyphen_values = true,
1091        value_name = "ARGS",
1092        help = "Arguments passed through to the underlying cargo command",
1093        long_help = "\
1094Arguments passed through to the underlying cargo command. Everything after -- is passed
1095directly to cargo/test runner. For test command, these are passed to the test binary.
1096Examples: test -- --nocapture --test-threads=1, run -- --arg1 --arg2"
1097    )]
1098    pub passthrough_args: Vec<String>,
1099}
1100
1101impl BuildArgs {
1102    /// Create default `BuildArgs` with proper version defaults
1103    #[must_use]
1104    pub fn default_for_host() -> Self {
1105        Self {
1106            profile: "dev".to_string(),
1107            glibc_version: DEFAULT_GLIBC_VERSION.to_string(),
1108            iphone_sdk_version: DEFAULT_IPHONE_SDK_VERSION.to_string(),
1109            macos_sdk_version: DEFAULT_MACOS_SDK_VERSION.to_string(),
1110            freebsd_version: DEFAULT_FREEBSD_VERSION.to_string(),
1111            ndk_version: DEFAULT_NDK_VERSION.to_string(),
1112            qemu_version: DEFAULT_QEMU_VERSION.to_string(),
1113            cross_make_version: DEFAULT_CROSS_MAKE_VERSION.to_string(),
1114            ..Default::default()
1115        }
1116    }
1117}
1118
1119/// Parse optional bool value (true/false)
1120fn parse_optional_bool(s: &str) -> std::result::Result<bool, String> {
1121    match s.to_lowercase().as_str() {
1122        "true" | "1" | "yes" => Ok(true),
1123        "false" | "0" | "no" => Ok(false),
1124        _ => Err(format!("invalid bool value: {s}")),
1125    }
1126}
1127
1128/// Parse build-std value (returns empty string for disabled, which is filtered later)
1129fn parse_build_std(s: &str) -> std::result::Result<String, String> {
1130    match s.to_lowercase().as_str() {
1131        "false" | "0" | "no" | "" => Ok(String::new()), // Will be converted to None later
1132        "true" | "1" | "yes" => Ok("true".to_string()),
1133        _ => Ok(s.to_string()),
1134    }
1135}
1136
1137/// Cargo or internal command descriptor
1138#[derive(Debug, Clone, PartialEq, Eq)]
1139pub struct Command(String);
1140
1141impl Default for Command {
1142    fn default() -> Self {
1143        Self::build()
1144    }
1145}
1146
1147impl Command {
1148    #[must_use]
1149    pub fn new(name: impl Into<String>) -> Self {
1150        Self(name.into())
1151    }
1152
1153    #[must_use]
1154    pub fn build() -> Self {
1155        Self::new("build")
1156    }
1157
1158    #[must_use]
1159    pub fn check() -> Self {
1160        Self::new("check")
1161    }
1162
1163    #[must_use]
1164    pub fn run() -> Self {
1165        Self::new("run")
1166    }
1167
1168    #[must_use]
1169    pub fn test() -> Self {
1170        Self::new("test")
1171    }
1172
1173    #[must_use]
1174    pub fn bench() -> Self {
1175        Self::new("bench")
1176    }
1177
1178    #[must_use]
1179    pub fn clippy() -> Self {
1180        Self::new("clippy")
1181    }
1182
1183    #[must_use]
1184    pub fn setup() -> Self {
1185        Self::new("setup")
1186    }
1187
1188    #[must_use]
1189    pub fn exec() -> Self {
1190        Self::new("exec")
1191    }
1192
1193    #[must_use]
1194    pub fn as_str(&self) -> &str {
1195        &self.0
1196    }
1197
1198    #[must_use]
1199    pub fn needs_runner(&self) -> bool {
1200        matches!(self.as_str(), "run" | "test" | "bench")
1201    }
1202}
1203
1204/// Parsed and validated arguments
1205#[derive(Debug, Clone)]
1206pub struct Args {
1207    /// Rust toolchain to use (e.g., "nightly", "stable")
1208    pub toolchain: Option<String>,
1209    /// Cargo command to execute
1210    pub command: Command,
1211    /// Expanded target list (after glob pattern expansion)
1212    pub targets: Vec<String>,
1213    /// Skip passing --target to cargo (for host builds)
1214    pub no_cargo_target: bool,
1215    /// Cross-make version for toolchain downloads
1216    pub cross_make_version: String,
1217    /// Directory for cross-compiler toolchains
1218    pub cross_compiler_dir: PathBuf,
1219    /// All build arguments from CLI
1220    pub build: BuildArgs,
1221}
1222
1223impl std::ops::Deref for Args {
1224    type Target = BuildArgs;
1225
1226    fn deref(&self) -> &Self::Target {
1227        &self.build
1228    }
1229}
1230
1231impl std::ops::DerefMut for Args {
1232    fn deref_mut(&mut self) -> &mut Self::Target {
1233        &mut self.build
1234    }
1235}
1236
1237impl Args {
1238    /// Create Args from `BuildArgs` and Command
1239    fn from_build_args(b: BuildArgs, command: Command, toolchain: Option<String>) -> Result<Self> {
1240        let cross_compiler_dir = b
1241            .cross_compiler_dir
1242            .clone()
1243            .unwrap_or_else(|| std::env::temp_dir().join("rust-cross-compiler"));
1244        let targets = expand_target_list(&b.targets)?;
1245
1246        Ok(Self {
1247            toolchain,
1248            command,
1249            targets,
1250            no_cargo_target: false,
1251            cross_make_version: b.cross_make_version.clone(),
1252            cross_compiler_dir,
1253            build: b,
1254        })
1255    }
1256}
1257
1258/// Result of parsing CLI arguments
1259pub enum ParseResult {
1260    /// Normal build/check/run/test/bench command
1261    Build(Box<Args>),
1262    /// Print configured environment variables
1263    Setup(Box<SetupArgs>),
1264    /// Execute an arbitrary command after environment setup
1265    Exec(Box<ExecArgs>),
1266    /// Show targets command
1267    ShowTargets(OutputFormat),
1268    /// Show version
1269    ShowVersion,
1270}
1271
1272/// Remove empty environment variables that clap would incorrectly treat as having values.
1273/// Clap's `env = "VAR"` attribute treats empty strings as valid values, which causes
1274/// parsing errors for `PathBuf` and other types that don't accept empty strings.
1275fn sanitize_clap_env() {
1276    let empty_vars: Vec<_> = std::env::vars()
1277        .filter(|(_, v)| v.is_empty())
1278        .map(|(k, _)| k)
1279        .collect();
1280
1281    for var in empty_vars {
1282        std::env::remove_var(&var);
1283    }
1284}
1285
1286/// Parse command-line arguments
1287pub fn parse_args() -> Result<ParseResult> {
1288    let args: Vec<String> = std::env::args().collect();
1289    parse_args_from(args)
1290}
1291
1292#[derive(Parser, Debug)]
1293#[command(name = "cargo-cross")]
1294#[command(styles = cli_styles())]
1295struct ExternalCargoCli {
1296    #[command(flatten)]
1297    build: BuildArgs,
1298}
1299
1300/// Parse arguments from a vector (for testing)
1301pub fn parse_args_from(args: Vec<String>) -> Result<ParseResult> {
1302    use std::env;
1303
1304    // Remove empty environment variables that clap would incorrectly treat as having values
1305    // This must be done before clap parses, as clap reads from env vars with `env = "VAR"`
1306    sanitize_clap_env();
1307
1308    let mut toolchain: Option<String> = None;
1309
1310    // When invoked as `cargo cross`, cargo sets the CARGO env var and passes
1311    // args as ["cargo-cross", "cross", ...]. We need to skip both.
1312    // When invoked directly as `cargo-cross`, only skip the program name.
1313    let is_cargo_subcommand = env::var("CARGO").is_ok()
1314        && env::var("CARGO_HOME").is_ok()
1315        && args.get(1).map(String::as_str) == Some(SUBCOMMAND);
1316
1317    let skip_count = if is_cargo_subcommand { 2 } else { 1 };
1318    let mut args: Vec<String> = args.iter().skip(skip_count).cloned().collect();
1319
1320    // Extract +toolchain from args (can appear at the beginning)
1321    // e.g., cargo cross +nightly build -t x86_64-unknown-linux-musl
1322    if let Some(tc) = args.first().and_then(|a| a.strip_prefix('+')) {
1323        toolchain = Some(tc.to_string());
1324        args.remove(0);
1325    }
1326
1327    if let Some(command_name) = args.first().cloned() {
1328        let remaining_args: Vec<String> = args.iter().skip(1).cloned().collect();
1329        let help_or_version_requested = has_wrapper_help_or_version_request(&remaining_args);
1330
1331        if let Some(canonical_name) = canonical_cargo_command_name(&command_name) {
1332            if !help_or_version_requested {
1333                return parse_cargo_command_args(
1334                    &command_name,
1335                    canonical_name,
1336                    remaining_args,
1337                    toolchain,
1338                );
1339            }
1340        }
1341        if should_parse_as_external_cargo_command(&command_name) {
1342            return parse_cargo_command_args(
1343                &command_name,
1344                &command_name,
1345                remaining_args,
1346                toolchain,
1347            );
1348        }
1349    }
1350
1351    // Prepend program name for clap (internal name, not displayed)
1352    args.insert(0, BIN_NAME.to_string());
1353
1354    // Build command with dynamic help text
1355    let cmd = build_command_with_dynamic_help();
1356
1357    // Try to parse with clap using modified command
1358    let cli = match cmd.try_get_matches_from(&args) {
1359        Ok(matches) => {
1360            Cli::from_arg_matches(&matches).map_err(|e| CrossError::ClapError(e.to_string()))?
1361        }
1362        Err(e) => {
1363            // For help/version/missing subcommand, let clap print and exit
1364            if matches!(
1365                e.kind(),
1366                clap::error::ErrorKind::DisplayHelp
1367                    | clap::error::ErrorKind::DisplayVersion
1368                    | clap::error::ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand
1369            ) {
1370                e.exit();
1371            }
1372            // For argument errors, return Err so tests can catch them
1373            return Err(CrossError::ClapError(e.render().to_string()));
1374        }
1375    };
1376
1377    process_cli(cli, toolchain)
1378}
1379
1380fn has_wrapper_help_or_version_request(args: &[String]) -> bool {
1381    args.iter()
1382        .take_while(|arg| arg.as_str() != "--")
1383        .any(|arg| matches!(arg.as_str(), "--help" | "-h" | "--version" | "-V"))
1384}
1385
1386/// Build the clap Command with dynamic help text based on invocation style
1387fn build_command_with_dynamic_help() -> clap::Command {
1388    let prog = program_name();
1389
1390    // Build dynamic help strings
1391    let usage = format!("{prog} [+toolchain] <COMMAND> [OPTIONS]");
1392    let after_help = format!(
1393        "Use '{prog} <COMMAND> --help' for more information about a command.\n\n\
1394TOOLCHAIN:\n    \
1395    If the first argument begins with +, it will be interpreted as a Rust toolchain\n    \
1396    name (such as +nightly, +stable, or +1.75.0). This follows the same convention\n    \
1397    as rustup and cargo.\n\n\
1398EXAMPLES:\n    \
1399    {prog} build -t x86_64-unknown-linux-musl\n    \
1400    {prog} +nightly build -t aarch64-unknown-linux-gnu --profile release\n    \
1401    {prog} build -t '*-linux-musl' --crt-static true\n    \
1402    {prog} test -t x86_64-unknown-linux-musl -- --nocapture"
1403    );
1404
1405    // Build dynamic version help texts
1406    let glibc_help = format!(
1407        "Specify glibc version for GNU libc targets. The version determines the minimum Linux kernel\n\
1408         version required. Lower versions provide better compatibility with older systems.\n\
1409         Supported: {}",
1410        supported_glibc_versions_str()
1411    );
1412    let freebsd_help = format!(
1413        "Specify FreeBSD version for FreeBSD targets. Supported: {}",
1414        supported_freebsd_versions_str()
1415    );
1416    let iphone_sdk_help = format!(
1417        "Specify iPhone SDK version for iOS targets. On Linux: uses pre-built SDK from releases.\n\
1418         On macOS: uses installed Xcode SDK. Supported on Linux: {}",
1419        supported_iphone_sdk_versions_str()
1420    );
1421    let macos_sdk_help = format!(
1422        "Specify macOS SDK version for Darwin targets. On Linux: uses osxcross with pre-built SDK.\n\
1423         On macOS: uses installed Xcode SDK. Supported on Linux: {}",
1424        supported_macos_sdk_versions_str()
1425    );
1426
1427    // Get base command and modify it
1428    let mut cmd = Cli::command().override_usage(usage).after_help(after_help);
1429
1430    // Update subcommand usage strings and version help texts
1431    for subcmd_name in &["build", "check", "run", "test", "bench", "clippy"] {
1432        let glibc_help = glibc_help.clone();
1433        let freebsd_help = freebsd_help.clone();
1434        let iphone_sdk_help = iphone_sdk_help.clone();
1435        let macos_sdk_help = macos_sdk_help.clone();
1436        cmd = cmd.mut_subcommand(*subcmd_name, |subcmd| {
1437            subcmd
1438                .override_usage(format!(
1439                    "{prog} [+toolchain] {subcmd_name} [OPTIONS] [-- <PASSTHROUGH_ARGS>...]"
1440                ))
1441                .mut_arg("glibc_version", |arg| arg.long_help(glibc_help))
1442                .mut_arg("freebsd_version", |arg| arg.long_help(freebsd_help))
1443                .mut_arg("iphone_sdk_version", |arg| arg.long_help(iphone_sdk_help))
1444                .mut_arg("macos_sdk_version", |arg| arg.long_help(macos_sdk_help))
1445        });
1446    }
1447
1448    cmd
1449}
1450
1451fn should_parse_as_external_cargo_command(command_name: &str) -> bool {
1452    if command_name.starts_with('-') {
1453        return false;
1454    }
1455
1456    canonical_cargo_command_name(command_name).is_none()
1457        && is_supported_external_cargo_command(command_name)
1458}
1459
1460fn canonical_cargo_command_name(command_name: &str) -> Option<&'static str> {
1461    match command_name {
1462        "build" | "b" => Some("build"),
1463        "check" | "c" => Some("check"),
1464        "run" | "r" => Some("run"),
1465        "test" | "t" => Some("test"),
1466        "bench" => Some("bench"),
1467        "clippy" => Some("clippy"),
1468        _ => None,
1469    }
1470}
1471
1472fn is_supported_external_cargo_command(command_name: &str) -> bool {
1473    matches!(command_name, "doc" | "fix" | "rustc" | "rustdoc")
1474}
1475
1476fn parse_cargo_command_args(
1477    display_name: &str,
1478    canonical_name: &str,
1479    args: Vec<String>,
1480    toolchain: Option<String>,
1481) -> Result<ParseResult> {
1482    let processed_args = preprocess_cargo_args(args);
1483    let mut clap_args = Vec::with_capacity(processed_args.len() + 1);
1484    clap_args.push(BIN_NAME.to_string());
1485    clap_args.extend(processed_args);
1486
1487    let cmd = build_external_cargo_command_with_dynamic_help(display_name);
1488    let cli = match cmd.try_get_matches_from(&clap_args) {
1489        Ok(matches) => ExternalCargoCli::from_arg_matches(&matches)
1490            .map_err(|e| CrossError::ClapError(e.to_string()))?,
1491        Err(e) => {
1492            if matches!(
1493                e.kind(),
1494                clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion
1495            ) {
1496                e.exit();
1497            }
1498            return Err(CrossError::ClapError(e.render().to_string()));
1499        }
1500    };
1501
1502    let args = finalize_args(cli.build, Command::new(canonical_name), toolchain)?;
1503    Ok(ParseResult::Build(Box::new(args)))
1504}
1505
1506#[derive(Clone, Copy, Debug)]
1507struct KnownArgSpec {
1508    takes_value: bool,
1509    min_values: usize,
1510    max_values: usize,
1511    allow_hyphen_values: bool,
1512}
1513
1514fn preprocess_cargo_args(args: Vec<String>) -> Vec<String> {
1515    let parser = ExternalCargoCli::command();
1516    let (long_args, short_args) = known_arg_maps(&parser);
1517    let mut processed = Vec::with_capacity(args.len());
1518    let mut pending_value: Option<KnownArgSpec> = None;
1519    let mut passthrough = false;
1520    let mut index = 0;
1521
1522    while index < args.len() {
1523        let token = &args[index];
1524
1525        if passthrough {
1526            processed.push(token.clone());
1527            index += 1;
1528            continue;
1529        }
1530
1531        if token == "--" {
1532            pending_value = None;
1533            passthrough = true;
1534            processed.push(token.clone());
1535            index += 1;
1536            continue;
1537        }
1538
1539        if matches!(token.as_str(), "--help" | "-h" | "--version" | "-V") {
1540            pending_value = None;
1541            processed.push(token.clone());
1542            index += 1;
1543            continue;
1544        }
1545
1546        if let Some(spec) = pending_value {
1547            if should_consume_known_value(token, spec) {
1548                processed.push(token.clone());
1549                pending_value = None;
1550                index += 1;
1551                continue;
1552            }
1553            pending_value = None;
1554        }
1555
1556        if let Some(spec) = classify_known_long_arg(token, &long_args) {
1557            processed.push(token.clone());
1558            pending_value = needs_following_value(token, spec);
1559            index += 1;
1560            continue;
1561        }
1562
1563        if let Some(spec) = classify_known_short_arg(token, &short_args) {
1564            processed.push(token.clone());
1565            pending_value = needs_following_value(token, spec);
1566            index += 1;
1567            continue;
1568        }
1569
1570        push_cargo_arg(&mut processed, token);
1571        if should_pair_unknown_value(token, args.get(index + 1).map(String::as_str)) {
1572            if let Some(value) = args.get(index + 1) {
1573                push_cargo_arg(&mut processed, value);
1574                index += 1;
1575            }
1576        }
1577        index += 1;
1578    }
1579
1580    processed
1581}
1582
1583fn known_arg_maps(
1584    command: &clap::Command,
1585) -> (HashMap<String, KnownArgSpec>, HashMap<char, KnownArgSpec>) {
1586    let mut long_args = HashMap::new();
1587    let mut short_args = HashMap::new();
1588
1589    for arg in command.get_arguments() {
1590        if arg.is_positional() {
1591            continue;
1592        }
1593
1594        let (takes_value, min_values, max_values) = infer_arg_value_shape(arg);
1595        let spec = KnownArgSpec {
1596            takes_value,
1597            min_values,
1598            max_values,
1599            allow_hyphen_values: arg.is_allow_hyphen_values_set(),
1600        };
1601
1602        if let Some(long) = arg.get_long() {
1603            long_args.insert(long.to_string(), spec);
1604        }
1605        if let Some(aliases) = arg.get_long_and_visible_aliases() {
1606            for alias in aliases {
1607                long_args.insert(alias.to_string(), spec);
1608            }
1609        }
1610        if let Some(short) = arg.get_short() {
1611            short_args.insert(short, spec);
1612        }
1613        if let Some(aliases) = arg.get_short_and_visible_aliases() {
1614            for alias in aliases {
1615                short_args.insert(alias, spec);
1616            }
1617        }
1618    }
1619
1620    (long_args, short_args)
1621}
1622
1623fn infer_arg_value_shape(arg: &clap::Arg) -> (bool, usize, usize) {
1624    if let Some(range) = arg.get_num_args() {
1625        return (range.takes_values(), range.min_values(), range.max_values());
1626    }
1627
1628    match arg.get_action() {
1629        ArgAction::Set | ArgAction::Append => (true, 1, 1),
1630        ArgAction::SetTrue
1631        | ArgAction::SetFalse
1632        | ArgAction::Count
1633        | ArgAction::Help
1634        | ArgAction::HelpShort
1635        | ArgAction::HelpLong
1636        | ArgAction::Version => (false, 0, 0),
1637        _ => (false, 0, 0),
1638    }
1639}
1640
1641fn classify_known_long_arg(
1642    token: &str,
1643    long_args: &HashMap<String, KnownArgSpec>,
1644) -> Option<KnownArgSpec> {
1645    if !token.starts_with("--") || token == "--" {
1646        return None;
1647    }
1648
1649    let name = token
1650        .trim_start_matches("--")
1651        .split_once('=')
1652        .map_or_else(|| token.trim_start_matches("--"), |(name, _)| name);
1653    long_args.get(name).copied()
1654}
1655
1656fn classify_known_short_arg(
1657    token: &str,
1658    short_args: &HashMap<char, KnownArgSpec>,
1659) -> Option<KnownArgSpec> {
1660    if !token.starts_with('-') || token.starts_with("--") || token == "-" {
1661        return None;
1662    }
1663
1664    let rest = token.strip_prefix('-')?;
1665    let mut chars = rest.chars();
1666    let first = chars.next()?;
1667    let spec = short_args.get(&first).copied()?;
1668
1669    if spec.takes_value {
1670        return Some(spec);
1671    }
1672
1673    if rest
1674        .chars()
1675        .all(|short| short_args.get(&short).is_some_and(|arg| !arg.takes_value))
1676    {
1677        return Some(spec);
1678    }
1679
1680    None
1681}
1682
1683fn needs_following_value(token: &str, spec: KnownArgSpec) -> Option<KnownArgSpec> {
1684    if !spec.takes_value || spec.max_values == 0 {
1685        return None;
1686    }
1687
1688    let has_inline_value = if token.starts_with("--") {
1689        token.contains('=')
1690    } else {
1691        token.len() > 2
1692    };
1693
1694    if has_inline_value {
1695        None
1696    } else {
1697        Some(spec)
1698    }
1699}
1700
1701fn should_consume_known_value(token: &str, spec: KnownArgSpec) -> bool {
1702    if token == "--" {
1703        return false;
1704    }
1705
1706    if spec.min_values == 0 && token.starts_with('-') && !spec.allow_hyphen_values {
1707        return false;
1708    }
1709
1710    !token.starts_with('-') || spec.allow_hyphen_values || spec.min_values > 0
1711}
1712
1713fn should_pair_unknown_value(current: &str, next: Option<&str>) -> bool {
1714    if current == "-" || current == "--" {
1715        return false;
1716    }
1717
1718    if !(current.starts_with("--") || (current.starts_with('-') && current.len() == 2)) {
1719        return false;
1720    }
1721
1722    let Some(next) = next else {
1723        return false;
1724    };
1725
1726    !next.starts_with('-')
1727}
1728
1729fn push_cargo_arg(processed: &mut Vec<String>, value: &str) {
1730    processed.push("--cargo-args".to_string());
1731    processed.push(value.to_string());
1732}
1733
1734fn build_external_cargo_command_with_dynamic_help(command_name: &str) -> clap::Command {
1735    let prog = program_name();
1736    let after_help = format!(
1737        "This command forwards to 'cargo {command_name}' after configuring the\n\
1738cross-compilation environment.\n\n\
1739EXAMPLES:\n    \
1740{prog} {command_name} -t x86_64-unknown-linux-musl\n    \
1741{prog} {command_name} -t aarch64-unknown-linux-gnu -- --help"
1742    );
1743
1744    ExternalCargoCli::command()
1745        .override_usage(format!(
1746            "{prog} [+toolchain] {command_name} [OPTIONS] [-- <PASSTHROUGH_ARGS>...]"
1747        ))
1748        .after_help(after_help)
1749}
1750
1751fn process_cli(cli: Cli, toolchain: Option<String>) -> Result<ParseResult> {
1752    match cli.command {
1753        CliCommand::Build(args) => {
1754            let args = finalize_args(args, Command::build(), toolchain)?;
1755            Ok(ParseResult::Build(Box::new(args)))
1756        }
1757        CliCommand::Check(args) => {
1758            let args = finalize_args(args, Command::check(), toolchain)?;
1759            Ok(ParseResult::Build(Box::new(args)))
1760        }
1761        CliCommand::Run(args) => {
1762            let args = finalize_args(args, Command::run(), toolchain)?;
1763            Ok(ParseResult::Build(Box::new(args)))
1764        }
1765        CliCommand::Test(args) => {
1766            let args = finalize_args(args, Command::test(), toolchain)?;
1767            Ok(ParseResult::Build(Box::new(args)))
1768        }
1769        CliCommand::Bench(args) => {
1770            let args = finalize_args(args, Command::bench(), toolchain)?;
1771            Ok(ParseResult::Build(Box::new(args)))
1772        }
1773        CliCommand::Clippy(args) => {
1774            let args = finalize_args(args, Command::clippy(), toolchain)?;
1775            Ok(ParseResult::Build(Box::new(args)))
1776        }
1777        CliCommand::Setup(setup) => {
1778            let args = finalize_args(setup.build, Command::setup(), toolchain)?;
1779            Ok(ParseResult::Setup(Box::new(SetupArgs {
1780                args,
1781                format: setup.format,
1782            })))
1783        }
1784        CliCommand::Exec(exec) => {
1785            let mut build = exec.build;
1786            populate_env_arg_fallbacks(&mut build);
1787            let command = std::mem::take(&mut build.passthrough_args);
1788            if command.is_empty() {
1789                return Err(CrossError::InvalidArgument(
1790                    "exec requires a command after `--`".to_string(),
1791                ));
1792            }
1793            let args = finalize_args(build, Command::exec(), toolchain)?;
1794            Ok(ParseResult::Exec(Box::new(ExecArgs { args, command })))
1795        }
1796        CliCommand::Targets(args) => Ok(ParseResult::ShowTargets(args.format)),
1797        CliCommand::Version => Ok(ParseResult::ShowVersion),
1798    }
1799}
1800
1801/// Check if a string is a glob pattern (contains *, ?, or [)
1802fn is_glob_pattern(s: &str) -> bool {
1803    s.contains('*') || s.contains('?') || s.contains('[')
1804}
1805
1806/// Validate that a target triple only contains valid characters (a-z, 0-9, -, _)
1807fn validate_target_triple(target: &str) -> Result<()> {
1808    for c in target.chars() {
1809        if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '-' && c != '_' {
1810            return Err(CrossError::InvalidTargetTriple {
1811                target: target.to_string(),
1812                char: c,
1813            });
1814        }
1815    }
1816    Ok(())
1817}
1818
1819/// Expand target list, handling glob patterns
1820fn expand_target_list(targets: &[String]) -> Result<Vec<String>> {
1821    let mut result = Vec::new();
1822    for target in targets {
1823        // Split by comma or newline to support multiple delimiters
1824        for part in target.split([',', '\n']) {
1825            let part = part.trim();
1826            if part.is_empty() {
1827                continue;
1828            }
1829            let expanded = config::expand_targets(part);
1830            if expanded.is_empty() {
1831                // If it was a glob pattern that matched nothing, error
1832                if is_glob_pattern(part) {
1833                    return Err(CrossError::NoMatchingTargets {
1834                        pattern: part.to_string(),
1835                    });
1836                }
1837                // Not a glob pattern, validate and use as-is
1838                validate_target_triple(part)?;
1839                if !result.contains(&part.to_string()) {
1840                    result.push(part.to_string());
1841                }
1842            } else {
1843                for t in expanded {
1844                    let t = t.to_string();
1845                    if !result.contains(&t) {
1846                        result.push(t);
1847                    }
1848                }
1849            }
1850        }
1851    }
1852    Ok(result)
1853}
1854
1855fn finalize_args(
1856    mut build_args: BuildArgs,
1857    command: Command,
1858    toolchain: Option<String>,
1859) -> Result<Args> {
1860    // Handle --release flag: set profile to "release"
1861    if build_args.release {
1862        build_args.profile = "release".to_string();
1863    }
1864
1865    // Handle build_std: empty string means disabled (from env var "false")
1866    if build_args
1867        .build_std
1868        .as_ref()
1869        .is_some_and(std::string::String::is_empty)
1870    {
1871        build_args.build_std = None;
1872    }
1873
1874    populate_env_arg_fallbacks(&mut build_args);
1875
1876    // Merge toolchain: +toolchain syntax takes precedence over --toolchain option
1877    let final_toolchain = toolchain.or_else(|| build_args.toolchain_option.clone());
1878
1879    let mut args = Args::from_build_args(build_args, command, final_toolchain)?;
1880
1881    // Validate versions
1882    validate_versions(&args)?;
1883
1884    // Handle empty targets - default to host
1885    if args.targets.is_empty() {
1886        let host = config::HostPlatform::detect();
1887        args.targets.push(host.triple);
1888        args.no_toolchain_setup = true;
1889        args.no_cargo_target = true;
1890    }
1891    // Note: "host-tuple" is handled dynamically in execute_target
1892
1893    Ok(args)
1894}
1895
1896fn populate_env_arg_fallbacks(build_args: &mut BuildArgs) {
1897    if build_args.cargo_args.is_empty() {
1898        if let Some(env_args) = parse_env_args("CARGO_ARGS") {
1899            build_args.cargo_args = env_args;
1900        }
1901    }
1902    if build_args.passthrough_args.is_empty() {
1903        if let Some(env_args) = parse_passthrough_env_args("CARGO_PASSTHROUGH_ARGS") {
1904            build_args.passthrough_args = env_args;
1905        }
1906    }
1907}
1908
1909fn parse_passthrough_env_args(env_name: &str) -> Option<Vec<String>> {
1910    let mut args = parse_env_args(env_name)?;
1911    if args.first().is_some_and(|arg| arg == "--") {
1912        args.remove(0);
1913    }
1914    if args.is_empty() {
1915        None
1916    } else {
1917        Some(args)
1918    }
1919}
1920
1921/// Parse environment variable as shell-style arguments using shlex
1922/// Returns None if env var is not set or empty
1923fn parse_env_args(env_name: &str) -> Option<Vec<String>> {
1924    let value = std::env::var(env_name).ok()?;
1925    let value = value.trim();
1926    if value.is_empty() {
1927        return None;
1928    }
1929    match shlex::split(value) {
1930        Some(parsed) if !parsed.is_empty() => Some(parsed),
1931        Some(_) => None,
1932        None => {
1933            eprintln!("Warning: Failed to parse {env_name} (mismatched quotes?): {value}");
1934            None
1935        }
1936    }
1937}
1938
1939/// Validate version options
1940fn validate_versions(args: &Args) -> Result<()> {
1941    // Only validate glibc version if it's specified (non-empty)
1942    // Empty string means use default version, which is valid for both gnu and musl targets
1943    if !args.glibc_version.is_empty()
1944        && !SUPPORTED_GLIBC_VERSIONS.contains(&args.glibc_version.as_str())
1945    {
1946        return Err(CrossError::UnsupportedGlibcVersion {
1947            version: args.glibc_version.clone(),
1948            supported: SUPPORTED_GLIBC_VERSIONS.join(", "),
1949        });
1950    }
1951
1952    let host = config::HostPlatform::detect();
1953    if !host.is_darwin()
1954        && !SUPPORTED_IPHONE_SDK_VERSIONS.contains(&args.iphone_sdk_version.as_str())
1955    {
1956        return Err(CrossError::UnsupportedIphoneSdkVersion {
1957            version: args.iphone_sdk_version.clone(),
1958            supported: SUPPORTED_IPHONE_SDK_VERSIONS.join(", "),
1959        });
1960    }
1961
1962    if !host.is_darwin() && !SUPPORTED_MACOS_SDK_VERSIONS.contains(&args.macos_sdk_version.as_str())
1963    {
1964        return Err(CrossError::UnsupportedMacosSdkVersion {
1965            version: args.macos_sdk_version.clone(),
1966            supported: SUPPORTED_MACOS_SDK_VERSIONS.join(", "),
1967        });
1968    }
1969
1970    if !SUPPORTED_FREEBSD_VERSIONS.contains(&args.freebsd_version.as_str()) {
1971        return Err(CrossError::UnsupportedFreebsdVersion {
1972            version: args.freebsd_version.clone(),
1973            supported: SUPPORTED_FREEBSD_VERSIONS.join(", "),
1974        });
1975    }
1976
1977    Ok(())
1978}
1979
1980/// Print all supported targets
1981pub fn print_all_targets(format: OutputFormat) {
1982    let mut targets: Vec<_> = config::all_targets().collect();
1983    targets.sort_unstable();
1984
1985    match format {
1986        OutputFormat::Text => {
1987            use colored::Colorize;
1988            println!("{}", "Supported Rust targets:".bright_green());
1989            for target in &targets {
1990                println!("  {}", target.bright_cyan());
1991            }
1992        }
1993        OutputFormat::Json => {
1994            let json_array = serde_json::to_string(&targets).unwrap_or_else(|_| "[]".to_string());
1995            println!("{json_array}");
1996        }
1997        OutputFormat::Plain => {
1998            for target in &targets {
1999                println!("{target}");
2000            }
2001        }
2002    }
2003
2004    // Output to GITHUB_OUTPUT if running in GitHub Actions
2005    if let Ok(github_output) = std::env::var("GITHUB_OUTPUT") {
2006        let json_array = serde_json::to_string(&targets).unwrap_or_else(|_| "[]".to_string());
2007        if let Ok(mut file) = std::fs::OpenOptions::new()
2008            .append(true)
2009            .open(&github_output)
2010        {
2011            use std::io::Write;
2012            let _ = writeln!(file, "all-targets={json_array}");
2013        }
2014    }
2015}
2016
2017/// Print version information
2018pub fn print_version() {
2019    use colored::Colorize;
2020
2021    let version = env!("CARGO_PKG_VERSION");
2022    let name = env!("CARGO_PKG_NAME");
2023    println!("{} {}", name.bright_green(), version.bright_cyan());
2024}
2025
2026#[cfg(test)]
2027mod tests {
2028    use super::*;
2029
2030    fn parse(args: &[&str]) -> Result<Args> {
2031        let args: Vec<String> = args.iter().map(std::string::ToString::to_string).collect();
2032        match parse_args_from(args)? {
2033            ParseResult::Build(args) => Ok(*args),
2034            ParseResult::ShowTargets(_) => panic!("unexpected ShowTargets"),
2035            ParseResult::Setup(_) => panic!("unexpected Setup"),
2036            ParseResult::Exec(_) => panic!("unexpected Exec"),
2037            ParseResult::ShowVersion => panic!("unexpected ShowVersion"),
2038        }
2039    }
2040
2041    fn parse_setup(args: &[&str]) -> Result<SetupArgs> {
2042        let args: Vec<String> = args.iter().map(std::string::ToString::to_string).collect();
2043        match parse_args_from(args)? {
2044            ParseResult::Setup(args) => Ok(*args),
2045            _ => panic!("unexpected parse result"),
2046        }
2047    }
2048
2049    fn parse_exec(args: &[&str]) -> Result<ExecArgs> {
2050        let args: Vec<String> = args.iter().map(std::string::ToString::to_string).collect();
2051        match parse_args_from(args)? {
2052            ParseResult::Exec(args) => Ok(*args),
2053            _ => panic!("unexpected parse result"),
2054        }
2055    }
2056
2057    // Note: test_parse_empty_requires_subcommand removed because MissingSubcommand
2058    // now calls exit() which cannot be tested
2059
2060    #[test]
2061    fn test_parse_build_command() {
2062        let args = parse(&["cargo-cross", "build"]).unwrap();
2063        assert_eq!(args.command, Command::build());
2064        assert_eq!(args.profile, "dev");
2065    }
2066
2067    #[test]
2068    fn test_parse_check_command() {
2069        let args = parse(&["cargo-cross", "check"]).unwrap();
2070        assert_eq!(args.command, Command::check());
2071    }
2072
2073    #[test]
2074    fn test_parse_clippy_command() {
2075        let args = parse(&["cargo-cross", "clippy"]).unwrap();
2076        assert_eq!(args.command, Command::clippy());
2077    }
2078
2079    #[test]
2080    fn test_parse_clippy_unknown_cargo_flags() {
2081        let args = parse(&[
2082            "cargo-cross",
2083            "clippy",
2084            "--workspace",
2085            "--all-targets",
2086            "--fix",
2087            "--allow-dirty",
2088            "--target",
2089            "x86_64-pc-windows-msvc",
2090        ])
2091        .unwrap();
2092        assert!(args.workspace);
2093        assert!(args.build_all_targets);
2094        assert_eq!(
2095            args.cargo_args,
2096            vec!["--fix".to_string(), "--allow-dirty".to_string()]
2097        );
2098        assert_eq!(args.targets, vec!["x86_64-pc-windows-msvc"]);
2099    }
2100
2101    #[test]
2102    fn test_parse_external_cargo_command() {
2103        let args = parse(&["cargo-cross", "doc"]).unwrap();
2104        assert_eq!(args.command, Command::new("doc"));
2105    }
2106
2107    #[test]
2108    fn test_reject_non_build_like_external_cargo_command() {
2109        let result = parse(&["cargo-cross", "metadata"]);
2110        assert!(result.is_err());
2111    }
2112
2113    #[test]
2114    fn test_parse_external_command_unknown_cargo_flags() {
2115        let args = parse(&[
2116            "cargo-cross",
2117            "doc",
2118            "--workspace",
2119            "--open",
2120            "--target",
2121            "x86_64-unknown-linux-musl",
2122        ])
2123        .unwrap();
2124        assert_eq!(args.command, Command::new("doc"));
2125        assert!(args.workspace);
2126        assert_eq!(args.cargo_args, vec!["--open".to_string()]);
2127        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2128    }
2129
2130    #[test]
2131    fn test_parse_setup_command() {
2132        let args =
2133            parse_setup(&["cargo-cross", "setup", "-t", "x86_64-unknown-linux-musl"]).unwrap();
2134        assert_eq!(args.args.command, Command::setup());
2135        assert_eq!(args.format, SetupOutputFormat::Auto);
2136    }
2137
2138    #[test]
2139    fn test_parse_setup_command_explicit_format() {
2140        let args = parse_setup(&[
2141            "cargo-cross",
2142            "setup",
2143            "-t",
2144            "x86_64-unknown-linux-musl",
2145            "--format",
2146            "fish",
2147        ])
2148        .unwrap();
2149        assert_eq!(args.format, SetupOutputFormat::Fish);
2150    }
2151
2152    #[test]
2153    fn test_parse_exec_command() {
2154        let args = parse_exec(&[
2155            "cargo-cross",
2156            "exec",
2157            "-t",
2158            "x86_64-unknown-linux-musl",
2159            "--",
2160            "env",
2161            "FOO=bar",
2162        ])
2163        .unwrap();
2164        assert_eq!(args.args.command, Command::exec());
2165        assert_eq!(args.command, vec!["env", "FOO=bar"]);
2166    }
2167
2168    #[test]
2169    fn test_parse_exec_command_disable_auto_target() {
2170        let args = parse_exec(&[
2171            "cargo-cross",
2172            "exec",
2173            "--no-append-target",
2174            "-t",
2175            "x86_64-unknown-linux-musl",
2176            "--",
2177            "cargo",
2178            "clippy",
2179        ])
2180        .unwrap();
2181        assert!(args.args.no_append_target);
2182        assert_eq!(args.command, vec!["cargo", "clippy"]);
2183    }
2184
2185    #[test]
2186    fn test_parse_exec_command_from_env_passthrough() {
2187        std::env::set_var("CARGO_PASSTHROUGH_ARGS", "-- env FOO=bar");
2188        let args = parse_exec(&["cargo-cross", "exec", "-t", "x86_64-unknown-linux-musl"]).unwrap();
2189        assert_eq!(args.args.command, Command::exec());
2190        assert_eq!(args.command, vec!["env", "FOO=bar"]);
2191        std::env::remove_var("CARGO_PASSTHROUGH_ARGS");
2192    }
2193
2194    #[test]
2195    fn test_parse_target() {
2196        let args = parse(&["cargo-cross", "build", "-t", "x86_64-unknown-linux-musl"]).unwrap();
2197        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2198    }
2199
2200    #[test]
2201    fn test_parse_multiple_targets() {
2202        let args = parse(&[
2203            "cargo-cross",
2204            "build",
2205            "-t",
2206            "x86_64-unknown-linux-musl,aarch64-unknown-linux-musl",
2207        ])
2208        .unwrap();
2209        assert_eq!(
2210            args.targets,
2211            vec!["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"]
2212        );
2213    }
2214
2215    #[test]
2216    fn test_parse_verbose() {
2217        let args = parse(&["cargo-cross", "build", "-vvv"]).unwrap();
2218        assert_eq!(args.verbose_level, 3);
2219    }
2220
2221    #[test]
2222    fn test_parse_crt_static_flag() {
2223        let args = parse(&["cargo-cross", "build", "--crt-static", "true"]).unwrap();
2224        assert_eq!(args.crt_static, Some(true));
2225    }
2226
2227    #[test]
2228    fn test_parse_crt_static_false() {
2229        let args = parse(&["cargo-cross", "build", "--crt-static", "false"]).unwrap();
2230        assert_eq!(args.crt_static, Some(false));
2231    }
2232
2233    #[test]
2234    fn test_parse_crt_static_no_value() {
2235        // --crt-static without value should default to true
2236        let args = parse(&["cargo-cross", "build", "--crt-static"]).unwrap();
2237        assert_eq!(args.crt_static, Some(true));
2238    }
2239
2240    #[test]
2241    fn test_parse_crt_static_not_provided() {
2242        // When --crt-static is not provided at all, value should be None
2243        let args = parse(&["cargo-cross", "build"]).unwrap();
2244        assert_eq!(args.crt_static, None);
2245    }
2246
2247    #[test]
2248    fn test_parse_build_std() {
2249        let args = parse(&["cargo-cross", "build", "--build-std", "true"]).unwrap();
2250        assert_eq!(args.build_std, Some("true".to_string()));
2251    }
2252
2253    #[test]
2254    fn test_parse_build_std_crates() {
2255        let args = parse(&["cargo-cross", "build", "--build-std", "core,alloc"]).unwrap();
2256        assert_eq!(args.build_std, Some("core,alloc".to_string()));
2257    }
2258
2259    #[test]
2260    fn test_parse_build_std_false() {
2261        let args = parse(&["cargo-cross", "build", "--build-std", "false"]).unwrap();
2262        assert_eq!(args.build_std, None);
2263    }
2264
2265    #[test]
2266    fn test_parse_build_std_no_value() {
2267        // --build-std without value should default to "true"
2268        let args = parse(&["cargo-cross", "build", "--build-std"]).unwrap();
2269        assert_eq!(args.build_std, Some("true".to_string()));
2270    }
2271
2272    #[test]
2273    fn test_parse_features() {
2274        let args = parse(&["cargo-cross", "build", "--features", "foo,bar"]).unwrap();
2275        assert_eq!(args.features, Some("foo,bar".to_string()));
2276    }
2277
2278    #[test]
2279    fn test_parse_no_default_features() {
2280        let args = parse(&["cargo-cross", "build", "--no-default-features"]).unwrap();
2281        assert!(args.no_default_features);
2282    }
2283
2284    #[test]
2285    fn test_parse_profile() {
2286        let args = parse(&["cargo-cross", "build", "--profile", "dev"]).unwrap();
2287        assert_eq!(args.profile, "dev");
2288    }
2289
2290    #[test]
2291    fn test_parse_jobs() {
2292        let args = parse(&["cargo-cross", "build", "-j", "4"]).unwrap();
2293        assert_eq!(args.jobs, Some("4".to_string()));
2294    }
2295
2296    #[test]
2297    fn test_parse_passthrough_args() {
2298        let args = parse(&["cargo-cross", "build", "--", "--foo", "--bar"]).unwrap();
2299        assert_eq!(args.passthrough_args, vec!["--foo", "--bar"]);
2300    }
2301
2302    #[test]
2303    fn test_parse_passthrough_args_from_env_with_legacy_separator() {
2304        std::env::set_var("CARGO_PASSTHROUGH_ARGS", "-- --foo --bar");
2305        let args = parse(&["cargo-cross", "build"]).unwrap();
2306        assert_eq!(args.passthrough_args, vec!["--foo", "--bar"]);
2307        std::env::remove_var("CARGO_PASSTHROUGH_ARGS");
2308    }
2309
2310    #[test]
2311    fn test_parse_z_flag() {
2312        let args = parse(&["cargo-cross", "build", "-Z", "build-std"]).unwrap();
2313        assert_eq!(args.cargo_z_flags, vec!["build-std"]);
2314    }
2315
2316    #[test]
2317    fn test_parse_config_flag() {
2318        let args = parse(&["cargo-cross", "build", "--config", "opt-level=3"]).unwrap();
2319        assert_eq!(args.cargo_config, vec!["opt-level=3"]);
2320    }
2321
2322    #[test]
2323    fn test_targets_subcommand() {
2324        let args: Vec<String> = vec!["cargo-cross".to_string(), "targets".to_string()];
2325        match parse_args_from(args).unwrap() {
2326            ParseResult::ShowTargets(format) => {
2327                assert_eq!(format, OutputFormat::Text);
2328            }
2329            _ => panic!("expected ShowTargets"),
2330        }
2331    }
2332
2333    #[test]
2334    fn test_targets_json_format() {
2335        let args: Vec<String> = vec![
2336            "cargo-cross".to_string(),
2337            "targets".to_string(),
2338            "--format".to_string(),
2339            "json".to_string(),
2340        ];
2341        match parse_args_from(args).unwrap() {
2342            ParseResult::ShowTargets(format) => {
2343                assert_eq!(format, OutputFormat::Json);
2344            }
2345            _ => panic!("expected ShowTargets"),
2346        }
2347    }
2348
2349    #[test]
2350    fn test_targets_plain_format() {
2351        let args: Vec<String> = vec![
2352            "cargo-cross".to_string(),
2353            "targets".to_string(),
2354            "-f".to_string(),
2355            "plain".to_string(),
2356        ];
2357        match parse_args_from(args).unwrap() {
2358            ParseResult::ShowTargets(format) => {
2359                assert_eq!(format, OutputFormat::Plain);
2360            }
2361            _ => panic!("expected ShowTargets"),
2362        }
2363    }
2364
2365    #[test]
2366    fn test_parse_toolchain() {
2367        let args = parse(&["cargo-cross", "+nightly", "build"]).unwrap();
2368        assert_eq!(args.toolchain, Some("nightly".to_string()));
2369        assert_eq!(args.command, Command::build());
2370    }
2371
2372    #[test]
2373    fn test_parse_toolchain_with_target() {
2374        let args = parse(&[
2375            "cargo-cross",
2376            "+nightly",
2377            "build",
2378            "-t",
2379            "x86_64-unknown-linux-musl",
2380        ])
2381        .unwrap();
2382        assert_eq!(args.toolchain, Some("nightly".to_string()));
2383        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2384    }
2385
2386    // Equals syntax vs space syntax tests
2387
2388    #[test]
2389    fn test_equals_syntax_target() {
2390        let args = parse(&["cargo-cross", "build", "-t=x86_64-unknown-linux-musl"]).unwrap();
2391        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2392    }
2393
2394    #[test]
2395    fn test_equals_syntax_long_target() {
2396        let args = parse(&["cargo-cross", "build", "--target=x86_64-unknown-linux-musl"]).unwrap();
2397        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2398    }
2399
2400    #[test]
2401    fn test_equals_syntax_profile() {
2402        let args = parse(&["cargo-cross", "build", "--profile=dev"]).unwrap();
2403        assert_eq!(args.profile, "dev");
2404    }
2405
2406    #[test]
2407    fn test_equals_syntax_features() {
2408        let args = parse(&["cargo-cross", "build", "--features=foo,bar"]).unwrap();
2409        assert_eq!(args.features, Some("foo,bar".to_string()));
2410    }
2411
2412    #[test]
2413    fn test_equals_syntax_short_features() {
2414        let args = parse(&["cargo-cross", "build", "-F=foo,bar"]).unwrap();
2415        assert_eq!(args.features, Some("foo,bar".to_string()));
2416    }
2417
2418    #[test]
2419    fn test_equals_syntax_jobs() {
2420        let args = parse(&["cargo-cross", "build", "-j=8"]).unwrap();
2421        assert_eq!(args.jobs, Some("8".to_string()));
2422    }
2423
2424    #[test]
2425    fn test_equals_syntax_crt_static() {
2426        let args = parse(&["cargo-cross", "build", "--crt-static=true"]).unwrap();
2427        assert_eq!(args.crt_static, Some(true));
2428    }
2429
2430    #[test]
2431    fn test_equals_syntax_build_std() {
2432        let args = parse(&["cargo-cross", "build", "--build-std=core,alloc"]).unwrap();
2433        assert_eq!(args.build_std, Some("core,alloc".to_string()));
2434    }
2435
2436    #[test]
2437    fn test_equals_syntax_manifest_path() {
2438        let args = parse(&[
2439            "cargo-cross",
2440            "build",
2441            "--manifest-path=/path/to/Cargo.toml",
2442        ])
2443        .unwrap();
2444        assert_eq!(
2445            args.manifest_path,
2446            Some(PathBuf::from("/path/to/Cargo.toml"))
2447        );
2448    }
2449
2450    #[test]
2451    fn test_equals_syntax_cross_compiler_dir() {
2452        let args = parse(&["cargo-cross", "build", "--cross-compiler-dir=/opt/cross"]).unwrap();
2453        assert_eq!(args.cross_compiler_dir, PathBuf::from("/opt/cross"));
2454    }
2455
2456    #[test]
2457    fn test_equals_syntax_glibc_version() {
2458        let args = parse(&["cargo-cross", "build", "--glibc-version=2.31"]).unwrap();
2459        assert_eq!(args.glibc_version, "2.31");
2460    }
2461
2462    #[test]
2463    fn test_equals_syntax_cc_cxx_ar() {
2464        let args = parse(&[
2465            "cargo-cross",
2466            "build",
2467            "--cc=/usr/bin/gcc",
2468            "--cxx=/usr/bin/g++",
2469            "--ar=/usr/bin/ar",
2470        ])
2471        .unwrap();
2472        assert_eq!(args.cc, Some(PathBuf::from("/usr/bin/gcc")));
2473        assert_eq!(args.cxx, Some(PathBuf::from("/usr/bin/g++")));
2474        assert_eq!(args.ar, Some(PathBuf::from("/usr/bin/ar")));
2475    }
2476
2477    #[test]
2478    fn test_equals_syntax_linker() {
2479        let args = parse(&["cargo-cross", "build", "--linker=/usr/bin/ld.lld"]).unwrap();
2480        assert_eq!(args.linker, Some(PathBuf::from("/usr/bin/ld.lld")));
2481    }
2482
2483    #[test]
2484    fn test_equals_syntax_cflags_with_spaces() {
2485        let args = parse(&["cargo-cross", "build", "--cflags=-O2 -Wall -Wextra"]).unwrap();
2486        assert_eq!(args.cflags, Some("-O2 -Wall -Wextra".to_string()));
2487    }
2488
2489    #[test]
2490    fn test_equals_syntax_ldflags() {
2491        let args = parse(&["cargo-cross", "build", "--ldflags=-L/usr/local/lib -static"]).unwrap();
2492        assert_eq!(args.ldflags, Some("-L/usr/local/lib -static".to_string()));
2493    }
2494
2495    #[test]
2496    fn test_equals_syntax_rustflag() {
2497        let args = parse(&["cargo-cross", "build", "--rustflag=-C opt-level=3"]).unwrap();
2498        assert_eq!(args.rustflags, vec!["-C opt-level=3"]);
2499    }
2500
2501    #[test]
2502    fn test_equals_syntax_github_proxy() {
2503        let args = parse(&[
2504            "cargo-cross",
2505            "build",
2506            "--github-proxy=https://mirror.example.com/",
2507        ])
2508        .unwrap();
2509        assert_eq!(
2510            args.github_proxy,
2511            Some("https://mirror.example.com/".to_string())
2512        );
2513    }
2514
2515    #[test]
2516    fn test_equals_syntax_sccache_options() {
2517        let args = parse(&[
2518            "cargo-cross",
2519            "build",
2520            "--enable-sccache",
2521            "--sccache-dir=/tmp/sccache",
2522            "--sccache-cache-size=20G",
2523        ])
2524        .unwrap();
2525        assert!(args.enable_sccache);
2526        assert_eq!(args.sccache_dir, Some(PathBuf::from("/tmp/sccache")));
2527        assert_eq!(args.sccache_cache_size, Some("20G".to_string()));
2528    }
2529
2530    #[test]
2531    fn test_equals_syntax_config_with_equals_in_value() {
2532        let args = parse(&["cargo-cross", "build", "--config=build.jobs=4"]).unwrap();
2533        assert_eq!(args.cargo_config, vec!["build.jobs=4"]);
2534    }
2535
2536    #[test]
2537    fn test_equals_syntax_multiple_options() {
2538        let args = parse(&[
2539            "cargo-cross",
2540            "build",
2541            "-t=x86_64-unknown-linux-musl",
2542            "--profile=release",
2543            "-F=serde,json",
2544            "-j=8",
2545            "--crt-static=true",
2546            "--build-std=core,alloc",
2547        ])
2548        .unwrap();
2549        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2550        assert_eq!(args.profile, "release");
2551        assert_eq!(args.features, Some("serde,json".to_string()));
2552        assert_eq!(args.jobs, Some("8".to_string()));
2553        assert_eq!(args.crt_static, Some(true));
2554        assert_eq!(args.build_std, Some("core,alloc".to_string()));
2555    }
2556
2557    #[test]
2558    fn test_equals_syntax_toolchain() {
2559        let args = parse(&["cargo-cross", "build", "--toolchain=nightly-2024-01-01"]).unwrap();
2560        assert_eq!(args.toolchain, Some("nightly-2024-01-01".to_string()));
2561    }
2562
2563    #[test]
2564    fn test_equals_syntax_target_dir() {
2565        let args = parse(&["cargo-cross", "build", "--target-dir=/tmp/target"]).unwrap();
2566        assert_eq!(args.cargo_target_dir, Some(PathBuf::from("/tmp/target")));
2567    }
2568
2569    #[test]
2570    fn test_equals_syntax_z_flag() {
2571        let args = parse(&["cargo-cross", "build", "-Z=build-std"]).unwrap();
2572        assert_eq!(args.cargo_z_flags, vec!["build-std"]);
2573    }
2574
2575    #[test]
2576    fn test_equals_syntax_directory() {
2577        let args = parse(&["cargo-cross", "build", "-C=/path/to/project"]).unwrap();
2578        assert_eq!(args.cargo_cwd, Some(PathBuf::from("/path/to/project")));
2579    }
2580
2581    #[test]
2582    fn test_equals_syntax_message_format() {
2583        let args = parse(&["cargo-cross", "build", "--message-format=json"]).unwrap();
2584        assert_eq!(args.message_format, Some("json".to_string()));
2585    }
2586
2587    #[test]
2588    fn test_equals_syntax_color() {
2589        let args = parse(&["cargo-cross", "build", "--color=always"]).unwrap();
2590        assert_eq!(args.color, Some("always".to_string()));
2591    }
2592
2593    // Mixed flags and options tests
2594
2595    #[test]
2596    fn test_mixed_crt_static_then_flag() {
2597        let args = parse(&[
2598            "cargo-cross",
2599            "build",
2600            "--crt-static",
2601            "true",
2602            "--no-default-features",
2603        ])
2604        .unwrap();
2605        assert_eq!(args.crt_static, Some(true));
2606        assert!(args.no_default_features);
2607    }
2608
2609    #[test]
2610    fn test_mixed_crt_static_then_short_option() {
2611        let args = parse(&[
2612            "cargo-cross",
2613            "build",
2614            "--crt-static",
2615            "false",
2616            "-F",
2617            "serde",
2618        ])
2619        .unwrap();
2620        assert_eq!(args.crt_static, Some(false));
2621        assert_eq!(args.features, Some("serde".to_string()));
2622    }
2623
2624    #[test]
2625    fn test_mixed_crt_static_with_target() {
2626        let args = parse(&[
2627            "cargo-cross",
2628            "build",
2629            "--crt-static",
2630            "true",
2631            "-t",
2632            "x86_64-unknown-linux-musl",
2633            "--profile",
2634            "release",
2635        ])
2636        .unwrap();
2637        assert_eq!(args.crt_static, Some(true));
2638        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2639        assert_eq!(args.profile, "release");
2640    }
2641
2642    #[test]
2643    fn test_mixed_flag_then_crt_static() {
2644        let args = parse(&[
2645            "cargo-cross",
2646            "build",
2647            "--no-default-features",
2648            "--crt-static",
2649            "true",
2650        ])
2651        .unwrap();
2652        assert!(args.no_default_features);
2653        assert_eq!(args.crt_static, Some(true));
2654    }
2655
2656    #[test]
2657    fn test_mixed_multiple_flags_and_options() {
2658        let args = parse(&[
2659            "cargo-cross",
2660            "build",
2661            "-t",
2662            "aarch64-unknown-linux-musl",
2663            "--no-default-features",
2664            "-F",
2665            "serde,json",
2666            "--crt-static",
2667            "true",
2668            "--profile",
2669            "release",
2670            "-vv",
2671        ])
2672        .unwrap();
2673        assert_eq!(args.targets, vec!["aarch64-unknown-linux-musl"]);
2674        assert!(args.no_default_features);
2675        assert_eq!(args.features, Some("serde,json".to_string()));
2676        assert_eq!(args.crt_static, Some(true));
2677        assert_eq!(args.profile, "release");
2678        assert_eq!(args.verbose_level, 2);
2679    }
2680
2681    // Complex option ordering tests
2682
2683    #[test]
2684    fn test_options_before_command_style() {
2685        // Options can come in any order
2686        let args = parse(&[
2687            "cargo-cross",
2688            "build",
2689            "--profile",
2690            "dev",
2691            "-t",
2692            "x86_64-unknown-linux-musl",
2693            "--features",
2694            "foo",
2695        ])
2696        .unwrap();
2697        assert_eq!(args.profile, "dev");
2698        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2699        assert_eq!(args.features, Some("foo".to_string()));
2700    }
2701
2702    #[test]
2703    fn test_interleaved_short_and_long_options() {
2704        let args = parse(&[
2705            "cargo-cross",
2706            "build",
2707            "-t",
2708            "x86_64-unknown-linux-musl",
2709            "--profile",
2710            "release",
2711            "-F",
2712            "foo",
2713            "--no-default-features",
2714            "-j",
2715            "4",
2716            "--locked",
2717        ])
2718        .unwrap();
2719        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2720        assert_eq!(args.profile, "release");
2721        assert_eq!(args.features, Some("foo".to_string()));
2722        assert!(args.no_default_features);
2723        assert_eq!(args.jobs, Some("4".to_string()));
2724        assert!(args.locked);
2725    }
2726
2727    // Verbose flag variations
2728
2729    #[test]
2730    fn test_verbose_single() {
2731        let args = parse(&["cargo-cross", "build", "-v"]).unwrap();
2732        assert_eq!(args.verbose_level, 1);
2733    }
2734
2735    #[test]
2736    fn test_verbose_double() {
2737        let args = parse(&["cargo-cross", "build", "-vv"]).unwrap();
2738        assert_eq!(args.verbose_level, 2);
2739    }
2740
2741    #[test]
2742    fn test_verbose_triple() {
2743        let args = parse(&["cargo-cross", "build", "-vvv"]).unwrap();
2744        assert_eq!(args.verbose_level, 3);
2745    }
2746
2747    #[test]
2748    fn test_verbose_separate() {
2749        let args = parse(&["cargo-cross", "build", "-v", "-v", "-v"]).unwrap();
2750        assert_eq!(args.verbose_level, 3);
2751    }
2752
2753    #[test]
2754    fn test_verbose_long_form() {
2755        let args = parse(&["cargo-cross", "build", "--verbose", "--verbose"]).unwrap();
2756        assert_eq!(args.verbose_level, 2);
2757    }
2758
2759    #[test]
2760    fn test_verbose_mixed_with_options() {
2761        let args = parse(&[
2762            "cargo-cross",
2763            "build",
2764            "-v",
2765            "-t",
2766            "x86_64-unknown-linux-musl",
2767            "-v",
2768        ])
2769        .unwrap();
2770        assert_eq!(args.verbose_level, 2);
2771        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2772    }
2773
2774    // Timings option (optional value) tests
2775
2776    #[test]
2777    fn test_timings_without_value() {
2778        let args = parse(&["cargo-cross", "build", "--timings"]).unwrap();
2779        assert_eq!(args.timings, Some("true".to_string()));
2780    }
2781
2782    #[test]
2783    fn test_timings_with_value() {
2784        let args = parse(&["cargo-cross", "build", "--timings=html"]).unwrap();
2785        assert_eq!(args.timings, Some("html".to_string()));
2786    }
2787
2788    #[test]
2789    fn test_timings_followed_by_flag() {
2790        let args = parse(&["cargo-cross", "build", "--timings", "--locked"]).unwrap();
2791        assert_eq!(args.timings, Some("true".to_string()));
2792        assert!(args.locked);
2793    }
2794
2795    #[test]
2796    fn test_timings_followed_by_option() {
2797        let args = parse(&[
2798            "cargo-cross",
2799            "build",
2800            "--timings",
2801            "-t",
2802            "x86_64-unknown-linux-musl",
2803        ])
2804        .unwrap();
2805        assert_eq!(args.timings, Some("true".to_string()));
2806        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2807    }
2808
2809    // Multiple values / repeated options tests
2810
2811    #[test]
2812    fn test_multiple_targets_comma_separated() {
2813        let args = parse(&[
2814            "cargo-cross",
2815            "build",
2816            "-t",
2817            "x86_64-unknown-linux-musl,aarch64-unknown-linux-musl,armv7-unknown-linux-musleabihf",
2818        ])
2819        .unwrap();
2820        assert_eq!(args.targets.len(), 3);
2821        assert_eq!(args.targets[0], "x86_64-unknown-linux-musl");
2822        assert_eq!(args.targets[1], "aarch64-unknown-linux-musl");
2823        assert_eq!(args.targets[2], "armv7-unknown-linux-musleabihf");
2824    }
2825
2826    #[test]
2827    fn test_multiple_targets_repeated_option() {
2828        let args = parse(&[
2829            "cargo-cross",
2830            "build",
2831            "-t",
2832            "x86_64-unknown-linux-musl",
2833            "-t",
2834            "aarch64-unknown-linux-musl",
2835        ])
2836        .unwrap();
2837        assert_eq!(args.targets.len(), 2);
2838    }
2839
2840    #[test]
2841    fn test_multiple_rustflags() {
2842        let args = parse(&[
2843            "cargo-cross",
2844            "build",
2845            "--rustflag",
2846            "-C opt-level=3",
2847            "--rustflag",
2848            "-C lto=thin",
2849        ])
2850        .unwrap();
2851        assert_eq!(args.rustflags.len(), 2);
2852        assert_eq!(args.rustflags[0], "-C opt-level=3");
2853        assert_eq!(args.rustflags[1], "-C lto=thin");
2854    }
2855
2856    #[test]
2857    fn test_multiple_config_flags() {
2858        let args = parse(&[
2859            "cargo-cross",
2860            "build",
2861            "--config",
2862            "build.jobs=4",
2863            "--config",
2864            "profile.release.lto=true",
2865        ])
2866        .unwrap();
2867        assert_eq!(args.cargo_config.len(), 2);
2868    }
2869
2870    #[test]
2871    fn test_multiple_z_flags() {
2872        let args = parse(&[
2873            "cargo-cross",
2874            "build",
2875            "-Z",
2876            "build-std",
2877            "-Z",
2878            "unstable-options",
2879        ])
2880        .unwrap();
2881        assert_eq!(args.cargo_z_flags.len(), 2);
2882    }
2883
2884    // Hyphen values tests (for compiler flags)
2885
2886    #[test]
2887    fn test_cflags_with_hyphen() {
2888        let args = parse(&["cargo-cross", "build", "--cflags", "-O2 -Wall"]).unwrap();
2889        assert_eq!(args.cflags, Some("-O2 -Wall".to_string()));
2890    }
2891
2892    #[test]
2893    fn test_ldflags_with_hyphen() {
2894        let args = parse(&["cargo-cross", "build", "--ldflags", "-L/usr/local/lib"]).unwrap();
2895        assert_eq!(args.ldflags, Some("-L/usr/local/lib".to_string()));
2896    }
2897
2898    #[test]
2899    fn test_rustflag_with_hyphen() {
2900        let args = parse(&["cargo-cross", "build", "--rustflag", "-C target-cpu=native"]).unwrap();
2901        assert_eq!(args.rustflags, vec!["-C target-cpu=native"]);
2902    }
2903
2904    // Passthrough arguments tests
2905
2906    #[test]
2907    fn test_passthrough_single() {
2908        let args = parse(&["cargo-cross", "build", "--", "--nocapture"]).unwrap();
2909        assert_eq!(args.passthrough_args, vec!["--nocapture"]);
2910    }
2911
2912    #[test]
2913    fn test_passthrough_multiple() {
2914        let args = parse(&[
2915            "cargo-cross",
2916            "test",
2917            "--",
2918            "--nocapture",
2919            "--test-threads=1",
2920        ])
2921        .unwrap();
2922        assert_eq!(
2923            args.passthrough_args,
2924            vec!["--nocapture", "--test-threads=1"]
2925        );
2926    }
2927
2928    #[test]
2929    fn test_passthrough_with_hyphen_values() {
2930        let args = parse(&["cargo-cross", "build", "--", "-v", "--foo", "-bar"]).unwrap();
2931        assert_eq!(args.passthrough_args, vec!["-v", "--foo", "-bar"]);
2932    }
2933
2934    #[test]
2935    fn test_passthrough_after_options() {
2936        let args = parse(&[
2937            "cargo-cross",
2938            "build",
2939            "-t",
2940            "x86_64-unknown-linux-musl",
2941            "--profile",
2942            "release",
2943            "--",
2944            "--foo",
2945            "--bar",
2946        ])
2947        .unwrap();
2948        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2949        assert_eq!(args.profile, "release");
2950        assert_eq!(args.passthrough_args, vec!["--foo", "--bar"]);
2951    }
2952
2953    // Alias tests
2954
2955    #[test]
2956    fn test_alias_targets() {
2957        let args = parse(&[
2958            "cargo-cross",
2959            "build",
2960            "--targets",
2961            "x86_64-unknown-linux-musl",
2962        ])
2963        .unwrap();
2964        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
2965    }
2966
2967    #[test]
2968    fn test_alias_workspace_all() {
2969        let args = parse(&["cargo-cross", "build", "--all"]).unwrap();
2970        assert!(args.workspace);
2971    }
2972
2973    #[test]
2974    fn test_alias_rustflags() {
2975        let args = parse(&["cargo-cross", "build", "--rustflags", "-C lto"]).unwrap();
2976        assert_eq!(args.rustflags, vec!["-C lto"]);
2977    }
2978
2979    #[test]
2980    fn test_alias_trim_paths() {
2981        let args = parse(&["cargo-cross", "build", "--trim-paths", "all"]).unwrap();
2982        assert_eq!(args.cargo_trim_paths, Some("all".to_string()));
2983    }
2984
2985    #[test]
2986    fn test_trim_paths_no_value() {
2987        // --trim-paths without value should default to "true"
2988        let args = parse(&["cargo-cross", "build", "--trim-paths"]).unwrap();
2989        assert_eq!(args.cargo_trim_paths, Some("true".to_string()));
2990    }
2991
2992    #[test]
2993    fn test_cargo_trim_paths_no_value() {
2994        // --cargo-trim-paths without value should default to "true"
2995        let args = parse(&["cargo-cross", "build", "--cargo-trim-paths"]).unwrap();
2996        assert_eq!(args.cargo_trim_paths, Some("true".to_string()));
2997    }
2998
2999    #[test]
3000    fn test_rustc_bootstrap_no_value() {
3001        // --rustc-bootstrap without value should default to "1"
3002        let args = parse(&["cargo-cross", "build", "--rustc-bootstrap"]).unwrap();
3003        assert_eq!(args.rustc_bootstrap, Some("1".to_string()));
3004    }
3005
3006    #[test]
3007    fn test_rustc_bootstrap_with_value() {
3008        let args = parse(&["cargo-cross", "build", "--rustc-bootstrap", "mycrate"]).unwrap();
3009        assert_eq!(args.rustc_bootstrap, Some("mycrate".to_string()));
3010    }
3011
3012    // Command alias tests
3013
3014    #[test]
3015    fn test_command_alias_b() {
3016        let args = parse(&["cargo-cross", "b"]).unwrap();
3017        assert_eq!(args.command, Command::build());
3018    }
3019
3020    #[test]
3021    fn test_command_alias_c() {
3022        let args = parse(&["cargo-cross", "c"]).unwrap();
3023        assert_eq!(args.command, Command::check());
3024    }
3025
3026    #[test]
3027    fn test_command_alias_r() {
3028        let args = parse(&["cargo-cross", "r"]).unwrap();
3029        assert_eq!(args.command, Command::run());
3030    }
3031
3032    #[test]
3033    fn test_command_alias_t() {
3034        let args = parse(&["cargo-cross", "t"]).unwrap();
3035        assert_eq!(args.command, Command::test());
3036    }
3037
3038    // Requires relationship tests
3039
3040    #[test]
3041    fn test_requires_exclude_needs_workspace() {
3042        let result = parse(&["cargo-cross", "build", "--exclude", "foo"]);
3043        assert!(
3044            result.is_err() || {
3045                // clap exits on error, so we might not get here
3046                false
3047            }
3048        );
3049    }
3050
3051    #[test]
3052    fn test_requires_exclude_with_workspace() {
3053        let args = parse(&["cargo-cross", "build", "--workspace", "--exclude", "foo"]).unwrap();
3054        assert!(args.workspace);
3055        assert_eq!(args.exclude, Some("foo".to_string()));
3056    }
3057
3058    #[test]
3059    fn test_requires_build_std_features_with_build_std() {
3060        let args = parse(&[
3061            "cargo-cross",
3062            "build",
3063            "--build-std",
3064            "core,alloc",
3065            "--build-std-features",
3066            "panic_immediate_abort",
3067        ])
3068        .unwrap();
3069        assert_eq!(args.build_std, Some("core,alloc".to_string()));
3070        assert_eq!(
3071            args.build_std_features,
3072            Some("panic_immediate_abort".to_string())
3073        );
3074    }
3075
3076    // Conflicts relationship tests
3077
3078    #[test]
3079    fn test_conflicts_quiet_verbose() {
3080        let result = parse(&["cargo-cross", "build", "--quiet", "--verbose"]);
3081        // This should fail due to conflict
3082        assert!(
3083            result.is_err() || {
3084                // clap exits, checking we don't panic
3085                false
3086            }
3087        );
3088    }
3089
3090    #[test]
3091    fn test_conflicts_features_all_features() {
3092        let result = parse(&[
3093            "cargo-cross",
3094            "build",
3095            "--features",
3096            "foo",
3097            "--all-features",
3098        ]);
3099        assert!(result.is_err());
3100    }
3101
3102    #[test]
3103    fn test_no_toolchain_setup() {
3104        let args = parse(&["cargo-cross", "build", "--no-toolchain-setup"]).unwrap();
3105        assert!(args.no_toolchain_setup);
3106    }
3107
3108    #[test]
3109    fn test_linker_with_no_toolchain_setup() {
3110        // --linker and --no-toolchain-setup can be used together
3111        let args = parse(&[
3112            "cargo-cross",
3113            "build",
3114            "--linker",
3115            "/usr/bin/ld",
3116            "--no-toolchain-setup",
3117        ])
3118        .unwrap();
3119        assert!(args.no_toolchain_setup);
3120        assert_eq!(args.linker, Some(PathBuf::from("/usr/bin/ld")));
3121    }
3122
3123    // Complex real-world scenario tests
3124
3125    #[test]
3126    fn test_real_world_linux_musl_build() {
3127        let args = parse(&[
3128            "cargo-cross",
3129            "+nightly",
3130            "build",
3131            "-t",
3132            "x86_64-unknown-linux-musl",
3133            "--profile",
3134            "release",
3135            "--crt-static",
3136            "true",
3137            "--no-default-features",
3138            "-F",
3139            "serde,json",
3140            "-j",
3141            "8",
3142            "--locked",
3143        ])
3144        .unwrap();
3145        assert_eq!(args.toolchain, Some("nightly".to_string()));
3146        assert_eq!(args.command, Command::build());
3147        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3148        assert_eq!(args.profile, "release");
3149        assert_eq!(args.crt_static, Some(true));
3150        assert!(args.no_default_features);
3151        assert_eq!(args.features, Some("serde,json".to_string()));
3152        assert_eq!(args.jobs, Some("8".to_string()));
3153        assert!(args.locked);
3154    }
3155
3156    #[test]
3157    fn test_real_world_multi_target_build() {
3158        let args = parse(&[
3159            "cargo-cross",
3160            "build",
3161            "-t",
3162            "x86_64-unknown-linux-musl,aarch64-unknown-linux-musl",
3163            "--profile",
3164            "release",
3165            "--build-std",
3166            "core,alloc",
3167            "--build-std-features",
3168            "panic_immediate_abort",
3169            "-vv",
3170        ])
3171        .unwrap();
3172        assert_eq!(args.targets.len(), 2);
3173        assert_eq!(args.build_std, Some("core,alloc".to_string()));
3174        assert_eq!(
3175            args.build_std_features,
3176            Some("panic_immediate_abort".to_string())
3177        );
3178        assert_eq!(args.verbose_level, 2);
3179    }
3180
3181    #[test]
3182    fn test_real_world_test_with_passthrough() {
3183        let args = parse(&[
3184            "cargo-cross",
3185            "test",
3186            "-t",
3187            "x86_64-unknown-linux-musl",
3188            "--",
3189            "--nocapture",
3190            "--test-threads=1",
3191        ])
3192        .unwrap();
3193        assert_eq!(args.command, Command::test());
3194        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3195        assert_eq!(
3196            args.passthrough_args,
3197            vec!["--nocapture", "--test-threads=1"]
3198        );
3199    }
3200
3201    #[test]
3202    fn test_real_world_with_compiler_options() {
3203        let args = parse(&[
3204            "cargo-cross",
3205            "build",
3206            "-t",
3207            "aarch64-unknown-linux-musl",
3208            "--cc",
3209            "/opt/cross/bin/aarch64-linux-musl-gcc",
3210            "--cxx",
3211            "/opt/cross/bin/aarch64-linux-musl-g++",
3212            "--ar",
3213            "/opt/cross/bin/aarch64-linux-musl-ar",
3214            "--cflags",
3215            "-O2 -march=armv8-a",
3216        ])
3217        .unwrap();
3218        assert_eq!(args.targets, vec!["aarch64-unknown-linux-musl"]);
3219        assert!(args.cc.is_some());
3220        assert!(args.cxx.is_some());
3221        assert!(args.ar.is_some());
3222        assert_eq!(args.cflags, Some("-O2 -march=armv8-a".to_string()));
3223    }
3224
3225    #[test]
3226    fn test_real_world_sccache_build() {
3227        let args = parse(&[
3228            "cargo-cross",
3229            "build",
3230            "-t",
3231            "x86_64-unknown-linux-musl",
3232            "--enable-sccache",
3233            "--sccache-dir",
3234            "/tmp/sccache",
3235            "--sccache-cache-size",
3236            "10G",
3237        ])
3238        .unwrap();
3239        assert!(args.enable_sccache);
3240        assert_eq!(args.sccache_dir, Some(PathBuf::from("/tmp/sccache")));
3241        assert_eq!(args.sccache_cache_size, Some("10G".to_string()));
3242    }
3243
3244    #[test]
3245    fn test_real_world_workspace_build() {
3246        let args = parse(&[
3247            "cargo-cross",
3248            "build",
3249            "--workspace",
3250            "--exclude",
3251            "test-crate",
3252            "-t",
3253            "x86_64-unknown-linux-musl",
3254            "--profile",
3255            "release",
3256        ])
3257        .unwrap();
3258        assert!(args.workspace);
3259        assert_eq!(args.exclude, Some("test-crate".to_string()));
3260        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3261    }
3262
3263    // Edge case tests
3264
3265    #[test]
3266    fn test_edge_case_equals_in_value() {
3267        let args = parse(&[
3268            "cargo-cross",
3269            "build",
3270            "--config",
3271            "build.rustflags=['-C', 'opt-level=3']",
3272        ])
3273        .unwrap();
3274        assert_eq!(
3275            args.cargo_config,
3276            vec!["build.rustflags=['-C', 'opt-level=3']"]
3277        );
3278    }
3279
3280    #[test]
3281    fn test_edge_case_empty_passthrough() {
3282        let args = parse(&["cargo-cross", "build", "--"]).unwrap();
3283        assert!(args.passthrough_args.is_empty());
3284    }
3285
3286    #[test]
3287    fn test_edge_case_target_with_numbers() {
3288        let args = parse(&[
3289            "cargo-cross",
3290            "build",
3291            "-t",
3292            "armv7-unknown-linux-musleabihf",
3293        ])
3294        .unwrap();
3295        assert_eq!(args.targets, vec!["armv7-unknown-linux-musleabihf"]);
3296    }
3297
3298    #[test]
3299    fn test_edge_case_all_bool_options() {
3300        let args = parse(&[
3301            "cargo-cross",
3302            "build",
3303            "--no-default-features",
3304            "--workspace",
3305            "--bins",
3306            "--lib",
3307            "--examples",
3308            "--tests",
3309            "--benches",
3310            "--all-targets",
3311            "--locked",
3312            "--offline",
3313            "--keep-going",
3314        ])
3315        .unwrap();
3316        assert!(args.no_default_features);
3317        assert!(args.workspace);
3318        assert!(args.build_bins);
3319        assert!(args.build_lib);
3320        assert!(args.build_examples);
3321        assert!(args.build_tests);
3322        assert!(args.build_benches);
3323        assert!(args.build_all_targets);
3324        assert!(args.locked);
3325        assert!(args.offline);
3326        assert!(args.keep_going);
3327    }
3328
3329    #[test]
3330    fn test_edge_case_mixed_equals_and_space() {
3331        let args = parse(&[
3332            "cargo-cross",
3333            "build",
3334            "-t=x86_64-unknown-linux-musl",
3335            "--profile",
3336            "release",
3337            "-F=serde",
3338            "--crt-static",
3339            "true",
3340        ])
3341        .unwrap();
3342        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3343        assert_eq!(args.profile, "release");
3344        assert_eq!(args.features, Some("serde".to_string()));
3345        assert_eq!(args.crt_static, Some(true));
3346    }
3347
3348    #[test]
3349    fn test_edge_case_directory_option() {
3350        let args = parse(&[
3351            "cargo-cross",
3352            "build",
3353            "-C",
3354            "/path/to/project",
3355            "-t",
3356            "x86_64-unknown-linux-musl",
3357        ])
3358        .unwrap();
3359        assert_eq!(args.cargo_cwd, Some(PathBuf::from("/path/to/project")));
3360    }
3361
3362    #[test]
3363    fn test_edge_case_manifest_path() {
3364        let args = parse(&[
3365            "cargo-cross",
3366            "build",
3367            "--manifest-path",
3368            "/path/to/Cargo.toml",
3369        ])
3370        .unwrap();
3371        assert_eq!(
3372            args.manifest_path,
3373            Some(PathBuf::from("/path/to/Cargo.toml"))
3374        );
3375    }
3376
3377    // Cargo cross invocation style tests
3378
3379    #[test]
3380    fn test_cargo_cross_style_build() {
3381        let args: Vec<String> = vec![
3382            "cargo-cross".to_string(),
3383            "cross".to_string(),
3384            "build".to_string(),
3385            "-t".to_string(),
3386            "x86_64-unknown-linux-musl".to_string(),
3387        ];
3388        match parse_args_from(args).unwrap() {
3389            ParseResult::Build(args) => {
3390                assert_eq!(args.command, Command::build());
3391                assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3392            }
3393            _ => panic!("expected Build"),
3394        }
3395    }
3396
3397    #[test]
3398    fn test_cargo_cross_style_with_toolchain() {
3399        let args: Vec<String> = vec![
3400            "cargo-cross".to_string(),
3401            "cross".to_string(),
3402            "+nightly".to_string(),
3403            "build".to_string(),
3404        ];
3405        match parse_args_from(args).unwrap() {
3406            ParseResult::Build(args) => {
3407                assert_eq!(args.toolchain, Some("nightly".to_string()));
3408                assert_eq!(args.command, Command::build());
3409            }
3410            _ => panic!("expected Build"),
3411        }
3412    }
3413
3414    #[test]
3415    fn test_cargo_cross_style_targets() {
3416        let args: Vec<String> = vec![
3417            "cargo-cross".to_string(),
3418            "cross".to_string(),
3419            "targets".to_string(),
3420        ];
3421        match parse_args_from(args).unwrap() {
3422            ParseResult::ShowTargets(_) => {}
3423            _ => panic!("expected ShowTargets"),
3424        }
3425    }
3426
3427    // New alias and option tests
3428
3429    #[test]
3430    fn test_github_proxy_mirror_alias() {
3431        let args = parse(&[
3432            "cargo-cross",
3433            "build",
3434            "--github-proxy-mirror",
3435            "https://mirror.example.com/",
3436        ])
3437        .unwrap();
3438        assert_eq!(
3439            args.github_proxy,
3440            Some("https://mirror.example.com/".to_string())
3441        );
3442    }
3443
3444    #[test]
3445    fn test_github_proxy_original() {
3446        let args = parse(&[
3447            "cargo-cross",
3448            "build",
3449            "--github-proxy",
3450            "https://proxy.example.com/",
3451        ])
3452        .unwrap();
3453        assert_eq!(
3454            args.github_proxy,
3455            Some("https://proxy.example.com/".to_string())
3456        );
3457    }
3458
3459    #[test]
3460    fn test_release_flag_short() {
3461        let args = parse(&["cargo-cross", "build", "-r"]).unwrap();
3462        assert!(args.release);
3463        assert_eq!(args.profile, "release");
3464    }
3465
3466    #[test]
3467    fn test_release_flag_long() {
3468        let args = parse(&["cargo-cross", "build", "--release"]).unwrap();
3469        assert!(args.release);
3470        assert_eq!(args.profile, "release");
3471    }
3472
3473    #[test]
3474    fn test_toolchain_option() {
3475        let args = parse(&["cargo-cross", "build", "--toolchain", "nightly"]).unwrap();
3476        assert_eq!(args.toolchain, Some("nightly".to_string()));
3477    }
3478
3479    #[test]
3480    fn test_toolchain_option_with_version() {
3481        let args = parse(&["cargo-cross", "build", "--toolchain", "1.75.0"]).unwrap();
3482        assert_eq!(args.toolchain, Some("1.75.0".to_string()));
3483    }
3484
3485    #[test]
3486    fn test_toolchain_plus_syntax_takes_precedence() {
3487        let args = parse(&["cargo-cross", "+nightly", "build", "--toolchain", "stable"]).unwrap();
3488        // +nightly syntax takes precedence over --toolchain
3489        assert_eq!(args.toolchain, Some("nightly".to_string()));
3490    }
3491
3492    #[test]
3493    fn test_target_dir_alias() {
3494        let args = parse(&["cargo-cross", "build", "--target-dir", "/tmp/target"]).unwrap();
3495        assert_eq!(args.cargo_target_dir, Some(PathBuf::from("/tmp/target")));
3496    }
3497
3498    #[test]
3499    fn test_cargo_target_dir_original() {
3500        let args = parse(&["cargo-cross", "build", "--cargo-target-dir", "/tmp/target"]).unwrap();
3501        assert_eq!(args.cargo_target_dir, Some(PathBuf::from("/tmp/target")));
3502    }
3503
3504    #[test]
3505    fn test_args_alias() {
3506        let args = parse(&["cargo-cross", "build", "--args", "--verbose"]).unwrap();
3507        assert_eq!(args.cargo_args, vec!["--verbose"]);
3508    }
3509
3510    #[test]
3511    fn test_cargo_args_original() {
3512        let args = parse(&["cargo-cross", "build", "--cargo-args", "--verbose"]).unwrap();
3513        assert_eq!(args.cargo_args, vec!["--verbose"]);
3514    }
3515
3516    #[test]
3517    fn test_cargo_args_multiple() {
3518        let args = parse(&[
3519            "cargo-cross",
3520            "build",
3521            "--cargo-args",
3522            "--verbose",
3523            "--cargo-args",
3524            "--locked",
3525        ])
3526        .unwrap();
3527        assert_eq!(args.cargo_args, vec!["--verbose", "--locked"]);
3528    }
3529
3530    // Target validation tests
3531
3532    #[test]
3533    fn test_invalid_target_triple_uppercase() {
3534        let result = parse(&["cargo-cross", "build", "-t", "X86_64-unknown-linux-musl"]);
3535        assert!(result.is_err());
3536        let err = result.unwrap_err();
3537        assert!(
3538            matches!(err, CrossError::InvalidTargetTriple { target, char } if target == "X86_64-unknown-linux-musl" && char == 'X')
3539        );
3540    }
3541
3542    #[test]
3543    fn test_glob_pattern_matches_target() {
3544        // x86_64*unknown-linux-musl matches x86_64-unknown-linux-musl (glob * matches -)
3545        let args = parse(&["cargo-cross", "build", "-t", "x86_64*unknown-linux-musl"]).unwrap();
3546        assert!(args
3547            .targets
3548            .contains(&"x86_64-unknown-linux-musl".to_string()));
3549    }
3550
3551    #[test]
3552    fn test_invalid_target_triple_slash() {
3553        // Slash is not a glob character, so it should fail validation as invalid character
3554        let result = parse(&["cargo-cross", "build", "-t", "x86_64/unknown-linux-musl"]);
3555        assert!(result.is_err());
3556        let err = result.unwrap_err();
3557        assert!(
3558            matches!(err, CrossError::InvalidTargetTriple { target, char } if target == "x86_64/unknown-linux-musl" && char == '/')
3559        );
3560    }
3561
3562    #[test]
3563    fn test_invalid_target_triple_dot() {
3564        let result = parse(&["cargo-cross", "build", "-t", "x86_64.unknown-linux-musl"]);
3565        assert!(result.is_err());
3566        let err = result.unwrap_err();
3567        assert!(
3568            matches!(err, CrossError::InvalidTargetTriple { target, char } if target == "x86_64.unknown-linux-musl" && char == '.')
3569        );
3570    }
3571
3572    #[test]
3573    fn test_no_matching_targets_glob() {
3574        let result = parse(&["cargo-cross", "build", "-t", "*mingw*"]);
3575        assert!(result.is_err());
3576        let err = result.unwrap_err();
3577        assert!(matches!(
3578            err,
3579            CrossError::NoMatchingTargets { pattern } if pattern == "*mingw*"
3580        ));
3581    }
3582
3583    #[test]
3584    fn test_no_matching_targets_glob_complex() {
3585        let result = parse(&["cargo-cross", "build", "-t", "*nonexistent-platform*"]);
3586        assert!(result.is_err());
3587        let err = result.unwrap_err();
3588        assert!(matches!(
3589            err,
3590            CrossError::NoMatchingTargets { pattern } if pattern == "*nonexistent-platform*"
3591        ));
3592    }
3593
3594    #[test]
3595    fn test_valid_target_triple_with_numbers() {
3596        let args = parse(&[
3597            "cargo-cross",
3598            "build",
3599            "-t",
3600            "armv7-unknown-linux-gnueabihf",
3601        ])
3602        .unwrap();
3603        assert_eq!(args.targets, vec!["armv7-unknown-linux-gnueabihf"]);
3604    }
3605
3606    #[test]
3607    fn test_valid_target_triple_underscore() {
3608        let args = parse(&["cargo-cross", "build", "-t", "x86_64_unknown_linux_musl"]).unwrap();
3609        assert_eq!(args.targets, vec!["x86_64_unknown_linux_musl"]);
3610    }
3611
3612    #[test]
3613    fn test_valid_glob_pattern_matches() {
3614        let args = parse(&["cargo-cross", "build", "-t", "*-linux-musl"]).unwrap();
3615        assert!(!args.targets.is_empty());
3616        for target in &args.targets {
3617            assert!(target.ends_with("-linux-musl"));
3618        }
3619    }
3620
3621    #[test]
3622    fn test_is_glob_pattern() {
3623        assert!(is_glob_pattern("*-linux-musl"));
3624        assert!(is_glob_pattern("x86_64-*-linux"));
3625        assert!(is_glob_pattern("x86_64-?-linux"));
3626        assert!(is_glob_pattern("[ab]-linux"));
3627        assert!(!is_glob_pattern("x86_64-unknown-linux-musl"));
3628        assert!(!is_glob_pattern("aarch64-linux-android"));
3629    }
3630
3631    #[test]
3632    fn test_validate_target_triple() {
3633        assert!(validate_target_triple("x86_64-unknown-linux-musl").is_ok());
3634        assert!(validate_target_triple("aarch64-unknown-linux-gnu").is_ok());
3635        assert!(validate_target_triple("armv7-unknown-linux-gnueabihf").is_ok());
3636        assert!(validate_target_triple("i686-pc-windows-msvc").is_ok());
3637        assert!(validate_target_triple("x86_64-pc-windows-msvc").is_ok());
3638        assert!(validate_target_triple("X86_64-unknown-linux-musl").is_err()); // uppercase X
3639        assert!(validate_target_triple("x86_64*linux").is_err()); // special char *
3640        assert!(validate_target_triple("x86_64.linux").is_err()); // special char .
3641        assert!(validate_target_triple("x86_64 linux").is_err()); // space
3642    }
3643
3644    // Short argument concatenation tests (no separator between flag and value)
3645
3646    #[test]
3647    fn test_short_concat_features() {
3648        let args = parse(&["cargo-cross", "build", "-Ffoo,bar"]).unwrap();
3649        assert_eq!(args.features, Some("foo,bar".to_string()));
3650    }
3651
3652    #[test]
3653    fn test_short_concat_jobs() {
3654        let args = parse(&["cargo-cross", "build", "-j4"]).unwrap();
3655        assert_eq!(args.jobs, Some("4".to_string()));
3656    }
3657
3658    #[test]
3659    fn test_short_concat_package() {
3660        let args = parse(&["cargo-cross", "build", "-pmypackage"]).unwrap();
3661        assert_eq!(args.package, Some("mypackage".to_string()));
3662    }
3663
3664    #[test]
3665    fn test_short_concat_target() {
3666        let args = parse(&["cargo-cross", "build", "-tx86_64-unknown-linux-musl"]).unwrap();
3667        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3668    }
3669
3670    #[test]
3671    fn test_short_concat_z_flag() {
3672        let args = parse(&["cargo-cross", "build", "-Zbuild-std"]).unwrap();
3673        assert_eq!(args.cargo_z_flags, vec!["build-std"]);
3674    }
3675
3676    #[test]
3677    fn test_short_concat_directory() {
3678        let args = parse(&["cargo-cross", "build", "-C/path/to/project"]).unwrap();
3679        assert_eq!(args.cargo_cwd, Some(PathBuf::from("/path/to/project")));
3680    }
3681
3682    #[test]
3683    fn test_short_concat_multiple() {
3684        let args = parse(&[
3685            "cargo-cross",
3686            "build",
3687            "-tx86_64-unknown-linux-musl",
3688            "-Ffoo,bar",
3689            "-j8",
3690            "-pmypkg",
3691        ])
3692        .unwrap();
3693        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3694        assert_eq!(args.features, Some("foo,bar".to_string()));
3695        assert_eq!(args.jobs, Some("8".to_string()));
3696        assert_eq!(args.package, Some("mypkg".to_string()));
3697    }
3698
3699    #[test]
3700    fn test_short_concat_mixed_with_space() {
3701        let args = parse(&[
3702            "cargo-cross",
3703            "build",
3704            "-j4",
3705            "-t",
3706            "x86_64-unknown-linux-musl",
3707            "-Fbar",
3708        ])
3709        .unwrap();
3710        assert_eq!(args.jobs, Some("4".to_string()));
3711        assert_eq!(args.targets, vec!["x86_64-unknown-linux-musl"]);
3712        assert_eq!(args.features, Some("bar".to_string()));
3713    }
3714
3715    // Tests for parse_env_args (shell-style argument parsing from env vars)
3716
3717    #[test]
3718    fn test_parse_env_args_not_set() {
3719        std::env::remove_var("TEST_PARSE_ENV_ARGS_NOT_SET");
3720        let result = parse_env_args("TEST_PARSE_ENV_ARGS_NOT_SET");
3721        assert!(result.is_none());
3722    }
3723
3724    #[test]
3725    fn test_parse_env_args_empty() {
3726        std::env::set_var("TEST_PARSE_ENV_ARGS_EMPTY", "");
3727        let result = parse_env_args("TEST_PARSE_ENV_ARGS_EMPTY");
3728        assert!(result.is_none());
3729        std::env::remove_var("TEST_PARSE_ENV_ARGS_EMPTY");
3730    }
3731
3732    #[test]
3733    fn test_parse_env_args_simple() {
3734        std::env::set_var("TEST_PARSE_ENV_ARGS_SIMPLE", "--verbose --locked");
3735        let result = parse_env_args("TEST_PARSE_ENV_ARGS_SIMPLE");
3736        assert_eq!(
3737            result,
3738            Some(vec!["--verbose".to_string(), "--locked".to_string()])
3739        );
3740        std::env::remove_var("TEST_PARSE_ENV_ARGS_SIMPLE");
3741    }
3742
3743    #[test]
3744    fn test_parse_env_args_with_single_quotes() {
3745        std::env::set_var(
3746            "TEST_PARSE_ENV_ARGS_SINGLE_QUOTES",
3747            "--config 'build.jobs=4' --verbose",
3748        );
3749        let result = parse_env_args("TEST_PARSE_ENV_ARGS_SINGLE_QUOTES");
3750        assert_eq!(
3751            result,
3752            Some(vec![
3753                "--config".to_string(),
3754                "build.jobs=4".to_string(),
3755                "--verbose".to_string()
3756            ])
3757        );
3758        std::env::remove_var("TEST_PARSE_ENV_ARGS_SINGLE_QUOTES");
3759    }
3760
3761    #[test]
3762    fn test_parse_env_args_with_double_quotes() {
3763        std::env::set_var(
3764            "TEST_PARSE_ENV_ARGS_DOUBLE_QUOTES",
3765            "--message-format \"json with spaces\"",
3766        );
3767        let result = parse_env_args("TEST_PARSE_ENV_ARGS_DOUBLE_QUOTES");
3768        assert_eq!(
3769            result,
3770            Some(vec![
3771                "--message-format".to_string(),
3772                "json with spaces".to_string()
3773            ])
3774        );
3775        std::env::remove_var("TEST_PARSE_ENV_ARGS_DOUBLE_QUOTES");
3776    }
3777
3778    #[test]
3779    fn test_parse_env_args_complex() {
3780        std::env::set_var(
3781            "TEST_PARSE_ENV_ARGS_COMPLEX",
3782            "--config 'key=\"value with spaces\"' --verbose",
3783        );
3784        let result = parse_env_args("TEST_PARSE_ENV_ARGS_COMPLEX");
3785        assert_eq!(
3786            result,
3787            Some(vec![
3788                "--config".to_string(),
3789                "key=\"value with spaces\"".to_string(),
3790                "--verbose".to_string()
3791            ])
3792        );
3793        std::env::remove_var("TEST_PARSE_ENV_ARGS_COMPLEX");
3794    }
3795
3796    #[test]
3797    fn test_parse_env_args_whitespace_only() {
3798        std::env::set_var("TEST_PARSE_ENV_ARGS_WHITESPACE", "   ");
3799        let result = parse_env_args("TEST_PARSE_ENV_ARGS_WHITESPACE");
3800        assert!(result.is_none());
3801        std::env::remove_var("TEST_PARSE_ENV_ARGS_WHITESPACE");
3802    }
3803
3804    #[test]
3805    fn test_musl_target_with_default_glibc() {
3806        // musl targets should work with default (empty) glibc version
3807        let args = parse(&[
3808            "cargo-cross",
3809            "build",
3810            "-t",
3811            "aarch64_be-unknown-linux-musl",
3812        ])
3813        .unwrap();
3814        assert_eq!(args.targets, vec!["aarch64_be-unknown-linux-musl"]);
3815        assert_eq!(args.glibc_version, ""); // default is empty string
3816    }
3817}