From 66af1dac88010c9dc597795a3eec79a880ce0942 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Thu, 4 Jun 2026 16:32:43 -0700
Subject: [PATCH 1/3] wasmtime-cli: component run's --invoke uses ItemName for
search
---
Cargo.toml | 1 +
src/commands/run.rs | 135 ++++++++++++++++++++++++++------------------
2 files changed, 81 insertions(+), 55 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 6bfd083b44c7..54d683eda6f7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -566,6 +566,7 @@ pooling-allocator = ["wasmtime/pooling-allocator", "wasmtime-cli-flags/pooling-a
backtrace = ["wasmtime/backtrace"]
component-model = [
"wasmtime/component-model",
+ "wasmtime/wit-parser",
"wasmtime-wast?/component-model",
"wasmtime-cli-flags/component-model",
"wasmtime-wizer?/component-model",
diff --git a/src/commands/run.rs b/src/commands/run.rs
index 1a69fab5476d..744dc39ad2a4 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -796,29 +796,20 @@ impl RunCommand {
)
})?;
- let name = untyped_call.name();
- let matches =
- Self::search_component_funcs(store.engine(), component.component_type(), name);
- let (names, func_type) = match matches.len() {
- 0 => bail!("No exported func named `{name}` in component."),
- 1 => &matches[0],
- _ => bail!(
- "Multiple exports named `{name}`: {matches:?}. FIXME: support some way to disambiguate names"
- ),
- };
+ let name = untyped_call.item_name().map_err(|e| {
+ wasmtime::Error::from_anyhow(e).context(format!(
+ "parsing `{}` as a wit item name",
+ untyped_call.name()
+ ))
+ })?;
+
+ let (export, func_type) = Self::search_component_funcs(store, &component, &name)?;
- let param_types = WasmFunc::params(func_type).collect::>();
+ let param_types = WasmFunc::params(&func_type).collect::>();
let params = untyped_call
.to_wasm_params(¶m_types)
.with_context(|| format!("while interpreting parameters in invoke \"{invoke}\""))?;
- let export = names
- .iter()
- .fold(None, |instance, name| {
- component.get_export_index(instance.as_ref(), name)
- })
- .expect("export has at least one name");
-
let instance = linker.instantiate_async(&mut *store, component).await?;
let func = instance
@@ -944,47 +935,81 @@ impl RunCommand {
#[cfg(feature = "component-model")]
fn search_component_funcs(
- engine: &Engine,
- component: wasmtime::component::types::Component,
- name: &str,
- ) -> Vec<(Vec, wasmtime::component::types::ComponentFunc)> {
+ store: &mut Store,
+ component: &wasmtime::component::Component,
+ item_name: &wasmtime::component::wit_parser::ItemName,
+ ) -> Result<(
+ wasmtime::component::ComponentExportIndex,
+ wasmtime::component::types::ComponentFunc,
+ )> {
use wasmtime::component::types::ComponentItem as CItem;
- fn collect_exports(
- engine: &Engine,
- item: CItem,
- basename: Vec,
- ) -> Vec<(Vec, CItem)> {
- match item {
- CItem::Component(c) => c
- .exports(engine)
- .flat_map(move |(name, item)| {
- let mut names = basename.clone();
- names.push(name.to_string());
- collect_exports(engine, item.ty, names)
- })
- .collect::>(),
- CItem::ComponentInstance(c) => c
- .exports(engine)
- .flat_map(move |(name, item)| {
- let mut names = basename.clone();
- names.push(name.to_string());
- collect_exports(engine, item.ty, names)
- })
- .collect::>(),
- _ => vec![(basename, item)],
- }
+ // Start by looking up the item name directly.
+ // Only match this as the search if it provides a function - it may
+ // provide an instance of the same name, in which case we want the
+ // below to search through it.
+ match component.get_export(None, item_name) {
+ Some((CItem::ComponentFunc(func), index)) => return Ok((index.clone(), func.clone())),
+ _ => {}
+ }
+ if item_name.interface.is_some() || item_name.package.is_some() {
+ // If the item name specified a package or interface, and the
+ // ItemName based lookup failed to find it, we do not consider
+ // that it may be exported under an instance and terminate the
+ // search immediately:
+ bail!("No exported func named `{item_name}` in component.")
}
+ // If the item name does not specify a package or interface, and it
+ // wasn't found in the root of the component above, then we search all
+ // instance exports for a function by that name.
+ let needle = item_name.to_string();
+ let mut search = component
+ .component_type()
+ .exports(store.engine())
+ .filter_map(|(instname, item)| match item.ty {
+ CItem::ComponentInstance(inst) => {
+ inst.exports(store.engine())
+ .find_map(|(leafname, item)| match item.ty {
+ CItem::ComponentFunc(func) => {
+ if leafname == needle {
+ // The type::Component traversal
+ // didn't give us an export index, get
+ // that now:
+ let (_item, inst_index) = component
+ .get_export(None, instname)
+ .expect("found exported component instance");
+ let (_item, index) = component
+ .get_export(Some(&inst_index), leafname)
+ .expect("found func");
+ Some((index, func.clone(), instname.to_string()))
+ } else {
+ None
+ }
+ }
+ _ => None,
+ })
+ }
- collect_exports(engine, CItem::Component(component), Vec::new())
- .into_iter()
- .filter_map(|(names, item)| {
- let CItem::ComponentFunc(func) = item else {
- return None;
- };
- let func_name = names.last().expect("at least one name");
- (func_name == name).then_some((names, func))
+ _ => None,
})
- .collect()
+ .collect::>();
+
+ match search.len() {
+ 0 => bail!("No exported func named `{needle}` in component."),
+ 1 => {
+ let (index, func, _instname) = search.pop().unwrap();
+ Ok((index, func))
+ }
+ _ => {
+ let candidates = search
+ .into_iter()
+ .map(|(_index, _func, instname)| format!("`{instname}.{needle}`"))
+ .collect::>();
+ bail!(
+ "Multiple instances contained funcs named `{needle}`, retry with a more specific name: {}",
+ candidates.join(", ")
+ )
+ }
+ }
}
async fn invoke_func(&self, store: &mut Store, func: Func) -> Result<()> {
From ec9dad23ea8532345ce03a16d03b3e5716c79b79 Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 5 Jun 2026 10:28:40 -0700
Subject: [PATCH 2/3] cargo feature, clippy fix
---
Cargo.toml | 1 +
src/commands/run.rs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/Cargo.toml b/Cargo.toml
index 54d683eda6f7..d9de149c973b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -567,6 +567,7 @@ backtrace = ["wasmtime/backtrace"]
component-model = [
"wasmtime/component-model",
"wasmtime/wit-parser",
+ "wasmtime/anyhow",
"wasmtime-wast?/component-model",
"wasmtime-cli-flags/component-model",
"wasmtime-wizer?/component-model",
diff --git a/src/commands/run.rs b/src/commands/run.rs
index 744dc39ad2a4..fed01285ac9c 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -948,7 +948,7 @@ impl RunCommand {
// provide an instance of the same name, in which case we want the
// below to search through it.
match component.get_export(None, item_name) {
- Some((CItem::ComponentFunc(func), index)) => return Ok((index.clone(), func.clone())),
+ Some((CItem::ComponentFunc(func), index)) => return Ok((index, func.clone())),
_ => {}
}
if item_name.interface.is_some() || item_name.package.is_some() {
From 1ae240c0eba7abb665ced0b7f8e9f84c74000bda Mon Sep 17 00:00:00 2001
From: Pat Hickey
Date: Fri, 5 Jun 2026 11:26:58 -0700
Subject: [PATCH 3/3] testing, and fix error message output
---
src/commands/run.rs | 11 ++++-
tests/all/cli_tests.rs | 49 +++++++++++++++++++
.../all/cli_tests/component-multiple-runs.wat | 22 +++++++++
3 files changed, 81 insertions(+), 1 deletion(-)
create mode 100644 tests/all/cli_tests/component-multiple-runs.wat
diff --git a/src/commands/run.rs b/src/commands/run.rs
index fed01285ac9c..b4bb27a3f9d4 100644
--- a/src/commands/run.rs
+++ b/src/commands/run.rs
@@ -1002,7 +1002,16 @@ impl RunCommand {
_ => {
let candidates = search
.into_iter()
- .map(|(_index, _func, instname)| format!("`{instname}.{needle}`"))
+ .map(|(_index, _func, instname)| {
+ // Manipulate as an ItemName to get package version
+ // correct in the output
+ let mut itemname: wasmtime::component::wit_parser::ItemName =
+ instname.parse().unwrap();
+ // Push the function name onto the itemname:
+ itemname.interface = Some(itemname.name.clone());
+ itemname.name = needle.to_string();
+ format!("`{itemname}`")
+ })
.collect::>();
bail!(
"Multiple instances contained funcs named `{needle}`, retry with a more specific name: {}",
diff --git a/tests/all/cli_tests.rs b/tests/all/cli_tests.rs
index a7f34fb7fcf8..51c52df85611 100644
--- a/tests/all/cli_tests.rs
+++ b/tests/all/cli_tests.rs
@@ -805,6 +805,55 @@ fn component_enabled_by_default() -> Result<()> {
Ok(())
}
+#[test]
+fn component_invoke_multiple_run_exports() -> Result<()> {
+ let path = "tests/all/cli_tests/component-multiple-runs.wat";
+ let wasm = build_wasm(path)?;
+
+ // demonstrate run --invoke can give a useful error message when
+ // there are multiple interfaces that export the function name specified
+ let output = get_wasmtime_command()?
+ .arg("run")
+ .arg("-Wcomponent-model")
+ .arg("-Ccache=n")
+ .arg("--invoke")
+ .arg("run()")
+ .arg(wasm.path())
+ .output()?;
+ assert!(
+ !output.status.success(),
+ "should fail because run() is ambigious for this component"
+ );
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ assert!(stderr.contains("Multiple instances contained funcs named `run`, retry with a more specific name: `wasi:cli/run.run@0.2.0`, `some:other/one.run`"));
+
+ // test program cli run gives `ok`:
+ let output = get_wasmtime_command()?
+ .arg("run")
+ .arg("-Wcomponent-model")
+ .arg("-Ccache=n")
+ .arg("--invoke")
+ .arg("wasi:cli/run.run@0.2.0()")
+ .arg(wasm.path())
+ .output()?;
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ assert_eq!(stdout, "ok\n");
+
+ // test program other run gives `err`:
+ let output = get_wasmtime_command()?
+ .arg("run")
+ .arg("-Wcomponent-model")
+ .arg("-Ccache=n")
+ .arg("--invoke")
+ .arg("some:other/one.run()")
+ .arg(wasm.path())
+ .output()?;
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ assert_eq!(stdout, "err\n");
+
+ Ok(())
+}
+
// If the text format is invalid then the filename should be mentioned in the
// error message.
#[test]
diff --git a/tests/all/cli_tests/component-multiple-runs.wat b/tests/all/cli_tests/component-multiple-runs.wat
new file mode 100644
index 000000000000..9212dc13b33e
--- /dev/null
+++ b/tests/all/cli_tests/component-multiple-runs.wat
@@ -0,0 +1,22 @@
+;; This component has two different interfaces each exporting a function
+;; called "run", which behave distinctly. wasi:cli/run's run returns ok,
+;; some:other's run returns error.
+(component
+ (core module $m
+ (func (export "run") (result i32)
+ i32.const 0)
+ (func (export "run2") (result i32)
+ i32.const 1)
+ )
+ (core instance $i (instantiate $m))
+ (func $run (result (result))
+ (canon lift (core func $i "run")))
+ (func $run2 (result (result))
+ (canon lift (core func $i "run2")))
+
+ (instance (export (interface "wasi:cli/run@0.2.0"))
+ (export "run" (func $run)))
+
+ (instance (export (interface "some:other/one"))
+ (export "run" (func $run2)))
+)