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, OverlayFileSystem, UnionFileSystem, WebcVolumeFileSystem};
13use wasmer_config::package::{PackageId, SuggestedCompilerOptimizations};
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 = 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 = count_file_system(&fs, Path::new("/"));
72
73 let loaded = BinaryPackage {
74 id: root.clone(),
75 package_ids,
76 when_cached: crate::syscalls::platform_clock_time_get(
77 wasmer_wasix_types::wasi::Snapshot0Clockid::Monotonic,
78 1_000_000,
79 )
80 .ok()
81 .map(|ts| ts as u128),
82 hash: OnceCell::new(),
83 entrypoint_cmd: resolution.package.entrypoint.clone(),
84 webc_fs: Arc::new(fs),
85 commands,
86 uses: Vec::new(),
87 file_system_memory_footprint,
88
89 additional_host_mapped_directories: vec![],
90 };
91
92 Ok(loaded)
93}
94
95fn commands(
96 commands: &BTreeMap<String, ItemLocation>,
97 containers: &HashMap<PackageId, Container>,
98 resolution: &Resolution,
99) -> Result<Vec<BinaryPackageCommand>, Error> {
100 let mut pkg_commands = Vec::new();
101
102 for (
103 name,
104 ItemLocation {
105 name: original_name,
106 package,
107 },
108 ) in commands
109 {
110 let webc = &containers[package];
111 let manifest = webc.manifest();
112 let command_metadata = &manifest.commands[original_name];
113
114 if let Some(cmd) =
115 load_binary_command(package, name, command_metadata, containers, resolution)?
116 {
117 pkg_commands.push(cmd);
118 }
119 }
120
121 Ok(pkg_commands)
122}
123
124#[tracing::instrument(skip_all, fields(%package_id, %command_name))]
127fn load_binary_command(
128 package_id: &PackageId,
129 command_name: &str,
130 cmd: &webc::metadata::Command,
131 containers: &HashMap<PackageId, Container>,
132 resolution: &Resolution,
133) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
134 let AtomAnnotation {
135 name: atom_name,
136 dependency,
137 ..
138 } = match atom_name_for_command(command_name, cmd)? {
139 Some(name) => name,
140 None => {
141 tracing::warn!(
142 cmd.name=command_name,
143 cmd.runner=%cmd.runner,
144 "Skipping unsupported command",
145 );
146 return Ok(None);
147 }
148 };
149
150 let package = &containers[package_id];
151
152 let (webc, resolved_package_id) = match dependency {
153 Some(dep) => {
154 let ix = resolution
155 .graph
156 .packages()
157 .get(package_id)
158 .copied()
159 .unwrap();
160 let graph = resolution.graph.graph();
161 let edge_reference = graph
162 .edges_directed(ix, petgraph::Direction::Outgoing)
163 .find(|edge| edge.weight().alias == dep)
164 .with_context(|| format!("Unable to find the \"{dep}\" dependency for the \"{command_name}\" command in \"{package_id}\""))?;
165
166 let other_package = graph.node_weight(edge_reference.target()).unwrap();
167 let id = &other_package.id;
168
169 tracing::debug!(
170 dependency=%dep,
171 resolved_package_id=%id,
172 "command atom resolution: resolved dependency",
173 );
174 (&containers[id], id)
175 }
176 None => (package, package_id),
177 };
178
179 let atom = webc.get_atom(&atom_name);
180
181 if atom.is_none() && cmd.annotations.is_empty() {
182 tracing::info!("applying legacy atom hack");
183 return legacy_atom_hack(webc, command_name, cmd);
184 }
185
186 let hash = to_module_hash(webc.manifest().atom_signature(&atom_name)?);
187
188 let atom = atom.with_context(|| {
189
190 let available_atoms = webc.atoms().keys().map(|x| x.as_str()).collect::<Vec<_>>().join(",");
191
192 tracing::warn!(
193 %atom_name,
194 %resolved_package_id,
195 %available_atoms,
196 "invalid command: could not find atom in package",
197 );
198
199 format!(
200 "The '{command_name}' command uses the '{atom_name}' atom, but it isn't present in the package: {resolved_package_id})"
201 )
202 })?;
203
204 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
206 extract_features_from_atom_metadata(atom_metadata)
207 } else {
208 None
209 };
210
211 let suggested_compiler_optimizations =
212 if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
213 extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
214 } else {
215 wasmer_config::package::SuggestedCompilerOptimizations::default()
216 };
217
218 let cmd = BinaryPackageCommand::new(
219 command_name.to_string(),
220 cmd.clone(),
221 atom,
222 hash,
223 features,
224 suggested_compiler_optimizations,
225 );
226
227 Ok(Some(cmd))
228}
229
230fn extract_suggested_compiler_opts_from_atom_metadata(
231 atom_metadata: &webc::metadata::Atom,
232) -> wasmer_config::package::SuggestedCompilerOptimizations {
233 let mut ret = SuggestedCompilerOptimizations::default();
234
235 if let Some(sco) = atom_metadata
236 .annotations
237 .get(SuggestedCompilerOptimizations::KEY)
238 {
239 if let Some((_, v)) = sco.as_map().and_then(|v| {
240 v.iter().find(|(k, _)| {
241 k.as_text()
242 .is_some_and(|v| v == SuggestedCompilerOptimizations::PASS_PARAMS_KEY)
243 })
244 }) {
245 ret.pass_params = v.as_bool()
246 }
247 }
248
249 ret
250}
251
252fn atom_name_for_command(
253 command_name: &str,
254 cmd: &webc::metadata::Command,
255) -> Result<Option<AtomAnnotation>, anyhow::Error> {
256 use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
257
258 if let Some(atom) = cmd
259 .atom()
260 .context("Unable to deserialize atom annotations")?
261 {
262 return Ok(Some(atom));
263 }
264
265 if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
266 .iter()
267 .any(|uri| cmd.runner.starts_with(uri))
268 {
269 tracing::debug!(
273 command = command_name,
274 "No annotations specifying the atom name found. Falling back to the command name"
275 );
276 return Ok(Some(AtomAnnotation::new(command_name, None)));
277 }
278
279 Ok(None)
280}
281
282fn legacy_atom_hack(
293 webc: &Container,
294 command_name: &str,
295 metadata: &webc::metadata::Command,
296) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
297 let (name, atom) = webc
298 .atoms()
299 .into_iter()
300 .next()
301 .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
302
303 tracing::debug!(
304 command_name,
305 atom.name = name.as_str(),
306 atom.len = atom.len(),
307 "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
308 );
309
310 let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
311
312 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
314 extract_features_from_atom_metadata(atom_metadata)
315 } else {
316 None
317 };
318
319 let suggested_opts_from_manifest = if let Some(atom_metadata) = webc.manifest().atoms.get(&name)
321 {
322 extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
323 } else {
324 SuggestedCompilerOptimizations::default()
325 };
326
327 Ok(Some(BinaryPackageCommand::new(
328 command_name.to_string(),
329 metadata.clone(),
330 atom,
331 hash,
332 features,
333 suggested_opts_from_manifest,
334 )))
335}
336
337async fn fetch_dependencies(
338 loader: &dyn PackageLoader,
339 pkg: &ResolvedPackage,
340 graph: &DependencyGraph,
341) -> Result<HashMap<PackageId, Container>, Error> {
342 let mut packages = HashSet::new();
343
344 for loc in pkg.commands.values() {
345 packages.insert(loc.package.clone());
346 }
347
348 for mapping in &pkg.filesystem {
349 packages.insert(mapping.package.clone());
350 }
351
352 packages.remove(&pkg.root_package);
354
355 let packages = packages.into_iter().filter_map(|id| {
356 let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
357 let summary = PackageSummary {
358 pkg: pkg.clone(),
359 dist: dist.clone()?,
360 };
361 Some((id, summary))
362 });
363 let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
364 .map(|(id, s)| async move {
365 match loader.load(&s).await {
366 Ok(webc) => Ok((id, webc)),
367 Err(e) => Err(e),
368 }
369 })
370 .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
371 .try_collect()
372 .await?;
373
374 Ok(packages)
375}
376
377fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 {
379 let mut total = 0;
380
381 let dir = match fs.read_dir(path) {
382 Ok(d) => d,
383 Err(_err) => {
384 return 0;
385 }
386 };
387
388 for entry in dir.flatten() {
389 if let Ok(meta) = entry.metadata() {
390 total += meta.len();
391 if meta.is_dir() {
392 total += count_file_system(fs, entry.path.as_path());
393 }
394 }
395 }
396
397 total
398}
399
400fn filesystem(
403 packages: &HashMap<PackageId, Container>,
404 pkg: &ResolvedPackage,
405 root_is_local_dir: bool,
406) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
407 if pkg.filesystem.is_empty() {
408 return Ok(Box::new(OverlayFileSystem::<
409 virtual_fs::EmptyFileSystem,
410 Vec<WebcVolumeFileSystem>,
411 >::new(
412 virtual_fs::EmptyFileSystem::default(), vec![]
413 )));
414 }
415
416 let mut found_v2 = None;
417 let mut found_v3 = None;
418
419 for ResolvedFileSystemMapping { package, .. } in &pkg.filesystem {
420 let container = packages.get(package).with_context(|| {
421 format!(
422 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
423 pkg.root_package, package,
424 )
425 })?;
426
427 if container.version() == webc::Version::V2 && found_v2.is_none() {
428 found_v2 = Some(package.clone());
429 }
430 if container.version() == webc::Version::V3 && found_v3.is_none() {
431 found_v3 = Some(package.clone());
432 }
433 }
434
435 match (found_v2, found_v3) {
436 (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir),
437 (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir),
438 (Some(v2), Some(v3)) => {
439 anyhow::bail!(
440 "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
441 )
442 }
443 (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
444 }
445}
446
447fn filesystem_v3(
449 packages: &HashMap<PackageId, Container>,
450 pkg: &ResolvedPackage,
451 root_is_local_dir: bool,
452) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
453 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
454
455 let mut mountings: Vec<_> = pkg.filesystem.iter().collect();
456 mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path()));
457
458 let union_fs = UnionFileSystem::new();
459
460 for ResolvedFileSystemMapping {
461 mount_path,
462 volume_name,
463 package,
464 ..
465 } in &pkg.filesystem
466 {
467 if *package == pkg.root_package && root_is_local_dir {
468 continue;
469 }
470
471 let container = packages.get(package).with_context(|| {
476 format!(
477 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
478 pkg.root_package, package,
479 )
480 })?;
481 let container_volumes = match volumes.entry(package) {
482 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
483 std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
484 };
485
486 let volume = container_volumes.get(volume_name).with_context(|| {
487 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
488 })?;
489
490 let webc_vol = WebcVolumeFileSystem::new(volume.clone());
491 union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?;
492 }
493
494 let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), [union_fs]);
495
496 Ok(Box::new(fs))
497}
498
499fn filesystem_v2(
523 packages: &HashMap<PackageId, Container>,
524 pkg: &ResolvedPackage,
525 root_is_local_dir: bool,
526) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
527 let mut filesystems = Vec::new();
528 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
529
530 let mut mountings: Vec<_> = pkg.filesystem.iter().collect();
531 mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path()));
532
533 for ResolvedFileSystemMapping {
534 mount_path,
535 volume_name,
536 package,
537 original_path,
538 } in &pkg.filesystem
539 {
540 if *package == pkg.root_package && root_is_local_dir {
541 continue;
542 }
543
544 let container_volumes = match volumes.entry(package) {
548 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
549 std::collections::hash_map::Entry::Vacant(entry) => {
550 let container = packages.get(package)
552 .with_context(|| format!(
553 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
554 pkg.root_package,
555 package,
556 ))?;
557 &*entry.insert(container.volumes())
558 }
559 };
560
561 let volume = container_volumes.get(volume_name).with_context(|| {
562 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
563 })?;
564
565 let mount_path = mount_path.clone();
566 let fs = if let Some(original) = original_path {
569 let original = PathBuf::from(original);
570
571 MappedPathFileSystem::new(
572 WebcVolumeFileSystem::new(volume.clone()),
573 Box::new(move |path: &Path| {
574 let without_mount_dir = path
575 .strip_prefix(&mount_path)
576 .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?;
577 Ok(original.join(without_mount_dir))
578 }) as DynPathMapper,
579 )
580 } else {
581 MappedPathFileSystem::new(
582 WebcVolumeFileSystem::new(volume.clone()),
583 Box::new(move |path: &Path| {
584 let without_mount_dir = path
585 .strip_prefix(&mount_path)
586 .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?;
587 Ok(without_mount_dir.to_owned())
588 }) as DynPathMapper,
589 )
590 };
591
592 filesystems.push(fs);
593 }
594
595 let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), filesystems);
596
597 Ok(Box::new(fs))
598}
599
600type DynPathMapper = Box<dyn Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync>;
601
602#[derive(Clone, PartialEq)]
605struct MappedPathFileSystem<F, M> {
606 inner: F,
607 map: M,
608}
609
610impl<F, M> MappedPathFileSystem<F, M>
611where
612 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
613{
614 fn new(inner: F, map: M) -> Self {
615 MappedPathFileSystem { inner, map }
616 }
617
618 fn path(&self, path: &Path) -> Result<PathBuf, virtual_fs::FsError> {
619 let path = (self.map)(path)?;
620
621 Ok(Path::new("/").join(path))
623 }
624}
625
626impl<M, F> FileSystem for MappedPathFileSystem<F, M>
627where
628 F: FileSystem,
629 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
630{
631 fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
632 let path = self.path(path)?;
633 self.inner.readlink(&path)
634 }
635
636 fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
637 let path = self.path(path)?;
638 self.inner.read_dir(&path)
639 }
640
641 fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
642 let path = self.path(path)?;
643 self.inner.create_dir(&path)
644 }
645
646 fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
647 let path = self.path(path)?;
648 self.inner.remove_dir(&path)
649 }
650
651 fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
652 let from = from.to_owned();
653 let to = to.to_owned();
654 Box::pin(async move {
655 let from = self.path(&from)?;
656 let to = self.path(&to)?;
657 self.inner.rename(&from, &to).await
658 })
659 }
660
661 fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
662 let path = self.path(path)?;
663 self.inner.metadata(&path)
664 }
665
666 fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
667 let path = self.path(path)?;
668 self.inner.symlink_metadata(&path)
669 }
670
671 fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
672 let path = self.path(path)?;
673 self.inner.remove_file(&path)
674 }
675
676 fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
677 virtual_fs::OpenOptions::new(self)
678 }
679
680 fn mount(
681 &self,
682 name: String,
683 path: &Path,
684 fs: Box<dyn FileSystem + Send + Sync>,
685 ) -> virtual_fs::Result<()> {
686 let path = self.path(path)?;
687 self.inner.mount(name, path.as_path(), fs)
688 }
689}
690
691impl<F, M> virtual_fs::FileOpener for MappedPathFileSystem<F, M>
692where
693 F: FileSystem,
694 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
695{
696 fn open(
697 &self,
698 path: &Path,
699 conf: &virtual_fs::OpenOptionsConfig,
700 ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
701 let path = self.path(path)?;
702 self.inner
703 .new_open_options()
704 .options(conf.clone())
705 .open(path)
706 }
707}
708
709impl<F, M> Debug for MappedPathFileSystem<F, M>
710where
711 F: Debug,
712{
713 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
714 f.debug_struct("MappedPathFileSystem")
715 .field("inner", &self.inner)
716 .field("map", &std::any::type_name::<M>())
717 .finish()
718 }
719}