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