wasmer_wasix/runners/
wasi.rs

1//! WebC container support for running WASI modules
2
3use std::{path::PathBuf, sync::Arc};
4
5use anyhow::{Context, Error};
6use tracing::Instrument;
7use virtual_fs::{ArcBoxFile, FileSystem, VirtualFile};
8use wasmer::{Engine, Module};
9use wasmer_types::ModuleHash;
10use webc::metadata::{Command, annotations::Wasi};
11
12use crate::{
13    Runtime, WasiEnvBuilder, WasiError, WasiRuntimeError,
14    bin_factory::BinaryPackage,
15    capabilities::Capabilities,
16    journal::{DynJournal, DynReadableJournal, SnapshotTrigger},
17    runners::{MappedDirectory, MountedDirectory, wasi_common::CommonWasiOptions},
18    runtime::task_manager::VirtualTaskManagerExt,
19};
20
21use super::wasi_common::{
22    ExistingMountConflictBehavior, MAPPED_CURRENT_DIR_DEFAULT_PATH, MappedCommand,
23};
24
25#[derive(Debug, Default, Clone)]
26pub struct WasiRunner {
27    wasi: CommonWasiOptions,
28    stdin: Option<ArcBoxFile>,
29    stdout: Option<ArcBoxFile>,
30    stderr: Option<ArcBoxFile>,
31}
32
33pub enum PackageOrHash<'a> {
34    Package(&'a BinaryPackage),
35    Hash(ModuleHash),
36}
37
38pub enum RuntimeOrEngine {
39    Runtime(Arc<dyn Runtime + Send + Sync>),
40    Engine(Engine),
41}
42
43impl WasiRunner {
44    /// Constructs a new `WasiRunner`.
45    pub fn new() -> Self {
46        WasiRunner::default()
47    }
48
49    /// Returns the current entry function for this `WasiRunner`
50    pub fn entry_function(&self) -> Option<String> {
51        self.wasi.entry_function.clone()
52    }
53
54    /// Builder method to set the name of the entry function for this `WasiRunner`
55    pub fn with_entry_function<S>(&mut self, entry_function: S) -> &mut Self
56    where
57        S: Into<String>,
58    {
59        self.wasi.entry_function = Some(entry_function.into());
60        self
61    }
62
63    /// Returns the current arguments for this `WasiRunner`
64    pub fn get_args(&self) -> Vec<String> {
65        self.wasi.args.clone()
66    }
67
68    /// Builder method to provide CLI args to the runner
69    pub fn with_args<A, S>(&mut self, args: A) -> &mut Self
70    where
71        A: IntoIterator<Item = S>,
72        S: Into<String>,
73    {
74        self.wasi.args = args.into_iter().map(|s| s.into()).collect();
75        self
76    }
77
78    /// Builder method to provide environment variables to the runner.
79    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
80        self.wasi.env.insert(key.into(), value.into());
81        self
82    }
83
84    pub fn with_envs<I, K, V>(&mut self, envs: I) -> &mut Self
85    where
86        I: IntoIterator<Item = (K, V)>,
87        K: Into<String>,
88        V: Into<String>,
89    {
90        for (key, value) in envs {
91            self.wasi.env.insert(key.into(), value.into());
92        }
93        self
94    }
95
96    pub fn with_forward_host_env(&mut self, forward: bool) -> &mut Self {
97        self.wasi.forward_host_env = forward;
98        self
99    }
100
101    pub fn with_mapped_directories<I, D>(&mut self, dirs: I) -> &mut Self
102    where
103        I: IntoIterator<Item = D>,
104        D: Into<MappedDirectory>,
105    {
106        self.with_mounted_directories(dirs.into_iter().map(Into::into).map(MountedDirectory::from))
107    }
108
109    pub fn with_home_mapped(&mut self, is_home_mapped: bool) -> &mut Self {
110        self.wasi.is_home_mapped = is_home_mapped;
111        self
112    }
113
114    pub fn with_mounted_directories<I, D>(&mut self, dirs: I) -> &mut Self
115    where
116        I: IntoIterator<Item = D>,
117        D: Into<MountedDirectory>,
118    {
119        self.wasi.mounts.extend(dirs.into_iter().map(Into::into));
120        self
121    }
122
123    /// Mount a [`FileSystem`] instance at a particular location.
124    pub fn with_mount(&mut self, dest: String, fs: Arc<dyn FileSystem + Send + Sync>) -> &mut Self {
125        self.wasi.mounts.push(MountedDirectory { guest: dest, fs });
126        self
127    }
128
129    pub fn with_existing_mount_conflict_behavior(
130        &mut self,
131        behavior: ExistingMountConflictBehavior,
132    ) -> &mut Self {
133        self.wasi.existing_mount_conflict_behavior = behavior;
134        self
135    }
136
137    /// Override the directory the WASIX instance will start in.
138    pub fn with_current_dir(&mut self, dir: impl Into<PathBuf>) -> &mut Self {
139        self.wasi.current_dir = Some(dir.into());
140        self
141    }
142
143    /// Add a package that should be available to the instance at runtime.
144    pub fn with_injected_package(&mut self, pkg: BinaryPackage) -> &mut Self {
145        self.wasi.injected_packages.push(pkg);
146        self
147    }
148
149    /// Add packages that should be available to the instance at runtime.
150    pub fn with_injected_packages(
151        &mut self,
152        packages: impl IntoIterator<Item = BinaryPackage>,
153    ) -> &mut Self {
154        self.wasi.injected_packages.extend(packages);
155        self
156    }
157
158    pub fn with_mapped_host_command(
159        &mut self,
160        alias: impl Into<String>,
161        target: impl Into<String>,
162    ) -> &mut Self {
163        self.wasi.mapped_host_commands.push(MappedCommand {
164            alias: alias.into(),
165            target: target.into(),
166        });
167        self
168    }
169
170    pub fn with_mapped_host_commands(
171        &mut self,
172        commands: impl IntoIterator<Item = MappedCommand>,
173    ) -> &mut Self {
174        self.wasi.mapped_host_commands.extend(commands);
175        self
176    }
177
178    pub fn capabilities_mut(&mut self) -> &mut Capabilities {
179        &mut self.wasi.capabilities
180    }
181
182    pub fn with_capabilities(&mut self, capabilities: Capabilities) -> &mut Self {
183        self.wasi.capabilities = capabilities;
184        self
185    }
186
187    #[cfg(feature = "journal")]
188    pub fn with_snapshot_trigger(&mut self, on: SnapshotTrigger) -> &mut Self {
189        self.wasi.snapshot_on.push(on);
190        self
191    }
192
193    #[cfg(feature = "journal")]
194    pub fn with_default_snapshot_triggers(&mut self) -> &mut Self {
195        for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
196            if !self.has_snapshot_trigger(on) {
197                self.with_snapshot_trigger(on);
198            }
199        }
200        self
201    }
202
203    #[cfg(feature = "journal")]
204    pub fn has_snapshot_trigger(&self, on: SnapshotTrigger) -> bool {
205        self.wasi.snapshot_on.contains(&on)
206    }
207
208    #[cfg(feature = "journal")]
209    pub fn with_snapshot_interval(&mut self, period: std::time::Duration) -> &mut Self {
210        if !self.has_snapshot_trigger(SnapshotTrigger::PeriodicInterval) {
211            self.with_snapshot_trigger(SnapshotTrigger::PeriodicInterval);
212        }
213        self.wasi.snapshot_interval.replace(period);
214        self
215    }
216
217    #[cfg(feature = "journal")]
218    pub fn with_stop_running_after_snapshot(&mut self, stop_running: bool) -> &mut Self {
219        self.wasi.stop_running_after_snapshot = stop_running;
220        self
221    }
222
223    #[cfg(feature = "journal")]
224    pub fn with_read_only_journal(&mut self, journal: Arc<DynReadableJournal>) -> &mut Self {
225        self.wasi.read_only_journals.push(journal);
226        self
227    }
228
229    #[cfg(feature = "journal")]
230    pub fn with_writable_journal(&mut self, journal: Arc<DynJournal>) -> &mut Self {
231        self.wasi.writable_journals.push(journal);
232        self
233    }
234
235    pub fn with_skip_stdio_during_bootstrap(&mut self, skip: bool) -> &mut Self {
236        self.wasi.skip_stdio_during_bootstrap = skip;
237        self
238    }
239
240    pub fn with_stdin(&mut self, stdin: Box<dyn VirtualFile + Send + Sync>) -> &mut Self {
241        self.stdin = Some(ArcBoxFile::new(stdin));
242        self
243    }
244
245    pub fn with_stdout(&mut self, stdout: Box<dyn VirtualFile + Send + Sync>) -> &mut Self {
246        self.stdout = Some(ArcBoxFile::new(stdout));
247        self
248    }
249
250    pub fn with_stderr(&mut self, stderr: Box<dyn VirtualFile + Send + Sync>) -> &mut Self {
251        self.stderr = Some(ArcBoxFile::new(stderr));
252        self
253    }
254
255    fn ensure_tokio_runtime() -> Option<tokio::runtime::Runtime> {
256        #[cfg(feature = "sys-thread")]
257        {
258            if tokio::runtime::Handle::try_current().is_ok() {
259                return None;
260            }
261
262            let rt = tokio::runtime::Builder::new_multi_thread()
263                .enable_all()
264                .build()
265                .expect(
266                    "Failed to build a multi-threaded tokio runtime. This is necessary \
267                for WASIX to work. You can provide a tokio runtime by building one \
268                yourself and entering it before using WasiRunner.",
269                );
270            Some(rt)
271        }
272
273        #[cfg(not(feature = "sys-thread"))]
274        {
275            None
276        }
277    }
278
279    pub fn with_asynchronous_threading(
280        &mut self,
281        enable_asynchronous_threading: bool,
282    ) -> &mut Self {
283        self.wasi
284            .capabilities
285            .threading
286            .enable_asynchronous_threading = enable_asynchronous_threading;
287        self
288    }
289
290    #[tracing::instrument(level = "debug", skip_all)]
291    pub fn prepare_webc_env(
292        &self,
293        program_name: &str,
294        wasi: &Wasi,
295        pkg_or_hash: PackageOrHash,
296        runtime_or_engine: RuntimeOrEngine,
297        root_fs: Option<crate::fs::WasiFsRoot>,
298    ) -> Result<WasiEnvBuilder, anyhow::Error> {
299        let mut builder = WasiEnvBuilder::new(program_name);
300
301        match runtime_or_engine {
302            RuntimeOrEngine::Runtime(runtime) => {
303                builder.set_runtime(runtime);
304            }
305            RuntimeOrEngine::Engine(engine) => {
306                builder.set_engine(engine);
307            }
308        }
309
310        let container_mounts = match pkg_or_hash {
311            PackageOrHash::Package(pkg) => {
312                builder.add_webc(pkg.clone());
313                builder.set_module_hash(pkg.hash());
314                builder.include_packages(pkg.package_ids.clone());
315
316                pkg.package_mounts.as_deref()
317            }
318            PackageOrHash::Hash(hash) => {
319                builder.set_module_hash(hash);
320                None
321            }
322        };
323
324        if self.wasi.is_home_mapped {
325            builder.set_current_dir(MAPPED_CURRENT_DIR_DEFAULT_PATH);
326        }
327
328        if let Some(current_dir) = &self.wasi.current_dir {
329            builder.set_current_dir(current_dir.clone());
330        }
331
332        if let Some(cwd) = &wasi.cwd {
333            builder.set_current_dir(cwd);
334        }
335
336        self.wasi
337            .prepare_webc_env(&mut builder, container_mounts, wasi, root_fs)?;
338
339        if let Some(stdin) = &self.stdin {
340            builder.set_stdin(Box::new(stdin.clone()));
341        }
342        if let Some(stdout) = &self.stdout {
343            builder.set_stdout(Box::new(stdout.clone()));
344        }
345        if let Some(stderr) = &self.stderr {
346            builder.set_stderr(Box::new(stderr.clone()));
347        }
348
349        Ok(builder)
350    }
351
352    pub fn run_wasm(
353        &self,
354        runtime_or_engine: RuntimeOrEngine,
355        program_name: &str,
356        module: Module,
357        module_hash: ModuleHash,
358    ) -> Result<(), Error> {
359        // Just keep the runtime and enter guard alive until we're done running the module
360        let tokio_runtime = Self::ensure_tokio_runtime();
361        let _guard = tokio_runtime.as_ref().map(|rt| rt.enter());
362
363        let runner = self.clone();
364
365        let wasi = webc::metadata::annotations::Wasi::new(program_name);
366
367        let mut builder = runner.prepare_webc_env(
368            program_name,
369            &wasi,
370            PackageOrHash::Hash(module_hash),
371            runtime_or_engine,
372            None,
373        )?;
374
375        #[cfg(feature = "ctrlc")]
376        {
377            builder = builder.attach_ctrl_c();
378        }
379
380        #[cfg(feature = "journal")]
381        {
382            for journal in runner.wasi.read_only_journals.iter().cloned() {
383                builder.add_read_only_journal(journal);
384            }
385            for journal in runner.wasi.writable_journals.iter().cloned() {
386                builder.add_writable_journal(journal);
387            }
388
389            if !runner.wasi.snapshot_on.is_empty() {
390                for trigger in runner.wasi.snapshot_on.iter().cloned() {
391                    builder.add_snapshot_trigger(trigger);
392                }
393            } else if !runner.wasi.writable_journals.is_empty() {
394                for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
395                    builder.add_snapshot_trigger(on);
396                }
397            }
398
399            if let Some(period) = runner.wasi.snapshot_interval {
400                if runner.wasi.writable_journals.is_empty() {
401                    return Err(anyhow::format_err!(
402                        "If you specify a snapshot interval then you must also specify a writable journal file"
403                    ));
404                }
405                builder.with_snapshot_interval(period);
406            }
407
408            builder.with_stop_running_after_snapshot(runner.wasi.stop_running_after_snapshot);
409            builder.with_skip_stdio_during_bootstrap(runner.wasi.skip_stdio_during_bootstrap);
410        }
411
412        let env = builder.build()?;
413        let runtime = env.runtime.clone();
414        let tasks = runtime.task_manager().clone();
415
416        let mut task_handle =
417            crate::bin_factory::spawn_exec_module(module, env, &runtime).context("Spawn failed")?;
418
419        #[cfg(feature = "ctrlc")]
420        task_handle.install_ctrlc_handler();
421        let task_handle = async move { task_handle.wait_finished().await }.in_current_span();
422
423        let result = tasks.spawn_and_block_on(task_handle)?;
424        let exit_code = result
425            .map_err(|err| {
426                // We do our best to recover the error
427                let msg = err.to_string();
428                let weak = Arc::downgrade(&err);
429                Arc::into_inner(err).unwrap_or_else(|| {
430                    weak.upgrade()
431                        .map(|err| wasi_runtime_error_to_owned(&err))
432                        .unwrap_or_else(|| {
433                            WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("{msg}")))
434                        })
435                })
436            })
437            .context("Unable to wait for the process to exit")?;
438
439        if exit_code.raw() == 0 {
440            Ok(())
441        } else {
442            Err(WasiRuntimeError::Wasi(crate::WasiError::Exit(exit_code)).into())
443        }
444    }
445
446    pub fn run_command(
447        &mut self,
448        command_name: &str,
449        pkg: &BinaryPackage,
450        runtime_or_engine: RuntimeOrEngine,
451    ) -> Result<(), Error> {
452        // Just keep the runtime and enter guard alive until we're done running the module
453        let tokio_runtime = Self::ensure_tokio_runtime();
454        let _guard = tokio_runtime.as_ref().map(|rt| rt.enter());
455
456        let cmd = pkg
457            .get_command(command_name)
458            .with_context(|| format!("The package doesn't contain a \"{command_name}\" command"))?;
459
460        let wasi = cmd
461            .metadata()
462            .annotation("wasi")?
463            .unwrap_or_else(|| Wasi::new(command_name));
464
465        let exec_name = if let Some(exec_name) = wasi.exec_name.as_ref() {
466            exec_name
467        } else {
468            command_name
469        };
470
471        #[allow(unused_mut)]
472        let mut builder = self
473            .prepare_webc_env(
474                exec_name,
475                &wasi,
476                PackageOrHash::Package(pkg),
477                runtime_or_engine,
478                None,
479            )
480            .context("Unable to prepare the WASI environment")?;
481
482        #[cfg(feature = "journal")]
483        {
484            for journal in self.wasi.read_only_journals.iter().cloned() {
485                builder.add_read_only_journal(journal);
486            }
487            for journal in self.wasi.writable_journals.iter().cloned() {
488                builder.add_writable_journal(journal);
489            }
490
491            if !self.wasi.snapshot_on.is_empty() {
492                for trigger in self.wasi.snapshot_on.iter().cloned() {
493                    builder.add_snapshot_trigger(trigger);
494                }
495            } else if !self.wasi.writable_journals.is_empty() {
496                for on in crate::journal::DEFAULT_SNAPSHOT_TRIGGERS {
497                    builder.add_snapshot_trigger(on);
498                }
499            }
500
501            if let Some(period) = self.wasi.snapshot_interval {
502                if self.wasi.writable_journals.is_empty() {
503                    return Err(anyhow::format_err!(
504                        "If you specify a snapshot interval then you must also specify a journal file"
505                    ));
506                }
507                builder.with_snapshot_interval(period);
508            }
509
510            builder.with_stop_running_after_snapshot(self.wasi.stop_running_after_snapshot);
511        }
512
513        let env = builder.build()?;
514        let runtime = env.runtime.clone();
515        let command_name = command_name.to_string();
516        let tasks = runtime.task_manager().clone();
517        let pkg = pkg.clone();
518
519        // Wrapping the call to `spawn_and_block_on` in a call to `spawn_await` could help to prevent deadlocks
520        // because then blocking in here won't block the tokio runtime
521        //
522        // See run_wasm above for a possible fix
523        let exit_code = tasks.spawn_and_block_on(
524            async move {
525                let mut task_handle =
526                    crate::bin_factory::spawn_exec(pkg, &command_name, env, &runtime)
527                        .await
528                        .context("Spawn failed")?;
529
530                #[cfg(feature = "ctrlc")]
531                task_handle.install_ctrlc_handler();
532
533                task_handle
534                    .wait_finished()
535                    .await
536                    .map_err(|err| {
537                        // We do our best to recover the error
538                        let msg = err.to_string();
539                        let weak = Arc::downgrade(&err);
540                        Arc::into_inner(err).unwrap_or_else(|| {
541                            weak.upgrade()
542                                .map(|err| wasi_runtime_error_to_owned(&err))
543                                .unwrap_or_else(|| {
544                                    WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("{msg}")))
545                                })
546                        })
547                    })
548                    .context("Unable to wait for the process to exit")
549            }
550            .in_current_span(),
551        )??;
552
553        if exit_code.raw() == 0 {
554            Ok(())
555        } else {
556            Err(WasiRuntimeError::Wasi(crate::WasiError::Exit(exit_code)).into())
557        }
558    }
559}
560
561impl crate::runners::Runner for WasiRunner {
562    fn can_run_command(command: &Command) -> Result<bool, Error> {
563        Ok(command
564            .runner
565            .starts_with(webc::metadata::annotations::WASI_RUNNER_URI))
566    }
567
568    #[tracing::instrument(skip_all)]
569    fn run_command(
570        &mut self,
571        command_name: &str,
572        pkg: &BinaryPackage,
573        runtime: Arc<dyn Runtime + Send + Sync>,
574    ) -> Result<(), Error> {
575        self.run_command(command_name, pkg, RuntimeOrEngine::Runtime(runtime))
576    }
577}
578
579fn wasi_runtime_error_to_owned(err: &WasiRuntimeError) -> WasiRuntimeError {
580    match err {
581        WasiRuntimeError::Init(a) => WasiRuntimeError::Init(a.clone()),
582        WasiRuntimeError::Export(a) => WasiRuntimeError::Export(a.clone()),
583        WasiRuntimeError::Instantiation(a) => WasiRuntimeError::Instantiation(a.clone()),
584        WasiRuntimeError::Wasi(WasiError::Exit(a)) => WasiRuntimeError::Wasi(WasiError::Exit(*a)),
585        WasiRuntimeError::Wasi(WasiError::ThreadExit) => {
586            WasiRuntimeError::Wasi(WasiError::ThreadExit)
587        }
588        WasiRuntimeError::Wasi(WasiError::UnknownWasiVersion) => {
589            WasiRuntimeError::Wasi(WasiError::UnknownWasiVersion)
590        }
591        WasiRuntimeError::Wasi(WasiError::DeepSleep(_)) => {
592            WasiRuntimeError::Anyhow(Arc::new(anyhow::format_err!("deep-sleep")))
593        }
594        WasiRuntimeError::Wasi(WasiError::DlSymbolResolutionFailed(symbol)) => {
595            WasiRuntimeError::Wasi(WasiError::DlSymbolResolutionFailed(symbol.clone()))
596        }
597        WasiRuntimeError::ControlPlane(a) => WasiRuntimeError::ControlPlane(a.clone()),
598        WasiRuntimeError::Runtime(a) => WasiRuntimeError::Runtime(a.clone()),
599        WasiRuntimeError::Thread(a) => WasiRuntimeError::Thread(a.clone()),
600        WasiRuntimeError::Anyhow(a) => WasiRuntimeError::Anyhow(a.clone()),
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn send_and_sync() {
610        fn assert_send<T: Send>() {}
611        fn assert_sync<T: Sync>() {}
612
613        assert_send::<WasiRunner>();
614        assert_sync::<WasiRunner>();
615    }
616
617    #[cfg(all(feature = "host-fs", feature = "sys"))]
618    #[tokio::test]
619    async fn test_volume_mount_without_webcs() {
620        use std::sync::Arc;
621
622        let tokrt = tokio::runtime::Handle::current();
623
624        let hostdir = virtual_fs::host_fs::FileSystem::new(tokrt.clone(), "/").unwrap();
625        let hostdir_dyn: Arc<dyn virtual_fs::FileSystem + Send + Sync> = Arc::new(hostdir);
626
627        let mut envb = crate::runners::wasi::WasiRunner::new();
628        envb.with_mount("/host".to_string(), hostdir_dyn);
629
630        let annotations = webc::metadata::annotations::Wasi::new("test");
631
632        let tm = Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(
633            tokrt.clone(),
634        ));
635        let rt = crate::PluggableRuntime::new(tm);
636
637        let envb = envb
638            .prepare_webc_env(
639                "test",
640                &annotations,
641                PackageOrHash::Hash(ModuleHash::random()),
642                RuntimeOrEngine::Runtime(Arc::new(rt)),
643                None,
644            )
645            .unwrap();
646
647        let init = envb.build_init().unwrap();
648
649        let fs = &init.state.fs.root_fs;
650
651        fs.read_dir(std::path::Path::new("/host")).unwrap();
652    }
653
654    #[cfg(all(feature = "host-fs", feature = "sys"))]
655    #[tokio::test]
656    async fn test_volume_mount_with_webcs() {
657        use std::sync::Arc;
658
659        use wasmer_package::utils::from_bytes;
660
661        let tokrt = tokio::runtime::Handle::current();
662
663        let hostdir = virtual_fs::host_fs::FileSystem::new(tokrt.clone(), "/").unwrap();
664        let hostdir_dyn: Arc<dyn virtual_fs::FileSystem + Send + Sync> = Arc::new(hostdir);
665
666        let mut envb = crate::runners::wasi::WasiRunner::new();
667        envb.with_mount("/host".to_string(), hostdir_dyn);
668
669        let annotations = webc::metadata::annotations::Wasi::new("test");
670
671        let tm = Arc::new(crate::runtime::task_manager::tokio::TokioTaskManager::new(
672            tokrt.clone(),
673        ));
674        let mut rt = crate::PluggableRuntime::new(tm);
675        rt.set_package_loader(crate::runtime::package_loader::BuiltinPackageLoader::new());
676
677        let webc_path = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
678            .join("../../wasmer-test-files/integration/webc/wasmer-tests--volume-static-webserver@0.1.0.webc");
679        let webc_data = std::fs::read(webc_path).unwrap();
680        let container = from_bytes(webc_data).unwrap();
681
682        let binpkg = crate::bin_factory::BinaryPackage::from_webc(&container, &rt)
683            .await
684            .unwrap();
685
686        let mut envb = envb
687            .prepare_webc_env(
688                "test",
689                &annotations,
690                PackageOrHash::Package(&binpkg),
691                RuntimeOrEngine::Runtime(Arc::new(rt)),
692                None,
693            )
694            .unwrap();
695
696        envb = envb.preopen_dir("/host").unwrap();
697
698        let init = envb.build_init().unwrap();
699
700        let fs = &init.state.fs.root_fs;
701
702        fs.read_dir(std::path::Path::new("/host")).unwrap();
703        fs.read_dir(std::path::Path::new("/settings")).unwrap();
704    }
705}