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