Skip to main content

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;
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 that declares this command in the resolved manifest graph.
33    ///
34    /// This identifies "who owns the command name" (the package that exposes
35    /// the command entry), even if execution uses an atom from another package.
36    package: PackageId,
37    /// Package that provides the module this command actually executes.
38    ///
39    /// Usually this matches `package`. It differs when the command's atom
40    /// annotation points at a dependency, so the command is declared by one
41    /// package but runs code from another package.
42    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    /// Get a reference to this [`BinaryPackageCommand`]'s atom as a cheap
75    /// clone of the internal OwnedBuffer.
76    pub fn atom(&self) -> SharedBytes {
77        self.atom.clone()
78    }
79
80    /// Get a reference to this [`BinaryPackageCommand`]'s atom as a cheap
81    /// clone of the internal OwnedBuffer.
82    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    /// Get the WebAssembly features required by this command's module
99    pub fn wasm_features(&self) -> Option<wasmer_types::Features> {
100        // Return only the pre-computed features from the container manifest
101        if let Some(features) = &self.features {
102            return Some(features.clone());
103        }
104
105        // If no annotations were found, return None
106        None
107    }
108}
109
110/// A WebAssembly package that has been loaded into memory.
111#[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    /// Includes the ids of all the packages in the tree
165    pub package_ids: Vec<PackageId>,
166    /// The WebC format version of the root package.
167    pub webc_version: webc::Version,
168
169    pub when_cached: Option<u128>,
170    /// The name of the [`BinaryPackageCommand`] which is this package's
171    /// entrypoint.
172    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        // since each package must be in its own directory, hash of the `dir` should provide a good enough
191        // unique identifier for the package
192        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        // HACK: webc has no way to return its deserialized manifest to us, so we need to do it again here
211        // We already read and parsed the manifest once, so it'll succeed again. Unwrapping is safe at this point.
212        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    /// Load a [`webc::Container`] and all its dependencies into a
232    /// [`BinaryPackage`].
233    #[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    /// Load a [`BinaryPackage`] and all its dependencies from a registry.
263    #[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    /// Resolve the entrypoint command name to a [`BinaryPackageCommand`].
302    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    /// Get the bytes for the entrypoint command.
309    #[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    /// Get a hash for this binary package.
318    ///
319    /// Usually the hash of the entrypoint.
320    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        // We should have mapped "./out/file.txt" on the host to
413        // "/public/file.txt" on the guest.
414        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}