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(String),
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 .bin_name("canic build")
56 .about("Build one Canic canister artifact for dfx")
57 .disable_help_flag(true)
58 .arg(
59 Arg::new("canister-name")
60 .value_name("canister-name")
61 .required(true),
62 )
63}
64
65pub fn run<I>(args: I) -> Result<(), BuildCommandError>
67where
68 I: IntoIterator<Item = OsString>,
69{
70 let args = args.into_iter().collect::<Vec<_>>();
71 if first_arg_is_help(&args) {
72 println!("{}", usage());
73 return Ok(());
74 }
75 if first_arg_is_version(&args) {
76 println!("{}", version_text());
77 return Ok(());
78 }
79
80 let options = BuildOptions::parse(args)?;
81 build_canister(options).map_err(BuildCommandError::from)
82}
83
84fn build_canister(options: BuildOptions) -> Result<(), Box<dyn std::error::Error>> {
86 let profile = CanisterBuildProfile::current();
87 print_current_workspace_build_context_once(profile)?;
88 eprintln!(
89 "Canic build start: canister={} profile={}",
90 options.canister_name,
91 profile.target_dir_name()
92 );
93
94 let started_at = Instant::now();
95 let output = build_current_workspace_canister_artifact(&options.canister_name, profile)?;
96 let elapsed = started_at.elapsed().as_secs_f64();
97
98 println!("{}", output.wasm_gz_path.display());
99 eprintln!(
100 "Canic build done: canister={} elapsed={elapsed:.2}s",
101 options.canister_name
102 );
103 eprintln!();
104 Ok(())
105}
106
107fn usage() -> String {
109 let mut command = build_command();
110 command.render_help().to_string()
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
119 fn parses_build_canister_name() {
120 let options = BuildOptions::parse([OsString::from("root")]).expect("parse build");
121
122 assert_eq!(options.canister_name, "root");
123 }
124
125 #[test]
127 fn rejects_missing_build_canister_name() {
128 assert!(matches!(
129 BuildOptions::parse([]),
130 Err(BuildCommandError::Usage(_))
131 ));
132 }
133}