1use std::{collections::HashMap, path::PathBuf};
2
3use anyhow::{Context, Result};
4use clap::Parser;
5use serde_json::json;
6
7use wash_lib::{
8 build::{build_project, sign_component_wasm, SignConfig},
9 cli::{CommandOutput, CommonPackageArgs},
10 parser::{load_config, TypeConfig},
11};
12
13#[derive(Debug, Parser, Clone)]
15#[clap(name = "build")]
16pub struct BuildCommand {
17 #[clap(short = 'p', long = "config-path")]
19 config_path: Option<PathBuf>,
20
21 #[clap(flatten)]
22 pub package_args: CommonPackageArgs,
23
24 #[clap(long = "keys-directory", env = "WASH_KEYS", hide_env_values = true)]
26 pub keys_directory: Option<PathBuf>,
27
28 #[clap(
30 short = 'i',
31 long = "issuer",
32 env = "WASH_ISSUER_KEY",
33 hide_env_values = true
34 )]
35 pub issuer: Option<String>,
36
37 #[clap(
39 short = 's',
40 long = "subject",
41 env = "WASH_SUBJECT_KEY",
42 hide_env_values = true
43 )]
44 pub subject: Option<String>,
45
46 #[clap(long = "disable-keygen")]
48 pub disable_keygen: bool,
49
50 #[clap(long = "build-only", conflicts_with = "sign_only")]
52 pub build_only: bool,
53
54 #[clap(long = "sign-only", conflicts_with = "build_only")]
56 pub sign_only: bool,
57
58 #[clap(long = "skip-fetch")]
61 pub skip_wit_fetch: bool,
62}
63
64pub async fn handle_command(command: BuildCommand) -> Result<CommandOutput> {
65 let config = load_config(command.config_path, Some(true)).await?;
66
67 match config.project_type {
68 TypeConfig::Component(ref component_config) => {
69 let sign_config = if command.build_only {
70 None
71 } else {
72 Some(SignConfig {
73 keys_directory: command
74 .keys_directory
75 .clone()
76 .or(Some(component_config.key_directory.to_path_buf())),
77 issuer: command.issuer,
78 subject: command.subject,
79 disable_keygen: command.disable_keygen,
80 })
81 };
82
83 let component_path = if command.sign_only {
84 std::env::set_current_dir(&config.common.project_dir)?;
85 let component_wasm_path =
86 if let Some(path) = component_config.build_artifact.as_ref() {
87 path.clone()
88 } else {
89 config
90 .common
91 .build_dir
92 .join(format!("{}.wasm", config.common.wasm_bin_name()))
93 };
94 let signed_path = sign_component_wasm(
95 &config.common,
96 component_config,
97 &sign_config.context("cannot supply --build-only and --sign-only")?,
99 component_wasm_path,
100 )?;
101 config.common.build_dir.join(signed_path)
102 } else {
103 build_project(
104 &config,
105 sign_config.as_ref(),
106 &command.package_args,
107 command.skip_wit_fetch,
108 )
109 .await?
110 };
111
112 let json_output = HashMap::from([
113 ("component_path".to_string(), json!(component_path)),
114 ("built".to_string(), json!(!command.sign_only)),
115 ("signed".to_string(), json!(!command.build_only)),
116 ]);
117 Ok(CommandOutput::new(
118 if command.build_only {
119 format!("Component built and can be found at {component_path:?}")
120 } else if command.sign_only {
121 format!("Component signed and can be found at {component_path:?}")
122 } else {
123 format!("Component built and signed and can be found at {component_path:?}")
124 },
125 json_output,
126 ))
127 }
128 TypeConfig::Provider(ref provider_config) => {
129 let path = build_project(
130 &config,
131 Some(&SignConfig {
132 keys_directory: command
133 .keys_directory
134 .clone()
135 .or(Some(provider_config.key_directory.to_path_buf())),
136 issuer: command.issuer,
137 subject: command.subject,
138 disable_keygen: command.disable_keygen,
139 }),
140 &command.package_args,
141 command.skip_wit_fetch,
142 )
143 .await
144 .context("failed to build provider")?;
145 Ok(CommandOutput::new(
146 format!("Built artifact can be found at {path:?}"),
147 HashMap::from([("path".to_string(), json!(path))]),
148 ))
149 }
150 }
151}
152
153#[cfg(test)]
154mod test {
155
156 use super::*;
157 use clap::Parser;
158
159 #[test]
160 fn test_build_comprehensive() {
161 let cmd: BuildCommand = Parser::try_parse_from(["build"]).unwrap();
162 assert!(cmd.config_path.is_none());
163 assert!(!cmd.disable_keygen);
164 assert!(cmd.issuer.is_none());
165 assert!(cmd.subject.is_none());
166 assert!(cmd.keys_directory.is_none());
167
168 let cmd: BuildCommand = Parser::try_parse_from([
169 "build",
170 "-p",
171 "/",
172 "--disable-keygen",
173 "--issuer",
174 "/tmp/iss.nk",
175 "--subject",
176 "/tmp/sub.nk",
177 "--keys-directory",
178 "/tmp",
179 ])
180 .unwrap();
181 assert_eq!(cmd.config_path, Some(PathBuf::from("/")));
182 assert!(cmd.disable_keygen);
183 assert_eq!(cmd.issuer, Some("/tmp/iss.nk".to_string()));
184 assert_eq!(cmd.subject, Some("/tmp/sub.nk".to_string()));
185 assert_eq!(cmd.keys_directory, Some(PathBuf::from("/tmp")));
186 }
187}