Skip to main content

cargo_airbender/
cli.rs

1use clap::{Args, Parser, Subcommand, ValueEnum};
2use std::path::PathBuf;
3
4#[derive(Parser, Debug)]
5#[command(
6    name = "cargo-airbender",
7    bin_name = "cargo airbender",
8    version,
9    about = "Airbender cargo subcommand"
10)]
11pub struct Cli {
12    #[command(subcommand)]
13    pub command: Commands,
14}
15
16impl Cli {
17    /// Cargo invokes subcommand binaries as `cargo-airbender airbender <args>`.
18    /// We strip the synthetic `airbender` token so clap can parse the command list naturally.
19    pub fn parse_for_cargo() -> Self {
20        let mut args: Vec<String> = std::env::args().collect();
21        if args.get(1).map(String::as_str) == Some("airbender") {
22            args.remove(1);
23        }
24        <Self as Parser>::parse_from(args)
25    }
26}
27
28#[derive(Subcommand, Debug)]
29pub enum Commands {
30    /// Build and package guest artifacts into dist/.
31    Build(BuildArgs),
32    /// Create a new host + guest project from templates.
33    New(NewArgs),
34    /// Run app.bin via the transpiler.
35    Run(RunArgs),
36    /// Run app.bin with transpiler profiling and emit flamegraph SVG.
37    Flamegraph(FlamegraphArgs),
38    /// Generate a proof and write it as bincode.
39    Prove(ProveArgs),
40    /// Generate verification keys and write them as bincode.
41    /// Requires GPU support in `cargo-airbender` (enabled by default).
42    GenerateVk(GenerateVkArgs),
43    /// Verify a proof against verification keys.
44    VerifyProof(VerifyProofArgs),
45    /// Remove Docker resources created by reproducible builds.
46    Clean,
47}
48
49#[derive(Args, Debug)]
50pub struct BuildArgs {
51    #[arg(long, default_value = "app")]
52    pub app_name: String,
53    #[arg(long)]
54    pub bin: Option<String>,
55    #[arg(long)]
56    pub target: Option<String>,
57    #[arg(long)]
58    pub dist: Option<PathBuf>,
59    #[arg(long)]
60    pub project: Option<PathBuf>,
61    #[arg(long, value_enum, conflicts_with_all = ["debug", "release"])]
62    pub profile: Option<BuildProfile>,
63    #[arg(long, conflicts_with = "release")]
64    pub debug: bool,
65    #[arg(long, conflicts_with = "debug")]
66    pub release: bool,
67    #[arg(last = true, value_name = "CARGO_ARGS")]
68    pub cargo_args: Vec<String>,
69    /// Build inside a pinned Docker container for bit-for-bit reproducible output.
70    /// Automatically passes `--locked` to cargo; the project must have a committed `Cargo.lock`.
71    #[arg(long)]
72    pub reproducible: bool,
73    /// Override the directory bind-mounted as /src inside the reproducible build container.
74    /// Only needed when the guest has path dependencies pointing outside its own cargo workspace
75    /// root, e.g. in a monorepo where the guest shares crates with the host via relative paths.
76    /// Has no effect without --reproducible.
77    #[arg(long, requires = "reproducible")]
78    pub workspace_root: Option<PathBuf>,
79}
80
81#[derive(ValueEnum, Debug, Clone, Copy)]
82pub enum BuildProfile {
83    Debug,
84    Release,
85}
86
87#[derive(Args, Debug)]
88pub struct NewArgs {
89    pub path: Option<PathBuf>,
90    #[arg(long)]
91    pub name: Option<String>,
92    #[arg(long)]
93    pub enable_std: bool,
94    #[arg(long, value_enum, default_value_t = NewAllocatorArg::Talc)]
95    pub allocator: NewAllocatorArg,
96    #[arg(
97        long,
98        value_enum,
99        default_value_t = NewProverBackendArg::Dev,
100        long_help = "Select host template proving backend.\n- dev: mock proof envelope for development (no cryptographic proving).\n- gpu: real GPU proving; requires CUDA-capable NVIDIA GPU at runtime. You can compile with ZKSYNC_USE_CUDA_STUBS=true, but running proving without CUDA setup panics."
101    )]
102    pub prover_backend: NewProverBackendArg,
103    #[arg(short = 'y', long)]
104    pub yes: bool,
105    #[arg(long, conflicts_with = "sdk_version")]
106    pub sdk_path: Option<PathBuf>,
107    #[arg(long, conflicts_with = "sdk_path")]
108    pub sdk_version: Option<String>,
109}
110
111#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
112pub enum NewAllocatorArg {
113    Talc,
114    Bump,
115    Custom,
116}
117
118#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
119pub enum NewProverBackendArg {
120    /// Use transpiler execution and emit a mock proof envelope (best for development).
121    Dev,
122    /// Use real GPU proving (requires CUDA-capable NVIDIA GPU at runtime).
123    Gpu,
124}
125
126#[derive(Args, Debug)]
127pub struct FlamegraphArgs {
128    pub app_bin: PathBuf,
129    #[arg(short, long)]
130    pub input: PathBuf,
131    #[arg(short, long, default_value = "flamegraph.svg")]
132    pub output: PathBuf,
133    #[arg(short, long)]
134    pub cycles: Option<usize>,
135    #[arg(long, default_value_t = 100)]
136    pub sampling_rate: usize,
137    #[arg(long)]
138    pub inverse: bool,
139    #[arg(long)]
140    pub elf_path: Option<PathBuf>,
141}
142
143#[derive(Args, Debug)]
144pub struct RunArgs {
145    pub app_bin: PathBuf,
146    #[arg(short, long)]
147    pub input: PathBuf,
148    #[arg(short, long)]
149    pub cycles: Option<usize>,
150    #[arg(long)]
151    pub text_path: Option<PathBuf>,
152    #[arg(
153        long,
154        help = "Enable transpiler JIT execution (x86_64 only); default is portable non-JIT mode"
155    )]
156    pub jit: bool,
157}
158
159#[derive(Args, Debug)]
160pub struct ProveArgs {
161    pub app_bin: PathBuf,
162    #[arg(short, long)]
163    pub input: PathBuf,
164    #[arg(long)]
165    pub output: PathBuf,
166    #[arg(
167        long,
168        value_enum,
169        default_value_t = ProverBackendArg::Dev,
170        long_help = "Select proving backend.\n- dev: mock proof envelope for development (no cryptographic proving).\n- cpu: real CPU proving (base level only).\n- gpu: real GPU proving; requires GPU-enabled `cargo-airbender` (enabled by default)."
171    )]
172    pub backend: ProverBackendArg,
173    #[arg(short, long)]
174    pub threads: Option<usize>,
175    #[arg(long)]
176    pub cycles: Option<usize>,
177    #[arg(long)]
178    pub ram_bound: Option<usize>,
179    #[arg(long, value_enum, default_value_t = ProverLevelArg::RecursionUnified)]
180    pub level: ProverLevelArg,
181    #[arg(
182        long,
183        value_enum,
184        default_value_t = SecurityLevelArg::default(),
185        help = "Security level recorded in proof artifacts (80 or 100 bits; default: 100)"
186    )]
187    pub security: SecurityLevelArg,
188}
189
190#[derive(Args, Debug)]
191pub struct GenerateVkArgs {
192    pub app_bin: PathBuf,
193    #[arg(short, long, default_value = "vk.bin")]
194    pub output: PathBuf,
195    #[arg(long, value_enum, default_value_t = ProverLevelArg::RecursionUnified)]
196    pub level: ProverLevelArg,
197    #[arg(
198        long,
199        value_enum,
200        default_value_t = SecurityLevelArg::default(),
201        help = "Security level for verification keys (80 or 100 bits; default: 100)"
202    )]
203    pub security: SecurityLevelArg,
204}
205
206#[derive(Args, Debug)]
207pub struct VerifyProofArgs {
208    pub proof: PathBuf,
209    #[arg(long)]
210    pub vk: PathBuf,
211    #[arg(
212        long,
213        value_name = "WORDS",
214        help = "Comma-separated expected public output words (x10..x17), decimal or 0x hex"
215    )]
216    pub expected_output: Option<String>,
217}
218
219#[derive(ValueEnum, Debug, Clone, Copy)]
220pub enum ProverBackendArg {
221    Dev,
222    Cpu,
223    Gpu,
224}
225
226#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
227pub enum ProverLevelArg {
228    Base,
229    RecursionUnrolled,
230    RecursionUnified,
231}
232
233#[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq)]
234pub enum SecurityLevelArg {
235    #[value(name = "80")]
236    Bits80,
237    #[value(name = "100")]
238    Bits100,
239}
240
241impl Default for SecurityLevelArg {
242    fn default() -> Self {
243        Self::from(airbender_host::SecurityLevel::default())
244    }
245}
246
247impl From<airbender_host::SecurityLevel> for SecurityLevelArg {
248    fn from(security: airbender_host::SecurityLevel) -> Self {
249        match security {
250            airbender_host::SecurityLevel::Bits80 => Self::Bits80,
251            airbender_host::SecurityLevel::Bits100 => Self::Bits100,
252        }
253    }
254}
255
256impl From<SecurityLevelArg> for airbender_host::SecurityLevel {
257    fn from(security: SecurityLevelArg) -> Self {
258        match security {
259            SecurityLevelArg::Bits80 => Self::Bits80,
260            SecurityLevelArg::Bits100 => Self::Bits100,
261        }
262    }
263}
264
265impl std::fmt::Display for SecurityLevelArg {
266    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
267        let host_security = airbender_host::SecurityLevel::from(*self);
268        write!(formatter, "{host_security}")
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn parse_run_jit_flag() {
278        let cli = Cli::parse_from([
279            "cargo-airbender",
280            "run",
281            "app.bin",
282            "--input",
283            "input.hex",
284            "--jit",
285        ]);
286        match cli.command {
287            Commands::Run(args) => {
288                assert_eq!(args.app_bin, PathBuf::from("app.bin"));
289                assert_eq!(args.input, PathBuf::from("input.hex"));
290                assert!(args.jit);
291            }
292            other => panic!("unexpected command: {other:?}"),
293        }
294    }
295
296    #[test]
297    fn parse_build_trailing_cargo_args() {
298        let cli = Cli::parse_from([
299            "cargo-airbender",
300            "build",
301            "--",
302            "--features",
303            "gpu",
304            "--color",
305            "always",
306        ]);
307        match cli.command {
308            Commands::Build(args) => {
309                assert_eq!(args.app_name, "app");
310                assert_eq!(
311                    args.cargo_args,
312                    vec!["--features", "gpu", "--color", "always"]
313                );
314            }
315            other => panic!("unexpected command: {other:?}"),
316        }
317    }
318
319    #[test]
320    fn parse_build_custom_app_name() {
321        let cli = Cli::parse_from(["cargo-airbender", "build", "--app-name", "gpu-profile"]);
322        match cli.command {
323            Commands::Build(args) => {
324                assert_eq!(args.app_name, "gpu-profile");
325            }
326            other => panic!("unexpected command: {other:?}"),
327        }
328    }
329
330    #[test]
331    fn parse_build_reproducible_flag() {
332        let cli = Cli::parse_from(["cargo-airbender", "build", "--reproducible"]);
333        match cli.command {
334            Commands::Build(args) => {
335                assert!(args.reproducible);
336                assert_eq!(args.app_name, "app");
337            }
338            other => panic!("unexpected command: {other:?}"),
339        }
340    }
341
342    #[test]
343    fn parse_build_workspace_root_with_reproducible() {
344        let cli = Cli::parse_from([
345            "cargo-airbender",
346            "build",
347            "--reproducible",
348            "--workspace-root",
349            "/repo",
350        ]);
351        match cli.command {
352            Commands::Build(args) => {
353                assert!(args.reproducible);
354                assert_eq!(args.workspace_root, Some(PathBuf::from("/repo")));
355            }
356            other => panic!("unexpected command: {other:?}"),
357        }
358    }
359
360    #[test]
361    fn parse_build_workspace_root_requires_reproducible() {
362        let err = Cli::try_parse_from(["cargo-airbender", "build", "--workspace-root", "/repo"])
363            .expect_err("--workspace-root without --reproducible should fail");
364        assert!(err.to_string().contains("--reproducible"));
365    }
366
367    #[test]
368    fn parse_build_rejects_top_level_cargo_flags() {
369        let err = Cli::try_parse_from(["cargo-airbender", "build", "--features", "gpu"])
370            .expect_err("parse should fail without -- forwarding separator");
371        assert!(err.to_string().contains("--features"));
372    }
373
374    #[test]
375    fn parse_new_enable_std() {
376        let cli = Cli::parse_from([
377            "cargo-airbender",
378            "new",
379            "./hello-airbender",
380            "--enable-std",
381        ]);
382        match cli.command {
383            Commands::New(args) => {
384                assert_eq!(args.path, Some(PathBuf::from("./hello-airbender")));
385                assert!(args.enable_std);
386                assert_eq!(args.allocator, NewAllocatorArg::Talc);
387                assert_eq!(args.prover_backend, NewProverBackendArg::Dev);
388            }
389            other => panic!("unexpected command: {other:?}"),
390        }
391    }
392
393    #[test]
394    fn parse_new_allocator_custom() {
395        let cli = Cli::parse_from([
396            "cargo-airbender",
397            "new",
398            "./hello-airbender",
399            "--allocator",
400            "custom",
401        ]);
402        match cli.command {
403            Commands::New(args) => {
404                assert_eq!(args.path, Some(PathBuf::from("./hello-airbender")));
405                assert_eq!(args.allocator, NewAllocatorArg::Custom);
406                assert_eq!(args.prover_backend, NewProverBackendArg::Dev);
407            }
408            other => panic!("unexpected command: {other:?}"),
409        }
410    }
411
412    #[test]
413    fn parse_new_prover_backend_gpu() {
414        let cli = Cli::parse_from([
415            "cargo-airbender",
416            "new",
417            "./hello-airbender",
418            "--prover-backend",
419            "gpu",
420        ]);
421        match cli.command {
422            Commands::New(args) => {
423                assert_eq!(args.path, Some(PathBuf::from("./hello-airbender")));
424                assert_eq!(args.prover_backend, NewProverBackendArg::Gpu);
425            }
426            other => panic!("unexpected command: {other:?}"),
427        }
428    }
429
430    #[test]
431    fn parse_new_without_path() {
432        let cli = Cli::parse_from(["cargo-airbender", "new"]);
433        match cli.command {
434            Commands::New(args) => {
435                assert_eq!(args.path, None);
436                assert!(!args.yes);
437                assert_eq!(args.allocator, NewAllocatorArg::Talc);
438                assert_eq!(args.prover_backend, NewProverBackendArg::Dev);
439            }
440            other => panic!("unexpected command: {other:?}"),
441        }
442    }
443
444    #[test]
445    fn parse_new_yes_flag() {
446        let cli = Cli::parse_from(["cargo-airbender", "new", "--yes"]);
447        match cli.command {
448            Commands::New(args) => {
449                assert!(args.yes);
450            }
451            other => panic!("unexpected command: {other:?}"),
452        }
453    }
454
455    #[test]
456    fn parse_verify_proof_expected_output() {
457        let cli = Cli::parse_from([
458            "cargo-airbender",
459            "verify-proof",
460            "proof.bin",
461            "--vk",
462            "vk.bin",
463            "--expected-output",
464            "42,0,0",
465        ]);
466        match cli.command {
467            Commands::VerifyProof(args) => {
468                assert_eq!(args.proof, PathBuf::from("proof.bin"));
469                assert_eq!(args.vk, PathBuf::from("vk.bin"));
470                assert_eq!(args.expected_output.as_deref(), Some("42,0,0"));
471            }
472            other => panic!("unexpected command: {other:?}"),
473        }
474    }
475
476    #[test]
477    fn parse_verify_proof_rejects_repeated_expected_output() {
478        let err = Cli::try_parse_from([
479            "cargo-airbender",
480            "verify-proof",
481            "proof.bin",
482            "--vk",
483            "vk.bin",
484            "--expected-output",
485            "1",
486            "--expected-output",
487            "2",
488        ])
489        .expect_err("repeated expected-output flag should fail");
490
491        assert!(err.to_string().contains("cannot be used multiple times"));
492    }
493
494    #[test]
495    fn parse_prove_security_100() {
496        let cli = Cli::parse_from([
497            "cargo-airbender",
498            "prove",
499            "app.bin",
500            "--input",
501            "input.hex",
502            "--output",
503            "proof.bin",
504            "--backend",
505            "gpu",
506            "--security",
507            "100",
508        ]);
509        match cli.command {
510            Commands::Prove(args) => {
511                assert_eq!(args.security, SecurityLevelArg::Bits100);
512            }
513            other => panic!("unexpected command: {other:?}"),
514        }
515    }
516
517    #[test]
518    fn parse_prove_security_defaults_to_100() {
519        let cli = Cli::parse_from([
520            "cargo-airbender",
521            "prove",
522            "app.bin",
523            "--input",
524            "input.hex",
525            "--output",
526            "proof.bin",
527            "--backend",
528            "gpu",
529        ]);
530        match cli.command {
531            Commands::Prove(args) => {
532                assert_eq!(args.security, SecurityLevelArg::Bits100);
533            }
534            other => panic!("unexpected command: {other:?}"),
535        }
536    }
537
538    #[test]
539    fn parse_generate_vk_security_80() {
540        let cli = Cli::parse_from([
541            "cargo-airbender",
542            "generate-vk",
543            "app.bin",
544            "--security",
545            "80",
546        ]);
547        match cli.command {
548            Commands::GenerateVk(args) => {
549                assert_eq!(args.security, SecurityLevelArg::Bits80);
550            }
551            other => panic!("unexpected command: {other:?}"),
552        }
553    }
554
555    #[test]
556    fn parse_generate_vk_security_defaults_to_100() {
557        let cli = Cli::parse_from(["cargo-airbender", "generate-vk", "app.bin"]);
558        match cli.command {
559            Commands::GenerateVk(args) => {
560                assert_eq!(args.security, SecurityLevelArg::Bits100);
561            }
562            other => panic!("unexpected command: {other:?}"),
563        }
564    }
565
566    #[test]
567    fn parse_prove_rejects_invalid_security() {
568        let err = Cli::try_parse_from([
569            "cargo-airbender",
570            "prove",
571            "app.bin",
572            "--input",
573            "input.hex",
574            "--output",
575            "proof.bin",
576            "--security",
577            "90",
578        ])
579        .expect_err("invalid security should fail");
580
581        assert!(err.to_string().contains("invalid value"));
582    }
583}