1#![deny(missing_docs)]
2
3mod manifest;
6
7use anyhow::{Context, Result, anyhow, bail};
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
19const LAST_BUILD_PROFILE_FILE: &str = "last-build.txt";
20const LAST_BUILD_ANON_VALUE: &str = "<anonymous>";
21
22pub async fn build(
24 manifest_file: &Path,
25 profile: Option<&str>,
26 component_ids: &[String],
27 target_checks: TargetChecking,
28 wit_generation: GenerateDependencyWits,
29 cache_root: Option<PathBuf>,
30) -> Result<()> {
31 let build_info = component_build_configs(manifest_file, profile)
32 .await
33 .with_context(|| {
34 format!(
35 "Cannot read manifest file from {}",
36 quoted_path(manifest_file)
37 )
38 })?;
39 let app_dir = parent_dir(manifest_file)?;
40
41 let components_to_build = components_to_build(component_ids, build_info.components())?;
42
43 if wit_generation.generate() {
44 let wit_gen_errs = regenerate_wits(&components_to_build, &app_dir).await;
45 if !wit_gen_errs.is_empty() {
46 terminal::warn!(
47 "One or more components specified dependencies for which Spin couldn't generate import interfaces."
48 );
49 eprintln!(
50 "If these components rely on Spin-generated interfaces they may fail to build."
51 );
52 eprintln!(
53 "Otherwise, to skip interface generation, use the --skip-generate-wits flag."
54 );
55 eprintln!("Error details:");
56 for (component, err) in wit_gen_errs {
57 terminal::einfo!("{component}:", "{err:#}");
58 }
59 }
60 }
61
62 let build_result = build_components(components_to_build, &app_dir);
63
64 if let Some(e) = build_info.load_error() {
66 terminal::warn!(
69 "The manifest has errors not related to the Wasm component build. Error details:\n{e:#}"
70 );
71 let should_have_checked_targets =
74 target_checks.check() && build_info.has_deployment_targets();
75 if should_have_checked_targets {
76 terminal::warn!(
77 "The manifest error(s) prevented Spin from checking the deployment targets."
78 );
79 }
80 }
81
82 build_result?;
84
85 if let Err(e) = save_last_build_profile(&app_dir, profile) {
86 tracing::warn!("Failed to save build profile: {e:?}");
87 }
88
89 let Some(manifest) = build_info.manifest() else {
90 return Ok(());
93 };
94
95 if target_checks.check() {
96 let application = spin_environments::ApplicationToValidate::new(
97 manifest.clone(),
98 component_ids,
99 profile,
100 manifest_file.parent().unwrap(),
101 )
102 .await
103 .context("unable to load application for checking against deployment targets")?;
104 let target_validation = spin_environments::validate_application_against_environment_ids(
105 &application,
106 build_info.deployment_targets(),
107 cache_root.clone(),
108 &app_dir,
109 )
110 .await
111 .context("unable to check if the application is compatible with deployment targets")?;
112
113 if !target_validation.is_ok() {
114 for error in target_validation.errors() {
115 terminal::error!("{error}");
116 }
117 anyhow::bail!(
118 "All components built successfully, but one or more was incompatible with one or more of the deployment targets."
119 );
120 }
121 }
122
123 Ok(())
124}
125
126pub async fn build_default(
130 manifest_file: &Path,
131 profile: Option<&str>,
132 cache_root: Option<PathBuf>,
133) -> Result<()> {
134 build(
135 manifest_file,
136 profile,
137 &[],
138 TargetChecking::Check,
139 GenerateDependencyWits::Generate,
140 cache_root,
141 )
142 .await
143}
144
145fn components_to_build(
146 component_ids: &[String],
147 components: Vec<ComponentBuildInfo>,
148) -> anyhow::Result<Vec<ComponentBuildInfo>> {
149 let components_to_build = if component_ids.is_empty() {
150 components
151 } else {
152 let all_ids: HashSet<_> = components.iter().map(|c| &c.id).collect();
153 let unknown_component_ids: Vec<_> = component_ids
154 .iter()
155 .filter(|id| !all_ids.contains(id))
156 .map(|s| s.as_str())
157 .collect();
158
159 if !unknown_component_ids.is_empty() {
160 bail!("Unknown component(s) {}", unknown_component_ids.join(", "));
161 }
162
163 components
164 .into_iter()
165 .filter(|c| component_ids.contains(&c.id))
166 .collect()
167 };
168
169 Ok(components_to_build)
170}
171
172#[must_use]
173async fn regenerate_wits(
174 components_to_build: &[ComponentBuildInfo],
175 app_root: &Path,
176) -> Vec<(String, anyhow::Error)> {
177 let mut errors = vec![];
178
179 for component in components_to_build {
180 let component_dir = match component.build.as_ref().and_then(|b| b.workdir.as_ref()) {
181 None => app_root.to_owned(),
182 Some(d) => app_root.join(d),
183 };
184 let dest_file = component_dir.join("spin-dependencies.wit");
185 let extract_result = spin_dependency_wit::extract_wits_into(
186 component.dependencies.inner.iter(),
187 app_root,
188 dest_file,
189 )
190 .await;
191 if let Err(e) = extract_result {
192 errors.push((component.id.clone(), e));
193 }
194 }
195
196 errors
197}
198
199fn build_components(
200 components_to_build: Vec<ComponentBuildInfo>,
201 app_dir: &Path,
202) -> anyhow::Result<()> {
203 if components_to_build.iter().all(|c| c.build.is_none()) {
204 println!("None of the components have a build command.");
205 println!(
206 "For information on specifying a build command, see https://spinframework.dev/build#setting-up-for-spin-build."
207 );
208 return Ok(());
209 }
210
211 let (components_to_build, has_cycle) = sort(components_to_build);
215
216 if has_cycle {
217 tracing::debug!(
218 "There is a dependency cycle among components. Spin cannot guarantee to build dependencies before consumers."
219 );
220 }
221
222 components_to_build
223 .into_iter()
224 .map(|c| build_component(c, app_dir))
225 .collect::<Result<Vec<_>, _>>()?;
226
227 terminal::step!("Finished", "building all Spin components");
228 Ok(())
229}
230
231fn build_component(build_info: ComponentBuildInfo, app_dir: &Path) -> Result<()> {
233 match build_info.build {
234 Some(b) => {
235 let command_count = b.commands().len();
236
237 if command_count > 1 {
238 terminal::step!(
239 "Building",
240 "component {} ({} commands)",
241 build_info.id,
242 command_count
243 );
244 }
245
246 for (index, command) in b.commands().enumerate() {
247 if command_count > 1 {
248 terminal::step!(
249 "Running build step",
250 "{}/{} for component {} with '{}'",
251 index + 1,
252 command_count,
253 build_info.id,
254 command
255 );
256 } else {
257 terminal::step!("Building", "component {} with `{}`", build_info.id, command);
258 }
259
260 let workdir = construct_workdir(app_dir, b.workdir.as_ref())?;
261 if b.workdir.is_some() {
262 println!("Working directory: {}", quoted_path(&workdir));
263 }
264
265 let exit_status = Exec::shell(command)
266 .cwd(workdir)
267 .stdout(Redirection::None)
268 .stderr(Redirection::None)
269 .stdin(Redirection::None)
270 .popen()
271 .map_err(|err| {
272 anyhow!(
273 "Cannot spawn build process '{:?}' for component {}: {}",
274 &b.command,
275 build_info.id,
276 err
277 )
278 })?
279 .wait()?;
280
281 if !exit_status.success() {
282 bail!(
283 "Build command for component {} failed with status {:?}",
284 build_info.id,
285 exit_status,
286 );
287 }
288 }
289
290 Ok(())
291 }
292 _ => Ok(()),
293 }
294}
295
296fn construct_workdir(app_dir: &Path, workdir: Option<impl AsRef<Path>>) -> Result<PathBuf> {
298 let mut cwd = app_dir.to_owned();
299
300 if let Some(workdir) = workdir {
301 if workdir.as_ref().has_root() {
305 bail!("The workdir specified in the application file must be relative.");
306 }
307 cwd.push(workdir);
308 }
309
310 Ok(cwd)
311}
312
313#[derive(Clone)]
314struct SortableBuildInfo {
315 source: Option<String>,
316 local_dependency_paths: Vec<String>,
317 build_info: ComponentBuildInfo,
318}
319
320impl From<&ComponentBuildInfo> for SortableBuildInfo {
321 fn from(value: &ComponentBuildInfo) -> Self {
322 fn local_dep_path(dep: &v2::ComponentDependency) -> Option<String> {
323 match dep {
324 v2::ComponentDependency::Local { path, .. } => Some(path.display().to_string()),
325 _ => None,
326 }
327 }
328
329 let source = match value.source.as_ref() {
330 Some(spin_manifest::schema::v2::ComponentSource::Local(path)) => Some(path.clone()),
331 _ => None,
332 };
333 let local_dependency_paths = value
334 .dependencies
335 .inner
336 .values()
337 .filter_map(local_dep_path)
338 .collect();
339
340 Self {
341 source,
342 local_dependency_paths,
343 build_info: value.clone(),
344 }
345 }
346}
347
348impl std::hash::Hash for SortableBuildInfo {
349 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
350 self.build_info.id.hash(state);
351 self.source.hash(state);
352 self.local_dependency_paths.hash(state);
353 }
354}
355
356impl PartialEq for SortableBuildInfo {
357 fn eq(&self, other: &Self) -> bool {
358 self.build_info.id == other.build_info.id
359 && self.source == other.source
360 && self.local_dependency_paths == other.local_dependency_paths
361 }
362}
363
364impl Eq for SortableBuildInfo {}
365
366fn sort(components: Vec<ComponentBuildInfo>) -> (Vec<ComponentBuildInfo>, bool) {
368 let sortables = components
369 .iter()
370 .map(SortableBuildInfo::from)
371 .collect::<Vec<_>>();
372 let mut sorter = topological_sort::TopologicalSort::<SortableBuildInfo>::new();
373
374 for s in &sortables {
375 sorter.insert(s.clone());
376 }
377
378 for s1 in &sortables {
379 for dep in &s1.local_dependency_paths {
380 for s2 in &sortables {
381 if s2.source.as_ref().is_some_and(|src| src == dep) {
382 sorter.add_link(topological_sort::DependencyLink {
384 prec: s2.clone(),
385 succ: s1.clone(),
386 });
387 }
388 }
389 }
390 }
391
392 let result = sorter.map(|s| s.build_info).collect::<Vec<_>>();
393
394 if result.len() == components.len() {
398 (result, false)
399 } else {
400 (components, true)
401 }
402}
403
404pub fn save_last_build_profile(app_dir: &Path, profile: Option<&str>) -> anyhow::Result<()> {
406 let app_stash_dir = app_dir.join(".spin");
407 let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
408
409 if profile.is_none() && !last_build_profile_file.exists() {
412 return Ok(());
413 }
414
415 std::fs::create_dir_all(&app_stash_dir)?;
416 std::fs::write(
417 &last_build_profile_file,
418 profile.unwrap_or(LAST_BUILD_ANON_VALUE),
419 )?;
420
421 Ok(())
422}
423
424pub fn read_last_build_profile(app_dir: &Path) -> anyhow::Result<Option<String>> {
426 let app_stash_dir = app_dir.join(".spin");
427 let last_build_profile_file = app_stash_dir.join(LAST_BUILD_PROFILE_FILE);
428 if !last_build_profile_file.exists() {
429 return Ok(None);
430 }
431
432 let last_build_str = std::fs::read_to_string(&last_build_profile_file)?;
433
434 if last_build_str == LAST_BUILD_ANON_VALUE {
435 Ok(None)
436 } else {
437 Ok(Some(last_build_str))
438 }
439}
440
441pub fn warn_if_not_latest_build(manifest_path: &Path, profile: Option<&str>) {
444 let Some(app_dir) = manifest_path.parent() else {
445 return;
446 };
447
448 let latest_build = match read_last_build_profile(app_dir) {
449 Ok(profile) => profile,
450 Err(e) => {
451 tracing::warn!(
452 "Failed to read last build profile: using anonymous profile. Error was {e:?}"
453 );
454 None
455 }
456 };
457
458 if profile != latest_build.as_deref() {
459 let profile_opt = match profile {
460 Some(p) => format!(" --profile {p}"),
461 None => "".to_string(),
462 };
463 terminal::warn!(
464 "You built a different profile more recently than the one you are running. If the app appears to be behaving like an older version then run `spin up --build{profile_opt}`."
465 );
466 }
467}
468
469pub enum TargetChecking {
471 Check,
473 Skip,
475}
476
477impl TargetChecking {
478 fn check(&self) -> bool {
480 matches!(self, Self::Check)
481 }
482}
483
484pub enum GenerateDependencyWits {
486 Generate,
488 Skip,
490}
491
492impl GenerateDependencyWits {
493 fn generate(&self) -> bool {
495 matches!(self, Self::Generate)
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 fn test_data_root() -> PathBuf {
504 let crate_dir = env!("CARGO_MANIFEST_DIR");
505 PathBuf::from(crate_dir).join("tests")
506 }
507
508 #[tokio::test]
509 async fn can_load_even_if_trigger_invalid() {
510 let bad_trigger_file = test_data_root().join("bad_trigger.toml");
511 build(
512 &bad_trigger_file,
513 None,
514 &[],
515 TargetChecking::Skip,
516 GenerateDependencyWits::Skip,
517 None,
518 )
519 .await
520 .unwrap();
521 }
522
523 #[tokio::test]
524 async fn succeeds_if_target_env_matches() {
525 let manifest_path = test_data_root().join("good_target_env.toml");
526 build(
527 &manifest_path,
528 None,
529 &[],
530 TargetChecking::Check,
531 GenerateDependencyWits::Skip,
532 None,
533 )
534 .await
535 .unwrap();
536 }
537
538 #[tokio::test]
539 async fn fails_if_target_env_does_not_match() {
540 let manifest_path = test_data_root().join("bad_target_env.toml");
541 let err = build(
542 &manifest_path,
543 None,
544 &[],
545 TargetChecking::Check,
546 GenerateDependencyWits::Skip,
547 None,
548 )
549 .await
550 .expect_err("should have failed")
551 .to_string();
552
553 assert!(
556 err.contains("one or more was incompatible with one or more of the deployment targets")
557 );
558 }
559
560 #[tokio::test]
561 async fn has_meaningful_error_if_target_env_does_not_match() {
562 let manifest_file = test_data_root().join("bad_target_env.toml");
563 let mut manifest = spin_manifest::manifest_from_file(&manifest_file).unwrap();
564 spin_manifest::normalize::normalize_manifest(&mut manifest, None).unwrap();
565 let application = spin_environments::ApplicationToValidate::new(
566 manifest.clone(),
567 &[],
568 None,
569 manifest_file.parent().unwrap(),
570 )
571 .await
572 .context("unable to load application for checking against deployment targets")
573 .unwrap();
574
575 let target_validation = spin_environments::validate_application_against_environment_ids(
576 &application,
577 spin_environments::Targets {
578 default: &manifest.application.targets,
579 overrides: std::collections::HashMap::new(),
580 },
581 None,
582 manifest_file.parent().unwrap(),
583 )
584 .await
585 .context("unable to check if the application is compatible with deployment targets")
586 .unwrap();
587
588 assert_eq!(1, target_validation.errors().len());
589
590 let err = target_validation.errors()[0].to_string();
591
592 assert!(err.contains("can't run in environment wasi-minimal"));
593 assert!(err.contains("world wasi:cli/command@0.2.0"));
594 assert!(
595 err.contains("requires the following imports, which the environment does not provide")
596 );
597 assert!(err.contains("wasi:cli/stdout"));
598 }
599
600 fn dummy_buildinfo(id: &str) -> ComponentBuildInfo {
601 dummy_build_info_deps(id, &[])
602 }
603
604 fn dummy_build_info_dep(id: &str, dep_on: &str) -> ComponentBuildInfo {
605 dummy_build_info_deps(id, &[dep_on])
606 }
607
608 fn dummy_build_info_deps(id: &str, dep_on: &[&str]) -> ComponentBuildInfo {
609 ComponentBuildInfo {
610 id: id.into(),
611 source: Some(v2::ComponentSource::Local(format!("{id}.wasm"))),
612 build: None,
613 dependencies: depends_on(dep_on),
614 targets: None,
615 }
616 }
617
618 fn depends_on(paths: &[&str]) -> v2::ComponentDependencies {
619 let mut deps = vec![];
620 for (index, path) in paths.iter().enumerate() {
621 let dep_name =
622 spin_serde::DependencyName::Plain(format!("dummy{index}").try_into().unwrap());
623 let dep = v2::ComponentDependency::Local {
624 path: path.into(),
625 export: None,
626 inherit_configuration: None,
627 };
628 deps.push((dep_name, dep));
629 }
630 v2::ComponentDependencies {
631 inner: deps.into_iter().collect(),
632 }
633 }
634
635 fn assert_before(cs: &[ComponentBuildInfo], before: &str, after: &str) {
637 assert!(
638 cs.iter().position(|c| c.id == before).unwrap()
639 < cs.iter().position(|c| c.id == after).unwrap()
640 );
641 }
642
643 #[test]
644 fn if_no_dependencies_then_all_build() {
645 let (cs, had_cycle) = sort(vec![dummy_buildinfo("1"), dummy_buildinfo("2")]);
646 assert_eq!(2, cs.len());
647 assert!(cs.iter().any(|c| c.id == "1"));
648 assert!(cs.iter().any(|c| c.id == "2"));
649 assert!(!had_cycle);
650 }
651
652 #[test]
653 fn dependencies_build_before_consumers() {
654 let (cs, had_cycle) = sort(vec![
655 dummy_buildinfo("1"),
656 dummy_build_info_dep("2", "3.wasm"),
657 dummy_buildinfo("3"),
658 dummy_build_info_dep("4", "1.wasm"),
659 ]);
660 assert_eq!(4, cs.len());
661 assert_before(&cs, "1", "4");
662 assert_before(&cs, "3", "2");
663 assert!(!had_cycle);
664 }
665
666 #[test]
667 fn multiple_dependencies_build_before_consumers() {
668 let (cs, had_cycle) = sort(vec![
669 dummy_buildinfo("1"),
670 dummy_build_info_dep("2", "3.wasm"),
671 dummy_buildinfo("3"),
672 dummy_build_info_dep("4", "1.wasm"),
673 dummy_build_info_dep("5", "3.wasm"),
674 dummy_build_info_deps("6", &["3.wasm", "2.wasm"]),
675 dummy_buildinfo("7"),
676 ]);
677 assert_eq!(7, cs.len());
678 assert_before(&cs, "1", "4");
679 assert_before(&cs, "3", "2");
680 assert_before(&cs, "3", "5");
681 assert_before(&cs, "3", "6");
682 assert_before(&cs, "2", "6");
683 assert!(!had_cycle);
684 }
685
686 #[test]
687 fn circular_dependencies_dont_prevent_build() {
688 let (cs, had_cycle) = sort(vec![
689 dummy_buildinfo("1"),
690 dummy_build_info_dep("2", "3.wasm"),
691 dummy_build_info_dep("3", "2.wasm"),
692 dummy_build_info_dep("4", "1.wasm"),
693 ]);
694 assert_eq!(4, cs.len());
695 assert!(cs.iter().any(|c| c.id == "1"));
696 assert!(cs.iter().any(|c| c.id == "2"));
697 assert!(cs.iter().any(|c| c.id == "3"));
698 assert!(cs.iter().any(|c| c.id == "4"));
699 assert!(had_cycle);
700 }
701
702 #[test]
703 fn non_path_dependencies_do_not_prevent_sorting() {
704 let mut depends_on_remote = dummy_buildinfo("2");
705 depends_on_remote.dependencies.inner.insert(
706 spin_serde::DependencyName::Plain("remote".to_owned().try_into().unwrap()),
707 v2::ComponentDependency::Version("1.2.3".to_owned()),
708 );
709
710 let mut depends_on_local_and_remote = dummy_build_info_dep("4", "1.wasm");
711 depends_on_local_and_remote.dependencies.inner.insert(
712 spin_serde::DependencyName::Plain("remote".to_owned().try_into().unwrap()),
713 v2::ComponentDependency::Version("1.2.3".to_owned()),
714 );
715
716 let (cs, _) = sort(vec![
717 dummy_buildinfo("1"),
718 depends_on_remote,
719 dummy_buildinfo("3"),
720 depends_on_local_and_remote,
721 ]);
722
723 assert_eq!(4, cs.len());
724 assert_before(&cs, "1", "4");
725 }
726
727 #[test]
728 fn non_path_sources_do_not_prevent_sorting() {
729 let mut remote_source = dummy_build_info_dep("2", "3.wasm");
730 remote_source.source = Some(v2::ComponentSource::Remote {
731 url: "far://away".into(),
732 digest: "loadsa-hex".into(),
733 });
734
735 let (cs, _) = sort(vec![
736 dummy_buildinfo("1"),
737 remote_source,
738 dummy_buildinfo("3"),
739 dummy_build_info_dep("4", "1.wasm"),
740 ]);
741
742 assert_eq!(4, cs.len());
743 assert_before(&cs, "1", "4");
744 }
745
746 #[test]
747 fn dependencies_on_non_manifest_components_do_not_prevent_sorting() {
748 let (cs, had_cycle) = sort(vec![
749 dummy_buildinfo("1"),
750 dummy_build_info_deps("2", &["3.wasm", "crikey.wasm"]),
751 dummy_buildinfo("3"),
752 dummy_build_info_dep("4", "1.wasm"),
753 ]);
754 assert_eq!(4, cs.len());
755 assert_before(&cs, "1", "4");
756 assert_before(&cs, "3", "2");
757 assert!(!had_cycle);
758 }
759}