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