1use std::{
2 collections::{BTreeSet, HashMap},
3 ffi::OsString,
4 path::{Path, PathBuf},
5 str::FromStr,
6 sync::{Arc, mpsc::Sender},
7 time::Duration,
8};
9
10use anyhow::{Context, Result, bail};
11use bytes::Bytes;
12use clap::Parser;
13use indexmap::IndexMap;
14use itertools::Itertools;
15use tokio::runtime::Handle;
16use url::Url;
17use virtual_fs::{
18 ArcFileSystem, DeviceFile, FileSystem, MountFileSystem, OverlayFileSystem,
19 RootFileSystemBuilder,
20};
21use virtual_net::ruleset::Ruleset;
22use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value};
23use wasmer_config::package::PackageSource as PackageSpecifier;
24use wasmer_types::ModuleHash;
25#[cfg(feature = "journal")]
26use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
27use wasmer_wasix::{
28 PluggableRuntime, RewindState, Runtime, WasiEnv, WasiEnvBuilder, WasiError, WasiFunctionEnv,
29 WasiVersion,
30 bin_factory::BinaryPackage,
31 capabilities::Capabilities,
32 get_wasi_versions,
33 http::HttpClient,
34 journal::{CompactingLogFileJournal, DynJournal, DynReadableJournal},
35 os::{TtyBridge, tty_sys::SysTty},
36 rewind_ext,
37 runners::MAPPED_CURRENT_DIR_DEFAULT_PATH,
38 runners::{MappedCommand, MappedDirectory, MountedDirectory},
39 runtime::{
40 module_cache::{FileSystemCache, ModuleCache},
41 package_loader::{BuiltinPackageLoader, PackageLoader},
42 resolver::{
43 BackendSource, FileSystemSource, InMemorySource, LocalRegistrySource, MultiSource,
44 Source, WebSource,
45 },
46 task_manager::{
47 VirtualTaskManagerExt,
48 tokio::{RuntimeOrHandle, TokioTaskManager},
49 },
50 },
51 types::__WASI_STDIN_FILENO,
52 wasmer_wasix_types::wasi::Errno,
53};
54
55use crate::{
56 config::{UserRegistry, WasmerEnv},
57 utils::{
58 WAPM_SOURCE_CACHE_TIMEOUT, parse_envvar, parse_mapdir, parse_volume,
59 registry_query_cache_dir,
60 },
61};
62
63use super::{
64 CliPackageSource, ExecutableTarget,
65 capabilities::{self, PkgCapabilityCache},
66};
67
68#[derive(Debug, Parser, Clone, Default)]
69pub struct Wasi {
71 #[clap(
73 long = "volume",
74 name = "[HOST_DIR:]GUEST_DIR",
75 value_parser = parse_volume,
76 )]
77 pub(crate) volumes: Vec<MappedDirectory>,
78
79 #[clap(long = "dir", group = "wasi", hide = true)]
81 pub(crate) pre_opened_directories: Vec<PathBuf>,
82
83 #[clap(
85 long = "mapdir",
86 value_parser = parse_mapdir,
87 hide = true
88 )]
89 pub(crate) mapped_dirs: Vec<MappedDirectory>,
90
91 #[clap(long = "cwd")]
94 pub(crate) cwd: Option<PathBuf>,
95
96 #[clap(
98 long = "env",
99 name = "KEY=VALUE",
100 value_parser=parse_envvar,
101 )]
102 pub(crate) env_vars: Vec<(String, String)>,
103
104 #[clap(long = "env-file", name = "PATH")]
106 pub(crate) env_file: Option<PathBuf>,
107
108 #[clap(long, env)]
110 pub(crate) forward_host_env: bool,
111
112 #[clap(long = "use", name = "USE")]
114 pub(crate) uses: Vec<String>,
115
116 #[clap(long = "include-webc", name = "WEBC")]
122 pub(super) include_webcs: Vec<PathBuf>,
123
124 #[clap(long = "offline")]
127 pub(super) offline: bool,
128
129 #[clap(long = "map-command", name = "MAPCMD")]
131 pub(super) map_commands: Vec<String>,
132
133 #[clap(long = "net", require_equals = true)]
152 pub networking: Option<Option<String>>,
155
156 #[clap(long = "no-tty")]
158 pub no_tty: bool,
159
160 #[clap(
164 long = "enable-async-threads",
165 require_equals = true,
166 default_missing_value = "true",
167 num_args = 0..=1,
168 action = clap::ArgAction::Set
169 )]
170 pub enable_async_threads: Option<bool>,
171
172 #[clap(long = "enable-cpu-backoff")]
177 pub enable_cpu_backoff: Option<u64>,
178
179 #[cfg(feature = "journal")]
185 #[clap(long = "journal")]
186 pub read_only_journals: Vec<PathBuf>,
187
188 #[cfg(feature = "journal")]
198 #[clap(long = "journal-writable")]
199 pub writable_journals: Vec<PathBuf>,
200
201 #[cfg(feature = "journal")]
204 #[clap(long = "enable-compaction")]
205 pub enable_compaction: bool,
206
207 #[cfg(feature = "journal")]
209 #[clap(long = "without-compact-on-drop")]
210 pub without_compact_on_drop: bool,
211
212 #[cfg(feature = "journal")]
218 #[clap(long = "with-compact-on-growth", default_value = "0.15")]
219 pub with_compact_on_growth: f32,
220
221 #[cfg(feature = "journal")]
232 #[clap(long = "snapshot-on")]
233 pub snapshot_on: Vec<SnapshotTrigger>,
234
235 #[cfg(feature = "journal")]
239 #[clap(long = "snapshot-period")]
240 pub snapshot_interval: Option<u64>,
241
242 #[cfg(feature = "journal")]
245 #[clap(long = "stop-after-snapshot")]
246 pub stop_after_snapshot: bool,
247
248 #[cfg(feature = "journal")]
250 #[clap(long = "skip-journal-stdio")]
251 pub skip_stdio_during_bootstrap: bool,
252
253 #[clap(long)]
257 pub http_client: bool,
258
259 #[clap(long = "deny-multiple-wasi-versions")]
261 pub deny_multiple_wasi_versions: bool,
262
263 #[clap(long = "disable-cache")]
268 disable_cache: bool,
269}
270
271pub struct RunProperties {
272 pub ctx: WasiFunctionEnv,
273 pub path: PathBuf,
274 pub invoke: Option<String>,
275 pub args: Vec<String>,
276}
277
278fn utf8_env_part(part: OsString) -> Result<String> {
281 part.into_string()
282 .map_err(|part| anyhow::anyhow!("environment variable is not valid UTF-8: {part:?}"))
283}
284
285#[allow(dead_code)]
286impl Wasi {
287 pub fn map_dir(&mut self, alias: &str, target_on_disk: PathBuf) {
288 self.volumes.push(MappedDirectory {
289 guest: alias.to_string(),
290 host: target_on_disk,
291 });
292 }
293
294 pub fn set_env(&mut self, key: &str, value: &str) {
295 self.env_vars.push((key.to_string(), value.to_string()));
296 }
297
298 pub(crate) fn resolved_env_vars(&self) -> Result<Vec<(String, String)>> {
299 let mut env = IndexMap::new();
300 if let Some(path) = &self.env_file {
301 let entries = dotenvy::from_path_iter(path)
302 .with_context(|| format!("Could not read env file '{}'", path.display()))?;
303 for entry in entries {
304 let (key, value) = entry
305 .with_context(|| format!("Could not parse env file '{}'", path.display()))?;
306 env.insert(key, value);
307 }
308 }
309 for (key, value) in &self.env_vars {
310 env.insert(key.clone(), value.clone());
311 }
312 Ok(env.into_iter().collect())
313 }
314
315 pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
317 get_wasi_versions(module, false)
322 }
323
324 pub fn has_wasi_imports(module: &Module) -> bool {
326 get_wasi_versions(module, false).is_some()
329 }
330
331 pub(crate) fn all_volumes(&self) -> Vec<MappedDirectory> {
332 self.volumes
333 .iter()
334 .cloned()
335 .chain(self.pre_opened_directories.iter().map(|d| MappedDirectory {
336 host: d.clone(),
337 guest: d.to_str().expect("must be a valid path string").to_string(),
338 }))
339 .chain(self.mapped_dirs.iter().cloned())
340 .collect_vec()
341 }
342
343 pub fn prepare(
344 &self,
345 module: &Module,
346 program_name: String,
347 args: Vec<String>,
348 rt: Arc<dyn Runtime + Send + Sync>,
349 ) -> Result<WasiEnvBuilder> {
350 let args = args.into_iter().map(|arg| arg.into_bytes());
351
352 let map_commands = self
353 .map_commands
354 .iter()
355 .map(|map| map.split_once('=').unwrap())
356 .map(|(a, b)| (a.to_string(), b.to_string()))
357 .collect::<HashMap<_, _>>();
358
359 let mut uses = Vec::new();
360 for name in &self.uses {
361 let specifier = PackageSpecifier::from_str(name)
362 .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
363 let pkg = {
364 let inner_rt = rt.clone();
365 rt.task_manager()
366 .spawn_and_block_on(async move {
367 BinaryPackage::from_registry(&specifier, &*inner_rt).await
368 })
369 .with_context(|| format!("Unable to load \"{name}\""))??
370 };
371 uses.push(pkg);
372 }
373
374 let mut builder = WasiEnv::builder(program_name)
375 .runtime(Arc::clone(&rt))
376 .args(args)
377 .envs(self.resolved_env_vars()?)
378 .uses(uses)
379 .map_commands(map_commands);
380
381 let mut builder = {
382 let mount_fs = RootFileSystemBuilder::new()
383 .with_tty(Box::new(DeviceFile::new(__WASI_STDIN_FILENO)))
384 .build();
385 let (have_current_dir, mapped_dirs) = self.build_mapped_directories(false)?;
386 let mut root_layers: Vec<Arc<dyn FileSystem + Send + Sync>> = Vec::new();
387
388 for mapped in mapped_dirs {
389 let MountedDirectory { guest, fs } = MountedDirectory::from(mapped);
390 if guest == "/" {
391 root_layers.push(fs);
392 } else {
393 mount_fs.mount(&guest, Arc::new(fs))?;
394 }
395 }
396
397 if !root_layers.is_empty() {
398 let existing_root = mount_fs
399 .filesystem_at(Path::new("/"))
400 .expect("root fs builder should always mount /");
401 mount_fs.set_mount(
402 Path::new("/"),
403 Arc::new(OverlayFileSystem::new(
404 ArcFileSystem::new(existing_root),
405 root_layers,
406 )),
407 )?;
408 };
409
410 if let Some(cwd) = self.cwd.as_ref() {
411 if !cwd.starts_with("/") {
412 bail!("The argument to --cwd must be an absolute path");
413 }
414 builder = builder.current_dir(cwd.clone());
415 }
416
417 builder = builder
419 .mount_fs(mount_fs)
420 .preopen_dir(Path::new("/"))
421 .unwrap();
422
423 let dot_path = if have_current_dir {
424 PathBuf::from(MAPPED_CURRENT_DIR_DEFAULT_PATH)
425 } else {
426 PathBuf::from("/")
427 };
428
429 builder.add_preopen_build(|p| {
430 p.directory(&dot_path)
431 .alias(".")
432 .read(true)
433 .write(true)
434 .create(true)
435 })?;
436
437 builder
438 };
439
440 *builder.capabilities_mut() = self.capabilities();
441
442 #[cfg(feature = "journal")]
443 {
444 for trigger in self.snapshot_on.iter().cloned() {
445 builder.add_snapshot_trigger(trigger);
446 }
447 if let Some(interval) = self.snapshot_interval {
448 builder.with_snapshot_interval(std::time::Duration::from_millis(interval));
449 }
450 if self.stop_after_snapshot {
451 builder.with_stop_running_after_snapshot(true);
452 }
453 let (r, w) = self.build_journals()?;
454 for journal in r {
455 builder.add_read_only_journal(journal);
456 }
457 for journal in w {
458 builder.add_writable_journal(journal);
459 }
460 builder.with_skip_stdio_during_bootstrap(self.skip_stdio_during_bootstrap);
461 }
462
463 Ok(builder)
464 }
465
466 #[cfg(feature = "journal")]
467 #[allow(clippy::type_complexity)]
468 pub fn build_journals(
469 &self,
470 ) -> anyhow::Result<(Vec<Arc<DynReadableJournal>>, Vec<Arc<DynJournal>>)> {
471 let mut readable = Vec::new();
472 for journal in self.read_only_journals.clone() {
473 if matches!(std::fs::metadata(&journal), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
474 {
475 bail!("Read-only journal file does not exist: {journal:?}");
476 }
477
478 readable
479 .push(Arc::new(LogFileJournal::new_readonly(journal)?) as Arc<DynReadableJournal>);
480 }
481
482 let mut writable = Vec::new();
483 for journal in self.writable_journals.clone() {
484 if self.enable_compaction {
485 let mut journal = CompactingLogFileJournal::new(journal)?;
486 if !self.without_compact_on_drop {
487 journal = journal.with_compact_on_drop()
488 }
489 if self.with_compact_on_growth.is_normal() && self.with_compact_on_growth != 0f32 {
490 journal = journal.with_compact_on_factor_size(self.with_compact_on_growth);
491 }
492 writable.push(Arc::new(journal) as Arc<DynJournal>);
493 } else {
494 writable.push(Arc::new(LogFileJournal::new(journal)?));
495 }
496 }
497 Ok((readable, writable))
498 }
499
500 #[cfg(not(feature = "journal"))]
501 pub fn build_journals(&self) -> anyhow::Result<Vec<Arc<DynJournal>>> {
502 Ok(Vec::new())
503 }
504
505 pub fn build_mapped_directories(
506 &self,
507 is_wasix: bool,
508 ) -> Result<(bool, Vec<MappedDirectory>), anyhow::Error> {
509 let mut mapped_dirs = Vec::new();
510
511 let mut have_current_dir = false;
513 for MappedDirectory { host, guest } in &self.all_volumes() {
514 let resolved_host = host.canonicalize().with_context(|| {
515 format!(
516 "could not canonicalize path for argument '--volume {}:{}'",
517 host.display(),
518 guest,
519 )
520 })?;
521
522 if guest == "/" && is_wasix {
523 tracing::warn!(
526 "Mounting on the guest's virtual root with --volume <HOST_DIR>:/ breaks WASIX modules' filesystems"
527 );
528 }
529
530 let mapping = if guest == "." {
531 if have_current_dir {
532 bail!(
533 "Cannot pre-open the current directory twice: '--volume=.' must only be specified once"
534 );
535 }
536 have_current_dir = true;
537
538 let host = if host == Path::new(".") {
539 std::env::current_dir().context("could not determine current directory")?
540 } else {
541 host.clone()
542 };
543 MappedDirectory {
544 host: resolved_host,
545 guest: if is_wasix {
546 MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string()
547 } else {
548 "/".to_string()
549 },
550 }
551 } else {
552 MappedDirectory {
553 host: resolved_host,
554 guest: guest.clone(),
555 }
556 };
557 mapped_dirs.push(mapping);
558 }
559
560 Ok((have_current_dir, mapped_dirs))
561 }
562
563 pub fn build_mapped_commands(&self) -> Result<Vec<MappedCommand>, anyhow::Error> {
564 self.map_commands
565 .iter()
566 .map(|item| {
567 let (a, b) = item.split_once('=').with_context(|| {
568 format!(
569 "Invalid --map-command flag: expected <ALIAS>=<HOST_PATH>, got '{item}'"
570 )
571 })?;
572
573 let a = a.trim();
574 let b = b.trim();
575
576 if a.is_empty() {
577 bail!("Invalid --map-command flag - alias cannot be empty: '{item}'");
578 }
579 if b.is_empty() {
581 bail!("Invalid --map-command flag - host path cannot be empty: '{item}'");
582 }
583
584 Ok(MappedCommand {
585 alias: a.to_string(),
586 target: b.to_string(),
587 })
588 })
589 .collect::<Result<Vec<_>, anyhow::Error>>()
590 }
591
592 pub fn capabilities(&self) -> Capabilities {
593 let mut caps = Capabilities::default();
594
595 if self.http_client {
596 caps.http_client = wasmer_wasix::http::HttpClientCapabilityV1::new_allow_all();
597 }
598
599 if let Some(enable_async_threads) = self.enable_async_threads {
600 caps.threading.enable_asynchronous_threading = enable_async_threads;
601 }
602 caps.threading.enable_exponential_cpu_backoff =
603 self.enable_cpu_backoff.map(Duration::from_millis);
604
605 caps
606 }
607
608 pub fn prepare_runtime<I>(
609 &self,
610 engine: Engine,
611 env: &WasmerEnv,
612 pkg_cache_path: &Path,
613 rt_or_handle: I,
614 preferred_webc_version: webc::Version,
615 compiler_debug_dir_used: bool,
616 ) -> Result<impl Runtime + Send + Sync + use<I>>
617 where
618 I: Into<RuntimeOrHandle>,
619 {
620 let tokio_task_manager = Arc::new(TokioTaskManager::new(rt_or_handle.into()));
621 let mut rt = PluggableRuntime::new(tokio_task_manager.clone());
622
623 let has_networking = self.networking.is_some()
624 || capabilities::get_cached_capability(pkg_cache_path)
625 .ok()
626 .is_some_and(|v| v.enable_networking);
627
628 let ruleset = self
629 .networking
630 .clone()
631 .flatten()
632 .map(|ruleset| Ruleset::from_str(&ruleset))
633 .transpose()?;
634
635 let network = if let Some(ruleset) = ruleset {
636 virtual_net::host::LocalNetworking::with_ruleset(ruleset)
637 } else {
638 virtual_net::host::LocalNetworking::default()
639 };
640
641 if has_networking {
642 rt.set_networking_implementation(network);
643 } else {
644 let net = super::capabilities::net::AskingNetworking::new(
645 pkg_cache_path.to_path_buf(),
646 Arc::new(network),
647 );
648
649 rt.set_networking_implementation(net);
650 }
651
652 #[cfg(feature = "journal")]
653 {
654 let (r, w) = self.build_journals()?;
655 for journal in r {
656 rt.add_read_only_journal(journal);
657 }
658 for journal in w {
659 rt.add_writable_journal(journal);
660 }
661 }
662
663 if !self.no_tty {
664 let tty = Arc::new(SysTty);
665 tty.reset();
666 rt.set_tty(tty);
667 }
668
669 let client =
670 wasmer_wasix::http::default_http_client().context("No HTTP client available")?;
671 let client = Arc::new(client);
672
673 let package_loader = self
674 .prepare_package_loader(env, client.clone())
675 .context("Unable to prepare the package loader")?;
676
677 let registry = self.prepare_source(env, client, preferred_webc_version)?;
678
679 if !self.disable_cache && !compiler_debug_dir_used {
680 let cache_dir = env.cache_dir().join("compiled");
681 let module_cache = wasmer_wasix::runtime::module_cache::in_memory()
682 .with_fallback(FileSystemCache::new(cache_dir, tokio_task_manager));
683 rt.set_module_cache(module_cache);
684 }
685
686 rt.set_package_loader(package_loader)
687 .set_source(registry)
688 .set_engine(engine);
689
690 Ok(rt)
691 }
692
693 pub fn instantiate(
695 &self,
696 module: &Module,
697 module_hash: ModuleHash,
698 program_name: String,
699 args: Vec<String>,
700 runtime: Arc<dyn Runtime + Send + Sync>,
701 store: &mut Store,
702 ) -> Result<(WasiFunctionEnv, Instance)> {
703 let builder = self.prepare(module, program_name, args, runtime)?;
704 let (instance, wasi_env) = builder.instantiate_ext(module.clone(), module_hash, store)?;
705
706 Ok((wasi_env, instance))
707 }
708
709 pub fn for_binfmt_interpreter() -> Result<Self> {
710 let dir = std::env::var_os("WASMER_BINFMT_MISC_PREOPEN")
711 .map(Into::into)
712 .unwrap_or_else(|| PathBuf::from("."));
713 Ok(Self {
714 deny_multiple_wasi_versions: true,
715 env_vars: std::env::vars_os()
716 .map(|(name, value)| Ok((utf8_env_part(name)?, utf8_env_part(value)?)))
717 .collect::<Result<_>>()?,
718 volumes: vec![MappedDirectory {
719 host: dir.clone(),
720 guest: dir
721 .to_str()
722 .expect("dir must be a valid string")
723 .to_string(),
724 }],
725 ..Self::default()
726 })
727 }
728
729 fn prepare_package_loader(
730 &self,
731 env: &WasmerEnv,
732 client: Arc<dyn HttpClient + Send + Sync>,
733 ) -> Result<BuiltinPackageLoader> {
734 let checkout_dir = env.cache_dir().join("checkouts");
735 let tokens = tokens_by_authority(env)?;
736
737 let loader = BuiltinPackageLoader::new()
738 .with_cache_dir(checkout_dir)
739 .with_shared_http_client(client)
740 .with_tokens(tokens);
741
742 Ok(loader)
743 }
744
745 fn prepare_source(
746 &self,
747 env: &WasmerEnv,
748 client: Arc<dyn HttpClient + Send + Sync>,
749 preferred_webc_version: webc::Version,
750 ) -> Result<MultiSource> {
751 let mut source = MultiSource::default();
752
753 let mut preloaded = InMemorySource::new();
757 for path in &self.include_webcs {
758 if path.is_dir() {
759 source.add_source(LocalRegistrySource::new(path)?);
760 } else {
761 preloaded
762 .add_webc(path)
763 .with_context(|| format!("Unable to load \"{}\"", path.display()))?;
764 }
765 }
766 source.add_source(preloaded);
767
768 if !self.offline {
772 let graphql_endpoint = self.graphql_endpoint(env)?;
773 let cache_dir = registry_query_cache_dir(env.cache_dir(), &graphql_endpoint);
774 let mut wapm_source = BackendSource::new(graphql_endpoint, Arc::clone(&client))
775 .with_local_cache(cache_dir, WAPM_SOURCE_CACHE_TIMEOUT)
776 .with_preferred_webc_version(preferred_webc_version);
777 if let Some(token) = env
778 .config()?
779 .registry
780 .get_login_token_for_registry(wapm_source.registry_endpoint().as_str())
781 {
782 wapm_source = wapm_source.with_auth_token(token);
783 }
784 source.add_source(wapm_source);
785
786 let cache_dir = env.cache_dir().join("downloads");
787 source.add_source(WebSource::new(cache_dir, client));
788 }
789
790 source.add_source(FileSystemSource::default());
791
792 Ok(source)
793 }
794
795 fn graphql_endpoint(&self, env: &WasmerEnv) -> Result<Url> {
796 if let Ok(endpoint) = env.registry_endpoint() {
797 return Ok(endpoint);
798 }
799
800 let config = env.config()?;
801 let graphql_endpoint = config.registry.get_graphql_url();
802 let graphql_endpoint = graphql_endpoint
803 .parse()
804 .with_context(|| format!("Unable to parse \"{graphql_endpoint}\" as a URL"))?;
805
806 Ok(graphql_endpoint)
807 }
808}
809
810fn parse_registry(r: &str) -> Result<Url> {
811 UserRegistry::from(r).graphql_endpoint()
812}
813
814fn tokens_by_authority(env: &WasmerEnv) -> Result<HashMap<String, String>> {
815 let mut tokens = HashMap::new();
816 let config = env.config()?;
817
818 for credentials in config.registry.tokens {
819 if let Ok(url) = Url::parse(&credentials.registry)
820 && url.has_authority()
821 {
822 tokens.insert(url.authority().to_string(), credentials.token);
823 }
824 }
825
826 if let (Ok(current_registry), Some(token)) = (env.registry_endpoint(), env.token())
827 && current_registry.has_authority()
828 {
829 tokens.insert(current_registry.authority().to_string(), token);
830 }
831
832 let mut frontend_tokens = HashMap::new();
845 for (hostname, token) in &tokens {
846 if let Some(frontend_url) = hostname.strip_prefix("registry.")
847 && !tokens.contains_key(frontend_url)
848 {
849 frontend_tokens.insert(frontend_url.to_string(), token.clone());
850 }
851 }
852 tokens.extend(frontend_tokens);
853
854 Ok(tokens)
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860
861 #[test]
862 fn env_file_is_merged_and_explicit_env_wins() {
863 let temporary = tempfile::tempdir().unwrap();
864 let path = temporary.path().join("runtime.env");
865 std::fs::write(&path, "FROM_FILE=yes\nSHARED=file\n").unwrap();
866 let wasi = Wasi {
867 env_file: Some(path),
868 env_vars: vec![("SHARED".to_owned(), "explicit".to_owned())],
869 ..Default::default()
870 };
871
872 assert_eq!(
873 wasi.resolved_env_vars().unwrap(),
874 [
875 ("FROM_FILE".to_owned(), "yes".to_owned()),
876 ("SHARED".to_owned(), "explicit".to_owned()),
877 ]
878 );
879 }
880}