wash_cli/
build.rs

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/// Build (and sign) a wasmCloud component, provider, or interface
14#[derive(Debug, Parser, Clone)]
15#[clap(name = "build")]
16pub struct BuildCommand {
17    /// Path to the wasmcloud.toml file or parent folder to use for building
18    #[clap(short = 'p', long = "config-path")]
19    config_path: Option<PathBuf>,
20
21    #[clap(flatten)]
22    pub package_args: CommonPackageArgs,
23
24    /// Location of key files for signing. Defaults to $WASH_KEYS ($HOME/.wash/keys)
25    #[clap(long = "keys-directory", env = "WASH_KEYS", hide_env_values = true)]
26    pub keys_directory: Option<PathBuf>,
27
28    /// Path to issuer seed key (account). If this flag is not provided, the seed will be sourced from $WASH_KEYS ($HOME/.wash/keys) or generated for you if it cannot be found.
29    #[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    /// Path to subject seed key (module or service). If this flag is not provided, the seed will be sourced from $WASH_KEYS ($HOME/.wash/keys) or generated for you if it cannot be found.
38    #[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    /// Disables autogeneration of keys if seed(s) are not provided
47    #[clap(long = "disable-keygen")]
48    pub disable_keygen: bool,
49
50    /// Skip signing the artifact and only use the native toolchain to build
51    #[clap(long = "build-only", conflicts_with = "sign_only")]
52    pub build_only: bool,
53
54    /// Skip building the artifact and only use configuration to sign
55    #[clap(long = "sign-only", conflicts_with = "build_only")]
56    pub sign_only: bool,
57
58    /// Skip wit dependency fetching and use only what is currently present in the wit directory
59    /// (useful for airgapped or disconnected environments)
60    #[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                    // We prevent supplying both fields in the CLI parser, so this `context` is just a safety fallback
98                    &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}