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