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 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(BuildArgs),
32 New(NewArgs),
34 Run(RunArgs),
36 Flamegraph(FlamegraphArgs),
38 Prove(ProveArgs),
40 GenerateVk(GenerateVkArgs),
43 VerifyProof(VerifyProofArgs),
45 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 #[arg(long)]
72 pub reproducible: bool,
73 #[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 Dev,
122 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}