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(&'static str),
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        .disable_help_flag(true)
56        .arg(Arg::new("canister-name").required(true))
57}
58
59/// Run one Canic canister artifact build.
60pub 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
78// Build the requested canister and print the artifact path for dfx custom builds.
79fn 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
101// Return build command usage text.
102const fn usage() -> &'static str {
103    "usage: canic build <canister-name>"
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    // Ensure build requires one canister name and preserves it exactly.
111    #[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    // Ensure build rejects missing canister names.
119    #[test]
120    fn rejects_missing_build_canister_name() {
121        assert!(matches!(
122            BuildOptions::parse([]),
123            Err(BuildCommandError::Usage(_))
124        ));
125    }
126}