wasmer_wasix/bin_factory/
binary_package.rs1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use anyhow::Context;
7use once_cell::sync::OnceCell;
8use sha2::Digest;
9use virtual_fs::{FileSystem, MountFileSystem};
10use wasmer_config::package::{PackageHash, PackageId, PackageSource};
11use wasmer_package::package::Package;
12use webc::Container;
13use webc::compat::SharedBytes;
14
15use crate::{
16 Runtime,
17 runners::MappedDirectory,
18 runtime::resolver::{PackageInfo, ResolveError},
19};
20use wasmer_types::ModuleHash;
21
22#[derive(derive_more::Debug, Clone)]
23pub struct BinaryPackageCommand {
24 name: String,
25 metadata: webc::metadata::Command,
26 #[debug(ignore)]
27 pub(crate) atom: SharedBytes,
28 hash: ModuleHash,
29 features: Option<wasmer_types::Features>,
30 package: PackageId,
35 origin_package: PackageId,
41}
42
43impl BinaryPackageCommand {
44 pub fn new(
45 name: String,
46 metadata: webc::metadata::Command,
47 atom: SharedBytes,
48 hash: ModuleHash,
49 features: Option<wasmer_types::Features>,
50 package: PackageId,
51 origin_package: PackageId,
52 ) -> Self {
53 Self {
54 name,
55 metadata,
56 atom,
57 hash,
58 features,
59 package,
60 origin_package,
61 }
62 }
63
64 pub fn name(&self) -> &str {
65 &self.name
66 }
67
68 pub fn metadata(&self) -> &webc::metadata::Command {
69 &self.metadata
70 }
71
72 pub fn atom(&self) -> SharedBytes {
75 self.atom.clone()
76 }
77
78 pub fn atom_ref(&self) -> &SharedBytes {
81 &self.atom
82 }
83
84 pub fn hash(&self) -> &ModuleHash {
85 &self.hash
86 }
87
88 pub fn package(&self) -> &PackageId {
89 &self.package
90 }
91
92 pub fn origin_package(&self) -> &PackageId {
93 &self.origin_package
94 }
95
96 pub fn wasm_features(&self) -> Option<wasmer_types::Features> {
98 if let Some(features) = &self.features {
100 return Some(features.clone());
101 }
102
103 None
105 }
106}
107
108#[derive(derive_more::Debug, Clone)]
110pub struct BinaryPackageMount {
111 pub guest_path: PathBuf,
112 #[debug(ignore)]
113 pub fs: Arc<dyn FileSystem + Send + Sync>,
114 pub source_path: PathBuf,
115}
116
117#[derive(derive_more::Debug, Clone, Default)]
118pub struct BinaryPackageMounts {
119 #[debug(ignore)]
120 pub root_layer: Option<Arc<dyn FileSystem + Send + Sync>>,
121 pub mounts: Vec<BinaryPackageMount>,
122}
123
124impl BinaryPackageMounts {
125 pub fn from_mount_fs(fs: MountFileSystem) -> Self {
126 let mut root_layer = None;
127 let mut mounts = Vec::new();
128
129 for entry in fs.mount_entries() {
130 if entry.path == Path::new("/") {
131 root_layer = Some(entry.fs);
132 } else {
133 mounts.push(BinaryPackageMount {
134 guest_path: entry.path,
135 fs: entry.fs,
136 source_path: entry.source_path,
137 });
138 }
139 }
140
141 Self { root_layer, mounts }
142 }
143
144 pub fn to_mount_fs(&self) -> Result<MountFileSystem, virtual_fs::FsError> {
145 let mount_fs = MountFileSystem::new();
146
147 if let Some(root_layer) = &self.root_layer {
148 mount_fs.mount(Path::new("/"), root_layer.clone())?;
149 }
150
151 for mount in &self.mounts {
152 mount_fs.mount_with_source(&mount.guest_path, &mount.source_path, mount.fs.clone())?;
153 }
154
155 Ok(mount_fs)
156 }
157}
158
159#[derive(Debug, Clone)]
160pub struct BinaryPackage {
161 pub id: PackageId,
162 pub package_ids: Vec<PackageId>,
164 pub webc_version: webc::Version,
166
167 pub when_cached: Option<u128>,
168 pub entrypoint_cmd: Option<String>,
171 pub hash: OnceCell<ModuleHash>,
172 pub package_mounts: Option<Arc<BinaryPackageMounts>>,
173 pub commands: Vec<BinaryPackageCommand>,
174 pub uses: Vec<String>,
175 pub file_system_memory_footprint: u64,
176
177 pub additional_host_mapped_directories: Vec<MappedDirectory>,
178}
179
180impl BinaryPackage {
181 #[tracing::instrument(level = "debug", skip_all)]
182 pub async fn from_dir(
183 dir: &Path,
184 rt: &(dyn Runtime + Send + Sync),
185 ) -> Result<Self, anyhow::Error> {
186 let source = rt.source();
187
188 let hash = sha2::Sha256::digest(dir.display().to_string().as_bytes()).into();
191 let id = PackageId::Hash(PackageHash::from_sha256_bytes(hash));
192
193 let manifest_path = dir.join("wasmer.toml");
194 let webc = Package::from_manifest(&manifest_path)?;
195 let container = Container::from(webc);
196 let manifest = container.manifest();
197
198 let root = PackageInfo::from_manifest(id, manifest, container.version())?;
199 let root_id = root.id.clone();
200
201 let resolution = crate::runtime::resolver::resolve(&root_id, &root, &*source).await?;
202 let mut pkg = rt
203 .package_loader()
204 .load_package_tree(&container, &resolution, true)
205 .await
206 .map_err(|e| anyhow::anyhow!(e))?;
207
208 let wasmer_toml = std::fs::read_to_string(&manifest_path).unwrap();
211 let wasmer_toml: wasmer_config::package::Manifest = toml::from_str(&wasmer_toml).unwrap();
212 pkg.additional_host_mapped_directories.extend(
213 wasmer_toml
214 .fs
215 .into_iter()
216 .map(|(guest, host)| {
217 anyhow::Ok(MappedDirectory {
218 host: dir.join(host).canonicalize()?,
219 guest,
220 })
221 })
222 .collect::<Result<Vec<_>, _>>()?
223 .into_iter(),
224 );
225
226 Ok(pkg)
227 }
228
229 #[tracing::instrument(level = "debug", skip_all)]
232 pub async fn from_webc(
233 container: &Container,
234 rt: &(dyn Runtime + Send + Sync),
235 ) -> Result<Self, anyhow::Error> {
236 let source = rt.source();
237
238 let manifest = container.manifest();
239 let id = PackageInfo::package_id_from_manifest(manifest)?
240 .or_else(|| {
241 container
242 .webc_hash()
243 .map(|hash| PackageId::Hash(PackageHash::from_sha256_bytes(hash)))
244 })
245 .ok_or_else(|| anyhow::Error::msg("webc file did not provide its hash"))?;
246
247 let root = PackageInfo::from_manifest(id, manifest, container.version())?;
248 let root_id = root.id.clone();
249
250 let resolution = crate::runtime::resolver::resolve(&root_id, &root, &*source).await?;
251 let pkg = rt
252 .package_loader()
253 .load_package_tree(container, &resolution, false)
254 .await
255 .map_err(|e| anyhow::anyhow!(e))?;
256
257 Ok(pkg)
258 }
259
260 #[tracing::instrument(level = "debug", skip_all)]
262 pub async fn from_registry(
263 specifier: &PackageSource,
264 runtime: &(dyn Runtime + Send + Sync),
265 ) -> Result<Self, anyhow::Error> {
266 let source = runtime.source();
267 let root_summary =
268 source
269 .latest(specifier)
270 .await
271 .map_err(|error| ResolveError::Registry {
272 package: specifier.clone(),
273 error,
274 })?;
275 let root = runtime.package_loader().load(&root_summary).await?;
276 let id = root_summary.package_id();
277
278 let resolution = crate::runtime::resolver::resolve(&id, &root_summary.pkg, &source)
279 .await
280 .context("Dependency resolution failed")?;
281 let pkg = runtime
282 .package_loader()
283 .load_package_tree(&root, &resolution, false)
284 .await
285 .map_err(|e| anyhow::anyhow!(e))?;
286
287 Ok(pkg)
288 }
289
290 pub fn get_command(&self, name: &str) -> Option<&BinaryPackageCommand> {
291 self.commands.iter().find(|cmd| cmd.name() == name)
292 }
293
294 pub fn get_command_origin_package(&self, name: &str) -> Option<&PackageId> {
295 self.get_command(name)
296 .map(BinaryPackageCommand::origin_package)
297 }
298
299 pub fn get_entrypoint_command(&self) -> Option<&BinaryPackageCommand> {
301 self.entrypoint_cmd
302 .as_deref()
303 .and_then(|name| self.get_command(name))
304 }
305
306 #[deprecated(
308 note = "Use BinaryPackage::get_entrypoint_command instead",
309 since = "0.22.0"
310 )]
311 pub fn entrypoint_bytes(&self) -> Option<SharedBytes> {
312 self.get_entrypoint_command().map(|entry| entry.atom())
313 }
314
315 pub fn hash(&self) -> ModuleHash {
319 *self.hash.get_or_init(|| {
320 if let Some(cmd) = self.get_entrypoint_command() {
321 cmd.hash
322 } else {
323 ModuleHash::new(self.id.to_string())
324 }
325 })
326 }
327
328 pub fn infer_entrypoint(&self) -> Result<&str, anyhow::Error> {
329 if let Some(entrypoint) = self.entrypoint_cmd.as_deref() {
330 return Ok(entrypoint);
331 }
332
333 match self.commands.as_slice() {
334 [] => anyhow::bail!("The package doesn't contain any executable commands"),
335 [one] => Ok(one.name()),
336 [..] => {
337 let mut commands: Vec<_> = self.commands.iter().map(|cmd| cmd.name()).collect();
338 commands.sort();
339 anyhow::bail!(
340 "Unable to determine the package's entrypoint. Please choose one of {commands:?}"
341 );
342 }
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use sha2::Digest;
350 use tempfile::TempDir;
351 use virtual_fs::AsyncReadExt;
352 use wasmer_package::utils::from_disk;
353
354 use crate::{
355 PluggableRuntime,
356 runtime::{package_loader::BuiltinPackageLoader, task_manager::VirtualTaskManager},
357 };
358
359 use super::*;
360
361 fn task_manager() -> Arc<dyn VirtualTaskManager + Send + Sync> {
362 cfg_select! {
363 feature = "sys-thread" => {
364 Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(tokio::runtime::Handle::current()))
365 }
366 _ => {
367 unimplemented!("Unable to get the task manager")
368 }
369 }
370 }
371
372 #[tokio::test]
373 #[cfg_attr(
374 not(feature = "sys-thread"),
375 ignore = "The tokio task manager isn't available on this platform"
376 )]
377 async fn fs_table_can_map_directories_to_different_names() {
378 let temp = TempDir::new().unwrap();
379 let wasmer_toml = r#"
380 [package]
381 name = "some/package"
382 version = "0.0.0"
383 description = "a dummy package"
384
385 [fs]
386 "/public" = "./out"
387 "#;
388 let manifest = temp.path().join("wasmer.toml");
389 std::fs::write(&manifest, wasmer_toml).unwrap();
390 let out = temp.path().join("out");
391 std::fs::create_dir_all(&out).unwrap();
392 let file_txt = "Hello, World!";
393 std::fs::write(out.join("file.txt"), file_txt).unwrap();
394 let tasks = task_manager();
395 let mut runtime = PluggableRuntime::new(tasks);
396 runtime.set_package_loader(
397 BuiltinPackageLoader::new()
398 .with_shared_http_client(runtime.http_client().unwrap().clone()),
399 );
400
401 let pkg = Package::from_manifest(&manifest).unwrap();
402 let data = pkg.serialize().unwrap();
403 let webc_path = temp.path().join("package.webc");
404 std::fs::write(&webc_path, data).unwrap();
405
406 let pkg = BinaryPackage::from_webc(&from_disk(&webc_path).unwrap(), &runtime)
407 .await
408 .unwrap();
409
410 let mut f = pkg
413 .package_mounts
414 .as_ref()
415 .expect("no package mounts")
416 .to_mount_fs()
417 .expect("mount fs reconstruction failed")
418 .new_open_options()
419 .read(true)
420 .open("/public/file.txt")
421 .unwrap();
422 let mut buffer = String::new();
423 f.read_to_string(&mut buffer).await.unwrap();
424 assert_eq!(buffer, file_txt);
425 }
426
427 #[tokio::test]
428 #[cfg_attr(
429 not(feature = "sys-thread"),
430 ignore = "The tokio task manager isn't available on this platform"
431 )]
432 async fn commands_use_the_atom_signature() {
433 let temp = TempDir::new().unwrap();
434 let wasmer_toml = r#"
435 [package]
436 name = "some/package"
437 version = "0.0.0"
438 description = "a dummy package"
439
440 [[module]]
441 name = "foo"
442 source = "foo.wasm"
443 abi = "wasi"
444
445 [[command]]
446 name = "cmd"
447 module = "foo"
448 "#;
449 let manifest = temp.path().join("wasmer.toml");
450 std::fs::write(&manifest, wasmer_toml).unwrap();
451
452 let atom_path = temp.path().join("foo.wasm");
453 std::fs::write(&atom_path, b"").unwrap();
454
455 let webc: Container = Package::from_manifest(&manifest).unwrap().into();
456
457 let tasks = task_manager();
458 let mut runtime = PluggableRuntime::new(tasks);
459 runtime.set_package_loader(
460 BuiltinPackageLoader::new()
461 .with_shared_http_client(runtime.http_client().unwrap().clone()),
462 );
463
464 let pkg = BinaryPackage::from_dir(temp.path(), &runtime)
465 .await
466 .unwrap();
467
468 assert_eq!(pkg.commands.len(), 1);
469 let command = pkg.get_command("cmd").unwrap();
470 let atom_sha256_hash = sha2::Sha256::digest(webc.get_atom("foo").unwrap()).into();
471 let module_hash = ModuleHash::from_bytes(atom_sha256_hash);
472 assert_eq!(command.hash(), &module_hash);
473 assert_eq!(command.package(), &pkg.id);
474 assert_eq!(pkg.get_command_origin_package("cmd"), Some(&pkg.id));
475 assert_eq!(command.origin_package(), &pkg.id);
476 }
477}