Skip to main content

wasmer_cli/commands/run/
wasi.rs

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)]
69/// WASI Options
70pub struct Wasi {
71    /// Map a host directory to a different location for the Wasm module
72    #[clap(
73        long = "volume",
74        name = "[HOST_DIR:]GUEST_DIR",
75        value_parser = parse_volume,
76    )]
77    pub(crate) volumes: Vec<MappedDirectory>,
78
79    // Legacy option
80    #[clap(long = "dir", group = "wasi", hide = true)]
81    pub(crate) pre_opened_directories: Vec<PathBuf>,
82
83    // Legacy option
84    #[clap(
85        long = "mapdir",
86        value_parser = parse_mapdir,
87        hide = true
88     )]
89    pub(crate) mapped_dirs: Vec<MappedDirectory>,
90
91    /// Set the module's initial CWD to this path; does not work with
92    /// WASI preview 1 modules.
93    #[clap(long = "cwd")]
94    pub(crate) cwd: Option<PathBuf>,
95
96    /// Pass custom environment variables
97    #[clap(
98        long = "env",
99        name = "KEY=VALUE",
100        value_parser=parse_envvar,
101    )]
102    pub(crate) env_vars: Vec<(String, String)>,
103
104    /// Load environment variables from a dotenv file.
105    #[clap(long = "env-file", name = "PATH")]
106    pub(crate) env_file: Option<PathBuf>,
107
108    /// Forward all host env variables to guest
109    #[clap(long, env)]
110    pub(crate) forward_host_env: bool,
111
112    /// List of other containers this module depends on
113    #[clap(long = "use", name = "USE")]
114    pub(crate) uses: Vec<String>,
115
116    /// Webc packages that are explicitly included for execution, taking
117    /// precedence over the registry ones: either a `*.webc` file, or a
118    /// directory laid out `<namespace>/<name>/<version>.webc` and queried on
119    /// demand like a registry. Resolves named dependencies from local files,
120    /// offline.
121    #[clap(long = "include-webc", name = "WEBC")]
122    pub(super) include_webcs: Vec<PathBuf>,
123
124    /// Resolve only from local sources (`--include-webc`, filesystem paths), never
125    /// the registry. Needed offline, where a registry query would fail resolution.
126    #[clap(long = "offline")]
127    pub(super) offline: bool,
128
129    /// List of injected atoms
130    #[clap(long = "map-command", name = "MAPCMD")]
131    pub(super) map_commands: Vec<String>,
132
133    /// Enable networking with the host network.
134    ///
135    /// Allows WASI modules to open TCP and UDP connections, create sockets, ...
136    ///
137    /// Optionally, a set of network filters could be defined which allows fine-grained
138    /// control over the network sandbox.
139    ///
140    /// Rule Syntax:
141    ///
142    /// <rule-type>:<allow|deny>=<rule-expression>
143    ///
144    /// Examples:
145    ///
146    ///  - Allow a specific domain and port: dns:allow=example.com:80
147    ///
148    ///  - Deny a domain and all its subdomains on all ports: dns:deny=*danger.xyz:*
149    ///
150    ///  - Allow opening ipv4 sockets only on a specific IP and port: ipv4:allow=127.0.0.1:80/in.
151    #[clap(long = "net", require_equals = true)]
152    // Note that when --net is passed to the cli, the first Option will be initialized: Some(None)
153    // and when --net=<ruleset> is specified, the inner Option will be initialized: Some(Some(ruleset))
154    pub networking: Option<Option<String>>,
155
156    /// Disables the TTY bridge
157    #[clap(long = "no-tty")]
158    pub no_tty: bool,
159
160    /// Enables or disables asynchronous threading.
161    ///
162    /// If omitted, the runtime default is used.
163    #[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    /// Enables an exponential backoff (measured in milli-seconds) of
173    /// the process CPU usage when there are no active run tokens (when set
174    /// holds the maximum amount of time that it will pause the CPU)
175    /// (default = off)
176    #[clap(long = "enable-cpu-backoff")]
177    pub enable_cpu_backoff: Option<u64>,
178
179    /// Specifies one or more journal files that Wasmer will use to restore
180    /// the state of the WASM process as it executes.
181    ///
182    /// The state of the WASM process and its sandbox will be reapplied using
183    /// the journals in the order that you specify here.
184    #[cfg(feature = "journal")]
185    #[clap(long = "journal")]
186    pub read_only_journals: Vec<PathBuf>,
187
188    /// Specifies one or more journal files that Wasmer will use to restore
189    /// and save the state of the WASM process as it executes.
190    ///
191    /// The state of the WASM process and its sandbox will be reapplied using
192    /// the journals in the order that you specify here.
193    ///
194    /// The last journal file specified will be created if it does not exist
195    /// and opened for read and write. New journal events will be written to this
196    /// file
197    #[cfg(feature = "journal")]
198    #[clap(long = "journal-writable")]
199    pub writable_journals: Vec<PathBuf>,
200
201    /// Flag that indicates if the journal will be automatically compacted
202    /// as it fills up and when the process exits
203    #[cfg(feature = "journal")]
204    #[clap(long = "enable-compaction")]
205    pub enable_compaction: bool,
206
207    /// Tells the compactor not to compact when the journal log file is closed
208    #[cfg(feature = "journal")]
209    #[clap(long = "without-compact-on-drop")]
210    pub without_compact_on_drop: bool,
211
212    /// Tells the compactor to compact when it grows by a certain factor of
213    /// its original size. (i.e. '0.2' would be it compacts after the journal
214    /// has grown by 20 percent)
215    ///
216    /// Default is to compact on growth that exceeds 15%
217    #[cfg(feature = "journal")]
218    #[clap(long = "with-compact-on-growth", default_value = "0.15")]
219    pub with_compact_on_growth: f32,
220
221    /// Indicates what events will cause a snapshot to be taken
222    /// and written to the journal file.
223    ///
224    /// If not specified, the default is to snapshot when the process idles, when
225    /// the process exits or periodically if an interval argument is also supplied,
226    /// as well as when the process requests a snapshot explicitly.
227    ///
228    /// Additionally if the snapshot-on is not specified it will also take a snapshot
229    /// on the first stdin, environ or socket listen - this can be used to accelerate
230    /// the boot up time of WASM processes.
231    #[cfg(feature = "journal")]
232    #[clap(long = "snapshot-on")]
233    pub snapshot_on: Vec<SnapshotTrigger>,
234
235    /// Adds a periodic interval (measured in milli-seconds) that the runtime will automatically
236    /// take snapshots of the running process and write them to the journal. When specifying
237    /// this parameter it implies that `--snapshot-on interval` has also been specified.
238    #[cfg(feature = "journal")]
239    #[clap(long = "snapshot-period")]
240    pub snapshot_interval: Option<u64>,
241
242    /// If specified, the runtime will stop executing the WASM module after the first snapshot
243    /// is taken.
244    #[cfg(feature = "journal")]
245    #[clap(long = "stop-after-snapshot")]
246    pub stop_after_snapshot: bool,
247
248    /// Skip writes to stdout and stderr when replying journal events to bootstrap a module.
249    #[cfg(feature = "journal")]
250    #[clap(long = "skip-journal-stdio")]
251    pub skip_stdio_during_bootstrap: bool,
252
253    /// Allow instances to send http requests.
254    ///
255    /// Access to domains is granted by default.
256    #[clap(long)]
257    pub http_client: bool,
258
259    /// Require WASI modules to only import 1 version of WASI.
260    #[clap(long = "deny-multiple-wasi-versions")]
261    pub deny_multiple_wasi_versions: bool,
262
263    /// Disable the cache for the compiled modules.
264    ///
265    /// Cache is used to speed up the loading of modules, as the
266    /// generated artifacts are cached.
267    #[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
278/// Environment variables are arbitrary byte strings on unix, but `Wasi` stores
279/// them as `String`, so reject the ones that cannot be represented.
280fn 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    /// Gets the WASI version (if any) for the provided module
316    pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
317        // Get the wasi version in non-strict mode, so multiple wasi versions
318        // are potentially allowed.
319        //
320        // Checking for multiple wasi versions is handled outside this function.
321        get_wasi_versions(module, false)
322    }
323
324    /// Checks if a given module has any WASI imports at all.
325    pub fn has_wasi_imports(module: &Module) -> bool {
326        // Get the wasi version in non-strict mode, so no other imports
327        // are allowed
328        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            // Open the root of the new filesystem
418            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        // Process the --volume flag.
512        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                // Note: it appears we canonicalize the path before this point and showing the value of
524                // `host` in the error message may throw users off, so we use a placeholder.
525                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                // TODO(theduke): check if host command exists, and canonicalize PathBuf.
580                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    /// Helper function for instantiating a module with Wasi imports for the `Run` command.
694    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        // Local packages go first so they override the registry. A directory
754        // is served on demand as a local registry; a single file is loaded
755        // eagerly under its manifest id.
756        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        // Drop the registry/web sources offline. Their network errors bubble out
769        // of the merging MultiSource and abort resolution even when a local
770        // source already matched.
771        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    // Note: The global wasmer.toml config file stores URLs for the GraphQL
833    // endpoint, however that's often on the backend (i.e.
834    // https://registry.wasmer.io/graphql) and we also want to use the same API
835    // token when sending requests to the frontend (e.g. downloading a package
836    // using the `Accept: application/webc` header).
837    //
838    // As a workaround to avoid needing to query *all* backends to find out
839    // their frontend URL every time the `wasmer` CLI runs, we'll assume that
840    // when a backend is called something like `registry.wasmer.io`, the
841    // frontend will be at `wasmer.io`. This works everywhere except for people
842    // developing the backend locally... Sorry, Ayush.
843
844    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}