wasmer_cli/commands/run/
wasi.rs

1use std::{
2    collections::{BTreeSet, HashMap},
3    path::{Path, PathBuf},
4    str::FromStr,
5    sync::{Arc, mpsc::Sender},
6    time::Duration,
7};
8
9use anyhow::{Context, Result, bail};
10use bytes::Bytes;
11use clap::Parser;
12use tokio::runtime::Handle;
13use url::Url;
14use virtual_fs::{DeviceFile, FileSystem, PassthruFileSystem, RootFileSystemBuilder};
15use virtual_net::ruleset::Ruleset;
16use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value};
17use wasmer_config::package::PackageSource as PackageSpecifier;
18use wasmer_types::ModuleHash;
19#[cfg(feature = "journal")]
20use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
21use wasmer_wasix::{
22    PluggableRuntime, RewindState, Runtime, WasiEnv, WasiEnvBuilder, WasiError, WasiFunctionEnv,
23    WasiVersion,
24    bin_factory::BinaryPackage,
25    capabilities::Capabilities,
26    default_fs_backing, get_wasi_versions,
27    http::HttpClient,
28    journal::{CompactingLogFileJournal, DynJournal, DynReadableJournal},
29    os::{TtyBridge, tty_sys::SysTty},
30    rewind_ext,
31    runners::MAPPED_CURRENT_DIR_DEFAULT_PATH,
32    runners::{MappedCommand, MappedDirectory},
33    runtime::{
34        module_cache::{FileSystemCache, ModuleCache},
35        package_loader::{BuiltinPackageLoader, PackageLoader},
36        resolver::{
37            BackendSource, FileSystemSource, InMemorySource, MultiSource, Source, WebSource,
38        },
39        task_manager::{
40            VirtualTaskManagerExt,
41            tokio::{RuntimeOrHandle, TokioTaskManager},
42        },
43    },
44    types::__WASI_STDIN_FILENO,
45    wasmer_wasix_types::wasi::Errno,
46};
47
48use crate::{
49    config::{UserRegistry, WasmerEnv},
50    utils::{parse_envvar, parse_mapdir},
51};
52
53use super::{
54    ExecutableTarget, PackageSource,
55    capabilities::{self, PkgCapabilityCache},
56};
57
58const WAPM_SOURCE_CACHE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
59
60#[derive(Debug, Parser, Clone, Default)]
61/// WASI Options
62pub struct Wasi {
63    /// WASI pre-opened directory
64    #[clap(long = "dir", name = "DIR", group = "wasi")]
65    pub(crate) pre_opened_directories: Vec<PathBuf>,
66
67    /// Map a host directory to a different location for the Wasm module
68    #[clap(
69        long = "mapdir",
70        name = "GUEST_DIR:HOST_DIR",
71        value_parser=parse_mapdir,
72    )]
73    pub(crate) mapped_dirs: Vec<MappedDirectory>,
74
75    /// Pass custom environment variables
76    #[clap(
77        long = "env",
78        name = "KEY=VALUE",
79        value_parser=parse_envvar,
80    )]
81    pub(crate) env_vars: Vec<(String, String)>,
82
83    /// Forward all host env variables to guest
84    #[clap(long, env)]
85    pub(crate) forward_host_env: bool,
86
87    /// List of other containers this module depends on
88    #[clap(long = "use", name = "USE")]
89    pub(crate) uses: Vec<String>,
90
91    /// List of webc packages that are explicitly included for execution
92    /// Note: these packages will be used instead of those in the registry
93    #[clap(long = "include-webc", name = "WEBC")]
94    pub(super) include_webcs: Vec<PathBuf>,
95
96    /// List of injected atoms
97    #[clap(long = "map-command", name = "MAPCMD")]
98    pub(super) map_commands: Vec<String>,
99
100    /// Enable networking with the host network.
101    ///
102    /// Allows WASI modules to open TCP and UDP connections, create sockets, ...
103    ///
104    /// Optionally, a set of network filters could be defined which allows fine-grained
105    /// control over the network sandbox.
106    ///
107    /// Rule Syntax:
108    ///
109    /// <rule-type>:<allow|deny>=<rule-expression>
110    ///
111    /// Examples:
112    ///
113    ///  - Allow a specific domain and port: dns:allow=example.com:80
114    ///
115    ///  - Deny a domain and all its subdomains on all ports: dns:deny=*danger.xyz:*
116    ///
117    ///  - Allow opening ipv4 sockets only on a specific IP and port: ipv4:allow=127.0.0.1:80/in.
118    #[clap(long = "net", require_equals = true)]
119    // Note that when --net is passed to the cli, the first Option will be initialized: Some(None)
120    // and when --net=<ruleset> is specified, the inner Option will be initialized: Some(Some(ruleset))
121    pub networking: Option<Option<String>>,
122
123    /// Disables the TTY bridge
124    #[clap(long = "no-tty")]
125    pub no_tty: bool,
126
127    /// Enables asynchronous threading
128    #[clap(long = "enable-async-threads")]
129    pub enable_async_threads: bool,
130
131    /// Enables an exponential backoff (measured in milli-seconds) of
132    /// the process CPU usage when there are no active run tokens (when set
133    /// holds the maximum amount of time that it will pause the CPU)
134    /// (default = off)
135    #[clap(long = "enable-cpu-backoff")]
136    pub enable_cpu_backoff: Option<u64>,
137
138    /// Specifies one or more journal files that Wasmer will use to restore
139    /// the state of the WASM process as it executes.
140    ///
141    /// The state of the WASM process and its sandbox will be reapplied using
142    /// the journals in the order that you specify here.
143    #[cfg(feature = "journal")]
144    #[clap(long = "journal")]
145    pub read_only_journals: Vec<PathBuf>,
146
147    /// Specifies one or more journal files that Wasmer will use to restore
148    /// and save the state of the WASM process as it executes.
149    ///
150    /// The state of the WASM process and its sandbox will be reapplied using
151    /// the journals in the order that you specify here.
152    ///
153    /// The last journal file specified will be created if it does not exist
154    /// and opened for read and write. New journal events will be written to this
155    /// file
156    #[cfg(feature = "journal")]
157    #[clap(long = "journal-writable")]
158    pub writable_journals: Vec<PathBuf>,
159
160    /// Flag that indicates if the journal will be automatically compacted
161    /// as it fills up and when the process exits
162    #[cfg(feature = "journal")]
163    #[clap(long = "enable-compaction")]
164    pub enable_compaction: bool,
165
166    /// Tells the compactor not to compact when the journal log file is closed
167    #[cfg(feature = "journal")]
168    #[clap(long = "without-compact-on-drop")]
169    pub without_compact_on_drop: bool,
170
171    /// Tells the compactor to compact when it grows by a certain factor of
172    /// its original size. (i.e. '0.2' would be it compacts after the journal
173    /// has grown by 20 percent)
174    ///
175    /// Default is to compact on growth that exceeds 15%
176    #[cfg(feature = "journal")]
177    #[clap(long = "with-compact-on-growth", default_value = "0.15")]
178    pub with_compact_on_growth: f32,
179
180    /// Indicates what events will cause a snapshot to be taken
181    /// and written to the journal file.
182    ///
183    /// If not specified, the default is to snapshot when the process idles, when
184    /// the process exits or periodically if an interval argument is also supplied,
185    /// as well as when the process requests a snapshot explicitly.
186    ///
187    /// Additionally if the snapshot-on is not specified it will also take a snapshot
188    /// on the first stdin, environ or socket listen - this can be used to accelerate
189    /// the boot up time of WASM processes.
190    #[cfg(feature = "journal")]
191    #[clap(long = "snapshot-on")]
192    pub snapshot_on: Vec<SnapshotTrigger>,
193
194    /// Adds a periodic interval (measured in milli-seconds) that the runtime will automatically
195    /// take snapshots of the running process and write them to the journal. When specifying
196    /// this parameter it implies that `--snapshot-on interval` has also been specified.
197    #[cfg(feature = "journal")]
198    #[clap(long = "snapshot-period")]
199    pub snapshot_interval: Option<u64>,
200
201    /// If specified, the runtime will stop executing the WASM module after the first snapshot
202    /// is taken.
203    #[cfg(feature = "journal")]
204    #[clap(long = "stop-after-snapshot")]
205    pub stop_after_snapshot: bool,
206
207    /// Skip writes to stdout and stderr when replying journal events to bootstrap a module.
208    #[cfg(feature = "journal")]
209    #[clap(long = "skip-journal-stdio")]
210    pub skip_stdio_during_bootstrap: bool,
211
212    /// Allow instances to send http requests.
213    ///
214    /// Access to domains is granted by default.
215    #[clap(long)]
216    pub http_client: bool,
217
218    /// Require WASI modules to only import 1 version of WASI.
219    #[clap(long = "deny-multiple-wasi-versions")]
220    pub deny_multiple_wasi_versions: bool,
221
222    /// Disable the cache for the compiled modules.
223    ///
224    /// Cache is used to speed up the loading of modules, as the
225    /// generated artifacts are cached.
226    #[clap(long = "disable-cache")]
227    disable_cache: bool,
228}
229
230pub struct RunProperties {
231    pub ctx: WasiFunctionEnv,
232    pub path: PathBuf,
233    pub invoke: Option<String>,
234    pub args: Vec<String>,
235}
236
237#[allow(dead_code)]
238impl Wasi {
239    pub fn map_dir(&mut self, alias: &str, target_on_disk: PathBuf) {
240        self.mapped_dirs.push(MappedDirectory {
241            guest: alias.to_string(),
242            host: target_on_disk,
243        });
244    }
245
246    pub fn set_env(&mut self, key: &str, value: &str) {
247        self.env_vars.push((key.to_string(), value.to_string()));
248    }
249
250    /// Gets the WASI version (if any) for the provided module
251    pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
252        // Get the wasi version in non-strict mode, so multiple wasi versions
253        // are potentially allowed.
254        //
255        // Checking for multiple wasi versions is handled outside this function.
256        get_wasi_versions(module, false)
257    }
258
259    /// Checks if a given module has any WASI imports at all.
260    pub fn has_wasi_imports(module: &Module) -> bool {
261        // Get the wasi version in non-strict mode, so no other imports
262        // are allowed
263        get_wasi_versions(module, false).is_some()
264    }
265
266    pub fn prepare(
267        &self,
268        module: &Module,
269        program_name: String,
270        args: Vec<String>,
271        rt: Arc<dyn Runtime + Send + Sync>,
272    ) -> Result<WasiEnvBuilder> {
273        let args = args.into_iter().map(|arg| arg.into_bytes());
274
275        let map_commands = self
276            .map_commands
277            .iter()
278            .map(|map| map.split_once('=').unwrap())
279            .map(|(a, b)| (a.to_string(), b.to_string()))
280            .collect::<HashMap<_, _>>();
281
282        let mut uses = Vec::new();
283        for name in &self.uses {
284            let specifier = PackageSpecifier::from_str(name)
285                .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
286            let pkg = {
287                let inner_rt = rt.clone();
288                rt.task_manager()
289                    .spawn_and_block_on(async move {
290                        BinaryPackage::from_registry(&specifier, &*inner_rt).await
291                    })
292                    .with_context(|| format!("Unable to load \"{name}\""))??
293            };
294            uses.push(pkg);
295        }
296
297        let builder = WasiEnv::builder(program_name)
298            .runtime(Arc::clone(&rt))
299            .args(args)
300            .envs(self.env_vars.clone())
301            .uses(uses)
302            .map_commands(map_commands);
303
304        let mut builder = {
305            // If we preopen anything from the host then shallow copy it over
306            let root_fs = RootFileSystemBuilder::new()
307                .with_tty(Box::new(DeviceFile::new(__WASI_STDIN_FILENO)))
308                .build();
309
310            let mut mapped_dirs = Vec::new();
311
312            // Process the --dirs flag and merge it with --mapdir.
313            let mut have_current_dir = false;
314            for dir in &self.pre_opened_directories {
315                let mapping = if dir == Path::new(".") {
316                    if have_current_dir {
317                        bail!(
318                            "Cannot pre-open the current directory twice: --dir=. must only be specified once"
319                        );
320                    }
321                    have_current_dir = true;
322
323                    let current_dir =
324                        std::env::current_dir().context("could not determine current directory")?;
325
326                    MappedDirectory {
327                        host: current_dir,
328                        guest: MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string(),
329                    }
330                } else {
331                    let resolved = dir.canonicalize().with_context(|| {
332                        format!(
333                            "could not canonicalize path for argument '--dir {}'",
334                            dir.display()
335                        )
336                    })?;
337
338                    if &resolved != dir {
339                        bail!(
340                            "Invalid argument '--dir {}': path must either be absolute, or '.'",
341                            dir.display(),
342                        );
343                    }
344
345                    let guest = resolved
346                        .to_str()
347                        .with_context(|| {
348                            format!(
349                                "invalid argument '--dir {}': path must be valid utf-8",
350                                dir.display(),
351                            )
352                        })?
353                        .to_string();
354
355                    MappedDirectory {
356                        host: resolved,
357                        guest,
358                    }
359                };
360
361                mapped_dirs.push(mapping);
362            }
363
364            for MappedDirectory { host, guest } in &self.mapped_dirs {
365                let resolved_host = host.canonicalize().with_context(|| {
366                    format!(
367                        "could not canonicalize path for argument '--mapdir {}:{}'",
368                        host.display(),
369                        guest,
370                    )
371                })?;
372
373                let mapping = if guest == "." {
374                    if have_current_dir {
375                        bail!(
376                            "Cannot pre-open the current directory twice: '--mapdir=?:.' / '--dir=.' must only be specified once"
377                        );
378                    }
379                    have_current_dir = true;
380
381                    MappedDirectory {
382                        host: resolved_host,
383                        guest: MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string(),
384                    }
385                } else {
386                    MappedDirectory {
387                        host: resolved_host,
388                        guest: guest.clone(),
389                    }
390                };
391                mapped_dirs.push(mapping);
392            }
393
394            if !mapped_dirs.is_empty() {
395                // TODO: should we expose the common ancestor instead of root?
396                let fs_backing: Arc<dyn FileSystem + Send + Sync> =
397                    Arc::new(PassthruFileSystem::new(default_fs_backing()));
398                for MappedDirectory { host, guest } in self.mapped_dirs.clone() {
399                    let host = if !host.is_absolute() {
400                        Path::new("/").join(host)
401                    } else {
402                        host
403                    };
404                    root_fs.mount(guest.into(), &fs_backing, host)?;
405                }
406            }
407
408            // Open the root of the new filesystem
409            let b = builder
410                .sandbox_fs(root_fs)
411                .preopen_dir(Path::new("/"))
412                .unwrap();
413
414            if have_current_dir {
415                b.map_dir(".", MAPPED_CURRENT_DIR_DEFAULT_PATH)?
416            } else {
417                b.map_dir(".", "/")?
418            }
419        };
420
421        *builder.capabilities_mut() = self.capabilities();
422
423        #[cfg(feature = "journal")]
424        {
425            for trigger in self.snapshot_on.iter().cloned() {
426                builder.add_snapshot_trigger(trigger);
427            }
428            if let Some(interval) = self.snapshot_interval {
429                builder.with_snapshot_interval(std::time::Duration::from_millis(interval));
430            }
431            if self.stop_after_snapshot {
432                builder.with_stop_running_after_snapshot(true);
433            }
434            let (r, w) = self.build_journals()?;
435            for journal in r {
436                builder.add_read_only_journal(journal);
437            }
438            for journal in w {
439                builder.add_writable_journal(journal);
440            }
441            builder.with_skip_stdio_during_bootstrap(self.skip_stdio_during_bootstrap);
442        }
443
444        Ok(builder)
445    }
446
447    #[cfg(feature = "journal")]
448    #[allow(clippy::type_complexity)]
449    pub fn build_journals(
450        &self,
451    ) -> anyhow::Result<(Vec<Arc<DynReadableJournal>>, Vec<Arc<DynJournal>>)> {
452        let mut readable = Vec::new();
453        for journal in self.read_only_journals.clone() {
454            if matches!(std::fs::metadata(&journal), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
455            {
456                bail!("Read-only journal file does not exist: {journal:?}");
457            }
458
459            readable
460                .push(Arc::new(LogFileJournal::new_readonly(journal)?) as Arc<DynReadableJournal>);
461        }
462
463        let mut writable = Vec::new();
464        for journal in self.writable_journals.clone() {
465            if self.enable_compaction {
466                let mut journal = CompactingLogFileJournal::new(journal)?;
467                if !self.without_compact_on_drop {
468                    journal = journal.with_compact_on_drop()
469                }
470                if self.with_compact_on_growth.is_normal() && self.with_compact_on_growth != 0f32 {
471                    journal = journal.with_compact_on_factor_size(self.with_compact_on_growth);
472                }
473                writable.push(Arc::new(journal) as Arc<DynJournal>);
474            } else {
475                writable.push(Arc::new(LogFileJournal::new(journal)?));
476            }
477        }
478        Ok((readable, writable))
479    }
480
481    #[cfg(not(feature = "journal"))]
482    pub fn build_journals(&self) -> anyhow::Result<Vec<Arc<DynJournal>>> {
483        Ok(Vec::new())
484    }
485
486    pub fn build_mapped_directories(
487        &self,
488    ) -> Result<(bool, bool, Vec<MappedDirectory>), anyhow::Error> {
489        let mut mapped_dirs = Vec::new();
490
491        // Process the --dirs flag and merge it with --mapdir.
492        let mut have_current_dir = false;
493        for dir in &self.pre_opened_directories {
494            let mapping = if dir == Path::new(".") {
495                if have_current_dir {
496                    bail!(
497                        "Cannot pre-open the current directory twice: --dir=. must only be specified once"
498                    );
499                }
500                have_current_dir = true;
501
502                let current_dir =
503                    std::env::current_dir().context("could not determine current directory")?;
504
505                MappedDirectory {
506                    host: current_dir,
507                    guest: MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string(),
508                }
509            } else {
510                let resolved = dir.canonicalize().with_context(|| {
511                    format!(
512                        "could not canonicalize path for argument '--dir {}'",
513                        dir.display()
514                    )
515                })?;
516
517                if &resolved != dir {
518                    bail!(
519                        "Invalid argument '--dir {}': path must either be absolute, or '.'",
520                        dir.display(),
521                    );
522                }
523
524                let guest = resolved
525                    .to_str()
526                    .with_context(|| {
527                        format!(
528                            "invalid argument '--dir {}': path must be valid utf-8",
529                            dir.display(),
530                        )
531                    })?
532                    .to_string();
533
534                MappedDirectory {
535                    host: resolved,
536                    guest,
537                }
538            };
539
540            mapped_dirs.push(mapping);
541        }
542
543        for MappedDirectory { host, guest } in &self.mapped_dirs {
544            let resolved_host = host.canonicalize().with_context(|| {
545                format!(
546                    "could not canonicalize path for argument '--mapdir {}:{}'",
547                    host.display(),
548                    guest,
549                )
550            })?;
551
552            let mapping = if guest == "." {
553                if have_current_dir {
554                    bail!(
555                        "Cannot pre-open the current directory twice: '--mapdir=?:.' / '--dir=.' must only be specified once"
556                    );
557                }
558                have_current_dir = true;
559
560                MappedDirectory {
561                    host: resolved_host,
562                    guest: MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string(),
563                }
564            } else {
565                MappedDirectory {
566                    host: resolved_host,
567                    guest: guest.clone(),
568                }
569            };
570            mapped_dirs.push(mapping);
571        }
572
573        let is_tmp_mapped = mapped_dirs.iter().any(|d| d.guest == "/tmp");
574
575        Ok((have_current_dir, is_tmp_mapped, mapped_dirs))
576    }
577
578    pub fn build_mapped_commands(&self) -> Result<Vec<MappedCommand>, anyhow::Error> {
579        self.map_commands
580            .iter()
581            .map(|item| {
582                let (a, b) = item.split_once('=').with_context(|| {
583                    format!(
584                        "Invalid --map-command flag: expected <ALIAS>=<HOST_PATH>, got '{item}'"
585                    )
586                })?;
587
588                let a = a.trim();
589                let b = b.trim();
590
591                if a.is_empty() {
592                    bail!("Invalid --map-command flag - alias cannot be empty: '{item}'");
593                }
594                // TODO(theduke): check if host command exists, and canonicalize PathBuf.
595                if b.is_empty() {
596                    bail!("Invalid --map-command flag - host path cannot be empty: '{item}'");
597                }
598
599                Ok(MappedCommand {
600                    alias: a.to_string(),
601                    target: b.to_string(),
602                })
603            })
604            .collect::<Result<Vec<_>, anyhow::Error>>()
605    }
606
607    pub fn capabilities(&self) -> Capabilities {
608        let mut caps = Capabilities::default();
609
610        if self.http_client {
611            caps.http_client = wasmer_wasix::http::HttpClientCapabilityV1::new_allow_all();
612        }
613
614        caps.threading.enable_asynchronous_threading = self.enable_async_threads;
615        caps.threading.enable_exponential_cpu_backoff =
616            self.enable_cpu_backoff.map(Duration::from_millis);
617
618        caps
619    }
620
621    pub fn prepare_runtime<I>(
622        &self,
623        engine: Engine,
624        env: &WasmerEnv,
625        pkg_cache_path: &Path,
626        rt_or_handle: I,
627        preferred_webc_version: webc::Version,
628    ) -> Result<impl Runtime + Send + Sync + use<I>>
629    where
630        I: Into<RuntimeOrHandle>,
631    {
632        let tokio_task_manager = Arc::new(TokioTaskManager::new(rt_or_handle.into()));
633        let mut rt = PluggableRuntime::new(tokio_task_manager.clone());
634
635        let has_networking = self.networking.is_some()
636            || capabilities::get_cached_capability(pkg_cache_path)
637                .ok()
638                .is_some_and(|v| v.enable_networking);
639
640        let ruleset = self
641            .networking
642            .clone()
643            .flatten()
644            .map(|ruleset| Ruleset::from_str(&ruleset))
645            .transpose()?;
646
647        let network = if let Some(ruleset) = ruleset {
648            virtual_net::host::LocalNetworking::with_ruleset(ruleset)
649        } else {
650            virtual_net::host::LocalNetworking::default()
651        };
652
653        if has_networking {
654            rt.set_networking_implementation(network);
655        } else {
656            let net = super::capabilities::net::AskingNetworking::new(
657                pkg_cache_path.to_path_buf(),
658                Arc::new(network),
659            );
660
661            rt.set_networking_implementation(net);
662        }
663
664        #[cfg(feature = "journal")]
665        {
666            let (r, w) = self.build_journals()?;
667            for journal in r {
668                rt.add_read_only_journal(journal);
669            }
670            for journal in w {
671                rt.add_writable_journal(journal);
672            }
673        }
674
675        if !self.no_tty {
676            let tty = Arc::new(SysTty);
677            tty.reset();
678            rt.set_tty(tty);
679        }
680
681        let client =
682            wasmer_wasix::http::default_http_client().context("No HTTP client available")?;
683        let client = Arc::new(client);
684
685        let package_loader = self
686            .prepare_package_loader(env, client.clone())
687            .context("Unable to prepare the package loader")?;
688
689        let registry = self.prepare_source(env, client, preferred_webc_version)?;
690
691        if !self.disable_cache {
692            let cache_dir = env.cache_dir().join("compiled");
693            let module_cache = wasmer_wasix::runtime::module_cache::in_memory()
694                .with_fallback(FileSystemCache::new(cache_dir, tokio_task_manager));
695            rt.set_module_cache(module_cache);
696        }
697
698        rt.set_package_loader(package_loader)
699            .set_source(registry)
700            .set_engine(engine);
701
702        Ok(rt)
703    }
704
705    /// Helper function for instantiating a module with Wasi imports for the `Run` command.
706    pub fn instantiate(
707        &self,
708        module: &Module,
709        module_hash: ModuleHash,
710        program_name: String,
711        args: Vec<String>,
712        runtime: Arc<dyn Runtime + Send + Sync>,
713        store: &mut Store,
714    ) -> Result<(WasiFunctionEnv, Instance)> {
715        let builder = self.prepare(module, program_name, args, runtime)?;
716        let (instance, wasi_env) = builder.instantiate_ext(module.clone(), module_hash, store)?;
717
718        Ok((wasi_env, instance))
719    }
720
721    pub fn for_binfmt_interpreter() -> Result<Self> {
722        let dir = std::env::var_os("WASMER_BINFMT_MISC_PREOPEN")
723            .map(Into::into)
724            .unwrap_or_else(|| PathBuf::from("."));
725        Ok(Self {
726            deny_multiple_wasi_versions: true,
727            env_vars: std::env::vars().collect(),
728            pre_opened_directories: vec![dir],
729            ..Self::default()
730        })
731    }
732
733    fn prepare_package_loader(
734        &self,
735        env: &WasmerEnv,
736        client: Arc<dyn HttpClient + Send + Sync>,
737    ) -> Result<BuiltinPackageLoader> {
738        let checkout_dir = env.cache_dir().join("checkouts");
739        let tokens = tokens_by_authority(env)?;
740
741        let loader = BuiltinPackageLoader::new()
742            .with_cache_dir(checkout_dir)
743            .with_shared_http_client(client)
744            .with_tokens(tokens);
745
746        Ok(loader)
747    }
748
749    fn prepare_source(
750        &self,
751        env: &WasmerEnv,
752        client: Arc<dyn HttpClient + Send + Sync>,
753        preferred_webc_version: webc::Version,
754    ) -> Result<MultiSource> {
755        let mut source = MultiSource::default();
756
757        // Note: This should be first so our "preloaded" sources get a chance to
758        // override the main registry.
759        let mut preloaded = InMemorySource::new();
760        for path in &self.include_webcs {
761            preloaded
762                .add_webc(path)
763                .with_context(|| format!("Unable to load \"{}\"", path.display()))?;
764        }
765        source.add_source(preloaded);
766
767        let graphql_endpoint = self.graphql_endpoint(env)?;
768        let cache_dir = env.cache_dir().join("queries");
769        let mut wapm_source = BackendSource::new(graphql_endpoint, Arc::clone(&client))
770            .with_local_cache(cache_dir, WAPM_SOURCE_CACHE_TIMEOUT)
771            .with_preferred_webc_version(preferred_webc_version);
772        if let Some(token) = env
773            .config()?
774            .registry
775            .get_login_token_for_registry(wapm_source.registry_endpoint().as_str())
776        {
777            wapm_source = wapm_source.with_auth_token(token);
778        }
779        source.add_source(wapm_source);
780
781        let cache_dir = env.cache_dir().join("downloads");
782        source.add_source(WebSource::new(cache_dir, client));
783
784        source.add_source(FileSystemSource::default());
785
786        Ok(source)
787    }
788
789    fn graphql_endpoint(&self, env: &WasmerEnv) -> Result<Url> {
790        if let Ok(endpoint) = env.registry_endpoint() {
791            return Ok(endpoint);
792        }
793
794        let config = env.config()?;
795        let graphql_endpoint = config.registry.get_graphql_url();
796        let graphql_endpoint = graphql_endpoint
797            .parse()
798            .with_context(|| format!("Unable to parse \"{graphql_endpoint}\" as a URL"))?;
799
800        Ok(graphql_endpoint)
801    }
802}
803
804fn parse_registry(r: &str) -> Result<Url> {
805    UserRegistry::from(r).graphql_endpoint()
806}
807
808fn tokens_by_authority(env: &WasmerEnv) -> Result<HashMap<String, String>> {
809    let mut tokens = HashMap::new();
810    let config = env.config()?;
811
812    for credentials in config.registry.tokens {
813        if let Ok(url) = Url::parse(&credentials.registry) {
814            if url.has_authority() {
815                tokens.insert(url.authority().to_string(), credentials.token);
816            }
817        }
818    }
819
820    if let (Ok(current_registry), Some(token)) = (env.registry_endpoint(), env.token()) {
821        if current_registry.has_authority() {
822            tokens.insert(current_registry.authority().to_string(), token);
823        }
824    }
825
826    // Note: The global wasmer.toml config file stores URLs for the GraphQL
827    // endpoint, however that's often on the backend (i.e.
828    // https://registry.wasmer.io/graphql) and we also want to use the same API
829    // token when sending requests to the frontend (e.g. downloading a package
830    // using the `Accept: application/webc` header).
831    //
832    // As a workaround to avoid needing to query *all* backends to find out
833    // their frontend URL every time the `wasmer` CLI runs, we'll assume that
834    // when a backend is called something like `registry.wasmer.io`, the
835    // frontend will be at `wasmer.io`. This works everywhere except for people
836    // developing the backend locally... Sorry, Ayush.
837
838    let mut frontend_tokens = HashMap::new();
839    for (hostname, token) in &tokens {
840        if let Some(frontend_url) = hostname.strip_prefix("registry.") {
841            if !tokens.contains_key(frontend_url) {
842                frontend_tokens.insert(frontend_url.to_string(), token.clone());
843            }
844        }
845    }
846    tokens.extend(frontend_tokens);
847
848    Ok(tokens)
849}