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