nishikaze 0.2.0

Zephyr build system companion.
Documentation
//! Cli command definitions.

use clap::{Args, Parser, Subcommand};

/// Top level CLI struct for `kaze` command configuration.
#[derive(Parser, Debug, Clone)]
#[command(
    name = "kaze",
    about = "Zephyr build system companion.",
    version,
    propagate_version = true,
    after_help = r#"Examples:
  # Simple project
  kaze -b nucleo_f767zi -r openocd build
  kaze flash

  # Sysbuild project
  kaze flash --list
  kaze flash --image app

For more info, see https://gilab.com/byacrates/nishikaze
"#
)]
pub struct Cli {
    /// Pre-clean the active build dir
    #[arg(short = 'c', long = "clean")]
    pub preclean: bool,

    /// Run command for all configured profiles (overrides --profile/default)
    #[arg(short = 'a', long = "all")]
    pub all: bool,

    /// Select project profile defined in kaze.toml
    #[arg(short = 'p', long = "profile")]
    pub profile: Option<String>,

    /// Zephyr OS board/target
    #[arg(short = 'b', long = "board")]
    pub board: Option<String>,

    /// Zephyr OS runner
    #[arg(short = 'r', long = "runner")]
    pub runner: Option<String>,

    /// Explicit project root
    #[arg(long = "project")]
    pub project: Option<std::path::PathBuf>,

    /// Logging verbosity
    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count, default_value_t = 2)]
    pub verbose: u8,

    /// Dry run
    #[arg(short = 'd', long = "dry-run")]
    pub dry_run: bool,

    /// Selected command
    #[command(subcommand)]
    pub command: Command,
}

impl Cli {
    /**
     * Creates new kaze cli
     *
     * # Returns
     * Constructed `Cli` with parsed args
     */
    #[must_use]
    pub fn parse_args() -> Self {
        Self::parse()
    }
}

/// Enum of kaze commands
#[derive(Subcommand, Debug, Clone)]
pub enum Command {
    /// Initializes project with kaze.toml
    #[command(alias = "i")]
    Init(InitArgs),

    /// Lists available boards
    #[command(alias = "bo")]
    Boards,

    /// Lists available runners
    #[command(alias = "rn")]
    Runners,

    /// Lists profiles configured in kaze.toml
    #[command(alias = "p")]
    Profiles,

    /// Cleans build dir
    #[command(alias = "c")]
    Clean,

    /// Configure project
    #[command(alias = "cf")]
    Conf(PhaseArgs),

    /// Build binary
    #[command(alias = "b")]
    Build(PhaseArgs),

    /// Run simulation
    #[command(alias = "r")]
    Run(RunArgs),

    /// Flash binary
    #[command(alias = "f")]
    Flash(FlashArgs),
}

/// Extra phase args captures everything after `--` aa a passthrough
#[derive(Args, Debug, Default, Clone)]
pub struct PhaseArgs {
    /// Extra args
    #[arg(trailing_var_arg = true)]
    pub extra: Vec<String>,
}

/// Common args for run/flash used for sysbuild projects
#[derive(Args, Debug, Default, Clone)]
pub struct SysbuildArgs {
    /// List available sysbuild images
    #[arg(short = 'l', long = "list")]
    pub list: bool,

    /// Select sysbuild image
    #[arg(short = 'i', long = "image")]
    pub image: Option<String>,
}

/// Run command args
#[derive(Args, Debug, Default, Clone)]
pub struct RunArgs {
    /// Skip pre-run rebuild when running simulator binary directly
    #[arg(short = 'n', long = "norebuild")]
    pub norebuild: bool,

    /// Sysbuild args
    #[command(flatten)]
    pub sys: SysbuildArgs,

    /// Extra args
    #[command(flatten)]
    pub phase: PhaseArgs,
}

/// Flash command args
#[derive(Args, Debug, Default, Clone)]
pub struct FlashArgs {
    /// Sysbuild args
    #[command(flatten)]
    pub sys: SysbuildArgs,

    /// Extra args
    #[command(flatten)]
    pub phase: PhaseArgs,
}

/// Init command args
#[derive(Args, Debug, Default, Clone)]
pub struct InitArgs {
    /// Force re-generate `kaze.toml`
    #[arg(short = 'f', long = "force")]
    pub force: bool,
}

#[cfg(test)]
mod tests {
    use clap::Parser;

    use super::*;

    #[test]
    fn parse_defaults() {
        let cli = Cli::try_parse_from(["kaze", "clean"]).expect("parse ok");
        assert!(!cli.preclean);
        assert!(!cli.all);
        assert_eq!(cli.profile, None);
        assert_eq!(cli.board, None);
        assert_eq!(cli.runner, None);
        assert_eq!(cli.verbose, 2);
        assert!(matches!(cli.command, Command::Clean));
    }

    #[test]
    fn parse_flags_and_options() {
        let cli = Cli::try_parse_from([
            "kaze",
            "-c",
            "-a",
            "-p",
            "dev",
            "-b",
            "native_sim",
            "-r",
            "native",
            "-v",
            "-v",
            "build",
        ])
        .expect("parse ok");
        assert!(cli.preclean);
        assert!(cli.all);
        assert_eq!(cli.profile.as_deref(), Some("dev"));
        assert_eq!(cli.board.as_deref(), Some("native_sim"));
        assert_eq!(cli.runner.as_deref(), Some("native"));
        assert_eq!(cli.verbose, 2);
        assert!(matches!(cli.command, Command::Build(_)));
    }

    #[test]
    fn parse_subcommands_with_args() {
        let cli = Cli::try_parse_from(["kaze", "run", "-n", "-l", "-i", "app", "--", "-DOPT=1"])
            .expect("parse ok");
        assert!(matches!(cli.command, Command::Run(_)));
        let Command::Run(args) = cli.command else {
            return;
        };
        assert!(args.norebuild);
        assert!(args.sys.list);
        assert_eq!(args.sys.image.as_deref(), Some("app"));
        assert_eq!(args.phase.extra, vec!["-DOPT=1"]);
    }

    #[test]
    fn parse_simple_subcommands() {
        let cli_boards = Cli::try_parse_from(["kaze", "boards"]).expect("parse ok");
        assert!(matches!(cli_boards.command, Command::Boards));

        let cli_runners = Cli::try_parse_from(["kaze", "runners"]).expect("parse ok");
        assert!(matches!(cli_runners.command, Command::Runners));

        let cli_profiles = Cli::try_parse_from(["kaze", "profiles"]).expect("parse ok");
        assert!(matches!(cli_profiles.command, Command::Profiles));
    }

    #[test]
    fn parse_conf_and_flash_with_passthrough() {
        let cli_conf = Cli::try_parse_from(["kaze", "conf", "--", "-DOPT=1"]).expect("parse ok");
        assert!(matches!(cli_conf.command, Command::Conf(_)));
        let Command::Conf(conf_args) = cli_conf.command else {
            return;
        };
        assert_eq!(conf_args.extra, vec!["-DOPT=1"]);

        let cli_flash =
            Cli::try_parse_from(["kaze", "flash", "-l", "-i", "app"]).expect("parse ok");
        assert!(matches!(cli_flash.command, Command::Flash(_)));
        let Command::Flash(flash_args) = cli_flash.command else {
            return;
        };
        assert!(flash_args.sys.list);
        assert_eq!(flash_args.sys.image.as_deref(), Some("app"));
    }

    #[test]
    fn parse_aliases_and_defaults() {
        let cli_init = Cli::try_parse_from(["kaze", "i"]).expect("parse ok");
        assert!(matches!(cli_init.command, Command::Init(_)));

        let cli_boards = Cli::try_parse_from(["kaze", "bo"]).expect("parse ok");
        assert!(matches!(cli_boards.command, Command::Boards));

        let cli_runners = Cli::try_parse_from(["kaze", "rn"]).expect("parse ok");
        assert!(matches!(cli_runners.command, Command::Runners));

        let cli_profiles = Cli::try_parse_from(["kaze", "p"]).expect("parse ok");
        assert!(matches!(cli_profiles.command, Command::Profiles));

        let cli_clean = Cli::try_parse_from(["kaze", "c"]).expect("parse ok");
        assert!(matches!(cli_clean.command, Command::Clean));

        let cli_conf = Cli::try_parse_from(["kaze", "cf"]).expect("parse ok");
        assert!(matches!(cli_conf.command, Command::Conf(_)));

        let cli_build = Cli::try_parse_from(["kaze", "b"]).expect("parse ok");
        assert!(matches!(cli_build.command, Command::Build(_)));

        let cli_run = Cli::try_parse_from(["kaze", "r"]).expect("parse ok");
        assert!(matches!(&cli_run.command, Command::Run(_)));
        if let Command::Run(ref run_args) = cli_run.command {
            assert!(!run_args.norebuild);
            assert!(!run_args.sys.list);
            assert_eq!(run_args.sys.image, None);
            assert!(run_args.phase.extra.is_empty());
        }

        let cli_flash = Cli::try_parse_from(["kaze", "f"]).expect("parse ok");
        assert!(matches!(&cli_flash.command, Command::Flash(_)));
        if let Command::Flash(ref flash_args) = cli_flash.command {
            assert!(!flash_args.sys.list);
            assert_eq!(flash_args.sys.image, None);
            assert!(flash_args.phase.extra.is_empty());
        }
    }
}