diff --git a/lib/cli/src/cli.rs b/lib/cli/src/cli.rs index f580175f5..5b06d554b 100644 --- a/lib/cli/src/cli.rs +++ b/lib/cli/src/cli.rs @@ -14,6 +14,7 @@ use crate::commands::{Cache, Config, Inspect, Run, RunWithoutFile, SelfUpdate, V use crate::error::PrettyError; use clap::{CommandFactory, ErrorKind, Parser}; use spinner::SpinnerHandle; +use std::fmt; use wasmer_registry::{get_all_local_packages, PackageDownloadInfo}; #[derive(Parser, Debug)] @@ -37,7 +38,6 @@ use wasmer_registry::{get_all_local_packages, PackageDownloadInfo}; )] /// The options for the wasmer Command Line Interface enum WasmerCLIOptions { - /// List all locally installed packages #[clap(name = "list")] List, @@ -253,11 +253,41 @@ fn try_run_package_or_file(args: &[String], r: &Run) -> Result<(), anyhow::Error let package = format!("{}", r.path.display()); + let mut is_fake_sv = false; let mut sv = match split_version(&package) { Ok(o) => o, - Err(_) => return r.execute(), + Err(_) => { + let mut fake_sv = SplitVersion { + package: package.to_string(), + version: None, + command: None, + }; + is_fake_sv = true; + match try_lookup_command(&mut fake_sv) { + Ok(o) => SplitVersion { + package: o.package, + version: Some(o.version), + command: r.command_name.clone(), + }, + Err(e) => { + return Err( + anyhow::anyhow!("No package for command {package:?} found, file {package:?} not found either") + .context(e) + .context(anyhow::anyhow!("{}", r.path.display())) + ); + } + } + } }; + if sv.command.is_none() { + sv.command = r.command_name.clone(); + } + + if sv.command.is_none() && is_fake_sv { + sv.command = Some(package.to_string()); + } + let mut package_download_info = None; if !sv.package.contains('/') { if let Ok(o) = try_lookup_command(&mut sv) { @@ -289,7 +319,7 @@ fn try_lookup_command(sv: &mut SplitVersion) -> Result Result<(), anyhow::Error> { @@ -303,6 +333,7 @@ fn try_execute_local_package(args: &[String], sv: &SplitVersion) -> Result<(), a &package.registry, &package.name, &package.version, + sv.command.as_ref().map(|s| s.as_str()), ) .map_err(|e| anyhow!("{e}"))?; @@ -310,11 +341,22 @@ fn try_execute_local_package(args: &[String], sv: &SplitVersion) -> Result<(), a let mut args_without_package = args.to_vec(); args_without_package.remove(1); - let mut run_args = RunWithoutFile::try_parse_from(args_without_package.iter())?; - run_args.command_name = sv.command.clone(); - run_args - .into_run_args(local_package_wasm_path, Some(package.manifest)) + if args_without_package.get(1) == Some(&sv.package) + || args_without_package.get(1) == sv.command.as_ref() + { + args_without_package.remove(1); + } + + if args_without_package.get(0) == Some(&sv.package) + || args_without_package.get(0) == sv.command.as_ref() + { + args_without_package.remove(0); + } + + RunWithoutFile::try_parse_from(args_without_package.iter())? + .into_run_args(local_package_wasm_path.clone(), Some(package.manifest)) .execute() + .map_err(|e| e.context(anyhow::anyhow!("{}", local_package_wasm_path.display()))) } fn try_autoinstall_package( @@ -362,6 +404,22 @@ struct SplitVersion { command: Option, } +impl fmt::Display for SplitVersion { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let version = self + .version + .as_ref() + .map(|s| s.as_str()) + .unwrap_or("latest"); + let command = self + .command + .as_ref() + .map(|s| format!(":{s}")) + .unwrap_or_default(); + write!(f, "{}@{version}{command}", self.package) + } +} + #[test] fn test_split_version() { assert_eq!( @@ -401,6 +459,9 @@ fn split_version(s: &str) -> Result { let re1 = regex::Regex::new(r#"(.*)/(.*)@(.*):(.*)"#).unwrap(); let re2 = regex::Regex::new(r#"(.*)/(.*)@(.*)"#).unwrap(); let re3 = regex::Regex::new(r#"(.*)/(.*)"#).unwrap(); + let re4 = regex::Regex::new(r#"(.*)/(.*):(.*)"#).unwrap(); + + let mut no_version = false; let captures = if re1.is_match(s) { re1.captures(s) @@ -420,6 +481,16 @@ fn split_version(s: &str) -> Result { .collect::>() }) .unwrap_or_default() + } else if re4.is_match(s) { + no_version = true; + re4.captures(s) + .map(|c| { + c.iter() + .flatten() + .map(|m| m.as_str().to_owned()) + .collect::>() + }) + .unwrap_or_default() } else if re3.is_match(s) { re3.captures(s) .map(|c| { @@ -449,8 +520,12 @@ fn split_version(s: &str) -> Result { let sv = SplitVersion { package: format!("{namespace}/{name}"), - version: captures.get(3).cloned(), - command: captures.get(4).cloned(), + version: if no_version { + None + } else { + captures.get(3).cloned() + }, + command: captures.get(if no_version { 3 } else { 4 }).cloned(), }; let svp = sv.package.clone(); diff --git a/lib/cli/src/commands/run.rs b/lib/cli/src/commands/run.rs index 0e0d88e34..5244ea69b 100644 --- a/lib/cli/src/commands/run.rs +++ b/lib/cli/src/commands/run.rs @@ -5,6 +5,7 @@ use crate::store::{CompilerType, StoreOptions}; use crate::suggestions::suggest_function_exports; use crate::warning; use anyhow::{anyhow, Context, Result}; +use std::collections::BTreeMap; use std::ops::Deref; use std::path::PathBuf; use std::str::FromStr; @@ -81,6 +82,9 @@ pub struct RunWithoutFile { impl RunWithoutFile { /// Given a local path, returns the `Run` command (overriding the `--path` argument). pub fn into_run_args(mut self, pathbuf: PathBuf, manifest: Option) -> Run { + #[cfg(feature = "wasi")] + let mut wasi_map_dir = Vec::new(); + #[cfg(feature = "wasi")] { let pkg_fs = match pathbuf.parent() { @@ -92,13 +96,13 @@ impl RunWithoutFile { .and_then(|m| m.package.pkg_fs_mount_point.clone()) { if m == "." { - self.wasi.map_dir("/", pkg_fs); + wasi_map_dir.push(("/".to_string(), pkg_fs)); } else { if m.starts_with('.') { m = format!("{}{}", pkg_fs.display(), &m[1..]); } let path = std::path::Path::new(&m).to_path_buf(); - self.wasi.map_dir("/", path); + wasi_map_dir.push(("/".to_string(), path)); } } } @@ -123,7 +127,7 @@ impl RunWithoutFile { continue; } let alias_pathbuf = std::path::Path::new(&real_dir).to_path_buf(); - self.wasi.map_dir(alias, alias_pathbuf.clone()); + wasi_map_dir.push((alias.to_string(), alias_pathbuf.clone())); fn is_dir(e: &walkdir::DirEntry) -> bool { let meta = match e.metadata() { @@ -142,12 +146,34 @@ impl RunWithoutFile { let pathbuf = entry.path().canonicalize().unwrap(); let path = format!("{}", pathbuf.display()); let relativepath = path.replacen(&root_display, "", 1); - self.wasi - .map_dir(&format!("/{alias}{relativepath}"), pathbuf); + wasi_map_dir.push((format!("/{alias}{relativepath}"), pathbuf)); } } } + // If a directory is mapped twice, the WASI runtime will throw an error + // We need to calculate the "key" path, then deduplicate the mapping + #[cfg(feature = "wasi")] + { + let parent = match pathbuf.parent() { + Some(parent) => parent.to_path_buf(), + None => pathbuf.clone(), + }; + let mut wasi_map = BTreeMap::new(); + for (k, v) in wasi_map_dir { + let path_v = v.canonicalize().unwrap_or(v.clone()); + let mut k_path = std::path::Path::new(&k).to_path_buf(); + if k_path.is_relative() { + k_path = parent.join(&k); + } + let key = format!("{}", k_path.canonicalize().unwrap_or(k_path).display()); + wasi_map.insert(key, (k, path_v)); + } + for (_, (k, v)) in wasi_map { + self.wasi.map_dir(&k, v); + } + } + Run { path: pathbuf, options: RunWithoutFile { @@ -155,7 +181,10 @@ impl RunWithoutFile { #[cfg(feature = "cache")] disable_cache: self.disable_cache, invoke: self.invoke, - command_name: self.command_name, + // If the RunWithoutFile was constructed via a package name, + // the correct syntax is "package:command-name" (--command-name would be + // interpreted as a CLI argument for the .wasm file) + command_name: None, #[cfg(feature = "cache")] cache_key: self.cache_key, store: self.store, diff --git a/lib/registry/src/lib.rs b/lib/registry/src/lib.rs index 89a23e517..5fe0dc68b 100644 --- a/lib/registry/src/lib.rs +++ b/lib/registry/src/lib.rs @@ -561,6 +561,7 @@ pub fn get_package_local_wasm_file( registry_host: &str, name: &str, version: &str, + command: Option<&str>, ) -> Result { let dir = get_package_local_dir(registry_host, name, version)?; let wapm_toml_path = dir.join("wapm.toml"); @@ -570,14 +571,17 @@ pub fn get_package_local_wasm_file( .map_err(|e| format!("cannot parse wapm.toml for {name}@{version}: {e}"))?; // TODO: this will just return the path for the first command, so this might not be correct - let module_name = wapm - .command - .unwrap_or_default() - .first() - .map(|m| m.get_module()) - .ok_or_else(|| { - format!("cannot get entrypoint for {name}@{version}: package has no commands") - })?; + let module_name = match command { + Some(s) => s.to_string(), + None => wapm + .command + .unwrap_or_default() + .first() + .map(|m| m.get_module()) + .ok_or_else(|| { + format!("cannot get entrypoint for {name}@{version}: package has no commands") + })?, + }; let wasm_file_name = wapm .module