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