1#![deny(missing_docs)]
2
3mod manifest;
6
7use anyhow::{anyhow, bail, Context, Result};
8use manifest::ComponentBuildInfo;
9use spin_common::{paths::parent_dir, ui::quoted_path};
10use std::{
11 collections::HashSet,
12 path::{Path, PathBuf},
13};
14use subprocess::{Exec, Redirection};
15
16use crate::manifest::component_build_configs;
17
18pub async fn build(manifest_file: &Path, component_ids: &[String]) -> Result<()> {
20 let (components, manifest_err) =
21 component_build_configs(manifest_file)
22 .await
23 .with_context(|| {
24 format!(
25 "Cannot read manifest file from {}",
26 quoted_path(manifest_file)
27 )
28 })?;
29 let app_dir = parent_dir(manifest_file)?;
30
31 let build_result = build_components(component_ids, components, app_dir);
32
33 if let Some(e) = manifest_err {
34 terminal::warn!("The manifest has errors not related to the Wasm component build. Error details:\n{e:#}");
35 }
36
37 build_result
38}
39
40fn build_components(
41 component_ids: &[String],
42 components: Vec<ComponentBuildInfo>,
43 app_dir: PathBuf,
44) -> Result<(), anyhow::Error> {
45 let components_to_build = if component_ids.is_empty() {
46 components
47 } else {
48 let all_ids: HashSet<_> = components.iter().map(|c| &c.id).collect();
49 let unknown_component_ids: Vec<_> = component_ids
50 .iter()
51 .filter(|id| !all_ids.contains(id))
52 .map(|s| s.as_str())
53 .collect();
54
55 if !unknown_component_ids.is_empty() {
56 bail!("Unknown component(s) {}", unknown_component_ids.join(", "));
57 }
58
59 components
60 .into_iter()
61 .filter(|c| component_ids.contains(&c.id))
62 .collect()
63 };
64
65 if components_to_build.iter().all(|c| c.build.is_none()) {
66 println!("None of the components have a build command.");
67 println!("For information on specifying a build command, see https://spinframework.dev/build#setting-up-for-spin-build.");
68 return Ok(());
69 }
70
71 components_to_build
72 .into_iter()
73 .map(|c| build_component(c, &app_dir))
74 .collect::<Result<Vec<_>, _>>()?;
75
76 terminal::step!("Finished", "building all Spin components");
77 Ok(())
78}
79
80fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()> {
82 match build_info.build {
83 Some(b) => {
84 let command_count = b.commands().len();
85
86 if command_count > 1 {
87 terminal::step!(
88 "Building",
89 "component {} ({} commands)",
90 build_info.id,
91 command_count
92 );
93 }
94
95 for (index, command) in b.commands().enumerate() {
96 if command_count > 1 {
97 terminal::step!(
98 "Running build step",
99 "{}/{} for component {} with '{}'",
100 index + 1,
101 command_count,
102 build_info.id,
103 command
104 );
105 } else {
106 terminal::step!("Building", "component {} with `{}`", build_info.id, command);
107 }
108
109 let workdir = construct_workdir(app_dir, b.workdir.as_ref())?;
110 if b.workdir.is_some() {
111 println!("Working directory: {}", quoted_path(&workdir));
112 }
113
114 let exit_status = Exec::shell(command)
115 .cwd(workdir)
116 .stdout(Redirection::None)
117 .stderr(Redirection::None)
118 .stdin(Redirection::None)
119 .popen()
120 .map_err(|err| {
121 anyhow!(
122 "Cannot spawn build process '{:?}' for component {}: {}",
123 &b.command,
124 build_info.id,
125 err
126 )
127 })?
128 .wait()?;
129
130 if !exit_status.success() {
131 bail!(
132 "Build command for component {} failed with status {:?}",
133 build_info.id,
134 exit_status,
135 );
136 }
137 }
138
139 Ok(())
140 }
141 _ => Ok(()),
142 }
143}
144
145fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Result<PathBuf> {
147 let mut cwd = app_dir.to_owned();
148
149 if let Some(workdir) = workdir {
150 if workdir.as_ref().has_root() {
154 bail!("The workdir specified in the application file must be relative.");
155 }
156 cwd.push(workdir);
157 }
158
159 Ok(cwd)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn test_data_root() -> PathBuf {
167 let crate_dir = env!("CARGO_MANIFEST_DIR");
168 PathBuf::from(crate_dir).join("tests")
169 }
170
171 #[tokio::test]
172 async fn can_load_even_if_trigger_invalid() {
173 let bad_trigger_file = test_data_root().join("bad_trigger.toml");
174 build(&bad_trigger_file, &[]).await.unwrap();
175 }
176}