wasmer_wasix/bin_factory/
binary_package.rs1use 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 pub fn atom(&self) -> SharedBytes {
57 self.atom.clone()
58 }
59
60 pub fn atom_ref(&self) -> &SharedBytes {
63 &self.atom
64 }
65
66 pub fn hash(&self) -> &ModuleHash {
67 &self.hash
68 }
69
70 pub fn wasm_features(&self) -> Option<wasmer_types::Features> {
72 if let Some(features) = &self.features {
74 return Some(features.clone());
75 }
76
77 None
79 }
80}
81
82#[derive(Debug, Clone)]
84pub struct BinaryPackage {
85 pub id: PackageId,
86 pub package_ids: Vec<PackageId>,
88
89 pub when_cached: Option<u128>,
90 pub entrypoint_cmd: Option<String>,
93 pub hash: OnceCell<ModuleHash>,
94 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 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 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 #[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 #[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 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 #[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 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 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}