Skip to main content

capsule_runtime/
precompile.rs

1//! Compiling WASM artifacts ahead of their first launch.
2//!
3//! `mur install` and `mur precompile` hand each payload to [`Precompiler::precompile`], which
4//! stores its compiled form through the same [`CompiledForms`] handle and the same
5//! [`stage_root_component`] that `stage_session` uses, on an engine from the same
6//! [`build_engine`]. A form written here therefore has the name, mode, sidecar and stamp a launch
7//! would have given it, and launch cannot tell the two apart. This module reads and writes no file
8//! itself.
9//!
10//! The key is the sha256 of the payload bytes in hand, recomputed here, so a form is only ever
11//! compiled from the root wasm of the payload it is named after, as the key contract in
12//! [`compiled_forms`](crate::compiled_forms) requires.
13//!
14//! [`build_engine`]: crate::runtime::build_engine
15//! [`stage_root_component`]: crate::runtime::stage_root_component
16
17use std::path::Path;
18
19use murmur_artifact::RuntimeType;
20use wasmtime::Engine;
21
22use crate::compiled_forms::CompiledForms;
23
24/// An engine and compiled-forms handle for storing the forms of installed artifacts.
25pub struct Precompiler {
26    engine: Engine,
27    forms: CompiledForms,
28}
29
30/// What [`Precompiler::precompile`] did with one payload. Serialised in `snake_case`, the outcome
31/// `mur precompile --json` reports.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum Precompiled {
35    /// The payload's root wasm was compiled and its form stored, or storing it was attempted:
36    /// a form that could not be written is compiled again on first launch.
37    Compiled,
38    /// A form for the payload under the current engine key was already stored and loads.
39    AlreadyStored,
40    /// The payload is not a WASM artifact staged under its own sha256: a native tool, a skill or
41    /// a capsule. No file was touched.
42    NotWasm,
43    /// The payload's manifest does not parse, or its root wasm is missing or does not compile.
44    /// Nothing was stored; a launch staging it fails or compiles exactly as it would have.
45    Failed,
46}
47
48impl Precompiler {
49    /// A precompiler whose forms are withheld, as a launch withholds them, when the murmur home or
50    /// `~/.murmur/compiled` is reachable from `capsule_writable`, the directory the consuming
51    /// capsule's writable root is created in. `None` for a writer that stages no capsule, such as
52    /// an install into the global store. `None` when the engine cannot be built.
53    pub fn new(capsule_writable: Option<&Path>) -> Option<Self> {
54        let engine = crate::runtime::build_engine().ok()?;
55        let forms = match capsule_writable {
56            Some(dir) => CompiledForms::new(&engine, dir),
57            None => CompiledForms::without_capsule(&engine),
58        };
59        Some(Self { engine, forms })
60    }
61
62    /// Stores the compiled form of the artifact `name@version` whose payload is `payload`, unless a
63    /// usable one is already stored. Never fails, panics or prints: whatever the outcome, a launch
64    /// that stages `payload` behaves as it would have without this call, only faster when it
65    /// returns [`Precompiled::Compiled`] or [`Precompiled::AlreadyStored`].
66    ///
67    /// Eligible payloads are those whose packed manifest resolves to wasm execution in any role but
68    /// `capsule`: a capsule's own component is staged under the sha256 of the component, not of
69    /// its payload. The role itself is left open because staging takes it from the consuming
70    /// manifest's entry, not from the packed `runtime:`.
71    pub fn precompile(&self, name: &str, version: &str, payload: &[u8]) -> Precompiled {
72        let key = murmur_artifact::sha256_hex(payload);
73        let Ok(manifest) = murmur_artifact::load_manifest_from_artifact_bytes(payload) else {
74            return Precompiled::Failed;
75        };
76        if manifest.registry_runtime() != RuntimeType::Wasm || manifest.runtime == "capsule" {
77            return Precompiled::NotWasm;
78        }
79        if self.forms.load(&self.engine, &key).is_some() {
80            return Precompiled::AlreadyStored;
81        }
82        match crate::runtime::stage_root_component(
83            &self.engine,
84            &self.forms,
85            name,
86            version,
87            payload,
88            &key,
89        ) {
90            Ok(_) => Precompiled::Compiled,
91            Err(_) => Precompiled::Failed,
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    use crate::artifact::test_support::archive_with_files as payload;
100    use std::path::PathBuf;
101
102    fn fixture(relative: &str) -> Vec<u8> {
103        let path = Path::new(env!("CARGO_MANIFEST_DIR"))
104            .join("../murmur-cli/tests/fixtures")
105            .join(relative);
106        std::fs::read(&path).unwrap_or_else(|err| panic!("{}: {err}", path.display()))
107    }
108
109    fn tool() -> Vec<u8> {
110        payload(&[
111            (
112                "murmur.yaml",
113                b"name: echo-tool\nversion: 0.1.0\nruntime: tool\n",
114            ),
115            ("tool.wasm", &fixture("run/components/echo-tool.wasm")),
116        ])
117    }
118
119    fn driver() -> Vec<u8> {
120        payload(&[
121            (
122                "murmur.yaml",
123                b"name: demo-driver\nversion: 0.1.0\nruntime: driver\n",
124            ),
125            (
126                "tool.wasm",
127                &fixture("drivers/anthropic/driver/murmur-driver-anthropic.wasm"),
128            ),
129        ])
130    }
131
132    fn hook() -> Vec<u8> {
133        payload(&[
134            (
135                "murmur.yaml",
136                b"name: demo-hook\nversion: 0.1.0\nruntime: hook\n",
137            ),
138            (
139                "hook.wasm",
140                &fixture("gateway-probe-hook/hook/gateway-probe-hook.wasm"),
141            ),
142        ])
143    }
144
145    fn compiled_dir() -> PathBuf {
146        crate::state_store::murmur_home_dir()
147            .unwrap()
148            .join("compiled")
149    }
150
151    /// The forms stored under `payload`'s sha256, under any engine key.
152    fn forms_of(payload: &[u8]) -> Vec<PathBuf> {
153        let prefix = format!("{}-", murmur_artifact::sha256_hex(payload));
154        std::fs::read_dir(compiled_dir())
155            .into_iter()
156            .flatten()
157            .filter_map(Result::ok)
158            .map(|entry| entry.path())
159            .filter(|path| {
160                path.extension().is_some_and(|ext| ext == "cwasm")
161                    && path
162                        .file_name()
163                        .and_then(|name| name.to_str())
164                        .is_some_and(|name| name.starts_with(&prefix))
165            })
166            .collect()
167    }
168
169    fn inode_and_mtime(path: &Path) -> (u64, std::time::SystemTime) {
170        use std::os::unix::fs::MetadataExt;
171        let metadata = std::fs::symlink_metadata(path).unwrap();
172        (metadata.ino(), metadata.modified().unwrap())
173    }
174
175    #[test]
176    fn a_precompiler_is_shareable_across_threads() {
177        fn shareable<T: Send + Sync>() {}
178        shareable::<Precompiler>();
179    }
180
181    #[test]
182    fn a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored() {
183        let home = tempfile::tempdir().unwrap();
184        crate::murmur_home::run_with_home(
185            "precompile::tests::inner_a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored",
186            home.path(),
187        );
188    }
189
190    #[test]
191    #[ignore = "run by a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored"]
192    fn inner_a_wasm_tool_driver_and_hook_are_each_compiled_then_already_stored() {
193        if !crate::murmur_home::in_scratch_home() {
194            return;
195        }
196        let workdir = tempfile::tempdir().unwrap();
197        let precompiler = Precompiler::new(Some(workdir.path())).unwrap();
198
199        for (name, payload) in [
200            ("echo-tool", tool()),
201            ("demo-driver", driver()),
202            ("demo-hook", hook()),
203        ] {
204            assert_eq!(
205                precompiler.precompile(name, "0.1.0", &payload),
206                Precompiled::Compiled,
207                "{name}"
208            );
209            let forms = forms_of(&payload);
210            let [form] = &forms[..] else {
211                panic!("{name}: expected one form, found {forms:?}");
212            };
213            let before = inode_and_mtime(form);
214            assert_eq!(
215                precompiler.precompile(name, "0.1.0", &payload),
216                Precompiled::AlreadyStored,
217                "{name}"
218            );
219            assert_eq!(inode_and_mtime(form), before, "{name}: form was rewritten");
220        }
221    }
222
223    #[test]
224    fn a_native_tool_and_a_skill_are_not_compiled() {
225        let home = tempfile::tempdir().unwrap();
226        crate::murmur_home::run_with_home(
227            "precompile::tests::inner_a_native_tool_and_a_skill_are_not_compiled",
228            home.path(),
229        );
230    }
231
232    #[test]
233    #[ignore = "run by a_native_tool_and_a_skill_are_not_compiled"]
234    fn inner_a_native_tool_and_a_skill_are_not_compiled() {
235        if !crate::murmur_home::in_scratch_home() {
236            return;
237        }
238        let precompiler = Precompiler::new(None).unwrap();
239        let native = payload(&[
240            (
241                "murmur.yaml",
242                b"name: native-tool\nversion: 0.1.0\nruntime: tool\nimplementation: native\n",
243            ),
244            ("native-tool", b"#!/bin/sh\necho hi\n"),
245        ]);
246        let skill = payload(&[
247            (
248                "murmur.yaml",
249                b"name: demo-skill\nversion: 0.1.0\nruntime: skill\n",
250            ),
251            ("skill.md", b"# Demo\n"),
252        ]);
253        let capsule = payload(&[
254            (
255                "murmur.yaml",
256                b"name: demo-capsule\nversion: 0.1.0\nruntime: capsule\n",
257            ),
258            ("capsule.wasm", &fixture("run/components/echo-tool.wasm")),
259        ]);
260
261        for (name, payload) in [
262            ("native-tool", native),
263            ("demo-skill", skill),
264            ("demo-capsule", capsule),
265        ] {
266            assert_eq!(
267                precompiler.precompile(name, "0.1.0", &payload),
268                Precompiled::NotWasm,
269                "{name}"
270            );
271        }
272        assert!(
273            !compiled_dir().exists(),
274            "{} was created",
275            compiled_dir().display()
276        );
277    }
278
279    #[test]
280    fn a_form_install_wrote_is_the_form_staging_loads() {
281        let home = tempfile::tempdir().unwrap();
282        crate::murmur_home::run_with_home(
283            "precompile::tests::inner_a_form_install_wrote_is_the_form_staging_loads",
284            home.path(),
285        );
286    }
287
288    #[test]
289    #[ignore = "run by a_form_install_wrote_is_the_form_staging_loads"]
290    fn inner_a_form_install_wrote_is_the_form_staging_loads() {
291        if !crate::murmur_home::in_scratch_home() {
292            return;
293        }
294        let payload = tool();
295        let key = murmur_artifact::sha256_hex(&payload);
296        assert_eq!(
297            Precompiler::new(None)
298                .unwrap()
299                .precompile("echo-tool", "0.1.0", &payload),
300            Precompiled::Compiled
301        );
302
303        let engine = crate::runtime::build_engine().unwrap();
304        let workdir = tempfile::tempdir().unwrap();
305        let staging = CompiledForms::new(&engine, workdir.path());
306        assert!(staging.load(&engine, &key).is_some());
307    }
308
309    #[test]
310    fn a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing() {
311        let home = tempfile::tempdir().unwrap();
312        crate::murmur_home::run_with_home(
313            "precompile::tests::inner_a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing",
314            home.path(),
315        );
316    }
317
318    #[test]
319    #[ignore = "run by a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing"]
320    fn inner_a_payload_whose_root_wasm_does_not_compile_is_failed_and_stores_nothing() {
321        if !crate::murmur_home::in_scratch_home() {
322            return;
323        }
324        let precompiler = Precompiler::new(None).unwrap();
325        let not_wasm = payload(&[
326            (
327                "murmur.yaml",
328                b"name: broken-tool\nversion: 0.1.0\nruntime: tool\n",
329            ),
330            ("tool.wasm", b"not wasm"),
331        ]);
332        let no_manifest = payload(&[("tool.wasm", &fixture("run/components/echo-tool.wasm"))]);
333
334        for payload in [not_wasm, no_manifest, b"not a zip".to_vec()] {
335            assert_eq!(
336                precompiler.precompile("broken-tool", "0.1.0", &payload),
337                Precompiled::Failed
338            );
339            assert!(forms_of(&payload).is_empty());
340        }
341    }
342}