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