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)]
67pub struct Wasi {
69 #[clap(
71 long = "volume",
72 name = "[HOST_DIR:]GUEST_DIR",
73 value_parser = parse_volume,
74 )]
75 pub(crate) volumes: Vec<MappedDirectory>,
76
77 #[clap(long = "dir", group = "wasi", hide = true)]
79 pub(crate) pre_opened_directories: Vec<PathBuf>,
80
81 #[clap(
83 long = "mapdir",
84 value_parser = parse_mapdir,
85 hide = true
86 )]
87 pub(crate) mapped_dirs: Vec<MappedDirectory>,
88
89 #[clap(long = "cwd")]
92 pub(crate) cwd: Option<PathBuf>,
93
94 #[clap(
96 long = "env",
97 name = "KEY=VALUE",
98 value_parser=parse_envvar,
99 )]
100 pub(crate) env_vars: Vec<(String, String)>,
101
102 #[clap(long, env)]
104 pub(crate) forward_host_env: bool,
105
106 #[clap(long = "use", name = "USE")]
108 pub(crate) uses: Vec<String>,
109
110 #[clap(long = "include-webc", name = "WEBC")]
116 pub(super) include_webcs: Vec<PathBuf>,
117
118 #[clap(long = "offline")]
121 pub(super) offline: bool,
122
123 #[clap(long = "map-command", name = "MAPCMD")]
125 pub(super) map_commands: Vec<String>,
126
127 #[clap(long = "net", require_equals = true)]
146 pub networking: Option<Option<String>>,
149
150 #[clap(long = "no-tty")]
152 pub no_tty: bool,
153
154 #[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 #[clap(long = "enable-cpu-backoff")]
171 pub enable_cpu_backoff: Option<u64>,
172
173 #[cfg(feature = "journal")]
179 #[clap(long = "journal")]
180 pub read_only_journals: Vec<PathBuf>,
181
182 #[cfg(feature = "journal")]
192 #[clap(long = "journal-writable")]
193 pub writable_journals: Vec<PathBuf>,
194
195 #[cfg(feature = "journal")]
198 #[clap(long = "enable-compaction")]
199 pub enable_compaction: bool,
200
201 #[cfg(feature = "journal")]
203 #[clap(long = "without-compact-on-drop")]
204 pub without_compact_on_drop: bool,
205
206 #[cfg(feature = "journal")]
212 #[clap(long = "with-compact-on-growth", default_value = "0.15")]
213 pub with_compact_on_growth: f32,
214
215 #[cfg(feature = "journal")]
226 #[clap(long = "snapshot-on")]
227 pub snapshot_on: Vec<SnapshotTrigger>,
228
229 #[cfg(feature = "journal")]
233 #[clap(long = "snapshot-period")]
234 pub snapshot_interval: Option<u64>,
235
236 #[cfg(feature = "journal")]
239 #[clap(long = "stop-after-snapshot")]
240 pub stop_after_snapshot: bool,
241
242 #[cfg(feature = "journal")]
244 #[clap(long = "skip-journal-stdio")]
245 pub skip_stdio_during_bootstrap: bool,
246
247 #[clap(long)]
251 pub http_client: bool,
252
253 #[clap(long = "deny-multiple-wasi-versions")]
255 pub deny_multiple_wasi_versions: bool,
256
257 #[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 pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
287 get_wasi_versions(module, false)
292 }
293
294 pub fn has_wasi_imports(module: &Module) -> bool {
296 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 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 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 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 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 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 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 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 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}