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 spin_manifest::schema::v2;
11use std::{
12 collections::HashSet,
13 path::{Path, PathBuf},
14};
15use subprocess::{Exec, Redirection};
16
17use crate::manifest::component_build_configs;
18
19pub async fn build(
21 manifest_file: &Path,
22 component_ids: &[String],
23 target_checks: TargetChecking,
24 cache_root: Option<PathBuf>,
25) -> Result<()> {
26 let build_info = component_build_configs(manifest_file)
27 .await
28 .with_context(|| {
29 format!(
30 "Cannot read manifest file from {}",
31 quoted_path(manifest_file)
32 )
33 })?;
34 let app_dir = parent_dir(manifest_file)?;
35
36 let build_result = build_components(component_ids, build_info.components(), &app_dir);
37
38 if let Some(e) = build_info.load_error() {
40 terminal::warn!("The manifest has errors not related to the Wasm component build. Error details:\n{e:#}");
43 let should_have_checked_targets =
46 target_checks.check() && build_info.has_deployment_targets();
47 if should_have_checked_targets {
48 terminal::warn!(
49 "The manifest error(s) prevented Spin from checking the deployment targets."
50 );
51 }
52 }
53
54 build_result?;
56
57 let Some(manifest) = build_info.manifest() else {
58 return Ok(());
61 };
62
63 if target_checks.check() {
64 let application = spin_environments::ApplicationToValidate::new(
65 manifest.clone(),
66 manifest_file.parent().unwrap(),
67 )
68 .await
69 .context("unable to load application for checking against deployment targets")?;
70 let target_validation = spin_environments::validate_application_against_environment_ids(
71 &application,
72 build_info.deployment_targets(),
73 cache_root.clone(),
74 &app_dir,
75 )
76 .await
77 .context("unable to check if the application is compatible with deployment targets")?;
78
79 if !target_validation.is_ok() {
80 for error in target_validation.errors() {
81 terminal::error!("{error}");
82 }
83 anyhow::bail!("All components built successfully, but one or more was incompatible with one or more of the deployment targets.");
84 }
85 }
86
87 Ok(())
88}
89
90pub async fn build_default(manifest_file: &Path, cache_root: Option<PathBuf>) -> Result<()> {
94 build(manifest_file, &[], TargetChecking::Check, cache_root).await
95}
96
97fn build_components(
98 component_ids: &[String],
99 components: Vec<ComponentBuildInfo>,
100 app_dir: &Path,
101) -> Result<(), anyhow::Error> {
102 let components_to_build = if component_ids.is_empty() {
103 components
104 } else {
105 let all_ids: HashSet<_> = components.iter().map(|c| &c.id).collect();
106 let unknown_component_ids: Vec<_> = component_ids
107 .iter()
108 .filter(|id| !all_ids.contains(id))
109 .map(|s| s.as_str())
110 .collect();
111
112 if !unknown_component_ids.is_empty() {
113 bail!("Unknown component(s) {}", unknown_component_ids.join(", "));
114 }
115
116 components
117 .into_iter()
118 .filter(|c| component_ids.contains(&c.id))
119 .collect()
120 };
121
122 if components_to_build.iter().all(|c| c.build.is_none()) {
123 println!("None of the components have a build command.");
124 println!("For information on specifying a build command, see https://spinframework.dev/build#setting-up-for-spin-build.");
125 return Ok(());
126 }
127
128 let (components_to_build, has_cycle) = sort(components_to_build);
132
133 if has_cycle {
134 tracing::debug!("There is a dependency cycle among components. Spin cannot guarantee to build dependencies before consumers.");
135 }
136
137 components_to_build
138 .into_iter()
139 .map(|c| build_component(c, app_dir))
140 .collect::<Result<Vec<_>, _>>()?;
141
142 terminal::step!("Finished", "building all Spin components");
143 Ok(())
144}
145
146fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()> {
148 match build_info.build {
149 Some(b) => {
150 let command_count = b.commands().len();
151
152 if command_count > 1 {
153 terminal::step!(
154 "Building",
155 "component {} ({} commands)",
156 build_info.id,
157 command_count
158 );
159 }
160
161 for (index, command) in b.commands().enumerate() {
162 if command_count > 1 {
163 terminal::step!(
164 "Running build step",
165 "{}/{} for component {} with '{}'",
166 index + 1,
167 command_count,
168 build_info.id,
169 command
170 );
171 } else {
172 terminal::step!("Building", "component {} with `{}`", build_info.id, command);
173 }
174
175 let workdir = construct_workdir(app_dir, b.workdir.as_ref())?;
176 if b.workdir.is_some() {
177 println!("Working directory: {}", quoted_path(&workdir));
178 }
179
180 let exit_status = Exec::shell(command)
181 .cwd(workdir)
182 .stdout(Redirection::None)
183 .stderr(Redirection::None)
184 .stdin(Redirection::None)
185 .popen()
186 .map_err(|err| {
187 anyhow!(
188 "Cannot spawn build process '{:?}' for component {}: {}",
189 &b.command,
190 build_info.id,
191 err
192 )
193 })?
194 .wait()?;
195
196 if !exit_status.success() {
197 bail!(
198 "Build command for component {} failed with status {:?}",
199 build_info.id,
200 exit_status,
201 );
202 }
203 }
204
205 Ok(())
206 }
207 _ => Ok(()),
208 }
209}
210
211fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Result<PathBuf> {
213 let mut cwd = app_dir.to_owned();
214
215 if let Some(workdir) = workdir {
216 if workdir.as_ref().has_root() {
220 bail!("The workdir specified in the application file must be relative.");
221 }
222 cwd.push(workdir);
223 }
224
225 Ok(cwd)
226}
227
228#[derive(Clone)]
229struct SortableBuildInfo {
230 source: Option<String>,
231 local_dependency_paths: Vec<String>,
232 build_info: ComponentBuildInfo,
233}
234
235impl From<&ComponentBuildInfo> for SortableBuildInfo {
236 fn from(value: &ComponentBuildInfo) -> Self {
237 fn local_dep_path(dep: &v2::ComponentDependency) -> Option<String> {
238 match dep {
239 v2::ComponentDependency::Local { path, .. } => Some(path.display().to_string()),
240 _ => None,
241 }
242 }
243
244 let source = match value.source.as_ref() {
245 Some(spin_manifest::schema::v2::ComponentSource::Local(path)) => Some(path.clone()),
246 _ => None,
247 };
248 let local_dependency_paths = value
249 .dependencies
250 .inner
251 .values()
252 .filter_map(local_dep_path)
253 .collect();
254
255 Self {
256 source,
257 local_dependency_paths,
258 build_info: value.clone(),
259 }
260 }
261}
262
263impl std::hash::Hash for SortableBuildInfo {
264 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
265 self.build_info.id.hash(state);
266 self.source.hash(state);
267 self.local_dependency_paths.hash(state);
268 }
269}
270
271impl PartialEq for SortableBuildInfo {
272 fn eq(&self, other: &Self) -> bool {
273 self.build_info.id == other.build_info.id
274 && self.source == other.source
275 && self.local_dependency_paths == other.local_dependency_paths
276 }
277}
278
279impl Eq for SortableBuildInfo {}
280
281fn sort(components: Vec<ComponentBuildInfo>) -> (Vec<ComponentBuildInfo>, bool) {
283 let sortables = components
284 .iter()
285 .map(SortableBuildInfo::from)
286 .collect::<Vec<_>>();
287 let mut sorter = topological_sort::TopologicalSort::<SortableBuildInfo>::new();
288
289 for s in &sortables {
290 sorter.insert(s.clone());
291 }
292
293 for s1 in &sortables {
294 for dep in &s1.local_dependency_paths {
295 for s2 in &sortables {
296 if s2.source.as_ref().is_some_and(|src| src == dep) {
297 sorter.add_link(topological_sort::DependencyLink {
299 prec: s2.clone(),
300 succ: s1.clone(),
301 });
302 }
303 }
304 }
305 }
306
307 let result = sorter.map(|s| s.build_info).collect::<Vec<_>>();
308
309 if result.len() == components.len() {
313 (result, false)
314 } else {
315 (components, true)
316 }
317}
318
319pub enum TargetChecking {
321 Check,
323 Skip,
325}
326
327impl TargetChecking {
328 fn check(&self) -> bool {
330 matches!(self, Self::Check)
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 fn test_data_root() -> PathBuf {
339 let crate_dir = env!("CARGO_MANIFEST_DIR");
340 PathBuf::from(crate_dir).join("tests")
341 }
342
343 #[tokio::test]
344 async fn can_load_even_if_trigger_invalid() {
345 let bad_trigger_file = test_data_root().join("bad_trigger.toml");
346 build(&bad_trigger_file, &[], TargetChecking::Skip, None)
347 .await
348 .unwrap();
349 }
350
351 #[tokio::test]
352 async fn succeeds_if_target_env_matches() {
353 let manifest_path = test_data_root().join("good_target_env.toml");
354 build(&manifest_path, &[], TargetChecking::Check, None)
355 .await
356 .unwrap();
357 }
358
359 #[tokio::test]
360 async fn fails_if_target_env_does_not_match() {
361 let manifest_path = test_data_root().join("bad_target_env.toml");
362 let err = build(&manifest_path, &[], TargetChecking::Check, None)
363 .await
364 .expect_err("should have failed")
365 .to_string();
366
367 assert!(
370 err.contains("one or more was incompatible with one or more of the deployment targets")
371 );
372 }
373
374 #[tokio::test]
375 async fn has_meaningful_error_if_target_env_does_not_match() {
376 let manifest_file = test_data_root().join("bad_target_env.toml");
377 let manifest = spin_manifest::manifest_from_file(&manifest_file).unwrap();
378 let application = spin_environments::ApplicationToValidate::new(
379 manifest.clone(),
380 manifest_file.parent().unwrap(),
381 )
382 .await
383 .context("unable to load application for checking against deployment targets")
384 .unwrap();
385
386 let target_validation = spin_environments::validate_application_against_environment_ids(
387 &application,
388 &manifest.application.targets,
389 None,
390 manifest_file.parent().unwrap(),
391 )
392 .await
393 .context("unable to check if the application is compatible with deployment targets")
394 .unwrap();
395
396 assert_eq!(1, target_validation.errors().len());
397
398 let err = target_validation.errors()[0].to_string();
399
400 assert!(err.contains("can't run in environment wasi-minimal"));
401 assert!(err.contains("world wasi:cli/command@0.2.0"));
402 assert!(err.contains("requires imports named"));
403 assert!(err.contains("wasi:cli/stdout"));
404 }
405
406 fn dummy_buildinfo(id: &str) -> ComponentBuildInfo {
407 dummy_build_info_deps(id, &[])
408 }
409
410 fn dummy_build_info_dep(id: &str, dep_on: &str) -> ComponentBuildInfo {
411 dummy_build_info_deps(id, &[dep_on])
412 }
413
414 fn dummy_build_info_deps(id: &str, dep_on: &[&str]) -> ComponentBuildInfo {
415 ComponentBuildInfo {
416 id: id.into(),
417 source: Some(v2::ComponentSource::Local(format!("{id}.wasm"))),
418 build: None,
419 dependencies: depends_on(dep_on),
420 }
421 }
422
423 fn depends_on(paths: &[&str]) -> v2::ComponentDependencies {
424 let mut deps = vec![];
425 for (index, path) in paths.iter().enumerate() {
426 let dep_name =
427 spin_serde::DependencyName::Plain(format!("dummy{index}").try_into().unwrap());
428 let dep = v2::ComponentDependency::Local {
429 path: path.into(),
430 export: None,
431 };
432 deps.push((dep_name, dep));
433 }
434 v2::ComponentDependencies {
435 inner: deps.into_iter().collect(),
436 }
437 }
438
439 fn assert_before(cs: &[ComponentBuildInfo], before: &str, after: &str) {
441 assert!(
442 cs.iter().position(|c| c.id == before).unwrap()
443 < cs.iter().position(|c| c.id == after).unwrap()
444 );
445 }
446
447 #[test]
448 fn if_no_dependencies_then_all_build() {
449 let (cs, had_cycle) = sort(vec![dummy_buildinfo("1"), dummy_buildinfo("2")]);
450 assert_eq!(2, cs.len());
451 assert!(cs.iter().any(|c| c.id == "1"));
452 assert!(cs.iter().any(|c| c.id == "2"));
453 assert!(!had_cycle);
454 }
455
456 #[test]
457 fn dependencies_build_before_consumers() {
458 let (cs, had_cycle) = sort(vec![
459 dummy_buildinfo("1"),
460 dummy_build_info_dep("2", "3.wasm"),
461 dummy_buildinfo("3"),
462 dummy_build_info_dep("4", "1.wasm"),
463 ]);
464 assert_eq!(4, cs.len());
465 assert_before(&cs, "1", "4");
466 assert_before(&cs, "3", "2");
467 assert!(!had_cycle);
468 }
469
470 #[test]
471 fn multiple_dependencies_build_before_consumers() {
472 let (cs, had_cycle) = sort(vec![
473 dummy_buildinfo("1"),
474 dummy_build_info_dep("2", "3.wasm"),
475 dummy_buildinfo("3"),
476 dummy_build_info_dep("4", "1.wasm"),
477 dummy_build_info_dep("5", "3.wasm"),
478 dummy_build_info_deps("6", &["3.wasm", "2.wasm"]),
479 dummy_buildinfo("7"),
480 ]);
481 assert_eq!(7, cs.len());
482 assert_before(&cs, "1", "4");
483 assert_before(&cs, "3", "2");
484 assert_before(&cs, "3", "5");
485 assert_before(&cs, "3", "6");
486 assert_before(&cs, "2", "6");
487 assert!(!had_cycle);
488 }
489
490 #[test]
491 fn circular_dependencies_dont_prevent_build() {
492 let (cs, had_cycle) = sort(vec![
493 dummy_buildinfo("1"),
494 dummy_build_info_dep("2", "3.wasm"),
495 dummy_build_info_dep("3", "2.wasm"),
496 dummy_build_info_dep("4", "1.wasm"),
497 ]);
498 assert_eq!(4, cs.len());
499 assert!(cs.iter().any(|c| c.id == "1"));
500 assert!(cs.iter().any(|c| c.id == "2"));
501 assert!(cs.iter().any(|c| c.id == "3"));
502 assert!(cs.iter().any(|c| c.id == "4"));
503 assert!(had_cycle);
504 }
505
506 #[test]
507 fn non_path_dependencies_do_not_prevent_sorting() {
508 let mut depends_on_remote = dummy_buildinfo("2");
509 depends_on_remote.dependencies.inner.insert(
510 spin_serde::DependencyName::Plain("remote".to_owned().try_into().unwrap()),
511 v2::ComponentDependency::Version("1.2.3".to_owned()),
512 );
513
514 let mut depends_on_local_and_remote = dummy_build_info_dep("4", "1.wasm");
515 depends_on_local_and_remote.dependencies.inner.insert(
516 spin_serde::DependencyName::Plain("remote".to_owned().try_into().unwrap()),
517 v2::ComponentDependency::Version("1.2.3".to_owned()),
518 );
519
520 let (cs, _) = sort(vec![
521 dummy_buildinfo("1"),
522 depends_on_remote,
523 dummy_buildinfo("3"),
524 depends_on_local_and_remote,
525 ]);
526
527 assert_eq!(4, cs.len());
528 assert_before(&cs, "1", "4");
529 }
530
531 #[test]
532 fn non_path_sources_do_not_prevent_sorting() {
533 let mut remote_source = dummy_build_info_dep("2", "3.wasm");
534 remote_source.source = Some(v2::ComponentSource::Remote {
535 url: "far://away".into(),
536 digest: "loadsa-hex".into(),
537 });
538
539 let (cs, _) = sort(vec![
540 dummy_buildinfo("1"),
541 remote_source,
542 dummy_buildinfo("3"),
543 dummy_build_info_dep("4", "1.wasm"),
544 ]);
545
546 assert_eq!(4, cs.len());
547 assert_before(&cs, "1", "4");
548 }
549
550 #[test]
551 fn dependencies_on_non_manifest_components_do_not_prevent_sorting() {
552 let (cs, had_cycle) = sort(vec![
553 dummy_buildinfo("1"),
554 dummy_build_info_deps("2", &["3.wasm", "crikey.wasm"]),
555 dummy_buildinfo("3"),
556 dummy_build_info_dep("4", "1.wasm"),
557 ]);
558 assert_eq!(4, cs.len());
559 assert_before(&cs, "1", "4");
560 assert_before(&cs, "3", "2");
561 assert!(!had_cycle);
562 }
563}