wasmer_wasix/bin_factory/
binary_package.rs

1use 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 that declares this command in the resolved manifest graph.
31    ///
32    /// This identifies "who owns the command name" (the package that exposes
33    /// the command entry), even if execution uses an atom from another package.
34    package: PackageId,
35    /// Package that provides the module this command actually executes.
36    ///
37    /// Usually this matches `package`. It differs when the command's atom
38    /// annotation points at a dependency, so the command is declared by one
39    /// package but runs code from another package.
40    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    /// Get a reference to this [`BinaryPackageCommand`]'s atom as a cheap
73    /// clone of the internal OwnedBuffer.
74    pub fn atom(&self) -> SharedBytes {
75        self.atom.clone()
76    }
77
78    /// Get a reference to this [`BinaryPackageCommand`]'s atom as a cheap
79    /// clone of the internal OwnedBuffer.
80    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    /// Get the WebAssembly features required by this command's module
97    pub fn wasm_features(&self) -> Option<wasmer_types::Features> {
98        // Return only the pre-computed features from the container manifest
99        if let Some(features) = &self.features {
100            return Some(features.clone());
101        }
102
103        // If no annotations were found, return None
104        None
105    }
106}
107
108/// A WebAssembly package that has been loaded into memory.
109#[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    /// Includes the ids of all the packages in the tree
163    pub package_ids: Vec<PackageId>,
164
165    pub when_cached: Option<u128>,
166    /// The name of the [`BinaryPackageCommand`] which is this package's
167    /// entrypoint.
168    pub entrypoint_cmd: Option<String>,
169    pub hash: OnceCell<ModuleHash>,
170    pub package_mounts: Option<Arc<BinaryPackageMounts>>,
171    pub commands: Vec<BinaryPackageCommand>,
172    pub uses: Vec<String>,
173    pub file_system_memory_footprint: u64,
174
175    pub additional_host_mapped_directories: Vec<MappedDirectory>,
176}
177
178impl BinaryPackage {
179    #[tracing::instrument(level = "debug", skip_all)]
180    pub async fn from_dir(
181        dir: &Path,
182        rt: &(dyn Runtime + Send + Sync),
183    ) -> Result<Self, anyhow::Error> {
184        let source = rt.source();
185
186        // since each package must be in its own directory, hash of the `dir` should provide a good enough
187        // unique identifier for the package
188        let hash = sha2::Sha256::digest(dir.display().to_string().as_bytes()).into();
189        let id = PackageId::Hash(PackageHash::from_sha256_bytes(hash));
190
191        let manifest_path = dir.join("wasmer.toml");
192        let webc = Package::from_manifest(&manifest_path)?;
193        let container = Container::from(webc);
194        let manifest = container.manifest();
195
196        let root = PackageInfo::from_manifest(id, manifest, container.version())?;
197        let root_id = root.id.clone();
198
199        let resolution = crate::runtime::resolver::resolve(&root_id, &root, &*source).await?;
200        let mut pkg = rt
201            .package_loader()
202            .load_package_tree(&container, &resolution, true)
203            .await
204            .map_err(|e| anyhow::anyhow!(e))?;
205
206        // HACK: webc has no way to return its deserialized manifest to us, so we need to do it again here
207        // We already read and parsed the manifest once, so it'll succeed again. Unwrapping is safe at this point.
208        let wasmer_toml = std::fs::read_to_string(&manifest_path).unwrap();
209        let wasmer_toml: wasmer_config::package::Manifest = toml::from_str(&wasmer_toml).unwrap();
210        pkg.additional_host_mapped_directories.extend(
211            wasmer_toml
212                .fs
213                .into_iter()
214                .map(|(guest, host)| {
215                    anyhow::Ok(MappedDirectory {
216                        host: dir.join(host).canonicalize()?,
217                        guest,
218                    })
219                })
220                .collect::<Result<Vec<_>, _>>()?
221                .into_iter(),
222        );
223
224        Ok(pkg)
225    }
226
227    /// Load a [`webc::Container`] and all its dependencies into a
228    /// [`BinaryPackage`].
229    #[tracing::instrument(level = "debug", skip_all)]
230    pub async fn from_webc(
231        container: &Container,
232        rt: &(dyn Runtime + Send + Sync),
233    ) -> Result<Self, anyhow::Error> {
234        let source = rt.source();
235
236        let manifest = container.manifest();
237        let id = PackageInfo::package_id_from_manifest(manifest)?
238            .or_else(|| {
239                container
240                    .webc_hash()
241                    .map(|hash| PackageId::Hash(PackageHash::from_sha256_bytes(hash)))
242            })
243            .ok_or_else(|| anyhow::Error::msg("webc file did not provide its hash"))?;
244
245        let root = PackageInfo::from_manifest(id, manifest, container.version())?;
246        let root_id = root.id.clone();
247
248        let resolution = crate::runtime::resolver::resolve(&root_id, &root, &*source).await?;
249        let pkg = rt
250            .package_loader()
251            .load_package_tree(container, &resolution, false)
252            .await
253            .map_err(|e| anyhow::anyhow!(e))?;
254
255        Ok(pkg)
256    }
257
258    /// Load a [`BinaryPackage`] and all its dependencies from a registry.
259    #[tracing::instrument(level = "debug", skip_all)]
260    pub async fn from_registry(
261        specifier: &PackageSource,
262        runtime: &(dyn Runtime + Send + Sync),
263    ) -> Result<Self, anyhow::Error> {
264        let source = runtime.source();
265        let root_summary =
266            source
267                .latest(specifier)
268                .await
269                .map_err(|error| ResolveError::Registry {
270                    package: specifier.clone(),
271                    error,
272                })?;
273        let root = runtime.package_loader().load(&root_summary).await?;
274        let id = root_summary.package_id();
275
276        let resolution = crate::runtime::resolver::resolve(&id, &root_summary.pkg, &source)
277            .await
278            .context("Dependency resolution failed")?;
279        let pkg = runtime
280            .package_loader()
281            .load_package_tree(&root, &resolution, false)
282            .await
283            .map_err(|e| anyhow::anyhow!(e))?;
284
285        Ok(pkg)
286    }
287
288    pub fn get_command(&self, name: &str) -> Option<&BinaryPackageCommand> {
289        self.commands.iter().find(|cmd| cmd.name() == name)
290    }
291
292    pub fn get_command_origin_package(&self, name: &str) -> Option<&PackageId> {
293        self.get_command(name)
294            .map(BinaryPackageCommand::origin_package)
295    }
296
297    /// Resolve the entrypoint command name to a [`BinaryPackageCommand`].
298    pub fn get_entrypoint_command(&self) -> Option<&BinaryPackageCommand> {
299        self.entrypoint_cmd
300            .as_deref()
301            .and_then(|name| self.get_command(name))
302    }
303
304    /// Get the bytes for the entrypoint command.
305    #[deprecated(
306        note = "Use BinaryPackage::get_entrypoint_command instead",
307        since = "0.22.0"
308    )]
309    pub fn entrypoint_bytes(&self) -> Option<SharedBytes> {
310        self.get_entrypoint_command().map(|entry| entry.atom())
311    }
312
313    /// Get a hash for this binary package.
314    ///
315    /// Usually the hash of the entrypoint.
316    pub fn hash(&self) -> ModuleHash {
317        *self.hash.get_or_init(|| {
318            if let Some(cmd) = self.get_entrypoint_command() {
319                cmd.hash
320            } else {
321                ModuleHash::new(self.id.to_string())
322            }
323        })
324    }
325
326    pub fn infer_entrypoint(&self) -> Result<&str, anyhow::Error> {
327        if let Some(entrypoint) = self.entrypoint_cmd.as_deref() {
328            return Ok(entrypoint);
329        }
330
331        match self.commands.as_slice() {
332            [] => anyhow::bail!("The package doesn't contain any executable commands"),
333            [one] => Ok(one.name()),
334            [..] => {
335                let mut commands: Vec<_> = self.commands.iter().map(|cmd| cmd.name()).collect();
336                commands.sort();
337                anyhow::bail!(
338                    "Unable to determine the package's entrypoint. Please choose one of {commands:?}"
339                );
340            }
341        }
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use sha2::Digest;
348    use tempfile::TempDir;
349    use virtual_fs::{AsyncReadExt, FileSystem as _};
350    use wasmer_package::utils::from_disk;
351
352    use crate::{
353        PluggableRuntime,
354        runtime::{package_loader::BuiltinPackageLoader, task_manager::VirtualTaskManager},
355    };
356
357    use super::*;
358
359    fn task_manager() -> Arc<dyn VirtualTaskManager + Send + Sync> {
360        cfg_if::cfg_if! {
361            if #[cfg(feature = "sys-thread")] {
362                Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(tokio::runtime::Handle::current()))
363            } else {
364                unimplemented!("Unable to get the task manager")
365            }
366        }
367    }
368
369    #[tokio::test]
370    #[cfg_attr(
371        not(feature = "sys-thread"),
372        ignore = "The tokio task manager isn't available on this platform"
373    )]
374    async fn fs_table_can_map_directories_to_different_names() {
375        let temp = TempDir::new().unwrap();
376        let wasmer_toml = r#"
377            [package]
378            name = "some/package"
379            version = "0.0.0"
380            description = "a dummy package"
381
382            [fs]
383            "/public" = "./out"
384        "#;
385        let manifest = temp.path().join("wasmer.toml");
386        std::fs::write(&manifest, wasmer_toml).unwrap();
387        let out = temp.path().join("out");
388        std::fs::create_dir_all(&out).unwrap();
389        let file_txt = "Hello, World!";
390        std::fs::write(out.join("file.txt"), file_txt).unwrap();
391        let tasks = task_manager();
392        let mut runtime = PluggableRuntime::new(tasks);
393        runtime.set_package_loader(
394            BuiltinPackageLoader::new()
395                .with_shared_http_client(runtime.http_client().unwrap().clone()),
396        );
397
398        let pkg = Package::from_manifest(&manifest).unwrap();
399        let data = pkg.serialize().unwrap();
400        let webc_path = temp.path().join("package.webc");
401        std::fs::write(&webc_path, data).unwrap();
402
403        let pkg = BinaryPackage::from_webc(&from_disk(&webc_path).unwrap(), &runtime)
404            .await
405            .unwrap();
406
407        // We should have mapped "./out/file.txt" on the host to
408        // "/public/file.txt" on the guest.
409        let mut f = pkg
410            .package_mounts
411            .as_ref()
412            .expect("no package mounts")
413            .to_mount_fs()
414            .expect("mount fs reconstruction failed")
415            .new_open_options()
416            .read(true)
417            .open("/public/file.txt")
418            .unwrap();
419        let mut buffer = String::new();
420        f.read_to_string(&mut buffer).await.unwrap();
421        assert_eq!(buffer, file_txt);
422    }
423
424    #[tokio::test]
425    #[cfg_attr(
426        not(feature = "sys-thread"),
427        ignore = "The tokio task manager isn't available on this platform"
428    )]
429    async fn commands_use_the_atom_signature() {
430        let temp = TempDir::new().unwrap();
431        let wasmer_toml = r#"
432            [package]
433            name = "some/package"
434            version = "0.0.0"
435            description = "a dummy package"
436
437            [[module]]
438            name = "foo"
439            source = "foo.wasm"
440            abi = "wasi"
441
442            [[command]]
443            name = "cmd"
444            module = "foo"
445        "#;
446        let manifest = temp.path().join("wasmer.toml");
447        std::fs::write(&manifest, wasmer_toml).unwrap();
448
449        let atom_path = temp.path().join("foo.wasm");
450        std::fs::write(&atom_path, b"").unwrap();
451
452        let webc: Container = Package::from_manifest(&manifest).unwrap().into();
453
454        let tasks = task_manager();
455        let mut runtime = PluggableRuntime::new(tasks);
456        runtime.set_package_loader(
457            BuiltinPackageLoader::new()
458                .with_shared_http_client(runtime.http_client().unwrap().clone()),
459        );
460
461        let pkg = BinaryPackage::from_dir(temp.path(), &runtime)
462            .await
463            .unwrap();
464
465        assert_eq!(pkg.commands.len(), 1);
466        let command = pkg.get_command("cmd").unwrap();
467        let atom_sha256_hash = sha2::Sha256::digest(webc.get_atom("foo").unwrap()).into();
468        let module_hash = ModuleHash::from_bytes(atom_sha256_hash);
469        assert_eq!(command.hash(), &module_hash);
470        assert_eq!(command.package(), &pkg.id);
471        assert_eq!(pkg.get_command_origin_package("cmd"), Some(&pkg.id));
472        assert_eq!(command.origin_package(), &pkg.id);
473    }
474}