wasmer_wasix/runtime/package_loader/
load_package_tree.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    path::{Path, PathBuf},
4    sync::Arc,
5};
6
7use anyhow::{Context, Error};
8use futures::{StreamExt, TryStreamExt};
9use once_cell::sync::OnceCell;
10use petgraph::visit::EdgeRef;
11use virtual_fs::{FileSystem, WebcVolumeFileSystem};
12use wasmer_config::package::PackageId;
13use wasmer_package::utils::wasm_annotations_to_features;
14use webc::metadata::annotations::Atom as AtomAnnotation;
15use webc::{Container, Volume};
16
17use crate::{
18    bin_factory::{BinaryPackage, BinaryPackageCommand, BinaryPackageMount, BinaryPackageMounts},
19    runtime::{
20        package_loader::PackageLoader,
21        resolver::{
22            DependencyGraph, ItemLocation, PackageSummary, Resolution, ResolvedFileSystemMapping,
23            ResolvedPackage,
24        },
25    },
26};
27
28use super::to_module_hash;
29
30/// Convert WebAssembly feature annotations to a Features object
31fn wasm_annotation_to_features(
32    wasm_annotation: &webc::metadata::annotations::Wasm,
33) -> Option<wasmer_types::Features> {
34    Some(wasm_annotations_to_features(&wasm_annotation.features))
35}
36
37/// Extract WebAssembly features from atom metadata if available
38fn extract_features_from_atom_metadata(
39    atom_metadata: &webc::metadata::Atom,
40) -> Option<wasmer_types::Features> {
41    if let Ok(Some(wasm_annotation)) = atom_metadata
42        .annotation::<webc::metadata::annotations::Wasm>(webc::metadata::annotations::Wasm::KEY)
43    {
44        wasm_annotation_to_features(&wasm_annotation)
45    } else {
46        None
47    }
48}
49
50/// The maximum number of packages that will be loaded in parallel.
51const MAX_PARALLEL_DOWNLOADS: usize = 32;
52
53/// Given a fully resolved package, load it into memory for execution.
54#[tracing::instrument(level = "debug", skip_all)]
55pub async fn load_package_tree(
56    root: &Container,
57    loader: &dyn PackageLoader,
58    resolution: &Resolution,
59    root_is_local_dir: bool,
60) -> Result<BinaryPackage, Error> {
61    let mut containers = fetch_dependencies(loader, &resolution.package, &resolution.graph).await?;
62    containers.insert(resolution.package.root_package.clone(), root.clone());
63    let package_ids = containers.keys().cloned().collect();
64    let fs_opt = filesystem(&containers, &resolution.package, root_is_local_dir)?;
65
66    let root = &resolution.package.root_package;
67    let commands = commands(&resolution.package.commands, &containers, resolution)?;
68
69    let file_system_memory_footprint = if let Some(fs) = &fs_opt {
70        count_package_mounts(fs)
71    } else {
72        0
73    };
74
75    let loaded = BinaryPackage {
76        id: root.clone(),
77        package_ids,
78        when_cached: crate::syscalls::platform_clock_time_get(
79            wasmer_wasix_types::wasi::Snapshot0Clockid::Monotonic,
80            1_000_000,
81        )
82        .ok()
83        .map(|ts| ts as u128),
84        hash: OnceCell::new(),
85        entrypoint_cmd: resolution.package.entrypoint.clone(),
86        package_mounts: fs_opt.map(Arc::new),
87        commands,
88        uses: Vec::new(),
89        file_system_memory_footprint,
90
91        additional_host_mapped_directories: vec![],
92    };
93
94    Ok(loaded)
95}
96
97fn commands(
98    commands: &BTreeMap<String, ItemLocation>,
99    containers: &HashMap<PackageId, Container>,
100    resolution: &Resolution,
101) -> Result<Vec<BinaryPackageCommand>, Error> {
102    let mut pkg_commands = Vec::new();
103
104    for (
105        name,
106        ItemLocation {
107            name: original_name,
108            package,
109        },
110    ) in commands
111    {
112        let webc = containers.get(package).with_context(|| {
113            format!("Unable to find the \"{package}\" package for the \"{name}\" command")
114        })?;
115        let manifest = webc.manifest();
116        let command_metadata = manifest.commands.get(original_name).with_context(|| {
117            format!(
118                "Unable to find the \"{original_name}\" command metadata in the \"{package}\" package"
119            )
120        })?;
121
122        if let Some(cmd) =
123            load_binary_command(package, name, command_metadata, containers, resolution)?
124        {
125            pkg_commands.push(cmd);
126        }
127    }
128
129    Ok(pkg_commands)
130}
131
132/// Given a [`webc::metadata::Command`], figure out which atom it uses and load
133/// that atom into a [`BinaryPackageCommand`].
134#[tracing::instrument(skip_all, fields(%package_id, %command_name))]
135fn load_binary_command(
136    package_id: &PackageId,
137    command_name: &str,
138    cmd: &webc::metadata::Command,
139    containers: &HashMap<PackageId, Container>,
140    resolution: &Resolution,
141) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
142    let AtomAnnotation {
143        name: atom_name,
144        dependency,
145        ..
146    } = match atom_name_for_command(command_name, cmd)? {
147        Some(name) => name,
148        None => {
149            tracing::warn!(
150                cmd.name=command_name,
151                cmd.runner=%cmd.runner,
152                "Skipping unsupported command",
153            );
154            return Ok(None);
155        }
156    };
157
158    let package = containers
159        .get(package_id)
160        .with_context(|| format!("Unable to find the \"{package_id}\" package"))?;
161
162    let (webc, resolved_package_id) = match dependency {
163        Some(dep) => {
164            let ix = resolution
165                .graph
166                .packages()
167                .get(package_id)
168                .copied()
169                .unwrap();
170            let graph = resolution.graph.graph();
171            let edge_reference = graph
172                .edges_directed(ix, petgraph::Direction::Outgoing)
173                .find(|edge| edge.weight().alias == dep)
174                .with_context(|| format!("Unable to find the \"{dep}\" dependency for the \"{command_name}\" command in \"{package_id}\""))?;
175
176            let other_package = graph.node_weight(edge_reference.target()).unwrap();
177            let id = &other_package.id;
178
179            tracing::debug!(
180                dependency=%dep,
181                resolved_package_id=%id,
182                "command atom resolution: resolved dependency",
183            );
184            let container = containers.get(id).ok_or_else(|| {
185                anyhow::anyhow!(
186                    "The \"{command_name}\" command in \"{package_id}\" shadows an entry/command of the \"{dep}\" dependency. Rename the local command."
187                )
188            })?;
189
190            (container, id)
191        }
192        None => (package, package_id),
193    };
194
195    let atom = webc.get_atom(&atom_name);
196
197    if atom.is_none() && cmd.annotations.is_empty() {
198        tracing::info!("applying legacy atom hack");
199        return legacy_atom_hack(webc, package_id, command_name, cmd);
200    }
201
202    let hash = to_module_hash(webc.manifest().atom_signature(&atom_name)?);
203
204    let atom = atom.with_context(|| {
205
206        let available_atoms = webc.atoms().keys().map(|x| x.as_str()).collect::<Vec<_>>().join(",");
207
208        tracing::warn!(
209            %atom_name,
210            %resolved_package_id,
211            %available_atoms,
212            "invalid command: could not find atom in package",
213        );
214
215        format!(
216            "The '{command_name}' command uses the '{atom_name}' atom, but it isn't present in the package: {resolved_package_id})"
217        )
218    })?;
219
220    // Get WebAssembly features from manifest atom annotations
221    let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
222        extract_features_from_atom_metadata(atom_metadata)
223    } else {
224        None
225    };
226
227    let cmd = BinaryPackageCommand::new(
228        command_name.to_string(),
229        cmd.clone(),
230        atom,
231        hash,
232        features,
233        package_id.clone(),
234        resolved_package_id.clone(),
235    );
236
237    Ok(Some(cmd))
238}
239
240fn atom_name_for_command(
241    command_name: &str,
242    cmd: &webc::metadata::Command,
243) -> Result<Option<AtomAnnotation>, anyhow::Error> {
244    use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
245
246    if let Some(atom) = cmd
247        .atom()
248        .context("Unable to deserialize atom annotations")?
249    {
250        return Ok(Some(atom));
251    }
252
253    if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
254        .iter()
255        .any(|uri| cmd.runner.starts_with(uri))
256    {
257        // Note: We use the command name as the atom name as a special case
258        // for known runner types because sometimes people will construct
259        // a manifest by hand instead of using wapm2pirita.
260        tracing::debug!(
261            command = command_name,
262            "No annotations specifying the atom name found. Falling back to the command name"
263        );
264        return Ok(Some(AtomAnnotation::new(command_name, None)));
265    }
266
267    Ok(None)
268}
269
270/// HACK: Some older packages like `sharrattj/bash` and `sharrattj/coreutils`
271/// contain commands with no annotations. When this happens, you can just assume
272/// it wants to use the first atom in the WEBC file.
273///
274/// That works because most of these packages only have a single atom (e.g. in
275/// `sharrattj/coreutils` there are commands for `ls`, `pwd`, and so on, but
276/// under the hood they all use the `coreutils` atom).
277///
278/// See <https://github.com/wasmerio/wasmer/commit/258903140680716da1431d92bced67d486865aeb>
279/// for more.
280fn legacy_atom_hack(
281    webc: &Container,
282    package_id: &PackageId,
283    command_name: &str,
284    metadata: &webc::metadata::Command,
285) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
286    let (name, atom) = webc
287        .atoms()
288        .into_iter()
289        .next()
290        .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
291
292    tracing::debug!(
293        command_name,
294        atom.name = name.as_str(),
295        atom.len = atom.len(),
296        "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
297    );
298
299    let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
300
301    // Get WebAssembly features from manifest atom annotations
302    let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
303        extract_features_from_atom_metadata(atom_metadata)
304    } else {
305        None
306    };
307
308    Ok(Some(BinaryPackageCommand::new(
309        command_name.to_string(),
310        metadata.clone(),
311        atom,
312        hash,
313        features,
314        package_id.clone(),
315        package_id.clone(),
316    )))
317}
318
319async fn fetch_dependencies(
320    loader: &dyn PackageLoader,
321    pkg: &ResolvedPackage,
322    graph: &DependencyGraph,
323) -> Result<HashMap<PackageId, Container>, Error> {
324    let packages = packages_needed_for_load(pkg);
325
326    let packages = packages.into_iter().filter_map(|id| {
327        let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
328        let summary = PackageSummary {
329            pkg: pkg.clone(),
330            dist: dist.clone()?,
331        };
332        Some((id, summary))
333    });
334    let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
335        .map(|(id, s)| async move {
336            match loader.load(&s).await {
337                Ok(webc) => Ok((id, webc)),
338                Err(e) => Err(e),
339            }
340        })
341        .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
342        .try_collect()
343        .await?;
344
345    Ok(packages)
346}
347
348fn packages_needed_for_load(pkg: &ResolvedPackage) -> HashSet<PackageId> {
349    let mut packages = HashSet::new();
350
351    for loc in pkg.commands.values() {
352        packages.insert(loc.package.clone());
353    }
354
355    for mapping in &pkg.filesystem {
356        packages.insert(mapping.package.clone());
357    }
358
359    // We don't need to download the root package
360    packages.remove(&pkg.root_package);
361
362    packages
363}
364
365/// How many bytes worth of files does a directory contain?
366fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 {
367    let mut total = 0;
368
369    let dir = match fs.read_dir(path) {
370        Ok(d) => d,
371        Err(_err) => {
372            return 0;
373        }
374    };
375
376    for entry in dir.flatten() {
377        if let Ok(meta) = entry.metadata() {
378            total += meta.len();
379            if meta.is_dir() {
380                total += count_file_system(fs, entry.path.as_path());
381            }
382        }
383    }
384
385    total
386}
387
388fn count_package_mounts(mounts: &BinaryPackageMounts) -> u64 {
389    let mut total = 0;
390
391    if let Some(root_layer) = &mounts.root_layer {
392        total += count_file_system(root_layer.as_ref(), Path::new("/"));
393    }
394
395    for mount in &mounts.mounts {
396        total += count_file_system(mount.fs.as_ref(), Path::new("/"));
397    }
398
399    total
400}
401
402/// Given a set of [`ResolvedFileSystemMapping`]s and the [`Container`] for each
403/// package in a dependency tree, construct the resulting filesystem.
404///
405/// Returns `Ok(None)` if no filesystem mappings were specified.
406fn filesystem(
407    packages: &HashMap<PackageId, Container>,
408    pkg: &ResolvedPackage,
409    root_is_local_dir: bool,
410) -> Result<Option<BinaryPackageMounts>, Error> {
411    if pkg.filesystem.is_empty() {
412        return Ok(None);
413    }
414
415    let mut found_v2 = None;
416    let mut found_v3 = None;
417
418    for ResolvedFileSystemMapping { package, .. } in &pkg.filesystem {
419        let container = packages.get(package).with_context(|| {
420            format!(
421                "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
422                pkg.root_package, package,
423            )
424        })?;
425
426        match container.version() {
427            webc::Version::V1 => {
428                anyhow::bail!(
429                    "the package '{package}' is a webc v1 package, but webc v1 support was removed"
430                );
431            }
432            webc::Version::V2 => {
433                if found_v2.is_none() {
434                    found_v2 = Some(package.clone());
435                }
436            }
437            webc::Version::V3 => {
438                if found_v3.is_none() {
439                    found_v3 = Some(package.clone());
440                }
441            }
442            other => {
443                anyhow::bail!("the package '{package}' has an unknown webc version: {other}");
444            }
445        }
446    }
447
448    match (found_v2, found_v3) {
449        (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir).map(Some),
450        (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir).map(Some),
451        (Some(v2), Some(v3)) => {
452            anyhow::bail!(
453                "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
454            )
455        }
456        (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
457    }
458}
459
460/// Build the filesystem for webc v3 packages.
461fn filesystem_v3(
462    packages: &HashMap<PackageId, Container>,
463    pkg: &ResolvedPackage,
464    root_is_local_dir: bool,
465) -> Result<BinaryPackageMounts, Error> {
466    let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
467    let mut root_layer = None;
468    let mut mounts = Vec::new();
469
470    for ResolvedFileSystemMapping {
471        mount_path,
472        volume_name,
473        package,
474        ..
475    } in &pkg.filesystem
476    {
477        if *package == pkg.root_package && root_is_local_dir {
478            continue;
479        }
480
481        if mount_path.as_path() == Path::new("/") {
482            tracing::warn!(
483                "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
484            );
485        }
486
487        // Note: We want to reuse existing Volume instances if we can. That way
488        // we can keep the memory usage down. A webc::compat::Volume is
489        // reference-counted, anyway.
490        // looks like we need to insert it
491        let container = packages.get(package).with_context(|| {
492            format!(
493                "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
494                pkg.root_package, package,
495            )
496        })?;
497        let container_volumes = match volumes.entry(package) {
498            std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
499            std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
500        };
501
502        let volume = container_volumes.get(volume_name).with_context(|| {
503            format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
504        })?;
505
506        let webc_vol = WebcVolumeFileSystem::new(volume.clone());
507        if mount_path.as_path() == Path::new("/") {
508            root_layer = Some(Arc::new(webc_vol) as Arc<dyn FileSystem + Send + Sync>);
509        } else {
510            mounts.push(BinaryPackageMount {
511                guest_path: mount_path.clone(),
512                fs: Arc::new(webc_vol),
513                source_path: PathBuf::from("/"),
514            });
515        }
516    }
517
518    Ok(BinaryPackageMounts { root_layer, mounts })
519}
520
521/// Build the filesystem for webc v2 packages.
522///
523// # Note to future readers
524//
525// Sooo... this code is a bit convoluted because we're constrained by the
526// filesystem implementations we've got available.
527//
528// Ideally, we would create a WebcVolumeFileSystem for each volume we're
529// using, then we'd have a single mount filesystem which lets you mount
530// filesystem objects under various paths.
531//
532// The OverlayFileSystem lets us make files from multiple filesystem
533// implementations available at the same time, however all of the
534// filesystems will be mounted at "/", when the user wants to mount volumes
535// at arbitrary locations.
536//
537// The TmpFileSystem *does* allow mounting at non-root paths, however it can't
538// handle nested paths (e.g. mounting to "/lib" and "/lib/python3.10" - see
539// <https://github.com/wasmerio/wasmer/issues/3678> for more) and you aren't
540// allowed to mount to "/" because it's a special directory that already
541// exists.
542//
543// As a result, we'll duct-tape things together and hope for the best 🤞
544fn filesystem_v2(
545    packages: &HashMap<PackageId, Container>,
546    pkg: &ResolvedPackage,
547    root_is_local_dir: bool,
548) -> Result<BinaryPackageMounts, Error> {
549    let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
550    let mut root_layer = None;
551    let mut mounts = Vec::new();
552
553    for ResolvedFileSystemMapping {
554        mount_path,
555        volume_name,
556        package,
557        original_path,
558    } in &pkg.filesystem
559    {
560        if *package == pkg.root_package && root_is_local_dir {
561            continue;
562        }
563
564        if mount_path.as_path() == Path::new("/") {
565            tracing::warn!(
566                "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
567            );
568        }
569
570        // Note: We want to reuse existing Volume instances if we can. That way
571        // we can keep the memory usage down. A webc::compat::Volume is
572        // reference-counted, anyway.
573        let container_volumes = match volumes.entry(package) {
574            std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
575            std::collections::hash_map::Entry::Vacant(entry) => {
576                // looks like we need to insert it
577                let container = packages.get(package)
578                    .with_context(|| format!(
579                        "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
580                        pkg.root_package,
581                        package,
582                    ))?;
583                &*entry.insert(container.volumes())
584            }
585        };
586
587        let volume = container_volumes.get(volume_name).with_context(|| {
588            format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
589        })?;
590
591        let mounted_fs = Arc::new(WebcVolumeFileSystem::new(volume.clone()))
592            as Arc<dyn FileSystem + Send + Sync>;
593        let source_path = original_path
594            .as_deref()
595            .map(PathBuf::from)
596            .unwrap_or_else(|| PathBuf::from("/"));
597
598        if mount_path.as_path() == Path::new("/") {
599            root_layer = Some(mounted_fs);
600        } else {
601            mounts.push(BinaryPackageMount {
602                guest_path: mount_path.clone(),
603                fs: mounted_fs,
604                source_path,
605            });
606        }
607    }
608
609    Ok(BinaryPackageMounts { root_layer, mounts })
610}
611
612#[cfg(test)]
613mod tests {
614    use std::{
615        collections::{BTreeMap, HashMap},
616        path::{Path, PathBuf},
617    };
618
619    use anyhow::Error;
620    use ciborium::value::Value;
621    use petgraph::graph::DiGraph;
622    use virtual_fs::FileSystem;
623    use wasmer_config::package::PackageId;
624    use webc::{
625        Container,
626        indexmap::IndexMap,
627        metadata::{
628            Command as WebcCommand, Manifest,
629            annotations::{
630                Atom as AtomAnnotation, FileSystemMapping, FileSystemMappings, WASI_RUNNER_URI,
631            },
632        },
633        v2::{
634            SignatureAlgorithm,
635            read::OwnedReader,
636            write::{DirEntry, Directory, FileEntry, Writer},
637        },
638    };
639
640    use super::{ResolvedFileSystemMapping, ResolvedPackage, filesystem_v2};
641    use crate::runtime::{
642        package_loader::PackageLoader,
643        resolver::{
644            Command, DependencyGraph, DistributionInfo, Edge, ItemLocation, Node, PackageInfo,
645            PackageSummary, Resolution, WebcHash,
646        },
647    };
648
649    #[derive(Debug, Default)]
650    struct TestLoader;
651
652    #[async_trait::async_trait]
653    impl PackageLoader for TestLoader {
654        async fn load(&self, summary: &PackageSummary) -> Result<Container, Error> {
655            anyhow::bail!("unexpected dependency fetch: {}", summary.package_id())
656        }
657
658        async fn load_package_tree(
659            &self,
660            root: &Container,
661            resolution: &Resolution,
662            root_is_local_dir: bool,
663        ) -> Result<crate::bin_factory::BinaryPackage, Error> {
664            super::load_package_tree(root, self, resolution, root_is_local_dir).await
665        }
666    }
667
668    #[test]
669    fn v2_filesystem_mapping_resolves_mount_paths() {
670        // Regression test: v2 fs mounts are already relative to the mount root,
671        // so stripping the mount path again breaks lookups like /public.
672        let mut manifest = Manifest::default();
673        let fs = FileSystemMappings(vec![FileSystemMapping {
674            from: None,
675            volume_name: "atom".to_string(),
676            host_path: Some("/public".to_string()),
677            mount_path: "/public".to_string(),
678        }]);
679        let mut package = IndexMap::new();
680        package.insert(
681            FileSystemMappings::KEY.to_string(),
682            Value::serialized(&fs).unwrap(),
683        );
684        manifest.package = package;
685
686        let mut public_children = BTreeMap::new();
687        public_children.insert(
688            "index.html".parse().unwrap(),
689            DirEntry::File(FileEntry::from(b"ok".as_slice())),
690        );
691        let public_mount_dir = Directory {
692            children: public_children,
693        };
694        let mut root_children = BTreeMap::new();
695        root_children.insert("public".parse().unwrap(), DirEntry::Dir(public_mount_dir));
696        let atom_dir = Directory {
697            children: root_children,
698        };
699
700        let writer = Writer::default().write_manifest(&manifest).unwrap();
701        let writer = writer.write_atoms(BTreeMap::new()).unwrap();
702        let writer = writer.with_volume("atom", atom_dir).unwrap();
703        let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
704
705        let reader = OwnedReader::parse(bytes).unwrap();
706        let container = Container::from(reader);
707
708        let pkg_id = PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap());
709        let mut packages = HashMap::new();
710        packages.insert(pkg_id.clone(), container);
711
712        let pkg = ResolvedPackage {
713            root_package: pkg_id.clone(),
714            commands: BTreeMap::new(),
715            entrypoint: None,
716            filesystem: vec![ResolvedFileSystemMapping {
717                mount_path: PathBuf::from("/public"),
718                volume_name: "atom".to_string(),
719                original_path: Some("/public".to_string()),
720                package: pkg_id,
721            }],
722        };
723
724        let mounts = filesystem_v2(&packages, &pkg, false).unwrap();
725        let mount_fs = mounts.to_mount_fs().unwrap();
726        assert!(mount_fs.metadata(Path::new("/public")).unwrap().is_dir());
727        assert!(
728            mount_fs
729                .metadata(Path::new("/public/index.html"))
730                .unwrap()
731                .is_file()
732        );
733    }
734
735    #[tokio::test]
736    async fn load_package_tree_reports_shadowed_dependency_command_without_panic() {
737        let root_id = PackageId::new_named("root", "0.1.0".parse().unwrap());
738        let dep_id = PackageId::new_named("wasmer/static-web-server", "1.0.0".parse().unwrap());
739        let dep_alias = "wasmer/static-web-server";
740
741        let root_info = PackageInfo {
742            id: root_id.clone(),
743            commands: vec![Command {
744                name: "webserver".to_string(),
745            }],
746            entrypoint: Some("webserver".to_string()),
747            dependencies: Vec::new(),
748            filesystem: Vec::new(),
749        };
750        let dep_info = PackageInfo {
751            id: dep_id.clone(),
752            commands: vec![Command {
753                name: "webserver".to_string(),
754            }],
755            entrypoint: Some("webserver".to_string()),
756            dependencies: Vec::new(),
757            filesystem: Vec::new(),
758        };
759        let root_container = test_container([(
760            "webserver",
761            command_for_atom("webserver", Some(dep_alias.to_string())),
762        )]);
763
764        let mut graph = DiGraph::new();
765        let root = graph.add_node(Node {
766            id: root_id.clone(),
767            pkg: root_info,
768            dist: None,
769        });
770        let dep = graph.add_node(Node {
771            id: dep_id.clone(),
772            pkg: dep_info,
773            dist: Some(DistributionInfo {
774                webc: "http://localhost/wasmer-static-web-server.webc"
775                    .parse()
776                    .unwrap(),
777                webc_sha256: WebcHash::from([0; 32]),
778            }),
779        });
780        graph.add_edge(
781            root,
782            dep,
783            Edge {
784                alias: dep_alias.to_string(),
785            },
786        );
787
788        let dependency_graph = DependencyGraph::new(
789            root,
790            graph,
791            BTreeMap::from([(root_id.clone(), root), (dep_id.clone(), dep)]),
792        );
793        let pkg = ResolvedPackage {
794            root_package: root_id.clone(),
795            commands: BTreeMap::from([(
796                "webserver".to_string(),
797                ItemLocation {
798                    name: "webserver".to_string(),
799                    package: root_id.clone(),
800                },
801            )]),
802            entrypoint: Some("webserver".to_string()),
803            filesystem: Vec::new(),
804        };
805
806        let err = super::load_package_tree(
807            &root_container,
808            &TestLoader,
809            &Resolution {
810                package: pkg,
811                graph: dependency_graph,
812            },
813            false,
814        )
815        .await
816        .unwrap_err();
817        let message = format!("{err:#}");
818
819        assert!(message.contains("shadows an entry/command"));
820        assert!(message.contains("Rename the local command"));
821    }
822
823    fn command_for_atom(atom: &str, dependency: Option<String>) -> WebcCommand {
824        let mut annotations = IndexMap::new();
825        annotations.insert(
826            AtomAnnotation::KEY.to_string(),
827            Value::serialized(&AtomAnnotation::new(atom, dependency)).unwrap(),
828        );
829
830        WebcCommand {
831            runner: WASI_RUNNER_URI.to_string(),
832            annotations,
833        }
834    }
835
836    fn test_container<'a>(commands: impl IntoIterator<Item = (&'a str, WebcCommand)>) -> Container {
837        let mut manifest = Manifest::default();
838
839        for (name, command) in commands {
840            manifest.commands.insert(name.to_string(), command);
841        }
842
843        let writer = Writer::default().write_manifest(&manifest).unwrap();
844        let writer = writer.write_atoms(BTreeMap::new()).unwrap();
845        let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
846        let reader = OwnedReader::parse(bytes).unwrap();
847        Container::from(reader)
848    }
849
850    #[test]
851    fn v2_filesystem_mapping_preserves_root_and_nested_mounts() {
852        let mut manifest = Manifest::default();
853        let fs = FileSystemMappings(vec![
854            FileSystemMapping {
855                from: None,
856                volume_name: "root".to_string(),
857                host_path: Some("/".to_string()),
858                mount_path: "/".to_string(),
859            },
860            FileSystemMapping {
861                from: None,
862                volume_name: "public".to_string(),
863                host_path: Some("/public".to_string()),
864                mount_path: "/public".to_string(),
865            },
866        ]);
867        let mut package = IndexMap::new();
868        package.insert(
869            FileSystemMappings::KEY.to_string(),
870            Value::serialized(&fs).unwrap(),
871        );
872        manifest.package = package;
873
874        let mut root_children = BTreeMap::new();
875        root_children.insert(
876            "root.txt".parse().unwrap(),
877            DirEntry::File(FileEntry::from(b"root".as_slice())),
878        );
879        let root_dir = Directory {
880            children: root_children,
881        };
882
883        let mut public_children = BTreeMap::new();
884        public_children.insert(
885            "index.html".parse().unwrap(),
886            DirEntry::File(FileEntry::from(b"ok".as_slice())),
887        );
888        let public_dir = Directory {
889            children: public_children,
890        };
891
892        let writer = Writer::default().write_manifest(&manifest).unwrap();
893        let writer = writer.write_atoms(BTreeMap::new()).unwrap();
894        let writer = writer.with_volume("root", root_dir).unwrap();
895        let writer = writer.with_volume("public", public_dir).unwrap();
896        let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
897
898        let reader = OwnedReader::parse(bytes).unwrap();
899        let container = Container::from(reader);
900
901        let pkg_id = PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap());
902        let mut packages = HashMap::new();
903        packages.insert(pkg_id.clone(), container);
904
905        let pkg = ResolvedPackage {
906            root_package: pkg_id.clone(),
907            commands: BTreeMap::new(),
908            entrypoint: None,
909            filesystem: vec![
910                ResolvedFileSystemMapping {
911                    mount_path: PathBuf::from("/"),
912                    volume_name: "root".to_string(),
913                    original_path: Some("/".to_string()),
914                    package: pkg_id.clone(),
915                },
916                ResolvedFileSystemMapping {
917                    mount_path: PathBuf::from("/public"),
918                    volume_name: "public".to_string(),
919                    original_path: Some("/public".to_string()),
920                    package: pkg_id,
921                },
922            ],
923        };
924
925        let mounts = filesystem_v2(&packages, &pkg, false).unwrap();
926        let root_layer = mounts
927            .root_layer
928            .as_ref()
929            .expect("expected root layer mount");
930        assert!(
931            root_layer
932                .metadata(Path::new("/root.txt"))
933                .unwrap()
934                .is_file()
935        );
936        assert_eq!(mounts.mounts.len(), 1);
937        assert_eq!(mounts.mounts[0].guest_path, Path::new("/public"));
938    }
939}