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