use anyhow::{Context, Result, anyhow, bail};
use clap::Parser;
use std::net::TcpListener;
use std::{fs::File, path::Path, time::Duration};
use wasmtime::{Engine, Module, Precompiled, StoreLimits, StoreLimitsBuilder};
use wasmtime_cli_flags::{CommonOptions, opt::WasmtimeOptionValue};
use wasmtime_wasi::WasiCtxBuilder;
#[cfg(feature = "component-model")]
use wasmtime::component::Component;
pub const P3_DEFAULT: bool = cfg!(feature = "component-model-async") && false;
pub enum RunTarget {
Core(Module),
#[cfg(feature = "component-model")]
Component(Component),
}
impl RunTarget {
pub fn unwrap_core(&self) -> &Module {
match self {
RunTarget::Core(module) => module,
#[cfg(feature = "component-model")]
RunTarget::Component(_) => panic!("expected a core wasm module, not a component"),
}
}
#[cfg(feature = "component-model")]
pub fn unwrap_component(&self) -> &Component {
match self {
RunTarget::Component(c) => c,
RunTarget::Core(_) => panic!("expected a component, not a core wasm module"),
}
}
}
#[derive(Parser)]
pub struct RunCommon {
#[command(flatten)]
pub common: CommonOptions,
#[arg(long = "allow-precompiled")]
pub allow_precompiled: bool,
#[arg(
long,
value_name = "STRATEGY",
value_parser = Profile::parse,
)]
pub profile: Option<Profile>,
#[arg(long = "dir", value_name = "HOST_DIR[::GUEST_DIR]", value_parser = parse_dirs)]
pub dirs: Vec<(String, String)>,
#[arg(long = "env", number_of_values = 1, value_name = "NAME[=VAL]", value_parser = parse_env_var)]
pub vars: Vec<(String, Option<String>)>,
}
fn parse_env_var(s: &str) -> Result<(String, Option<String>)> {
let mut parts = s.splitn(2, '=');
Ok((
parts.next().unwrap().to_string(),
parts.next().map(|s| s.to_string()),
))
}
fn parse_dirs(s: &str) -> Result<(String, String)> {
let mut parts = s.split("::");
let host = parts.next().unwrap();
let guest = match parts.next() {
Some(guest) => guest,
None => host,
};
Ok((host.into(), guest.into()))
}
impl RunCommon {
pub fn store_limits(&self) -> StoreLimits {
let mut limits = StoreLimitsBuilder::new();
if let Some(max) = self.common.wasm.max_memory_size {
limits = limits.memory_size(max);
}
if let Some(max) = self.common.wasm.max_table_elements {
limits = limits.table_elements(max);
}
if let Some(max) = self.common.wasm.max_instances {
limits = limits.instances(max);
}
if let Some(max) = self.common.wasm.max_tables {
limits = limits.tables(max);
}
if let Some(max) = self.common.wasm.max_memories {
limits = limits.memories(max);
}
if let Some(enable) = self.common.wasm.trap_on_grow_failure {
limits = limits.trap_on_grow_failure(enable);
}
limits.build()
}
pub fn ensure_allow_precompiled(&self) -> Result<()> {
if self.allow_precompiled {
Ok(())
} else {
bail!("running a precompiled module requires the `--allow-precompiled` flag")
}
}
#[cfg(feature = "component-model")]
fn ensure_allow_components(&self) -> Result<()> {
if self.common.wasm.component_model == Some(false) {
bail!("cannot execute a component without `--wasm component-model`");
}
Ok(())
}
pub fn load_module(&self, engine: &Engine, path: &Path) -> Result<RunTarget> {
let path = match path.to_str() {
#[cfg(unix)]
Some("-") => "/dev/stdin".as_ref(),
_ => path,
};
let file =
File::open(path).with_context(|| format!("failed to open wasm module {path:?}"))?;
match wasmtime::_internal::MmapVec::from_file(file) {
Ok(map) => self.load_module_contents(
engine,
path,
&map,
|| unsafe { Module::deserialize_file(engine, path) },
#[cfg(feature = "component-model")]
|| unsafe { Component::deserialize_file(engine, path) },
),
Err(_) => {
let bytes = std::fs::read(path)
.with_context(|| format!("failed to read file: {}", path.display()))?;
self.load_module_contents(
engine,
path,
&bytes,
|| unsafe { Module::deserialize(engine, &bytes) },
#[cfg(feature = "component-model")]
|| unsafe { Component::deserialize(engine, &bytes) },
)
}
}
}
pub fn load_module_contents(
&self,
engine: &Engine,
path: &Path,
bytes: &[u8],
deserialize_module: impl FnOnce() -> Result<Module>,
#[cfg(feature = "component-model")] deserialize_component: impl FnOnce() -> Result<Component>,
) -> Result<RunTarget> {
Ok(match Engine::detect_precompiled(bytes) {
Some(Precompiled::Module) => {
self.ensure_allow_precompiled()?;
RunTarget::Core(deserialize_module()?)
}
#[cfg(feature = "component-model")]
Some(Precompiled::Component) => {
self.ensure_allow_precompiled()?;
self.ensure_allow_components()?;
RunTarget::Component(deserialize_component()?)
}
#[cfg(not(feature = "component-model"))]
Some(Precompiled::Component) => {
bail!("support for components was not enabled at compile time");
}
#[cfg(any(feature = "cranelift", feature = "winch"))]
None => {
let mut code = wasmtime::CodeBuilder::new(engine);
code.wasm_binary_or_text(bytes, Some(path))?;
match code.hint() {
Some(wasmtime::CodeHint::Component) => {
#[cfg(feature = "component-model")]
{
self.ensure_allow_components()?;
RunTarget::Component(code.compile_component()?)
}
#[cfg(not(feature = "component-model"))]
{
bail!("support for components was not enabled at compile time");
}
}
Some(wasmtime::CodeHint::Module) | None => {
RunTarget::Core(code.compile_module()?)
}
}
}
#[cfg(not(any(feature = "cranelift", feature = "winch")))]
None => {
let _ = (path, engine);
bail!("support for compiling modules was disabled at compile time");
}
})
}
pub fn configure_wasip2(&self, builder: &mut WasiCtxBuilder) -> Result<()> {
builder.allow_blocking_current_thread(self.common.wasm.timeout.is_none());
if self.common.wasi.inherit_env == Some(true) {
for (k, v) in std::env::vars() {
builder.env(&k, &v);
}
}
for (key, value) in self.vars.iter() {
let value = match value {
Some(value) => value.clone(),
None => match std::env::var_os(key) {
Some(val) => val
.into_string()
.map_err(|_| anyhow!("environment variable `{key}` not valid utf-8"))?,
None => {
continue;
}
},
};
builder.env(key, &value);
}
for (host, guest) in self.dirs.iter() {
builder.preopened_dir(
host,
guest,
wasmtime_wasi::DirPerms::all(),
wasmtime_wasi::FilePerms::all(),
)?;
}
if self.common.wasi.listenfd == Some(true) {
bail!("components do not support --listenfd");
}
for _ in self.compute_preopen_sockets()? {
bail!("components do not support --tcplisten");
}
if self.common.wasi.inherit_network == Some(true) {
builder.inherit_network();
}
if let Some(enable) = self.common.wasi.allow_ip_name_lookup {
builder.allow_ip_name_lookup(enable);
}
if let Some(enable) = self.common.wasi.tcp {
builder.allow_tcp(enable);
}
if let Some(enable) = self.common.wasi.udp {
builder.allow_udp(enable);
}
Ok(())
}
pub fn compute_preopen_sockets(&self) -> Result<Vec<TcpListener>> {
let mut listeners = vec![];
for address in &self.common.wasi.tcplisten {
let stdlistener = std::net::TcpListener::bind(address)
.with_context(|| format!("failed to bind to address '{address}'"))?;
let _ = stdlistener.set_nonblocking(true)?;
listeners.push(stdlistener)
}
Ok(listeners)
}
pub fn validate_p3_option(&self) -> Result<()> {
let p3 = self.common.wasi.p3.unwrap_or(P3_DEFAULT);
if p3 && !cfg!(feature = "component-model-async") {
bail!("support for WASIp3 disabled at compile time");
}
Ok(())
}
pub fn validate_cli_enabled(&self) -> Result<Option<bool>> {
let mut cli = self.common.wasi.cli;
if let Some(common) = self.common.wasi.common {
if cli.is_some() {
bail!(
"The -Scommon option should not be use with -Scli as it is a deprecated alias"
);
} else {
cli = Some(common);
}
}
Ok(cli)
}
#[cfg(feature = "component-model")]
pub fn add_wasmtime_wasi_to_linker<T>(
&self,
linker: &mut wasmtime::component::Linker<T>,
) -> Result<()>
where
T: wasmtime_wasi::WasiView,
{
let mut p2_options = wasmtime_wasi::p2::bindings::LinkOptions::default();
p2_options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false));
p2_options.network_error_code(self.common.wasi.network_error_code.unwrap_or(false));
wasmtime_wasi::p2::add_to_linker_with_options_async(linker, &p2_options)?;
#[cfg(feature = "component-model-async")]
if self.common.wasi.p3.unwrap_or(P3_DEFAULT) {
let mut p3_options = wasmtime_wasi::p3::bindings::LinkOptions::default();
p3_options.cli_exit_with_code(self.common.wasi.cli_exit_with_code.unwrap_or(false));
wasmtime_wasi::p3::add_to_linker_with_options(linker, &p3_options)
.context("failed to link `wasi:[email protected]`")?;
}
Ok(())
}
}
#[derive(Clone, PartialEq)]
pub enum Profile {
Native(wasmtime::ProfilingStrategy),
Guest { path: String, interval: Duration },
}
impl Profile {
pub fn parse(s: &str) -> Result<Profile> {
let parts = s.split(',').collect::<Vec<_>>();
match &parts[..] {
["perfmap"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::PerfMap)),
["jitdump"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::JitDump)),
["vtune"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::VTune)),
["pulley"] => Ok(Profile::Native(wasmtime::ProfilingStrategy::Pulley)),
["guest"] => Ok(Profile::Guest {
path: "wasmtime-guest-profile.json".to_string(),
interval: Duration::from_millis(10),
}),
["guest", path] => Ok(Profile::Guest {
path: path.to_string(),
interval: Duration::from_millis(10),
}),
["guest", path, dur] => Ok(Profile::Guest {
path: path.to_string(),
interval: WasmtimeOptionValue::parse(Some(dur))?,
}),
_ => bail!("unknown profiling strategy: {s}"),
}
}
}