1use spin_common::{ui::quoted_path, url::parse_file_url};
2use spin_compose::ComponentSourceLoaderFs;
3use spin_core::{Component, async_trait, wasmtime};
4use spin_factors::{AppComponent, RuntimeFactors};
5use spin_factors_executor::TriggerDependencyData;
6use wasmtime::error::Context as _;
7
8#[derive(Default)]
9pub struct ComponentLoader {
10 _private: (),
11 #[cfg(feature = "unsafe-aot-compilation")]
12 aot_compilation_enabled: bool,
13}
14
15impl ComponentLoader {
16 pub fn new() -> Self {
18 Self::default()
19 }
20
21 #[cfg(feature = "unsafe-aot-compilation")]
46 pub unsafe fn enable_loading_aot_compiled_components(&mut self) {
47 self.aot_compilation_enabled = true;
48 }
49
50 #[cfg(feature = "unsafe-aot-compilation")]
51 fn load_precompiled_component(
52 &self,
53 engine: &wasmtime::Engine,
54 path: &std::path::Path,
55 ) -> wasmtime::Result<Component> {
56 assert!(self.aot_compilation_enabled);
57 match wasmtime::Engine::detect_precompiled_file(path)? {
58 Some(wasmtime::Precompiled::Component) => unsafe {
59 Component::deserialize_file(engine, path)
60 },
61 Some(wasmtime::Precompiled::Module) => {
62 wasmtime::bail!("expected AOT compiled component but found module");
63 }
64 None => {
65 wasmtime::bail!("expected AOT compiled component but found other data");
66 }
67 }
68 }
69
70 pub(crate) async fn load_composed(
71 &self,
72 component: &AppComponent<'_>,
73 trigger_dependencies_composer: &impl spin_factors_executor::TriggerDependenciesComposer,
74 ) -> anyhow::Result<Vec<u8>> {
75 let loader = ComponentSourceLoaderFs;
76
77 let trigger_deps = &component.locked.trigger_dependencies;
78
79 let trigger_deps = load_trigger_dependencies(&mut trigger_deps.iter(), &loader).await?;
80
81 let apply_trigger_deps = async |c: Vec<u8>| {
82 trigger_dependencies_composer
83 .compose_trigger_dependencies(&trigger_deps, c)
84 .await
85 .map_err(spin_compose::ComposeError::PrepareError)
86 };
87
88 let composed = spin_compose::compose(&loader, component.locked, apply_trigger_deps)
89 .await
90 .with_context(|| {
91 format!(
92 "failed to resolve dependencies for component {:?}",
93 component.locked.id
94 )
95 })?;
96
97 Ok(composed)
98 }
99}
100
101#[async_trait]
102impl<T: RuntimeFactors, U> spin_factors_executor::ComponentLoader<T, U> for ComponentLoader {
103 async fn load_component(
104 &self,
105 engine: &wasmtime::Engine,
106 component: &AppComponent,
107 trigger_dependencies_composer: &impl spin_factors_executor::TriggerDependenciesComposer,
108 ) -> anyhow::Result<Component> {
109 let source = component
110 .source()
111 .content
112 .source
113 .as_ref()
114 .context("LockedComponentSource missing source field")?;
115 let path = parse_file_url(source)?;
116
117 #[cfg(feature = "unsafe-aot-compilation")]
118 if self.aot_compilation_enabled {
119 let component = self
120 .load_precompiled_component(engine, &path)
121 .with_context(|| format!("error deserializing component from {path:?}"))?;
122 return Ok(component);
123 }
124
125 let composed = self
126 .load_composed(component, trigger_dependencies_composer)
127 .await?;
128
129 let component = spin_core::Component::new(engine, composed)
130 .with_context(|| format!("failed to compile component from {}", quoted_path(&path)))?;
131 Ok(component)
132 }
133}
134
135pub(crate) async fn load_trigger_dependencies(
136 trigger_dependencies: &mut impl ExactSizeIterator<
137 Item = (&String, &Vec<spin_app::locked::LockedComponentDependency>),
138 >,
139 loader: &spin_compose::ComponentSourceLoaderFs,
140) -> Result<
141 std::collections::HashMap<String, Vec<spin_factors_executor::TriggerDependency>>,
142 anyhow::Error,
143> {
144 use spin_factors_executor::TriggerDependency;
145 use std::collections::HashMap;
146
147 let mut resolved_trigger_deps = HashMap::with_capacity(trigger_dependencies.len());
148
149 for (role, role_components) in trigger_dependencies {
150 let mut deps_for_role = Vec::with_capacity(role_components.len());
151
152 for locked_dep in role_components {
153 let data = load_trigger_dep_data(loader, &locked_dep.source).await?;
154 deps_for_role.push(TriggerDependency {
155 data,
156 dependency: locked_dep.clone(),
157 });
158 }
159 resolved_trigger_deps.insert(role.clone(), deps_for_role);
160 }
161
162 Ok(resolved_trigger_deps)
163}
164
165async fn load_trigger_dep_data(
166 loader: &ComponentSourceLoaderFs,
167 source: &spin_app::locked::LockedComponentSource,
168) -> anyhow::Result<TriggerDependencyData> {
169 use spin_compose::ComponentSourceLoader;
170
171 if let Some(path) = source
172 .content
173 .source
174 .as_ref()
175 .and_then(|url| parse_file_url(url).ok())
176 {
177 Ok(TriggerDependencyData::OnDisk(path))
178 } else {
179 Ok(TriggerDependencyData::InMemory(
180 loader.load_source(source).await?,
181 ))
182 }
183}