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