Skip to main content

spin_trigger/
loader.rs

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    /// Create a new `ComponentLoader`
17    pub fn new() -> Self {
18        Self::default()
19    }
20
21    /// Updates the TriggerLoader to load AOT precompiled components
22    ///
23    /// **Warning: This feature may bypass important security guarantees of the
24    /// Wasmtime security sandbox if used incorrectly! Read this documentation
25    /// carefully.**
26    ///
27    /// Usually, components are compiled just-in-time from portable Wasm
28    /// sources. This method causes components to instead be loaded
29    /// ahead-of-time as Wasmtime-precompiled native executable binaries.
30    /// Precompiled binaries must be produced with a compatible Wasmtime engine
31    /// using the same Wasmtime version and compiler target settings - typically
32    /// by a host with the same processor that will be executing them. See the
33    /// Wasmtime documentation for more information:
34    /// https://docs.rs/wasmtime/latest/wasmtime/struct.Module.html#method.deserialize
35    ///
36    /// # Safety
37    ///
38    /// This method is marked as `unsafe` because it enables potentially unsafe
39    /// behavior if used to load malformed or malicious precompiled binaries.
40    /// Loading sources from an incompatible Wasmtime engine will fail but is
41    /// otherwise safe. This method is safe if it can be guaranteed that
42    /// `<TriggerLoader as Loader>::load_component` will only ever be called
43    /// with a trusted `LockedComponentSource`. **Precompiled binaries must
44    /// never be loaded from untrusted sources.**
45    #[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}