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