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
30fn 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
37fn 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
50const MAX_PARALLEL_DOWNLOADS: usize = 32;
52
53#[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#[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 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 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
272fn 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 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 packages.remove(&pkg.root_package);
363
364 packages
365}
366
367fn 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
404fn 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
457fn 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 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
518fn 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 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 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 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}