wasmer_wasix/bin_factory/
binary_package.rs

1use std::{path::Path, sync::Arc};
2
3use anyhow::Context;
4use once_cell::sync::OnceCell;
5use sha2::Digest;
6use virtual_fs::UnionFileSystem;
7use wasmer_config::package::{PackageHash, PackageId, PackageSource};
8use wasmer_package::package::Package;
9use webc::Container;
10use webc::compat::SharedBytes;
11
12use crate::{
13    Runtime,
14    runners::MappedDirectory,
15    runtime::resolver::{PackageInfo, ResolveError},
16};
17use wasmer_types::ModuleHash;
18
19#[derive(derive_more::Debug, Clone)]
20pub struct BinaryPackageCommand {
21    name: String,
22    metadata: webc::metadata::Command,
23    #[debug(ignore)]
24    pub(crate) atom: SharedBytes,
25    hash: ModuleHash,
26    features: Option<wasmer_types::Features>,
27    /// Package that declares this command in the resolved manifest graph.
28    ///
29    /// This identifies "who owns the command name" (the package that exposes
30    /// the command entry), even if execution uses an atom from another package.
31    package: PackageId,
32    /// Package that provides the module this command actually executes.
33    ///
34    /// Usually this matches `package`. It differs when the command's atom
35    /// annotation points at a dependency, so the command is declared by one
36    /// package but runs code from another package.
37    origin_package: PackageId,
38}
39
40impl BinaryPackageCommand {
41    pub fn new(
42        name: String,
43        metadata: webc::metadata::Command,
44        atom: SharedBytes,
45        hash: ModuleHash,
46        features: Option<wasmer_types::Features>,
47        package: PackageId,
48        origin_package: PackageId,
49    ) -> Self {
50        Self {
51            name,
52            metadata,
53            atom,
54            hash,
55            features,
56            package,
57            origin_package,
58        }
59    }
60
61    pub fn name(&self) -> &str {
62        &self.name
63    }
64
65    pub fn metadata(&self) -> &webc::metadata::Command {
66        &self.metadata
67    }
68
69    /// Get a reference to this [`BinaryPackageCommand`]'s atom as a cheap
70    /// clone of the internal OwnedBuffer.
71    pub fn atom(&self) -> SharedBytes {
72        self.atom.clone()
73    }
74
75    /// Get a reference to this [`BinaryPackageCommand`]'s atom as a cheap
76    /// clone of the internal OwnedBuffer.
77    pub fn atom_ref(&self) -> &SharedBytes {
78        &self.atom
79    }
80
81    pub fn hash(&self) -> &ModuleHash {
82        &self.hash
83    }
84
85    pub fn package(&self) -> &PackageId {
86        &self.package
87    }
88
89    pub fn origin_package(&self) -> &PackageId {
90        &self.origin_package
91    }
92
93    /// Get the WebAssembly features required by this command's module
94    pub fn wasm_features(&self) -> Option<wasmer_types::Features> {
95        // Return only the pre-computed features from the container manifest
96        if let Some(features) = &self.features {
97            return Some(features.clone());
98        }
99
100        // If no annotations were found, return None
101        None
102    }
103}
104
105/// A WebAssembly package that has been loaded into memory.
106#[derive(Debug, Clone)]
107pub struct BinaryPackage {
108    pub id: PackageId,
109    /// Includes the ids of all the packages in the tree
110    pub package_ids: Vec<PackageId>,
111
112    pub when_cached: Option<u128>,
113    /// The name of the [`BinaryPackageCommand`] which is this package's
114    /// entrypoint.
115    pub entrypoint_cmd: Option<String>,
116    pub hash: OnceCell<ModuleHash>,
117    // TODO: using a UnionFileSystem here directly is suboptimal, since cloning
118    // it is expensive. Should instead store an immutable map that can easily
119    // be converted into a dashmap.
120    pub webc_fs: Option<Arc<UnionFileSystem>>,
121    pub commands: Vec<BinaryPackageCommand>,
122    pub uses: Vec<String>,
123    pub file_system_memory_footprint: u64,
124
125    pub additional_host_mapped_directories: Vec<MappedDirectory>,
126}
127
128impl BinaryPackage {
129    #[tracing::instrument(level = "debug", skip_all)]
130    pub async fn from_dir(
131        dir: &Path,
132        rt: &(dyn Runtime + Send + Sync),
133    ) -> Result<Self, anyhow::Error> {
134        let source = rt.source();
135
136        // since each package must be in its own directory, hash of the `dir` should provide a good enough
137        // unique identifier for the package
138        let hash = sha2::Sha256::digest(dir.display().to_string().as_bytes()).into();
139        let id = PackageId::Hash(PackageHash::from_sha256_bytes(hash));
140
141        let manifest_path = dir.join("wasmer.toml");
142        let webc = Package::from_manifest(&manifest_path)?;
143        let container = Container::from(webc);
144        let manifest = container.manifest();
145
146        let root = PackageInfo::from_manifest(id, manifest, container.version())?;
147        let root_id = root.id.clone();
148
149        let resolution = crate::runtime::resolver::resolve(&root_id, &root, &*source).await?;
150        let mut pkg = rt
151            .package_loader()
152            .load_package_tree(&container, &resolution, true)
153            .await
154            .map_err(|e| anyhow::anyhow!(e))?;
155
156        // HACK: webc has no way to return its deserialized manifest to us, so we need to do it again here
157        // We already read and parsed the manifest once, so it'll succeed again. Unwrapping is safe at this point.
158        let wasmer_toml = std::fs::read_to_string(&manifest_path).unwrap();
159        let wasmer_toml: wasmer_config::package::Manifest = toml::from_str(&wasmer_toml).unwrap();
160        pkg.additional_host_mapped_directories.extend(
161            wasmer_toml
162                .fs
163                .into_iter()
164                .map(|(guest, host)| {
165                    anyhow::Ok(MappedDirectory {
166                        host: dir.join(host).canonicalize()?,
167                        guest,
168                    })
169                })
170                .collect::<Result<Vec<_>, _>>()?
171                .into_iter(),
172        );
173
174        Ok(pkg)
175    }
176
177    /// Load a [`webc::Container`] and all its dependencies into a
178    /// [`BinaryPackage`].
179    #[tracing::instrument(level = "debug", skip_all)]
180    pub async fn from_webc(
181        container: &Container,
182        rt: &(dyn Runtime + Send + Sync),
183    ) -> Result<Self, anyhow::Error> {
184        let source = rt.source();
185
186        let manifest = container.manifest();
187        let id = PackageInfo::package_id_from_manifest(manifest)?
188            .or_else(|| {
189                container
190                    .webc_hash()
191                    .map(|hash| PackageId::Hash(PackageHash::from_sha256_bytes(hash)))
192            })
193            .ok_or_else(|| anyhow::Error::msg("webc file did not provide its hash"))?;
194
195        let root = PackageInfo::from_manifest(id, manifest, container.version())?;
196        let root_id = root.id.clone();
197
198        let resolution = crate::runtime::resolver::resolve(&root_id, &root, &*source).await?;
199        let pkg = rt
200            .package_loader()
201            .load_package_tree(container, &resolution, false)
202            .await
203            .map_err(|e| anyhow::anyhow!(e))?;
204
205        Ok(pkg)
206    }
207
208    /// Load a [`BinaryPackage`] and all its dependencies from a registry.
209    #[tracing::instrument(level = "debug", skip_all)]
210    pub async fn from_registry(
211        specifier: &PackageSource,
212        runtime: &(dyn Runtime + Send + Sync),
213    ) -> Result<Self, anyhow::Error> {
214        let source = runtime.source();
215        let root_summary =
216            source
217                .latest(specifier)
218                .await
219                .map_err(|error| ResolveError::Registry {
220                    package: specifier.clone(),
221                    error,
222                })?;
223        let root = runtime.package_loader().load(&root_summary).await?;
224        let id = root_summary.package_id();
225
226        let resolution = crate::runtime::resolver::resolve(&id, &root_summary.pkg, &source)
227            .await
228            .context("Dependency resolution failed")?;
229        let pkg = runtime
230            .package_loader()
231            .load_package_tree(&root, &resolution, false)
232            .await
233            .map_err(|e| anyhow::anyhow!(e))?;
234
235        Ok(pkg)
236    }
237
238    pub fn get_command(&self, name: &str) -> Option<&BinaryPackageCommand> {
239        self.commands.iter().find(|cmd| cmd.name() == name)
240    }
241
242    pub fn get_command_origin_package(&self, name: &str) -> Option<&PackageId> {
243        self.get_command(name)
244            .map(BinaryPackageCommand::origin_package)
245    }
246
247    /// Resolve the entrypoint command name to a [`BinaryPackageCommand`].
248    pub fn get_entrypoint_command(&self) -> Option<&BinaryPackageCommand> {
249        self.entrypoint_cmd
250            .as_deref()
251            .and_then(|name| self.get_command(name))
252    }
253
254    /// Get the bytes for the entrypoint command.
255    #[deprecated(
256        note = "Use BinaryPackage::get_entrypoint_command instead",
257        since = "0.22.0"
258    )]
259    pub fn entrypoint_bytes(&self) -> Option<SharedBytes> {
260        self.get_entrypoint_command().map(|entry| entry.atom())
261    }
262
263    /// Get a hash for this binary package.
264    ///
265    /// Usually the hash of the entrypoint.
266    pub fn hash(&self) -> ModuleHash {
267        *self.hash.get_or_init(|| {
268            if let Some(cmd) = self.get_entrypoint_command() {
269                cmd.hash
270            } else {
271                ModuleHash::new(self.id.to_string())
272            }
273        })
274    }
275
276    pub fn infer_entrypoint(&self) -> Result<&str, anyhow::Error> {
277        if let Some(entrypoint) = self.entrypoint_cmd.as_deref() {
278            return Ok(entrypoint);
279        }
280
281        match self.commands.as_slice() {
282            [] => anyhow::bail!("The package doesn't contain any executable commands"),
283            [one] => Ok(one.name()),
284            [..] => {
285                let mut commands: Vec<_> = self.commands.iter().map(|cmd| cmd.name()).collect();
286                commands.sort();
287                anyhow::bail!(
288                    "Unable to determine the package's entrypoint. Please choose one of {commands:?}"
289                );
290            }
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use sha2::Digest;
298    use tempfile::TempDir;
299    use virtual_fs::{AsyncReadExt, FileSystem as _};
300    use wasmer_package::utils::from_disk;
301
302    use crate::{
303        PluggableRuntime,
304        runtime::{package_loader::BuiltinPackageLoader, task_manager::VirtualTaskManager},
305    };
306
307    use super::*;
308
309    fn task_manager() -> Arc<dyn VirtualTaskManager + Send + Sync> {
310        cfg_if::cfg_if! {
311            if #[cfg(feature = "sys-thread")] {
312                Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(tokio::runtime::Handle::current()))
313            } else {
314                unimplemented!("Unable to get the task manager")
315            }
316        }
317    }
318
319    #[tokio::test]
320    #[cfg_attr(
321        not(feature = "sys-thread"),
322        ignore = "The tokio task manager isn't available on this platform"
323    )]
324    async fn fs_table_can_map_directories_to_different_names() {
325        let temp = TempDir::new().unwrap();
326        let wasmer_toml = r#"
327            [package]
328            name = "some/package"
329            version = "0.0.0"
330            description = "a dummy package"
331
332            [fs]
333            "/public" = "./out"
334        "#;
335        let manifest = temp.path().join("wasmer.toml");
336        std::fs::write(&manifest, wasmer_toml).unwrap();
337        let out = temp.path().join("out");
338        std::fs::create_dir_all(&out).unwrap();
339        let file_txt = "Hello, World!";
340        std::fs::write(out.join("file.txt"), file_txt).unwrap();
341        let tasks = task_manager();
342        let mut runtime = PluggableRuntime::new(tasks);
343        runtime.set_package_loader(
344            BuiltinPackageLoader::new()
345                .with_shared_http_client(runtime.http_client().unwrap().clone()),
346        );
347
348        let pkg = Package::from_manifest(&manifest).unwrap();
349        let data = pkg.serialize().unwrap();
350        let webc_path = temp.path().join("package.webc");
351        std::fs::write(&webc_path, data).unwrap();
352
353        let pkg = BinaryPackage::from_webc(&from_disk(&webc_path).unwrap(), &runtime)
354            .await
355            .unwrap();
356
357        // We should have mapped "./out/file.txt" on the host to
358        // "/public/file.txt" on the guest.
359        let mut f = pkg
360            .webc_fs
361            .as_ref()
362            .expect("no webc fs")
363            .new_open_options()
364            .read(true)
365            .open("/public/file.txt")
366            .unwrap();
367        let mut buffer = String::new();
368        f.read_to_string(&mut buffer).await.unwrap();
369        assert_eq!(buffer, file_txt);
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 commands_use_the_atom_signature() {
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            [[module]]
386            name = "foo"
387            source = "foo.wasm"
388            abi = "wasi"
389
390            [[command]]
391            name = "cmd"
392            module = "foo"
393        "#;
394        let manifest = temp.path().join("wasmer.toml");
395        std::fs::write(&manifest, wasmer_toml).unwrap();
396
397        let atom_path = temp.path().join("foo.wasm");
398        std::fs::write(&atom_path, b"").unwrap();
399
400        let webc: Container = Package::from_manifest(&manifest).unwrap().into();
401
402        let tasks = task_manager();
403        let mut runtime = PluggableRuntime::new(tasks);
404        runtime.set_package_loader(
405            BuiltinPackageLoader::new()
406                .with_shared_http_client(runtime.http_client().unwrap().clone()),
407        );
408
409        let pkg = BinaryPackage::from_dir(temp.path(), &runtime)
410            .await
411            .unwrap();
412
413        assert_eq!(pkg.commands.len(), 1);
414        let command = pkg.get_command("cmd").unwrap();
415        let atom_sha256_hash = sha2::Sha256::digest(webc.get_atom("foo").unwrap()).into();
416        let module_hash = ModuleHash::from_bytes(atom_sha256_hash);
417        assert_eq!(command.hash(), &module_hash);
418        assert_eq!(command.package(), &pkg.id);
419        assert_eq!(pkg.get_command_origin_package("cmd"), Some(&pkg.id));
420        assert_eq!(command.origin_package(), &pkg.id);
421    }
422}