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, 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_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 suggested_compiler_optimizations =
216 if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
217 extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
218 } else {
219 wasmer_config::package::SuggestedCompilerOptimizations::default()
220 };
221
222 let cmd = BinaryPackageCommand::new(
223 command_name.to_string(),
224 cmd.clone(),
225 atom,
226 hash,
227 features,
228 suggested_compiler_optimizations,
229 );
230
231 Ok(Some(cmd))
232}
233
234fn extract_suggested_compiler_opts_from_atom_metadata(
235 atom_metadata: &webc::metadata::Atom,
236) -> wasmer_config::package::SuggestedCompilerOptimizations {
237 let mut ret = SuggestedCompilerOptimizations::default();
238
239 if let Some(sco) = atom_metadata
240 .annotations
241 .get(SuggestedCompilerOptimizations::KEY)
242 && let Some((_, v)) = sco.as_map().and_then(|v| {
243 v.iter().find(|(k, _)| {
244 k.as_text()
245 .is_some_and(|v| v == SuggestedCompilerOptimizations::PASS_PARAMS_KEY)
246 })
247 })
248 {
249 ret.pass_params = v.as_bool()
250 }
251
252 ret
253}
254
255fn atom_name_for_command(
256 command_name: &str,
257 cmd: &webc::metadata::Command,
258) -> Result<Option<AtomAnnotation>, anyhow::Error> {
259 use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
260
261 if let Some(atom) = cmd
262 .atom()
263 .context("Unable to deserialize atom annotations")?
264 {
265 return Ok(Some(atom));
266 }
267
268 if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
269 .iter()
270 .any(|uri| cmd.runner.starts_with(uri))
271 {
272 tracing::debug!(
276 command = command_name,
277 "No annotations specifying the atom name found. Falling back to the command name"
278 );
279 return Ok(Some(AtomAnnotation::new(command_name, None)));
280 }
281
282 Ok(None)
283}
284
285fn legacy_atom_hack(
296 webc: &Container,
297 command_name: &str,
298 metadata: &webc::metadata::Command,
299) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
300 let (name, atom) = webc
301 .atoms()
302 .into_iter()
303 .next()
304 .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
305
306 tracing::debug!(
307 command_name,
308 atom.name = name.as_str(),
309 atom.len = atom.len(),
310 "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
311 );
312
313 let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
314
315 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
317 extract_features_from_atom_metadata(atom_metadata)
318 } else {
319 None
320 };
321
322 let suggested_opts_from_manifest = if let Some(atom_metadata) = webc.manifest().atoms.get(&name)
324 {
325 extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
326 } else {
327 SuggestedCompilerOptimizations::default()
328 };
329
330 Ok(Some(BinaryPackageCommand::new(
331 command_name.to_string(),
332 metadata.clone(),
333 atom,
334 hash,
335 features,
336 suggested_opts_from_manifest,
337 )))
338}
339
340async fn fetch_dependencies(
341 loader: &dyn PackageLoader,
342 pkg: &ResolvedPackage,
343 graph: &DependencyGraph,
344) -> Result<HashMap<PackageId, Container>, Error> {
345 let mut packages = HashSet::new();
346
347 for loc in pkg.commands.values() {
348 packages.insert(loc.package.clone());
349 }
350
351 for mapping in &pkg.filesystem {
352 packages.insert(mapping.package.clone());
353 }
354
355 packages.remove(&pkg.root_package);
357
358 let packages = packages.into_iter().filter_map(|id| {
359 let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
360 let summary = PackageSummary {
361 pkg: pkg.clone(),
362 dist: dist.clone()?,
363 };
364 Some((id, summary))
365 });
366 let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
367 .map(|(id, s)| async move {
368 match loader.load(&s).await {
369 Ok(webc) => Ok((id, webc)),
370 Err(e) => Err(e),
371 }
372 })
373 .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
374 .try_collect()
375 .await?;
376
377 Ok(packages)
378}
379
380fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 {
382 let mut total = 0;
383
384 let dir = match fs.read_dir(path) {
385 Ok(d) => d,
386 Err(_err) => {
387 return 0;
388 }
389 };
390
391 for entry in dir.flatten() {
392 if let Ok(meta) = entry.metadata() {
393 total += meta.len();
394 if meta.is_dir() {
395 total += count_file_system(fs, entry.path.as_path());
396 }
397 }
398 }
399
400 total
401}
402
403fn filesystem(
408 packages: &HashMap<PackageId, Container>,
409 pkg: &ResolvedPackage,
410 root_is_local_dir: bool,
411) -> Result<Option<UnionFileSystem>, Error> {
412 if pkg.filesystem.is_empty() {
413 return Ok(None);
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 match container.version() {
428 webc::Version::V1 => {
429 anyhow::bail!(
430 "the package '{package}' is a webc v1 package, but webc v1 support was removed"
431 );
432 }
433 webc::Version::V2 => {
434 if found_v2.is_none() {
435 found_v2 = Some(package.clone());
436 }
437 }
438 webc::Version::V3 => {
439 if found_v3.is_none() {
440 found_v3 = Some(package.clone());
441 }
442 }
443 other => {
444 anyhow::bail!("the package '{package}' has an unknown webc version: {other}");
445 }
446 }
447 }
448
449 match (found_v2, found_v3) {
450 (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir).map(Some),
451 (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir).map(Some),
452 (Some(v2), Some(v3)) => {
453 anyhow::bail!(
454 "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
455 )
456 }
457 (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
458 }
459}
460
461fn filesystem_v3(
463 packages: &HashMap<PackageId, Container>,
464 pkg: &ResolvedPackage,
465 root_is_local_dir: bool,
466) -> Result<UnionFileSystem, Error> {
467 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
468
469 let mut mountings: Vec<_> = pkg.filesystem.iter().collect();
470 mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path()));
471
472 let union_fs = UnionFileSystem::new();
473
474 for ResolvedFileSystemMapping {
475 mount_path,
476 volume_name,
477 package,
478 ..
479 } in &pkg.filesystem
480 {
481 if *package == pkg.root_package && root_is_local_dir {
482 continue;
483 }
484
485 let container = packages.get(package).with_context(|| {
490 format!(
491 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
492 pkg.root_package, package,
493 )
494 })?;
495 let container_volumes = match volumes.entry(package) {
496 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
497 std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
498 };
499
500 let volume = container_volumes.get(volume_name).with_context(|| {
501 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
502 })?;
503
504 let webc_vol = WebcVolumeFileSystem::new(volume.clone());
505 union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?;
506 }
507
508 Ok(union_fs)
509}
510
511fn filesystem_v2(
535 packages: &HashMap<PackageId, Container>,
536 pkg: &ResolvedPackage,
537 root_is_local_dir: bool,
538) -> Result<UnionFileSystem, Error> {
539 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
540
541 let mut mountings: Vec<_> = pkg.filesystem.iter().collect();
542 mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path()));
543
544 let union_fs = UnionFileSystem::new();
545
546 for ResolvedFileSystemMapping {
547 mount_path,
548 volume_name,
549 package,
550 original_path,
551 } in &pkg.filesystem
552 {
553 if *package == pkg.root_package && root_is_local_dir {
554 continue;
555 }
556
557 let container_volumes = match volumes.entry(package) {
561 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
562 std::collections::hash_map::Entry::Vacant(entry) => {
563 let container = packages.get(package)
565 .with_context(|| format!(
566 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
567 pkg.root_package,
568 package,
569 ))?;
570 &*entry.insert(container.volumes())
571 }
572 };
573
574 let volume = container_volumes.get(volume_name).with_context(|| {
575 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
576 })?;
577
578 let mount_path_clone = mount_path.clone();
579 let fs = if let Some(original) = original_path {
582 let original = PathBuf::from(original);
583
584 MappedPathFileSystem::new(
585 WebcVolumeFileSystem::new(volume.clone()),
586 Box::new(move |path: &Path| {
587 let without_mount_dir = path
588 .strip_prefix(&mount_path_clone)
589 .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?;
590 Ok(original.join(without_mount_dir))
591 }) as DynPathMapper,
592 )
593 } else {
594 MappedPathFileSystem::new(
595 WebcVolumeFileSystem::new(volume.clone()),
596 Box::new(move |path: &Path| {
597 let without_mount_dir = path
598 .strip_prefix(&mount_path_clone)
599 .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?;
600 Ok(without_mount_dir.to_owned())
601 }) as DynPathMapper,
602 )
603 };
604
605 union_fs.mount(volume_name.clone(), mount_path, Box::new(fs))?;
606 }
607
608 Ok(union_fs)
609}
610
611type DynPathMapper = Box<dyn Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync>;
612
613struct MappedPathFileSystem<F, M> {
614 inner: F,
615 map: M,
616}
617
618impl<F, M> MappedPathFileSystem<F, M>
619where
620 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
621{
622 fn new(inner: F, map: M) -> Self {
623 MappedPathFileSystem { inner, map }
624 }
625
626 fn path(&self, path: &Path) -> Result<PathBuf, virtual_fs::FsError> {
627 let path = (self.map)(path)?;
628
629 Ok(Path::new("/").join(path))
631 }
632}
633
634impl<M, F> FileSystem for MappedPathFileSystem<F, M>
635where
636 F: FileSystem,
637 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
638{
639 fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
640 let path = self.path(path)?;
641 self.inner.readlink(&path)
642 }
643
644 fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
645 let path = self.path(path)?;
646 self.inner.read_dir(&path)
647 }
648
649 fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
650 let path = self.path(path)?;
651 self.inner.create_dir(&path)
652 }
653
654 fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
655 let path = self.path(path)?;
656 self.inner.remove_dir(&path)
657 }
658
659 fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
660 let from = from.to_owned();
661 let to = to.to_owned();
662 Box::pin(async move {
663 let from = self.path(&from)?;
664 let to = self.path(&to)?;
665 self.inner.rename(&from, &to).await
666 })
667 }
668
669 fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
670 let path = self.path(path)?;
671 self.inner.metadata(&path)
672 }
673
674 fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
675 let path = self.path(path)?;
676 self.inner.symlink_metadata(&path)
677 }
678
679 fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
680 let path = self.path(path)?;
681 self.inner.remove_file(&path)
682 }
683
684 fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
685 virtual_fs::OpenOptions::new(self)
686 }
687
688 fn mount(
689 &self,
690 name: String,
691 path: &Path,
692 fs: Box<dyn FileSystem + Send + Sync>,
693 ) -> virtual_fs::Result<()> {
694 let path = self.path(path)?;
695 self.inner.mount(name, path.as_path(), fs)
696 }
697}
698
699impl<F, M> virtual_fs::FileOpener for MappedPathFileSystem<F, M>
700where
701 F: FileSystem,
702 M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
703{
704 fn open(
705 &self,
706 path: &Path,
707 conf: &virtual_fs::OpenOptionsConfig,
708 ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
709 let path = self.path(path)?;
710 self.inner
711 .new_open_options()
712 .options(conf.clone())
713 .open(path)
714 }
715}
716
717impl<F, M> Debug for MappedPathFileSystem<F, M>
718where
719 F: Debug,
720{
721 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
722 f.debug_struct("MappedPathFileSystem")
723 .field("inner", &self.inner)
724 .field("map", &std::any::type_name::<M>())
725 .finish()
726 }
727}