Skip to main content

wasmer_cli/commands/
mod.rs

1//! The commands available in the Wasmer binary.
2mod add;
3mod app;
4mod auth;
5#[cfg(target_os = "linux")]
6mod binfmt;
7mod cache;
8#[cfg(feature = "compiler")]
9mod compile;
10mod config;
11mod connect;
12mod container;
13mod cron;
14pub(crate) mod domain;
15mod gen_completions;
16mod gen_manpage;
17mod init;
18mod inspect;
19#[cfg(feature = "journal")]
20mod journal;
21pub(crate) mod namespace;
22mod package;
23mod run;
24mod self_update;
25pub mod ssh;
26mod validate;
27#[cfg(feature = "wast")]
28mod wast;
29use itertools::Itertools;
30use std::io::IsTerminal as _;
31use tokio::task::JoinHandle;
32
33#[cfg(target_os = "linux")]
34pub use binfmt::*;
35use clap::{CommandFactory, Parser};
36#[cfg(feature = "compiler")]
37pub use compile::*;
38#[cfg(feature = "wast")]
39pub use wast::*;
40
41#[cfg(feature = "journal")]
42pub use self::journal::*;
43pub use self::{
44    add::*, auth::*, cache::*, config::*, container::*, init::*, inspect::*, package::*,
45    publish::*, run::Run, self_update::*, validate::*,
46};
47use crate::error::PrettyError;
48use git_version::git_version;
49
50/// An executable CLI command.
51pub(crate) trait CliCommand {
52    type Output;
53
54    fn run(self) -> Result<(), anyhow::Error>;
55}
56
57/// An executable CLI command that runs in an async context.
58///
59/// An [`AsyncCliCommand`] automatically implements [`CliCommand`] by creating
60/// a new tokio runtime and blocking.
61#[async_trait::async_trait]
62pub(crate) trait AsyncCliCommand: Send + Sync {
63    type Output: Send + Sync;
64
65    async fn run_async(self) -> Result<Self::Output, anyhow::Error>;
66
67    fn setup(
68        &self,
69        done: tokio::sync::oneshot::Receiver<()>,
70    ) -> Option<JoinHandle<anyhow::Result<()>>> {
71        if std::io::stdin().is_terminal() {
72            return Some(tokio::task::spawn(async move {
73                tokio::select! {
74                    _ = done => {}
75
76                    _ = tokio::signal::ctrl_c() => {
77                        let term = console::Term::stdout();
78                        let _ = term.show_cursor();
79                        // https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-constants
80                        #[cfg(target_os = "windows")]
81                        std::process::exit(3);
82
83                        // POSIX compliant OSs: 128 + SIGINT (2)
84                        #[cfg(not(target_os = "windows"))]
85                        std::process::exit(130);
86                    }
87                }
88
89                Ok::<(), anyhow::Error>(())
90            }));
91        }
92
93        None
94    }
95}
96
97impl<O: Send + Sync, C: AsyncCliCommand<Output = O>> CliCommand for C {
98    type Output = O;
99
100    fn run(self) -> Result<(), anyhow::Error> {
101        tokio::runtime::Runtime::new()?.block_on(async {
102            let (snd, rcv) = tokio::sync::oneshot::channel();
103            let handle = self.setup(rcv);
104
105            if let Err(e) = AsyncCliCommand::run_async(self).await {
106                if let Some(handle) = handle {
107                    handle.abort();
108                }
109                return Err(e);
110            }
111
112            if let Some(handle) = handle {
113                if snd.send(()).is_err() {
114                    tracing::warn!("Failed to send 'done' signal to setup thread!");
115                    handle.abort();
116                } else {
117                    handle.await??;
118                }
119            }
120
121            Ok::<(), anyhow::Error>(())
122        })?;
123
124        Ok(())
125    }
126}
127
128/// Command-line arguments for the Wasmer CLI.
129#[derive(clap::Parser, Debug)]
130#[clap(author, version)]
131#[clap(disable_version_flag = true)] // handled manually
132#[cfg_attr(feature = "headless", clap(
133    name = "wasmer-headless",
134    about = concat!("wasmer-headless ", env!("CARGO_PKG_VERSION")),
135))]
136#[cfg_attr(not(feature = "headless"), clap(
137    name = "wasmer",
138    about = concat!("wasmer ", env!("CARGO_PKG_VERSION")),
139))]
140pub struct WasmerCmd {
141    /// Print version info and exit.
142    #[clap(short = 'V', long)]
143    version: bool,
144    #[clap(flatten)]
145    output: crate::logging::Output,
146    #[clap(subcommand)]
147    cmd: Option<Cmd>,
148}
149
150impl WasmerCmd {
151    fn execute(self) -> Result<(), anyhow::Error> {
152        let WasmerCmd {
153            cmd,
154            version,
155            output,
156        } = self;
157
158        output.initialize_logging();
159
160        if version {
161            return print_version(output.is_verbose());
162        }
163
164        match cmd {
165            Some(Cmd::GenManPage(cmd)) => cmd.execute(),
166            Some(Cmd::GenCompletions(cmd)) => cmd.execute(),
167            Some(Cmd::Run(options)) => options.execute(output),
168            Some(Cmd::SelfUpdate(options)) => options.execute(),
169            Some(Cmd::Cache(cache)) => cache.execute(),
170            Some(Cmd::Validate(validate)) => validate.execute(),
171            #[cfg(feature = "compiler")]
172            Some(Cmd::Compile(compile)) => compile.execute(),
173            Some(Cmd::Config(config)) => config.run(),
174            Some(Cmd::Inspect(inspect)) => inspect.execute(),
175            Some(Cmd::Init(init)) => init.run(),
176            Some(Cmd::Login(login)) => login.run(),
177            Some(Cmd::Auth(auth)) => auth.run(),
178            Some(Cmd::Publish(publish)) => publish.run().map(|_| ()),
179            Some(Cmd::Package(cmd)) => match cmd {
180                Package::Download(cmd) => cmd.execute(),
181                Package::Build(cmd) => cmd.execute().map(|_| ()),
182                Package::Tag(cmd) => cmd.run(),
183                Package::Push(cmd) => cmd.run(),
184                Package::Publish(cmd) => cmd.run().map(|_| ()),
185                Package::Tree(cmd) => cmd.run(),
186                Package::Unpack(cmd) => cmd.execute(),
187                Package::Search(cmd) => cmd.run(),
188                Package::Get(cmd) => cmd.run(),
189            },
190            Some(Cmd::Container(cmd)) => match cmd {
191                crate::commands::Container::Unpack(cmd) => cmd.execute(),
192            },
193            #[cfg(feature = "wast")]
194            Some(Cmd::Wast(wast)) => wast.execute(),
195            #[cfg(target_os = "linux")]
196            Some(Cmd::Binfmt(binfmt)) => binfmt.execute(),
197            Some(Cmd::Whoami(whoami)) => whoami.run(),
198            Some(Cmd::Add(add)) => add.run(),
199
200            // Deploy commands.
201            Some(Cmd::Deploy(c)) => c.run(),
202            Some(Cmd::App(apps)) => apps.run(),
203            Some(Cmd::Cron(cron)) => cron.run(),
204            #[cfg(feature = "journal")]
205            Some(Cmd::Journal(journal)) => journal.run(),
206            Some(Cmd::Ssh(ssh)) => ssh.run(),
207            Some(Cmd::Namespace(namespace)) => namespace.run(),
208            Some(Cmd::Domain(namespace)) => namespace.run(),
209            None => {
210                WasmerCmd::command().print_long_help()?;
211                // Note: clap uses an exit code of 2 when CLI parsing fails
212                std::process::exit(2);
213            }
214        }
215    }
216
217    /// The main function for the Wasmer CLI tool.
218    pub fn run() {
219        // We allow windows to print properly colors
220        #[cfg(windows)]
221        colored::control::set_virtual_terminal(true).unwrap();
222
223        PrettyError::report(Self::run_inner())
224    }
225
226    fn run_inner() -> Result<(), anyhow::Error> {
227        let mut args_os = std::env::args_os();
228
229        let args = args_os.next().into_iter();
230
231        let mut binfmt_args = Vec::new();
232        if is_binfmt_interpreter() {
233            // In case of binfmt misc the first argument is wasmer-binfmt-interpreter, the second is the full path to the executable
234            // and the third is the original string for the executable as originally called by the user.
235
236            // For now we are only using the real path and ignoring the original executable name.
237            // Ideally we would use the real path to load the file and the original name to pass it as argv[0] to the wasm module.
238
239            let current_dir = std::env::current_dir().unwrap();
240            let mut mount_paths = ["/home", "/etc", "/tmp", "/var", "/nix", "/opt", "/root"]
241                .into_iter()
242                .map(std::path::PathBuf::from)
243                .filter(|path| {
244                    if !path.is_dir() {
245                        // Not a directory
246                        return false;
247                    }
248                    if std::fs::read_dir(path).is_err() {
249                        // No permissions
250                        return false;
251                    }
252                    true
253                })
254                .collect_vec();
255            if mount_paths
256                .iter()
257                .all(|path| !current_dir.starts_with(path))
258            {
259                // Mount the current dir if it is not already covered by a common path
260                mount_paths.push(current_dir.clone());
261            }
262
263            binfmt_args.push("run".into());
264            binfmt_args.push("--net".into());
265            // TODO: This does not seem to work, needs further investigation.
266            binfmt_args.push("--forward-host-env".into());
267            for mount_path in mount_paths {
268                if let Some(mount_path_str) = mount_path.to_str() {
269                    binfmt_args.push(format!("--volume={mount_path_str}:{mount_path_str}").into());
270                }
271            }
272            if let Some(current_dir_str) = current_dir.to_str() {
273                binfmt_args.push(format!("--cwd={current_dir_str}").into());
274            }
275            binfmt_args.push("--quiet".into());
276            binfmt_args.push("--".into());
277            binfmt_args.push(args_os.next().unwrap());
278            args_os.next().unwrap();
279        };
280        let args_vec = args.chain(binfmt_args).chain(args_os).collect_vec();
281
282        match WasmerCmd::try_parse_from(args_vec.iter()) {
283            Ok(args) => args.execute(),
284            Err(e) => {
285                let first_arg_is_subcommand = if let Some(first_arg) = args_vec.get(1) {
286                    let mut ret = false;
287                    let cmd = WasmerCmd::command();
288
289                    for cmd in cmd.get_subcommands() {
290                        if cmd.get_name() == first_arg {
291                            ret = true;
292                            break;
293                        }
294                    }
295
296                    ret
297                } else {
298                    false
299                };
300
301                let might_be_wasmer_run = matches!(
302                    e.kind(),
303                    clap::error::ErrorKind::InvalidSubcommand
304                        | clap::error::ErrorKind::UnknownArgument
305                ) && !first_arg_is_subcommand;
306
307                if might_be_wasmer_run && let Ok(run) = Run::try_parse_from(args_vec.iter()) {
308                    // Try to parse the command using the `wasmer some/package`
309                    // shorthand. Note that this has discoverability issues
310                    // because it's not shown as part of the main argument
311                    // parser's help, but that's fine.
312                    let output = crate::logging::Output::default();
313                    output.initialize_logging();
314                    run.execute(output);
315                }
316
317                e.exit();
318            }
319        }
320    }
321}
322
323#[derive(clap::Parser, Debug)]
324#[allow(clippy::large_enum_variant)]
325/// The options for the wasmer Command Line Interface
326enum Cmd {
327    /// Login into Wasmer
328    Login(Login),
329
330    #[clap(subcommand)]
331    Auth(CmdAuth),
332
333    /// Publish a package to a registry [alias: package publish]
334    #[clap(name = "publish")]
335    Publish(PackagePublish),
336
337    /// Manage the local Wasmer cache
338    Cache(Cache),
339
340    /// Validate a WebAssembly binary
341    Validate(Validate),
342
343    /// Compile a WebAssembly binary
344    #[cfg(feature = "compiler")]
345    Compile(Compile),
346
347    /// Get various configuration information needed
348    /// to compile programs which use Wasmer
349    Config(Config),
350
351    /// Update wasmer to the latest version
352    #[clap(name = "self-update")]
353    SelfUpdate(SelfUpdate),
354
355    /// Inspect a WebAssembly file
356    Inspect(Inspect),
357
358    /// Initializes a new wasmer.toml file
359    #[clap(name = "init")]
360    Init(Init),
361
362    /// Run spec testsuite
363    #[cfg(feature = "wast")]
364    Wast(Wast),
365
366    /// Unregister and/or register wasmer as binfmt interpreter
367    #[cfg(target_os = "linux")]
368    Binfmt(Binfmt),
369
370    /// Shows the current logged in user for the current active registry
371    Whoami(Whoami),
372
373    /// Add a Wasmer package's bindings to your application
374    Add(CmdAdd),
375
376    /// Run a WebAssembly file or Wasmer container
377    #[clap(alias = "run-unstable")]
378    Run(Run),
379
380    /// Manage journals (compacting, inspecting, filtering, ...)
381    #[cfg(feature = "journal")]
382    #[clap(subcommand)]
383    Journal(CmdJournal),
384
385    #[clap(subcommand)]
386    Package(crate::commands::Package),
387
388    #[clap(subcommand)]
389    Container(crate::commands::Container),
390
391    // Edge commands
392    /// Deploy apps to Wasmer Edge [alias: app deploy]
393    Deploy(crate::commands::app::deploy::CmdAppDeploy),
394
395    /// Create and manage Wasmer Edge apps
396    #[clap(subcommand, alias = "apps")]
397    App(crate::commands::app::CmdApp),
398
399    /// Manage cron jobs for Wasmer Edge apps
400    #[clap(subcommand)]
401    Cron(crate::commands::cron::CmdCron),
402
403    /// Run commands/packages on Wasmer Edge in an interactive shell session
404    Ssh(crate::commands::ssh::CmdSsh),
405
406    /// Manage Wasmer namespaces
407    #[clap(subcommand, alias = "namespaces")]
408    Namespace(crate::commands::namespace::CmdNamespace),
409
410    /// Manage DNS records
411    #[clap(subcommand, alias = "domains")]
412    Domain(crate::commands::domain::CmdDomain),
413
414    /// Generate autocompletion for different shells
415    #[clap(name = "gen-completions")]
416    GenCompletions(crate::commands::gen_completions::CmdGenCompletions),
417
418    /// Generate man pages
419    #[clap(name = "gen-man", hide = true)]
420    GenManPage(crate::commands::gen_manpage::CmdGenManPage),
421}
422
423fn is_binfmt_interpreter() -> bool {
424    cfg_select! {
425        target_os = "linux" => {
426            // Note: we'll be invoked by the kernel as Binfmt::FILENAME
427            let binary_path = match std::env::args_os().next() {
428                Some(path) => std::path::PathBuf::from(path),
429                None => return false,
430            };
431            binary_path.file_name().and_then(|f| f.to_str()) == Some(Binfmt::FILENAME)
432        }
433        _ => {
434            false
435        }
436    }
437}
438
439fn print_version(verbose: bool) -> Result<(), anyhow::Error> {
440    if !verbose {
441        println!("wasmer {}", env!("CARGO_PKG_VERSION"));
442        return Ok(());
443    }
444
445    println!("wasmer {}", env!("CARGO_PKG_VERSION"));
446    println!("binary: {}", env!("CARGO_PKG_NAME"));
447    let git_hash = git_version!(
448        args = [
449            "--abbrev=40",
450            "--always",
451            "--dirty=-modified",
452            "--exclude=*"
453        ],
454        fallback = "",
455    )
456    .to_string();
457    if !git_hash.is_empty() {
458        println!("commit-hash: {git_hash}",);
459    }
460    if !env!("WASMER_REPRODUCIBLE_BUILD")
461        .parse::<bool>()
462        .expect("build-time variable expected")
463    {
464        println!("commit-date: {}", env!("WASMER_BUILD_DATE"));
465    }
466    println!("host: {}", target_lexicon::HOST);
467
468    let cpu_features = wasmer_types::target::CpuFeature::for_host()
469        .iter()
470        .map(|f| f.to_string())
471        .join(" ");
472    println!("CPU flags: {cpu_features}");
473
474    let mut runtimes = Vec::new();
475    if cfg!(feature = "singlepass") {
476        runtimes.push("Singlepass");
477    }
478    if cfg!(feature = "cranelift") {
479        runtimes.push("Cranelift");
480    }
481    if cfg!(feature = "llvm") {
482        runtimes.push("LLVM");
483    }
484    if cfg!(feature = "v8") {
485        runtimes.push("V8");
486    }
487    println!("runtimes: {}", runtimes.join(", "));
488
489    #[allow(clippy::useless_vec)]
490    #[allow(unused_mut)]
491    let mut features = vec!["wasix".to_string()];
492    #[cfg(feature = "napi-v8")]
493    {
494        for napi_version in enum_iterator::all::<wasmer_napi::NapiVersion>() {
495            if !matches!(napi_version, wasmer_napi::NapiVersion::Unknown) {
496                features.push(napi_version.to_string());
497            }
498        }
499        features.push(wasmer_napi::NAPI_EXTENSION_WASMER_MODULE_NAME.to_string());
500    }
501    println!("features: {}", features.join(", "));
502
503    Ok(())
504}