1use crate::{
2 args::{first_arg_is_help, first_arg_is_version, parse_matches},
3 version_text,
4};
5use canic_host::canister_build::{
6 CanisterBuildProfile, build_current_workspace_canister_artifact,
7 print_current_workspace_build_context_once,
8};
9use clap::{Arg, Command as ClapCommand};
10use std::{ffi::OsString, time::Instant};
11use thiserror::Error as ThisError;
12
13#[derive(Debug, ThisError)]
18pub enum BuildCommandError {
19 #[error("{0}")]
20 Usage(&'static str),
21
22 #[error(transparent)]
23 Build(#[from] Box<dyn std::error::Error>),
24}
25
26#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct BuildOptions {
32 pub canister_name: String,
33}
34
35impl BuildOptions {
36 pub fn parse<I>(args: I) -> Result<Self, BuildCommandError>
38 where
39 I: IntoIterator<Item = OsString>,
40 {
41 let matches =
42 parse_matches(build_command(), args).map_err(|_| BuildCommandError::Usage(usage()))?;
43 let canister_name = matches
44 .get_one::<String>("canister-name")
45 .expect("clap requires canister-name")
46 .clone();
47
48 Ok(Self { canister_name })
49 }
50}
51
52fn build_command() -> ClapCommand {
54 ClapCommand::new("build")
55 .disable_help_flag(true)
56 .arg(Arg::new("canister-name").required(true))
57}
58
59pub fn run<I>(args: I) -> Result<(), BuildCommandError>
61where
62 I: IntoIterator<Item = OsString>,
63{
64 let args = args.into_iter().collect::<Vec<_>>();
65 if first_arg_is_help(&args) {
66 println!("{}", usage());
67 return Ok(());
68 }
69 if first_arg_is_version(&args) {
70 println!("{}", version_text());
71 return Ok(());
72 }
73
74 let options = BuildOptions::parse(args)?;
75 build_canister(options).map_err(BuildCommandError::from)
76}
77
78fn build_canister(options: BuildOptions) -> Result<(), Box<dyn std::error::Error>> {
80 let profile = CanisterBuildProfile::current();
81 print_current_workspace_build_context_once(profile)?;
82 eprintln!(
83 "Canic build start: canister={} profile={}",
84 options.canister_name,
85 profile.target_dir_name()
86 );
87
88 let started_at = Instant::now();
89 let output = build_current_workspace_canister_artifact(&options.canister_name, profile)?;
90 let elapsed = started_at.elapsed().as_secs_f64();
91
92 println!("{}", output.wasm_gz_path.display());
93 eprintln!(
94 "Canic build done: canister={} elapsed={elapsed:.2}s",
95 options.canister_name
96 );
97 eprintln!();
98 Ok(())
99}
100
101const fn usage() -> &'static str {
103 "usage: canic build <canister-name>"
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
112 fn parses_build_canister_name() {
113 let options = BuildOptions::parse([OsString::from("root")]).expect("parse build");
114
115 assert_eq!(options.canister_name, "root");
116 }
117
118 #[test]
120 fn rejects_missing_build_canister_name() {
121 assert!(matches!(
122 BuildOptions::parse([]),
123 Err(BuildCommandError::Usage(_))
124 ));
125 }
126}