1use std::path::Path;
18
19use murmur_artifact::RuntimeType;
20use wasmtime::Engine;
21
22use crate::compiled_forms::CompiledForms;
23
24pub struct Precompiler {
26 engine: Engine,
27 forms: CompiledForms,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum Precompiled {
35 Compiled,
38 AlreadyStored,
40 NotWasm,
43 Failed,
46}
47
48impl Precompiler {
49 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 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 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}