Skip to content

Commit 9312267

Browse files
alexcrichtoneduardomourar
authored andcommitted
Require Wasmtime options come before wasm modules (bytecodealliance#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 bytecodealliance#6737 * Reverted for 12.0.0 in bytecodealliance#6830 * Reverted for 13.0.0 in bytecodealliance#6944 This PR is intended to be landed as a sibling of bytecodealliance#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 bytecodealliance#6737.
1 parent 0ea66d3 commit 9312267

File tree

3 files changed

+81
-93
lines changed

3 files changed

+81
-93
lines changed

src/bin/wasmtime.rs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! See `wasmtime --help` for usage.
55
66
use anyhow::Result;
7-
use clap::{error::ErrorKind, Parser};
7+
use clap::Parser;
88
use wasmtime_cli::commands::{
99
CompileCommand, ConfigCommand, ExploreCommand, RunCommand, SettingsCommand, WastCommand,
1010
};
@@ -27,10 +27,24 @@ use wasmtime_cli::commands::{
2727
\n\
2828
Invoking a specific function (e.g. `add`) in a WebAssembly module:\n\
2929
\n \
30-
wasmtime example.wasm --invoke add 1 2\n"
30+
wasmtime example.wasm --invoke add 1 2\n",
31+
32+
// This option enables the pattern below where we ask clap to parse twice
33+
// sorta: once where it's trying to find a subcommand and once assuming
34+
// a subcommand doesn't get passed. Clap should then, apparently,
35+
// fill in the `subcommand` if found and otherwise fill in the
36+
// `RunCommand`.
37+
args_conflicts_with_subcommands = true
3138
)]
32-
enum Wasmtime {
33-
// !!! IMPORTANT: if subcommands are added or removed, update `parse_module` in `src/commands/run.rs`. !!!
39+
struct Wasmtime {
40+
#[clap(subcommand)]
41+
subcommand: Option<Subcommand>,
42+
#[clap(flatten)]
43+
run: RunCommand,
44+
}
45+
46+
#[derive(Parser)]
47+
enum Subcommand {
3448
/// Controls Wasmtime configuration settings
3549
Config(ConfigCommand),
3650
/// Compiles a WebAssembly module.
@@ -48,26 +62,20 @@ enum Wasmtime {
4862
impl Wasmtime {
4963
/// Executes the command.
5064
pub fn execute(self) -> Result<()> {
51-
match self {
52-
Self::Config(c) => c.execute(),
53-
Self::Compile(c) => c.execute(),
54-
Self::Explore(c) => c.execute(),
55-
Self::Run(c) => c.execute(),
56-
Self::Settings(c) => c.execute(),
57-
Self::Wast(c) => c.execute(),
65+
let subcommand = self.subcommand.unwrap_or(Subcommand::Run(self.run));
66+
match subcommand {
67+
Subcommand::Config(c) => c.execute(),
68+
Subcommand::Compile(c) => c.execute(),
69+
Subcommand::Explore(c) => c.execute(),
70+
Subcommand::Run(c) => c.execute(),
71+
Subcommand::Settings(c) => c.execute(),
72+
Subcommand::Wast(c) => c.execute(),
5873
}
5974
}
6075
}
6176

6277
fn main() -> Result<()> {
63-
Wasmtime::try_parse()
64-
.unwrap_or_else(|e| match e.kind() {
65-
ErrorKind::InvalidSubcommand | ErrorKind::UnknownArgument => {
66-
Wasmtime::Run(RunCommand::parse())
67-
}
68-
_ => e.exit(),
69-
})
70-
.execute()
78+
Wasmtime::parse().execute()
7179
}
7280

7381
#[test]

src/commands/run.rs

Lines changed: 44 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
)]
77

88
use anyhow::{anyhow, bail, Context as _, Error, Result};
9-
use clap::builder::{OsStringValueParser, TypedValueParser};
109
use clap::Parser;
11-
use std::ffi::OsStr;
12-
use std::ffi::OsString;
1310
use std::fs::File;
1411
use std::io::Write;
1512
use std::path::{Path, PathBuf};
@@ -38,18 +35,6 @@ use wasmtime_wasi_threads::WasiThreadsCtx;
3835
#[cfg(feature = "wasi-http")]
3936
use wasmtime_wasi_http::WasiHttpCtx;
4037

41-
fn parse_module(s: OsString) -> anyhow::Result<PathBuf> {
42-
// Do not accept wasmtime subcommand names as the module name
43-
match s.to_str() {
44-
Some("help") | Some("config") | Some("run") | Some("wast") | Some("compile") => {
45-
bail!("module name cannot be the same as a subcommand")
46-
}
47-
#[cfg(unix)]
48-
Some("-") => Ok(PathBuf::from("/dev/stdin")),
49-
_ => Ok(s.into()),
50-
}
51-
}
52-
5338
fn parse_env_var(s: &str) -> Result<(String, Option<String>)> {
5439
let mut parts = s.splitn(2, '=');
5540
Ok((
@@ -100,7 +85,7 @@ fn parse_profile(s: &str) -> Result<Profile> {
10085

10186
/// Runs a WebAssembly module
10287
#[derive(Parser)]
103-
#[structopt(name = "run", trailing_var_arg = true)]
88+
#[structopt(name = "run")]
10489
pub struct RunCommand {
10590
#[clap(flatten)]
10691
common: CommonOptions,
@@ -137,14 +122,6 @@ pub struct RunCommand {
137122
#[clap(long, value_name = "FUNCTION")]
138123
invoke: Option<String>,
139124

140-
/// The path of the WebAssembly module to run
141-
#[clap(
142-
required = true,
143-
value_name = "MODULE",
144-
value_parser = OsStringValueParser::new().try_map(parse_module),
145-
)]
146-
module: PathBuf,
147-
148125
/// Load the given WebAssembly module before the main module
149126
#[clap(
150127
long = "preload",
@@ -176,10 +153,13 @@ pub struct RunCommand {
176153
)]
177154
profile: Option<Profile>,
178155

179-
// NOTE: this must come last for trailing varargs
180-
/// The arguments to pass to the module
181-
#[clap(value_name = "ARGS")]
182-
module_args: Vec<String>,
156+
/// The WebAssembly module to run and arguments to pass to it.
157+
///
158+
/// Arguments passed to the wasm module will be configured as WASI CLI
159+
/// arguments unless the `--invoke` CLI argument is passed in which case
160+
/// arguments will be interpreted as arguments to the function specified.
161+
#[clap(value_name = "WASM", trailing_var_arg = true, required = true)]
162+
module_and_args: Vec<PathBuf>,
183163
}
184164

185165
#[derive(Clone)]
@@ -242,7 +222,7 @@ impl RunCommand {
242222
let engine = Engine::new(&config)?;
243223

244224
// Read the wasm module binary either as `*.wat` or a raw binary.
245-
let main = self.load_module(&engine, &self.module)?;
225+
let main = self.load_module(&engine, &self.module_and_args[0])?;
246226

247227
// Validate coredump-on-trap argument
248228
if let Some(coredump_path) = &self.common.debug.coredump {
@@ -335,8 +315,12 @@ impl RunCommand {
335315
// Load the main wasm module.
336316
match self
337317
.load_main_module(&mut store, &mut linker, &main, modules)
338-
.with_context(|| format!("failed to run main module `{}`", self.module.display()))
339-
{
318+
.with_context(|| {
319+
format!(
320+
"failed to run main module `{}`",
321+
self.module_and_args[0].display()
322+
)
323+
}) {
340324
Ok(()) => (),
341325
Err(e) => {
342326
// Exit the process if Wasmtime understands the error;
@@ -377,27 +361,25 @@ impl RunCommand {
377361
Ok(listeners)
378362
}
379363

380-
fn compute_argv(&self) -> Vec<String> {
364+
fn compute_argv(&self) -> Result<Vec<String>> {
381365
let mut result = Vec::new();
382366

383-
// Add argv[0], which is the program name. Only include the base name of the
384-
// main wasm module, to avoid leaking path information.
385-
result.push(
386-
self.module
387-
.components()
388-
.next_back()
389-
.map(|c| c.as_os_str())
390-
.and_then(OsStr::to_str)
391-
.unwrap_or("")
392-
.to_owned(),
393-
);
394-
395-
// Add the remaining arguments.
396-
for arg in self.module_args.iter() {
397-
result.push(arg.clone());
367+
for (i, arg) in self.module_and_args.iter().enumerate() {
368+
// For argv[0], which is the program name. Only include the base
369+
// name of the main wasm module, to avoid leaking path information.
370+
let arg = if i == 0 {
371+
arg.components().next_back().unwrap().as_os_str()
372+
} else {
373+
arg.as_ref()
374+
};
375+
result.push(
376+
arg.to_str()
377+
.ok_or_else(|| anyhow!("failed to convert {arg:?} to utf-8"))?
378+
.to_string(),
379+
);
398380
}
399381

400-
result
382+
Ok(result)
401383
}
402384

403385
fn setup_epoch_handler(
@@ -406,7 +388,7 @@ impl RunCommand {
406388
modules: Vec<(String, Module)>,
407389
) -> Box<dyn FnOnce(&mut Store<Host>)> {
408390
if let Some(Profile::Guest { path, interval }) = &self.profile {
409-
let module_name = self.module.to_str().unwrap_or("<main module>");
391+
let module_name = self.module_and_args[0].to_str().unwrap_or("<main module>");
410392
let interval = *interval;
411393
store.data_mut().guest_profiler =
412394
Some(Arc::new(GuestProfiler::new(module_name, interval, modules)));
@@ -512,9 +494,10 @@ impl RunCommand {
512494
CliLinker::Core(linker) => {
513495
// Use "" as a default module name.
514496
let module = module.unwrap_core();
515-
linker
516-
.module(&mut *store, "", &module)
517-
.context(format!("failed to instantiate {:?}", self.module))?;
497+
linker.module(&mut *store, "", &module).context(format!(
498+
"failed to instantiate {:?}",
499+
self.module_and_args[0]
500+
))?;
518501

519502
// If a function to invoke was given, invoke it.
520503
let func = if let Some(name) = &self.invoke {
@@ -569,7 +552,7 @@ impl RunCommand {
569552
is experimental and may break in the future"
570553
);
571554
}
572-
let mut args = self.module_args.iter();
555+
let mut args = self.module_and_args.iter().skip(1);
573556
let mut values = Vec::new();
574557
for ty in ty.params() {
575558
let val = match args.next() {
@@ -582,6 +565,9 @@ impl RunCommand {
582565
}
583566
}
584567
};
568+
let val = val
569+
.to_str()
570+
.ok_or_else(|| anyhow!("argument is not valid utf-8: {val:?}"))?;
585571
values.push(match ty {
586572
// TODO: integer parsing here should handle hexadecimal notation
587573
// like `0x0...`, but the Rust standard library currently only
@@ -639,7 +625,9 @@ impl RunCommand {
639625
if !err.is::<wasmtime::Trap>() {
640626
return err;
641627
}
642-
let source_name = self.module.to_str().unwrap_or_else(|| "unknown");
628+
let source_name = self.module_and_args[0]
629+
.to_str()
630+
.unwrap_or_else(|| "unknown");
643631

644632
if let Err(coredump_err) = generate_coredump(&err, &source_name, coredump_path) {
645633
eprintln!("warning: coredump failed to generate: {}", coredump_err);
@@ -884,7 +872,7 @@ impl RunCommand {
884872

885873
fn set_preview1_ctx(&self, store: &mut Store<Host>) -> Result<()> {
886874
let mut builder = WasiCtxBuilder::new();
887-
builder.inherit_stdio().args(&self.compute_argv())?;
875+
builder.inherit_stdio().args(&self.compute_argv()?)?;
888876

889877
for (key, value) in self.vars.iter() {
890878
let value = match value {
@@ -916,7 +904,7 @@ impl RunCommand {
916904

917905
fn set_preview2_ctx(&self, store: &mut Store<Host>) -> Result<()> {
918906
let mut builder = preview2::WasiCtxBuilder::new();
919-
builder.inherit_stdio().args(&self.compute_argv());
907+
builder.inherit_stdio().args(&self.compute_argv()?);
920908

921909
for (key, value) in self.vars.iter() {
922910
let value = match value {

tests/all/cli_tests.rs

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -590,7 +590,6 @@ fn wasm_flags() -> Result<()> {
590590
let stdout = run_wasmtime(&[
591591
"run",
592592
"tests/all/cli_tests/print-arguments.wat",
593-
"--",
594593
"--argument",
595594
"-for",
596595
"the",
@@ -606,15 +605,15 @@ fn wasm_flags() -> Result<()> {
606605
command\n\
607606
"
608607
);
609-
let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--", "-"])?;
608+
let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "-"])?;
610609
assert_eq!(
611610
stdout,
612611
"\
613612
print-arguments.wat\n\
614613
-\n\
615614
"
616615
);
617-
let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--", "--"])?;
616+
let stdout = run_wasmtime(&["run", "tests/all/cli_tests/print-arguments.wat", "--"])?;
618617
assert_eq!(
619618
stdout,
620619
"\
@@ -635,6 +634,7 @@ fn wasm_flags() -> Result<()> {
635634
"\
636635
print-arguments.wat\n\
637636
--\n\
637+
--\n\
638638
-a\n\
639639
b\n\
640640
"
@@ -644,37 +644,30 @@ fn wasm_flags() -> Result<()> {
644644

645645
#[test]
646646
fn name_same_as_builtin_command() -> Result<()> {
647-
// A bare subcommand shouldn't run successfully.
648-
//
649-
// This is ambiguous between a missing module argument and a module named
650-
// `run` with no other options.
647+
// a bare subcommand shouldn't run successfully
651648
let output = get_wasmtime_command()?
652649
.current_dir("tests/all/cli_tests")
653650
.arg("run")
654651
.output()?;
655652
assert!(!output.status.success());
656653

657-
// Currently even `--` isn't enough to disambiguate, so this still is an
658-
// error.
659-
//
660-
// NB: this will change in Wasmtime 14 when #6737 is relanded.
654+
// a `--` prefix should let everything else get interpreted as a wasm
655+
// module and arguments, even if the module has a name like `run`
661656
let output = get_wasmtime_command()?
662657
.current_dir("tests/all/cli_tests")
663658
.arg("--")
664659
.arg("run")
665660
.output()?;
666-
assert!(!output.status.success(), "expected failure got {output:#?}");
661+
assert!(output.status.success(), "expected success got {output:#?}");
667662

668-
// Passing `--foo` options before the module is also not enough to
669-
// disambiguate for now.
670-
//
671-
// NB: this will change in Wasmtime 14 when #6737 is relanded.
663+
// Passing options before the subcommand should work and doesn't require
664+
// `--` to disambiguate
672665
let output = get_wasmtime_command()?
673666
.current_dir("tests/all/cli_tests")
674667
.arg("-Ccache=n")
675668
.arg("run")
676669
.output()?;
677-
assert!(!output.status.success(), "expected failure got {output:#?}");
670+
assert!(output.status.success(), "expected success got {output:#?}");
678671
Ok(())
679672
}
680673

@@ -694,7 +687,6 @@ fn wasm_flags_without_subcommand() -> Result<()> {
694687
let output = get_wasmtime_command()?
695688
.current_dir("tests/all/cli_tests/")
696689
.arg("print-arguments.wat")
697-
.arg("--")
698690
.arg("-foo")
699691
.arg("bar")
700692
.output()?;

0 commit comments

Comments
 (0)