1use std::{
2 collections::{BTreeMap, HashMap, HashSet},
3 fmt::Debug,
4 path::{Path, PathBuf},
5 sync::Arc,
6};
7
8use anyhow::{Context, Error};
9use futures::{StreamExt, TryStreamExt, future::BoxFuture};
10use once_cell::sync::OnceCell;
11use petgraph::visit::EdgeRef;
12use virtual_fs::{FileSystem, UnionFileSystem, WebcVolumeFileSystem};
13use wasmer_config::package::PackageId;
14use wasmer_package::utils::wasm_annotations_to_features;
15use webc::metadata::annotations::Atom as AtomAnnotation;
16use webc::{Container, Volume};
17
18use crate::{
19 bin_factory::{BinaryPackage, BinaryPackageCommand},
20 runtime::{
21 package_loader::PackageLoader,
22 resolver::{
23 DependencyGraph, ItemLocation, PackageSummary, Resolution, ResolvedFileSystemMapping,
24 ResolvedPackage,
25 },
26 },
27};
28
29use super::to_module_hash;
30
31fn wasm_annotation_to_features(
33 wasm_annotation: &webc::metadata::annotations::Wasm,
34) -> Option<wasmer_types::Features> {
35 Some(wasm_annotations_to_features(&wasm_annotation.features))
36}
37
38fn extract_features_from_atom_metadata(
40 atom_metadata: &webc::metadata::Atom,
41) -> Option<wasmer_types::Features> {
42 if let Ok(Some(wasm_annotation)) = atom_metadata
43 .annotation::<webc::metadata::annotations::Wasm>(webc::metadata::annotations::Wasm::KEY)
44 {
45 wasm_annotation_to_features(&wasm_annotation)
46 } else {
47 None
48 }
49}
50
51const MAX_PARALLEL_DOWNLOADS: usize = 32;
53
54#[tracing::instrument(level = "debug", skip_all)]
56pub async fn load_package_tree(
57 root: &Container,
58 loader: &dyn PackageLoader,
59 resolution: &Resolution,
60 root_is_local_dir: bool,
61) -> Result<BinaryPackage, Error> {
62 let mut containers = fetch_dependencies(loader, &resolution.package, &resolution.graph).await?;
63 containers.insert(resolution.package.root_package.clone(), root.clone());
64 let package_ids = containers.keys().cloned().collect();
65 let fs_opt = filesystem(&containers, &resolution.package, root_is_local_dir)?;
66
67 let root = &resolution.package.root_package;
68 let commands: Vec<BinaryPackageCommand> =
69 commands(&resolution.package.commands, &containers, resolution)?;
70
71 let file_system_memory_footprint = if let Some(fs) = &fs_opt {
72 count_file_system(fs, Path::new("/"))
73 } else {
74 0
75 };
76
77 let loaded = BinaryPackage {
78 id: root.clone(),
79 package_ids,
80 when_cached: crate::syscalls::platform_clock_time_get(
81 wasmer_wasix_types::wasi::Snapshot0Clockid::Monotonic,
82 1_000_000,
83 )
84 .ok()
85 .map(|ts| ts as u128),
86 hash: OnceCell::new(),
87 entrypoint_cmd: resolution.package.entrypoint.clone(),
88 webc_fs: fs_opt.map(Arc::new),
89 commands,
90 uses: Vec::new(),
91 file_system_memory_footprint,
92
93 additional_host_mapped_directories: vec![],
94 };
95
96 Ok(loaded)
97}
98
99fn commands(
100 commands: &BTreeMap<String, ItemLocation>,
101 containers: &HashMap<PackageId, Container>,
102 resolution: &Resolution,
103) -> Result<Vec<BinaryPackageCommand>, Error> {
104 let mut pkg_commands = Vec::new();
105
106 for (
107 name,
108 ItemLocation {
109 name: original_name,
110 package,
111 },
112 ) in commands
113 {
114 let webc = &containers[package];
115 let manifest = webc.manifest();
116 let command_metadata = &manifest.commands[original_name];
117
118 if let Some(cmd) =
119 load_binary_command(package, name, command_metadata, containers, resolution)?
120 {
121 pkg_commands.push(cmd);
122 }
123 }
124
125 Ok(pkg_commands)
126}
127
128#[tracing::instrument(skip_all, fields(%package_id, %command_name))]
131fn load_binary_command(
132 package_id: &PackageId,
133 command_name: &str,
134 cmd: &webc::metadata::Command,
135 containers: &HashMap<PackageId, Container>,
136 resolution: &Resolution,
137) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
138 let AtomAnnotation {
139 name: atom_name,
140 dependency,
141 ..
142 } = match atom_name_for_command(command_name, cmd)? {
143 Some(name) => name,
144 None => {
145 tracing::warn!(
146 cmd.name=command_name,
147 cmd.runner=%cmd.runner,
148 "Skipping unsupported command",
149 );
150 return Ok(None);
151 }
152 };
153
154 let package = &containers[package_id];
155
156 let (webc, resolved_package_id) = match dependency {
157 Some(dep) => {
158 let ix = resolution
159 .graph
160 .packages()
161 .get(package_id)
162 .copied()
163 .unwrap();
164 let graph = resolution.graph.graph();
165 let edge_reference = graph
166 .edges_directed(ix, petgraph::Direction::Outgoing)
167 .find(|edge| edge.weight().alias == dep)
168 .with_context(|| format!("Unable to find the \"{dep}\" dependency for the \"{command_name}\" command in \"{package_id}\""))?;
169
170 let other_package = graph.node_weight(edge_reference.target()).unwrap();
171 let id = &other_package.id;
172
173 tracing::debug!(
174 dependency=%dep,
175 resolved_package_id=%id,
176 "command atom resolution: resolved dependency",
177 );
178 (&containers[id], id)
179 }
180 None => (package, package_id),
181 };
182
183 let atom = webc.get_atom(&atom_name);
184
185 if atom.is_none() && cmd.annotations.is_empty() {
186 tracing::info!("applying legacy atom hack");
187 return legacy_atom_hack(webc, command_name, cmd);
188 }
189
190 let hash = to_module_hash(webc.manifest().atom_signature(&atom_name)?);
191
192 let atom = atom.with_context(|| {
193
194 let available_atoms = webc.atoms().keys().map(|x| x.as_str()).collect::<Vec<_>>().join(",");
195
196 tracing::warn!(
197 %atom_name,
198 %resolved_package_id,
199 %available_atoms,
200 "invalid command: could not find atom in package",
201 );
202
203 format!(
204 "The '{command_name}' command uses the '{atom_name}' atom, but it isn't present in the package: {resolved_package_id})"
205 )
206 })?;
207
208 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
210 extract_features_from_atom_metadata(atom_metadata)
211 } else {
212 None
213 };
214
215 let cmd =
216 BinaryPackageCommand::new(command_name.to_string(), cmd.clone(), atom, hash, features);
217
218 Ok(Some(cmd))
219}
220
221fn atom_name_for_command(
222 command_name: &str,
223 cmd: &webc::metadata::Command,
224) -> Result<Option<AtomAnnotation>, anyhow::Error> {
225 use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
226
227 if let Some(atom) = cmd
228 .atom()
229 .context("Unable to deserialize atom annotations")?
230 {
231 return Ok(Some(atom));
232 }
233
234 if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
235 .iter()
236 .any(|uri| cmd.runner.starts_with(uri))
237 {
238 tracing::debug!(
242 command = command_name,
243 "No annotations specifying the atom name found. Falling back to the command name"
244 );
245 return Ok(Some(AtomAnnotation::new(command_name, None)));
246 }
247
248 Ok(None)
249}
250
251fn legacy_atom_hack(
262 webc: &Container,
263 command_name: &str,
264 metadata: &webc::metadata::Command,
265) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
266 let (name, atom) = webc
267 .atoms()
268 .into_iter()
269 .next()
270 .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
271
272 tracing::debug!(
273 command_name,
274 atom.name = name.as_str(),
275 atom.len = atom.len(),
276 "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
277 );
278
279 let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
280
281 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
283 extract_features_from_atom_metadata(atom_metadata)
284 } else {
285 None
286 };
287
288 Ok(Some(BinaryPackageCommand::new(
289 command_name.to_string(),
290 metadata.clone(),
291 atom,
292 hash,
293 features,
294 )))
295}
296
297async fn fetch_dependencies(
298 loader: &dyn PackageLoader,
299 pkg: &ResolvedPackage,
300 graph: &DependencyGraph,
301) -> Result<HashMap<PackageId, Container>, Error> {
302 let mut packages = HashSet::new();
303
304 for loc in pkg.commands.values() {
305 packages.insert(loc.package.clone());
306 }
307
308 for mapping in &pkg.filesystem {
309 packages.insert(mapping.package.clone());
310 }
311
312 packages.remove(&pkg.root_package);
314
315 let packages = packages.into_iter().filter_map(|id| {
316 let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
317 let summary = PackageSummary {
318 pkg: pkg.clone(),
319 dist: dist.clone()?,
320 };
321 Some((id, summary))
322 });
323 let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
324 .map(|(id, s)| async move {
325 match loader.load(&s).await {
326 Ok(webc) => Ok((id, webc)),
327 Err(e) => Err(e),
328 }
329 })
330 .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
331 .try_collect()
332 .await?;
333
334 Ok(packages)
335}
336
337fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 {
339 let mut total = 0;
340
341 let dir = match fs.read_dir(path) {
342 Ok(d) => d,
343 Err(_err) => {
344 return 0;
345 }
346 };
347
348 for entry in dir.flatten() {
349 if let Ok(meta) = entry.metadata() {
350 total += meta.len();
351 if meta.is_dir() {
352 total += count_file_system(fs, entry.path.as_path());
353 }
354 }
355 }
356
357 total
358}
359
360fn filesystem(
365 packages: &HashMap<PackageId, Container>,
366 pkg: &ResolvedPackage,
367 root_is_local_dir: bool,
368) -> Result<Option<UnionFileSystem>, Error> {
369 if pkg.filesystem.is_empty() {
370 return Ok(None);
371 }
372
373 let mut found_v2 = None;
374 let mut found_v3 = None;
375
376 for ResolvedFileSystemMapping { package, .. } in &pkg.filesystem {
377 let container = packages.get(package).with_context(|| {
378 format!(
379 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
380 pkg.root_package, package,
381 )
382 })?;
383
384 match container.version() {
385 webc::Version::V1 => {
386 anyhow::bail!(
387 "the package '{package}' is a webc v1 package, but webc v1 support was removed"
388 );
389 }
390 webc::Version::V2 => {
391 if found_v2.is_none() {
392 found_v2 = Some(package.clone());
393 }
394 }
395 webc::Version::V3 => {
396 if found_v3.is_none() {
397 found_v3 = Some(package.clone());
398 }
399 }
400 other => {
401 anyhow::bail!("the package '{package}' has an unknown webc version: {other}");
402 }
403 }
404 }
405
406 match (found_v2, found_v3) {
407 (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir).map(Some),
408 (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir).map(Some),
409 (Some(v2), Some(v3)) => {
410 anyhow::bail!(
411 "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
412 )
413 }
414 (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
415 }
416}
417
418fn filesystem_v3(
420 packages: &HashMap<PackageId, Container>,
421 pkg: &ResolvedPackage,
422 root_is_local_dir: bool,
423) -> Result<UnionFileSystem, Error> {
424 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
425
426 let union_fs = UnionFileSystem::new();
427
428 for ResolvedFileSystemMapping {
429 mount_path,
430 volume_name,
431 package,
432 ..
433 } in &pkg.filesystem
434 {
435 if *package == pkg.root_package && root_is_local_dir {
436 continue;
437 }
438
439 if mount_path.as_path() == Path::new("/") {
440 tracing::warn!(
441 "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
442 );
443 }
444
445 let container = packages.get(package).with_context(|| {
450 format!(
451 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
452 pkg.root_package, package,
453 )
454 })?;
455 let container_volumes = match volumes.entry(package) {
456 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
457 std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
458 };
459
460 let volume = container_volumes.get(volume_name).with_context(|| {
461 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
462 })?;
463
464 let webc_vol = WebcVolumeFileSystem::new(volume.clone());
465 union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?;
466 }
467
468 Ok(union_fs)
469}
470
471fn filesystem_v2(
495 packages: &HashMap<PackageId, Container>,
496 pkg: &ResolvedPackage,
497 root_is_local_dir: bool,
498) -> Result<UnionFileSystem, Error> {
499 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
500
501 let union_fs = UnionFileSystem::new();
502
503 for ResolvedFileSystemMapping {
504 mount_path,
505 volume_name,
506 package,
507 original_path,
508 } in &pkg.filesystem
509 {
510 if *package == pkg.root_package && root_is_local_dir {
511 continue;
512 }
513
514 if mount_path.as_path() == Path::new("/") {
515 tracing::warn!(
516 "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
517 );
518 }
519
520 let container_volumes = match volumes.entry(package) {
524 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
525 std::collections::hash_map::Entry::Vacant(entry) => {
526 let container = packages.get(package)
528 .with_context(|| format!(
529 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
530 pkg.root_package,
531 package,
532 ))?;
533 &*entry.insert(container.volumes())
534 }
535 };
536
537 let volume = container_volumes.get(volume_name).with_context(|| {
538 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
539 })?;
540
541 let fs = if let Some(original) = original_path {
545 let original = PathBuf::from(original);
546
547 MappedPathFileSystem::new(
548 WebcVolumeFileSystem::new(volume.clone()),
549 Box::new(move |path: &Path| Ok(original.join(strip_root_prefix(path))))
550 as DynPathMapper,
551 )
552 } else {
553 MappedPathFileSystem::new(
554 WebcVolumeFileSystem::new(volume.clone()),
555 Box::new(move |path: &Path| Ok(strip_root_prefix(path))) as DynPathMapper,
556 )
557 };
558
559 union_fs.mount(volume_name.clone(), mount_path, Box::new(fs))?;
560 }
561
562 Ok(union_fs)
563}
564
565fn strip_root_prefix(path: &Path) -> PathBuf {
566 path.strip_prefix("/").unwrap_or(path).to_owned()
567}
568
569type DynPathMapper = Box<dyn Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync>;
570
571struct MappedPathFileSystem<F, M> {
572 inner: F,
573 map: M,
574}
575
576impl<F, M> MappedPathFileSystem<F, M>
577where
578 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
579{
580 fn new(inner: F, map: M) -> Self {
581 MappedPathFileSystem { inner, map }
582 }
583
584 fn path(&self, path: &Path) -> Result<PathBuf, virtual_fs::FsError> {
585 let path = (self.map)(path)?;
586
587 Ok(Path::new("/").join(path))
589 }
590}
591
592impl<M, F> FileSystem for MappedPathFileSystem<F, M>
593where
594 F: FileSystem,
595 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
596{
597 fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
598 let path = self.path(path)?;
599 self.inner.readlink(&path)
600 }
601
602 fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
603 let path = self.path(path)?;
604 self.inner.read_dir(&path)
605 }
606
607 fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
608 let path = self.path(path)?;
609 self.inner.create_dir(&path)
610 }
611
612 fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
613 let path = self.path(path)?;
614 self.inner.remove_dir(&path)
615 }
616
617 fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
618 let from = from.to_owned();
619 let to = to.to_owned();
620 Box::pin(async move {
621 let from = self.path(&from)?;
622 let to = self.path(&to)?;
623 self.inner.rename(&from, &to).await
624 })
625 }
626
627 fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
628 let path = self.path(path)?;
629 self.inner.metadata(&path)
630 }
631
632 fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
633 let path = self.path(path)?;
634 self.inner.symlink_metadata(&path)
635 }
636
637 fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
638 let path = self.path(path)?;
639 self.inner.remove_file(&path)
640 }
641
642 fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
643 virtual_fs::OpenOptions::new(self)
644 }
645
646 fn mount(
647 &self,
648 name: String,
649 path: &Path,
650 fs: Box<dyn FileSystem + Send + Sync>,
651 ) -> virtual_fs::Result<()> {
652 let path = self.path(path)?;
653 self.inner.mount(name, path.as_path(), fs)
654 }
655}
656
657impl<F, M> virtual_fs::FileOpener for MappedPathFileSystem<F, M>
658where
659 F: FileSystem,
660 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
661{
662 fn open(
663 &self,
664 path: &Path,
665 conf: &virtual_fs::OpenOptionsConfig,
666 ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
667 let path = self.path(path)?;
668 self.inner
669 .new_open_options()
670 .options(conf.clone())
671 .open(path)
672 }
673}
674
675impl<F, M> Debug for MappedPathFileSystem<F, M>
676where
677 F: Debug,
678{
679 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680 f.debug_struct("MappedPathFileSystem")
681 .field("inner", &self.inner)
682 .field("map", &std::any::type_name::<M>())
683 .finish()
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use std::{
690 collections::{BTreeMap, HashMap},
691 path::{Path, PathBuf},
692 };
693
694 use ciborium::value::Value;
695 use virtual_fs::FileSystem;
696 use wasmer_config::package::PackageId;
697 use webc::{
698 Container,
699 indexmap::IndexMap,
700 metadata::{
701 Manifest,
702 annotations::{FileSystemMapping, FileSystemMappings},
703 },
704 v2::{
705 SignatureAlgorithm,
706 read::OwnedReader,
707 write::{DirEntry, Directory, FileEntry, Writer},
708 },
709 };
710
711 use super::{ResolvedFileSystemMapping, ResolvedPackage, filesystem_v2};
712
713 #[test]
714 fn v2_filesystem_mapping_resolves_mount_paths() {
715 let mut manifest = Manifest::default();
718 let fs = FileSystemMappings(vec![FileSystemMapping {
719 from: None,
720 volume_name: "atom".to_string(),
721 host_path: Some("/public".to_string()),
722 mount_path: "/public".to_string(),
723 }]);
724 let mut package = IndexMap::new();
725 package.insert(
726 FileSystemMappings::KEY.to_string(),
727 Value::serialized(&fs).unwrap(),
728 );
729 manifest.package = package;
730
731 let mut public_children = BTreeMap::new();
732 public_children.insert(
733 "index.html".parse().unwrap(),
734 DirEntry::File(FileEntry::from(b"ok".as_slice())),
735 );
736 let public_dir = Directory {
737 children: public_children,
738 };
739 let mut root_children = BTreeMap::new();
740 root_children.insert("public".parse().unwrap(), DirEntry::Dir(public_dir));
741 let atom_dir = Directory {
742 children: root_children,
743 };
744
745 let writer = Writer::default().write_manifest(&manifest).unwrap();
746 let writer = writer.write_atoms(BTreeMap::new()).unwrap();
747 let writer = writer.with_volume("atom", atom_dir).unwrap();
748 let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
749
750 let reader = OwnedReader::parse(bytes).unwrap();
751 let container = Container::from(reader);
752
753 let pkg_id = PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap());
754 let mut packages = HashMap::new();
755 packages.insert(pkg_id.clone(), container);
756
757 let pkg = ResolvedPackage {
758 root_package: pkg_id.clone(),
759 commands: BTreeMap::new(),
760 entrypoint: None,
761 filesystem: vec![ResolvedFileSystemMapping {
762 mount_path: PathBuf::from("/public"),
763 volume_name: "atom".to_string(),
764 original_path: Some("/public".to_string()),
765 package: pkg_id,
766 }],
767 };
768
769 let union_fs = filesystem_v2(&packages, &pkg, false).unwrap();
770 assert!(union_fs.metadata(Path::new("/public")).unwrap().is_dir());
771 assert!(
772 union_fs
773 .metadata(Path::new("/public/index.html"))
774 .unwrap()
775 .is_file()
776 );
777 }
778}