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