yosh_plugin_manager/
precompile.rs1use std::path::{Path, PathBuf};
21
22use serde::{Deserialize, Serialize};
23use sha2::{Digest, Sha256};
24
25pub const WASMTIME_VERSION: &str = "27";
33
34pub fn target_triple() -> &'static str {
39 option_env!("TARGET").unwrap_or("host")
40}
41
42pub const ENGINE_FINGERPRINT: &str =
47 "v2;component_model=true;async=false;fuel=false;epoch=true;cranelift";
48
49pub fn engine_config_hash(fingerprint: &str) -> String {
53 let mut hasher = Sha256::new();
54 hasher.update(fingerprint.as_bytes());
55 hex::encode(hasher.finalize())
56}
57
58pub fn sha256_hex(bytes: &[u8]) -> String {
60 let mut hasher = Sha256::new();
61 hasher.update(bytes);
62 hex::encode(hasher.finalize())
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69pub struct CacheKey {
70 pub wasm_sha256: String,
72 pub wasmtime_version: String,
74 pub target_triple: String,
76 pub engine_config_hash: String,
78}
79
80impl CacheKey {
81 pub fn for_precompile(wasm_sha256: impl Into<String>) -> Self {
83 CacheKey {
84 wasm_sha256: wasm_sha256.into(),
85 wasmtime_version: WASMTIME_VERSION.to_string(),
86 target_triple: target_triple().to_string(),
87 engine_config_hash: engine_config_hash(ENGINE_FINGERPRINT),
88 }
89 }
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct SidecarMeta {
96 pub schema: u32,
97 pub key: CacheKey,
98}
99
100impl SidecarMeta {
101 pub const SCHEMA_VERSION: u32 = 1;
102
103 pub fn new(key: CacheKey) -> Self {
104 SidecarMeta {
105 schema: Self::SCHEMA_VERSION,
106 key,
107 }
108 }
109
110 pub fn write_to(&self, path: &Path) -> Result<(), String> {
111 let s = toml::to_string(self)
112 .map_err(|e| format!("serialize cwasm sidecar {}: {}", path.display(), e))?;
113 std::fs::write(path, s)
114 .map_err(|e| format!("write cwasm sidecar {}: {}", path.display(), e))
115 }
116}
117
118pub fn make_engine() -> Result<wasmtime::Engine, String> {
134 let mut config = wasmtime::Config::new();
135 config.wasm_component_model(true);
136 config.async_support(false);
137 config.consume_fuel(false);
138 config.epoch_interruption(true);
139 wasmtime::Engine::new(&config).map_err(|e| format!("wasmtime Engine::new: {}", e))
140}
141
142#[derive(Debug, Clone)]
144pub struct PrecompileOutput {
145 pub cwasm_path: PathBuf,
147 pub sidecar_path: PathBuf,
149 pub cache_key: CacheKey,
151}
152
153pub fn precompile(
163 wasm_path: &Path,
164 cache_dir: &Path,
165 engine: &wasmtime::Engine,
166) -> Result<PrecompileOutput, String> {
167 let wasm_bytes =
168 std::fs::read(wasm_path).map_err(|e| format!("read {}: {}", wasm_path.display(), e))?;
169 let wasm_sha = sha256_hex(&wasm_bytes);
170
171 ensure_cache_dir(cache_dir)?;
173
174 let stem = wasm_path
175 .file_stem()
176 .and_then(|s| s.to_str())
177 .ok_or_else(|| format!("invalid wasm filename: {}", wasm_path.display()))?;
178 let cwasm_path = cache_dir.join(format!("{}.cwasm", stem));
179 let sidecar_path = cache_dir.join(format!("{}.cwasm.meta", stem));
180
181 let serialized = engine
186 .precompile_component(&wasm_bytes)
187 .map_err(|e| format!("precompile_component {}: {}", wasm_path.display(), e))?;
188
189 write_with_mode(&cwasm_path, &serialized, 0o600)?;
190
191 let cache_key = CacheKey::for_precompile(wasm_sha);
192 SidecarMeta::new(cache_key.clone()).write_to(&sidecar_path)?;
193 set_mode(&sidecar_path, 0o600)?;
194
195 Ok(PrecompileOutput {
196 cwasm_path,
197 sidecar_path,
198 cache_key,
199 })
200}
201
202fn ensure_cache_dir(dir: &Path) -> Result<(), String> {
205 std::fs::create_dir_all(dir)
206 .map_err(|e| format!("create cache dir {}: {}", dir.display(), e))?;
207 set_mode(dir, 0o700)?;
208 Ok(())
209}
210
211#[cfg(unix)]
212fn set_mode(path: &Path, mode: u32) -> Result<(), String> {
213 use std::os::unix::fs::PermissionsExt;
214 std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
215 .map_err(|e| format!("chmod {}: {}", path.display(), e))
216}
217
218#[cfg(not(unix))]
219fn set_mode(_path: &Path, _mode: u32) -> Result<(), String> {
220 Ok(())
221}
222
223#[cfg(unix)]
224fn write_with_mode(path: &Path, bytes: &[u8], mode: u32) -> Result<(), String> {
225 use std::io::Write;
226 use std::os::unix::fs::OpenOptionsExt;
227 let mut f = std::fs::OpenOptions::new()
228 .write(true)
229 .create(true)
230 .truncate(true)
231 .mode(mode)
232 .open(path)
233 .map_err(|e| format!("open {}: {}", path.display(), e))?;
234 f.write_all(bytes)
235 .map_err(|e| format!("write {}: {}", path.display(), e))?;
236 Ok(())
237}
238
239#[cfg(not(unix))]
240fn write_with_mode(path: &Path, bytes: &[u8], _mode: u32) -> Result<(), String> {
241 std::fs::write(path, bytes).map_err(|e| format!("write {}: {}", path.display(), e))
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn engine_config_hash_is_deterministic() {
250 let a = engine_config_hash(ENGINE_FINGERPRINT);
251 let b = engine_config_hash(ENGINE_FINGERPRINT);
252 assert_eq!(a, b);
253 }
254
255 #[test]
256 fn engine_config_hash_differs_for_different_fingerprints() {
257 let a = engine_config_hash("a");
258 let b = engine_config_hash("b");
259 assert_ne!(a, b);
260 }
261
262 #[test]
263 fn cache_key_for_precompile_uses_pinned_constants() {
264 let k = CacheKey::for_precompile("abc");
265 assert_eq!(k.wasm_sha256, "abc");
266 assert_eq!(k.wasmtime_version, WASMTIME_VERSION);
267 assert_eq!(k.engine_config_hash, engine_config_hash(ENGINE_FINGERPRINT));
268 }
269
270 #[test]
271 fn sidecar_round_trip() {
272 let dir = tempfile::tempdir().unwrap();
273 let path = dir.path().join("plugin.cwasm.meta");
274 let key = CacheKey::for_precompile("deadbeef");
275 let meta = SidecarMeta::new(key.clone());
276 meta.write_to(&path).unwrap();
277 let bytes = std::fs::read_to_string(&path).unwrap();
278 let parsed: SidecarMeta = toml::from_str(&bytes).unwrap();
279 assert_eq!(parsed.schema, SidecarMeta::SCHEMA_VERSION);
280 assert_eq!(parsed.key, key);
281 }
282
283 #[test]
284 fn make_engine_succeeds() {
285 let _engine = make_engine().expect("engine");
286 }
287}