wasmer_cli/commands/
add.rs

1use super::AsyncCliCommand;
2use crate::config::WasmerEnv;
3use anyhow::{Context, Error};
4use clap::Parser;
5use std::process::{Command, Stdio};
6use wasmer_backend_api::{
7    WasmerClient,
8    types::{Bindings, ProgrammingLanguage},
9};
10use wasmer_config::package::NamedPackageIdent;
11
12/// Add a Wasmer package's bindings to your application.
13#[derive(Debug, Parser)]
14pub struct CmdAdd {
15    #[clap(flatten)]
16    env: WasmerEnv,
17    /// Add the JavaScript bindings using "npm install".
18    #[clap(long, groups = &["bindings", "js"])]
19    npm: bool,
20    /// Add the JavaScript bindings using "yarn add".
21    #[clap(long, groups = &["bindings", "js"])]
22    yarn: bool,
23    /// Add the JavaScript bindings using "pnpm add".
24    #[clap(long, groups = &["bindings", "js"])]
25    pnpm: bool,
26    /// Add the package as a dev-dependency.
27    #[clap(long, requires = "js")]
28    dev: bool,
29    /// Add the Python bindings using "pip install".
30    #[clap(long, groups = &["bindings", "py"])]
31    pip: bool,
32    /// The packages to add (e.g. "wasmer/wasmer-pack@0.5.0" or "python/python")
33    packages: Vec<NamedPackageIdent>,
34}
35
36#[async_trait::async_trait]
37impl AsyncCliCommand for CmdAdd {
38    type Output = ();
39
40    /// Execute [`Add`].
41    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
42        anyhow::ensure!(!self.packages.is_empty(), "No packages specified");
43
44        let client = self.env.client_unauthennticated()?;
45        let bindings = self.lookup_bindings(&client).await?;
46
47        let mut cmd = self.target()?.command(&bindings)?;
48        cmd.stdin(Stdio::null())
49            .stdout(Stdio::inherit())
50            .stderr(Stdio::inherit());
51
52        println!("Running: {cmd:?}");
53
54        let status = cmd.status().with_context(|| {
55            format!(
56                "Unable to start \"{:?}\". Is it installed?",
57                cmd.get_program()
58            )
59        })?;
60
61        anyhow::ensure!(status.success(), "Command failed: {cmd:?}");
62
63        Ok(())
64    }
65}
66
67impl CmdAdd {
68    async fn lookup_bindings(&self, client: &WasmerClient) -> Result<Vec<Bindings>, Error> {
69        println!("Querying Wasmer for package bindings");
70
71        let mut bindings_to_add = Vec::new();
72        let language = self.target()?.language();
73
74        for pkg in &self.packages {
75            let bindings = lookup_bindings_for_package(client, pkg, &language)
76                .await
77                .with_context(|| format!("Unable to find bindings for {pkg}"))?;
78            bindings_to_add.push(bindings);
79        }
80
81        Ok(bindings_to_add)
82    }
83
84    fn target(&self) -> Result<Target, Error> {
85        match (self.pip, self.npm, self.yarn, self.pnpm) {
86            (false, false, false, false) => Err(anyhow::anyhow!(
87                "at least one of --npm, --pip, --yarn or --pnpm has to be specified"
88            )),
89            (true, false, false, false) => Ok(Target::Pip),
90            (false, true, false, false) => Ok(Target::Npm { dev: self.dev }),
91            (false, false, true, false) => Ok(Target::Yarn { dev: self.dev }),
92            (false, false, false, true) => Ok(Target::Pnpm { dev: self.dev }),
93            _ => Err(anyhow::anyhow!(
94                "only one of --npm, --pip or --yarn has to be specified"
95            )),
96        }
97    }
98}
99
100async fn lookup_bindings_for_package(
101    client: &WasmerClient,
102    pkg: &NamedPackageIdent,
103    language: &ProgrammingLanguage,
104) -> Result<Bindings, Error> {
105    let all_bindings = wasmer_backend_api::query::list_bindings(
106        client,
107        &pkg.name,
108        pkg.version_opt().map(|v| v.to_string()).as_deref(),
109    )
110    .await?;
111
112    match all_bindings.iter().find(|b| b.language == *language) {
113        Some(b) => {
114            let Bindings { url, generator, .. } = b;
115            log::debug!("Found {pkg} bindings generated by {generator:?} at {url}");
116
117            Ok(b.clone())
118        }
119        None => {
120            if all_bindings.is_empty() {
121                anyhow::bail!("The package doesn't contain any bindings");
122            } else {
123                todo!();
124            }
125        }
126    }
127}
128
129#[derive(Debug, Copy, Clone)]
130enum Target {
131    Pip,
132    Yarn { dev: bool },
133    Npm { dev: bool },
134    Pnpm { dev: bool },
135}
136
137impl Target {
138    fn language(self) -> ProgrammingLanguage {
139        match self {
140            Target::Pip => ProgrammingLanguage::Python,
141            Target::Pnpm { .. } | Target::Yarn { .. } | Target::Npm { .. } => {
142                ProgrammingLanguage::Javascript
143            }
144        }
145    }
146
147    /// Construct a command which we can run to add packages.
148    ///
149    /// This deliberately runs the command using the OS shell instead of
150    /// invoking the tool directly. That way we can handle when a version
151    /// manager (e.g. `nvm` or `asdf`) replaces the tool with a script (e.g.
152    /// `npm.cmd` or `yarn.ps1`).
153    ///
154    /// See <https://github.com/wasmerio/wapm-cli/issues/291> for more.
155    fn command(self, packages: &[Bindings]) -> Result<Command, Error> {
156        let command_line = match self {
157            Target::Pip => {
158                if Command::new("pip").arg("--version").output().is_ok() {
159                    "pip install"
160                } else if Command::new("pip3").arg("--version").output().is_ok() {
161                    "pip3 install"
162                } else if Command::new("python").arg("--version").output().is_ok() {
163                    "python -m pip install"
164                } else if Command::new("python3").arg("--version").output().is_ok() {
165                    "python3 -m pip install"
166                } else {
167                    return Err(anyhow::anyhow!(
168                        "neither pip, pip3, python or python3 installed"
169                    ));
170                }
171            }
172            Target::Yarn { dev } => {
173                if Command::new("yarn").arg("--version").output().is_err() {
174                    return Err(anyhow::anyhow!("yarn not installed"));
175                }
176                if dev { "yarn add --dev" } else { "yarn add" }
177            }
178            Target::Npm { dev } => {
179                if Command::new("npm").arg("--version").output().is_err() {
180                    return Err(anyhow::anyhow!("npm not installed"));
181                }
182                if dev {
183                    "npm install --dev"
184                } else {
185                    "npm install"
186                }
187            }
188            Target::Pnpm { dev } => {
189                if Command::new("pnpm").arg("--version").output().is_err() {
190                    return Err(anyhow::anyhow!("pnpm not installed"));
191                }
192                if dev { "pnpm add --dev" } else { "pnpm add" }
193            }
194        };
195        let mut command_line = command_line.to_string();
196
197        for pkg in packages {
198            command_line.push(' ');
199            command_line.push_str(&pkg.url);
200        }
201
202        if cfg!(windows) {
203            let mut cmd = Command::new("cmd");
204            cmd.arg("/C").arg(command_line);
205            Ok(cmd)
206        } else {
207            let mut cmd = Command::new("sh");
208            cmd.arg("-c").arg(command_line);
209            Ok(cmd)
210        }
211    }
212}