Skip to main content

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