spin_build/
lib.rs

1#![deny(missing_docs)]
2
3//! A library for building Spin components.
4
5mod 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
18/// If present, run the build command of each component.
19pub 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
80/// Run the build command of the component.
81fn 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
145/// Constructs the absolute working directory in which to run the build command.
146fn 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        // Using `Path::has_root` as `is_relative` and `is_absolute` have
151        // surprising behavior on Windows, see:
152        // https://doc.rust-lang.org/std/path/struct.Path.html#method.is_absolute
153        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}