Skip to main content

wasmer_cli/commands/run/
wasi.rs

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