Skip to main content

canic_cli/build/
mod.rs

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///
14/// BuildCommandError
15///
16
17#[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///
27/// BuildOptions
28///
29
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct BuildOptions {
32    pub canister_name: String,
33}
34
35impl BuildOptions {
36    /// Parse build options from CLI arguments.
37    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
52// Build the canister-artifact parser.
53fn 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
65/// Run one Canic canister artifact build.
66pub 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
84// Build the requested canister and print the artifact path for dfx custom builds.
85fn 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
107// Return build command usage text.
108fn 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    // Ensure build requires one canister name and preserves it exactly.
118    #[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    // Ensure build rejects missing canister names.
126    #[test]
127    fn rejects_missing_build_canister_name() {
128        assert!(matches!(
129            BuildOptions::parse([]),
130            Err(BuildCommandError::Usage(_))
131        ));
132    }
133}