From 93122678b769e884054b04a7168b0719ec02283d Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Wed, 13 Sep 2023 13:44:36 -0500 Subject: [PATCH] Require Wasmtime options come before wasm modules (#6946) This commit implements a new behavior for the CLI of the `wasmtime` executable which will require that options for Wasmtime itself come before the wasm module being run. Currently they're allowed to come afterwards, but instead all arguments and flags coming after a module will be interpreted as arguments for the module itself. This feature has a bit of a storied history at this point, and the breadcrumbs are: * Originally landed in #6737 * Reverted for 12.0.0 in #6830 * Reverted for 13.0.0 in #6944 This PR is intended to be landed as a sibling of #6925, another independent overhaul of Wasmtime's own options on the CLI, for the Wasmtime 14.0.0 release. More information about the motivation for this change, as well as consequences of the fallout, can be found on #6737. --- src/bin/wasmtime.rs | 46 +++++++++++-------- src/commands/run.rs | 100 ++++++++++++++++++----------------------- tests/all/cli_tests.rs | 28 +++++------- 3 files changed, 81 insertions(+), 93 deletions(-) diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index 6d425625dfa4..d32a0b63f373 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -4,7 +4,7 @@ //! See `wasmtime --help` for usage. use anyhow::Result; -use clap::{error::ErrorKind, Parser}; +use clap::Parser; use wasmtime_cli::commands::{ CompileCommand, ConfigCommand, ExploreCommand, RunCommand, SettingsCommand, WastCommand, }; @@ -27,10 +27,24 @@ use wasmtime_cli::commands::{ \n\ Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\ \n \ - wasmtime example.wasm --invoke add 1 2\n" + wasmtime example.wasm --invoke add 1 2\n", + + // This option enables the pattern below where we ask clap to parse twice + // sorta: once where it's trying to find a subcommand and once assuming + // a subcommand doesn't get passed. Clap should then, apparently, + // fill in the `subcommand` if found and otherwise fill in the + // `RunCommand`. + args_conflicts_with_subcommands = true )] -enum Wasmtime { - // !!! IMPORTANT: if subcommands are added or removed, update `parse_module` in `src/commands/run.rs`. !!! +struct Wasmtime { + #[clap(subcommand)] + subcommand: Option, + #[clap(flatten)] + run: RunCommand, +} + +#[derive(Parser)] +enum Subcommand { /// Controls Wasmtime configuration settings Config(ConfigCommand), /// Compiles a WebAssembly module. @@ -48,26 +62,20 @@ enum Wasmtime { impl Wasmtime { /// Executes the command. pub fn execute(self) -> Result<()> { - match self { - Self::Config(c) => c.execute(), - Self::Compile(c) => c.execute(), - Self::Explore(c) => c.execute(), - Self::Run(c) => c.execute(), - Self::Settings(c) => c.execute(), - Self::Wast(c) => c.execute(), + let subcommand = self.subcommand.unwrap_or(Subcommand::Run(self.run)); + match subcommand { + Subcommand::Config(c) => c.execute(), + Subcommand::Compile(c) => c.execute(), + Subcommand::Explore(c) => c.execute(), + Subcommand::Run(c) => c.execute(), + Subcommand::Settings(c) => c.execute(), + Subcommand::Wast(c) => c.execute(), } } } fn main() -> Result<()> { - Wasmtime::try_parse() - .unwrap_or_else(|e| match e.kind() { - ErrorKind::InvalidSubcommand | ErrorKind::UnknownArgument => { - Wasmtime::Run(RunCommand::parse()) - } - _ => e.exit(), - }) - .execute() + Wasmtime::parse().execute() } #[test] diff --git a/src/commands/run.rs b/src/commands/run.rs index 975f21bbd737..32e083cb81ca 100644 --- a/src/commands/run.rs +++ b/src/commands/run.rs @@ -6,10 +6,7 @@ )] use anyhow::{anyhow, bail, Context as _, Error, Result}; -use clap::builder::{OsStringValueParser, TypedValueParser}; use clap::Parser; -use std::ffi::OsStr; -use std::ffi::OsString; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; @@ -38,18 +35,6 @@ use wasmtime_wasi_threads::WasiThreadsCtx; #[cfg(feature = "wasi-http")] use wasmtime_wasi_http::WasiHttpCtx; -fn parse_module(s: OsString) -> anyhow::Result { - // Do not accept wasmtime subcommand names as the module name - match s.to_str() { - Some("help") | Some("config") | Some("run") | Some("wast") | Some("compile") => { - bail!("module name cannot be the same as a subcommand") - } - #[cfg(unix)] - Some("-") => Ok(PathBuf::from("/dev/stdin")), - _ => Ok(s.into()), - } -} - fn parse_env_var(s: &str) -> Result<(String, Option)> { let mut parts = s.splitn(2, '='); Ok(( @@ -100,7 +85,7 @@ fn parse_profile(s: &str) -> Result { /// Runs a WebAssembly module #[derive(Parser)] -#[structopt(name = "run", trailing_var_arg = true)] +#[structopt(name = "run")] pub struct RunCommand { #[clap(flatten)] common: CommonOptions, @@ -137,14 +122,6 @@ pub struct RunCommand { #[clap(long, value_name = "FUNCTION")] invoke: Option, - /// The path of the WebAssembly module to run - #[clap( - required = true, - value_name = "MODULE", - value_parser = OsStringValueParser::new().try_map(parse_module), - )] - module: PathBuf, - /// Load the given WebAssembly module before the main module #[clap( long = "preload", @@ -176,10 +153,13 @@ pub struct RunCommand { )] profile: Option, - // NOTE: this must come last for trailing varargs - /// The arguments to pass to the module - #[clap(value_name = "ARGS")] - module_args: Vec, + /// The WebAssembly module to run and arguments to pass to it. + /// + /// Arguments passed to the wasm module will be configured as WASI CLI + /// arguments unless the `--invoke` CLI argument is passed in which case + /// arguments will be interpreted as arguments to the function specified. + #[clap(value_name = "WASM", trailing_var_arg = true, required = true)] + module_and_args: Vec, } #[derive(Clone)] @@ -242,7 +222,7 @@ impl RunCommand { let engine = Engine::new(&config)?; // Read the wasm module binary either as `*.wat` or a raw binary. - let main = self.load_module(&engine, &self.module)?; + let main = self.load_module(&engine, &self.module_and_args[0])?; // Validate coredump-on-trap argument if let Some(coredump_path) = &self.common.debug.coredump { @@ -335,8 +315,12 @@ impl RunCommand { // Load the main wasm module. match self .load_main_module(&mut store, &mut linker, &main, modules) - .with_context(|| format!("failed to run main module `{}`", self.module.display())) - { + .with_context(|| { + format!( + "failed to run main module `{}`", + self.module_and_args[0].display() + ) + }) { Ok(()) => (), Err(e) => { // Exit the process if Wasmtime understands the error; @@ -377,27 +361,25 @@ impl RunCommand { Ok(listeners) } - fn compute_argv(&self) -> Vec { + fn compute_argv(&self) -> Result> { let mut result = Vec::new(); - // Add argv[0], which is the program name. Only include the base name of the - // main wasm module, to avoid leaking path information. - result.push( - self.module - .components() - .next_back() - .map(|c| c.as_os_str()) - .and_then(OsStr::to_str) - .unwrap_or("") - .to_owned(), - ); - - // Add the remaining arguments. - for arg in self.module_args.iter() { - result.push(arg.clone()); + for (i, arg) in self.module_and_args.iter().enumerate() { + // For argv[0], which is the program name. Only include the base + // name of the main wasm module, to avoid leaking path information. + let arg = if i == 0 { + arg.components().next_back().unwrap().as_os_str() + } else { + arg.as_ref() + }; + result.push( + arg.to_str() + .ok_or_else(|| anyhow!("failed to convert {arg:?} to utf-8"))? + .to_string(), + ); } - result + Ok(result) } fn setup_epoch_handler( @@ -406,7 +388,7 @@ impl RunCommand { modules: Vec<(String, Module)>, ) -> Box)> { if let Some(Profile::Guest { path, interval }) = &self.profile { - let module_name = self.module.to_str().unwrap_or("
"); + let module_name = self.module_and_args[0].to_str().unwrap_or("
"); let interval = *interval; store.data_mut().guest_profiler = Some(Arc::new(GuestProfiler::new(module_name, interval, modules))); @@ -512,9 +494,10 @@ impl RunCommand { CliLinker::Core(linker) => { // Use "" as a default module name. let module = module.unwrap_core(); - linker - .module(&mut *store, "", &module) - .context(format!("failed to instantiate {:?}", self.module))?; + linker.module(&mut *store, "", &module).context(format!( + "failed to instantiate {:?}", + self.module_and_args[0] + ))?; // If a function to invoke was given, invoke it. let func = if let Some(name) = &self.invoke { @@ -569,7 +552,7 @@ impl RunCommand { is experimental and may break in the future" ); } - let mut args = self.module_args.iter(); + let mut args = self.module_and_args.iter().skip(1); let mut values = Vec::new(); for ty in ty.params() { let val = match args.next() { @@ -582,6 +565,9 @@ impl RunCommand { } } }; + let val = val + .to_str() + .ok_or_else(|| anyhow!("argument is not valid utf-8: {val:?}"))?; values.push(match ty { // TODO: integer parsing here should handle hexadecimal notation // like `0x0...`, but the Rust standard library currently only @@ -639,7 +625,9 @@ impl RunCommand { if !err.is::() { return err; } - let source_name = self.module.to_str().unwrap_or_else(|| "unknown"); + let source_name = self.module_and_args[0] + .to_str() + .unwrap_or_else(|| "unknown"); if let Err(coredump_err) = generate_coredump(&err, &source_name, coredump_path) { eprintln!("warning: coredump failed to generate: {}", coredump_err); @@ -884,7 +872,7 @@ impl RunCommand { fn set_preview1_ctx(&self, store: &mut Store) -> Result<()> { let mut builder = WasiCtxBuilder::new(); - builder.inherit_stdio().args(&self.compute_argv())?; + builder.inherit_stdio().args(&self.compute_argv()?)?; for (key, value) in self.vars.iter() { let value = match value { @@ -916,7 +904,7 @@ impl RunCommand { fn set_preview2_ctx(&self, store: &mut Store) -> Result<()> { let mut builder = preview2::WasiCtxBuilder::new(); - builder.inherit_stdio().args(&self.compute_argv()); + builder.inherit_stdio().args(&self.compute_argv()?); for (key, value) in self.vars.iter() { let value = match value { diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs index 9b8f0b8ba248..cf6ce1f751b0 100644 --- a/tests/all/cli_tests.rs +++ b/tests/all/cli_tests.rs @@ -590,7 +590,6 @@ fn wasm_flags() -> Result<()> { let stdout = run_wasmtime(&[ "run", "tests/all/cli_tests/print-arguments.wat", - "--", "--argument", "-for", "the", @@ -606,7 +605,7 @@ fn wasm_flags() -> Result<()> { command\n\ " ); - let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--", "-"])?; + let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "-"])?; assert_eq!( stdout, "\ @@ -614,7 +613,7 @@ fn wasm_flags() -> Result<()> { -\n\ " ); - let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--", "--"])?; + let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--"])?; assert_eq!( stdout, "\ @@ -635,6 +634,7 @@ fn wasm_flags() -> Result<()> { "\ print-arguments.wat\n\ --\n\ + --\n\ -a\n\ b\n\ " @@ -644,37 +644,30 @@ fn wasm_flags() -> Result<()> { #[test] fn name_same_as_builtin_command() -> Result<()> { - // A bare subcommand shouldn't run successfully. - // - // This is ambiguous between a missing module argument and a module named - // `run` with no other options. + // a bare subcommand shouldn't run successfully let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests") .arg("run") .output()?; assert!(!output.status.success()); - // Currently even `--` isn't enough to disambiguate, so this still is an - // error. - // - // NB: this will change in Wasmtime 14 when #6737 is relanded. + // a `--` prefix should let everything else get interpreted as a wasm + // module and arguments, even if the module has a name like `run` let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests") .arg("--") .arg("run") .output()?; - assert!(!output.status.success(), "expected failure got {output:#?}"); + assert!(output.status.success(), "expected success got {output:#?}"); - // Passing `--foo` options before the module is also not enough to - // disambiguate for now. - // - // NB: this will change in Wasmtime 14 when #6737 is relanded. + // Passing options before the subcommand should work and doesn't require + // `--` to disambiguate let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests") .arg("-Ccache=n") .arg("run") .output()?; - assert!(!output.status.success(), "expected failure got {output:#?}"); + assert!(output.status.success(), "expected success got {output:#?}"); Ok(()) } @@ -694,7 +687,6 @@ fn wasm_flags_without_subcommand() -> Result<()> { let output = get_wasmtime_command()? .current_dir("tests/all/cli_tests/") .arg("print-arguments.wat") - .arg("--") .arg("-foo") .arg("bar") .output()?;