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