Skip to main content

wapm_cli/commands/
install.rs

1//! Code pertaining to the `install` subcommand
2
3use crate::{
4    commands::install::get_package_query::GetPackageQueryPackageLastVersion,
5    dataflow::bindings::Language, graphql::execute_query,
6};
7
8use anyhow::Context;
9use graphql_client::*;
10
11use crate::config::Config;
12use crate::dataflow;
13use crate::util;
14use std::{
15    borrow::Cow,
16    convert::TryInto,
17    path::PathBuf,
18    process::{Command, Stdio},
19};
20use std::{convert::TryFrom, path::Path};
21use structopt::StructOpt;
22use thiserror::Error;
23
24/// Options for the `install` subcommand
25#[derive(StructOpt, Debug)]
26pub struct InstallOpt {
27    packages: Vec<String>,
28    /// Install the package(s) globally
29    #[structopt(short = "g", long = "global")]
30    global: bool,
31    /// Agree to all prompts. Useful for non-interactive uses. (WARNING: this may cause undesired behavior)
32    #[structopt(long = "force-yes", short = "y")]
33    force_yes: bool,
34    /// Add the JavaScript bindings using "yarn add".
35    #[structopt(long, groups = &["bindings", "js"], conflicts_with = "global")]
36    yarn: bool,
37    /// Add the JavaScript bindings using "npm install".
38    #[structopt(long, groups = &["bindings", "js"], conflicts_with = "global")]
39    npm: bool,
40    /// Add the package as a dev dependency (JavaScript only)
41    #[structopt(long, requires = "js")]
42    dev: bool,
43    /// Add the Python bindings using "pip install".
44    #[structopt(long, group = "bindings", conflicts_with = "global")]
45    pip: bool,
46}
47
48#[derive(Debug, Error)]
49enum InstallError {
50    #[error("Package not found in the registry {registry:?}: {name}")]
51    PackageNotFound { name: String, registry: String },
52
53    #[error("No package versions available for package {name}")]
54    NoVersionsAvailable { name: String },
55
56    #[error("Failed to install packages. {0}")]
57    CannotRegenLockFile(dataflow::Error),
58
59    #[error("Failed to create the install directory. {0}")]
60    CannotCreateInstallDirectory(std::io::Error),
61
62    #[error("Failed to install packages in manifest. {0}")]
63    FailureInstallingPackages(dataflow::Error),
64
65    #[error(
66        "Failed to install package because package identifier {0} is invalid, expected <name>@<version> or <name>",
67        name
68    )]
69    InvalidPackageIdentifier { name: String },
70    #[error("Must supply package names to install command when using --global/-g flag.")]
71    MustSupplyPackagesWithGlobalFlag,
72}
73
74#[derive(GraphQLQuery)]
75#[graphql(
76    schema_path = "graphql/schema.graphql",
77    query_path = "graphql/queries/get_package.graphql",
78    response_derives = "Debug"
79)]
80struct GetPackageQuery;
81
82mod global_flag {
83    pub const GLOBAL_INSTALL: bool = true;
84    pub const LOCAL_INSTALL: bool = false;
85}
86
87mod package_args {
88    /// Command run with no package arguments, it will install packages from the manifest
89    pub const NO_PACKAGES: bool = true;
90    pub const SOME_PACKAGES: bool = false;
91}
92
93/// Run the install command
94pub fn install(options: InstallOpt) -> anyhow::Result<()> {
95    let current_directory = crate::config::Config::get_current_dir()?;
96    let _value = util::set_wapm_should_accept_all_prompts(options.force_yes);
97    debug_assert!(
98        _value.is_some(),
99        "this function should only be called once!"
100    );
101
102    match Target::from_options(&options) {
103        Some(language) => install_bindings(language, &options.packages, current_directory),
104        None => wapm_install(options, current_directory),
105    }
106}
107
108fn install_bindings(
109    target: Target,
110    packages: &[String],
111    current_directory: PathBuf,
112) -> Result<(), anyhow::Error> {
113    let VersionedPackage { name, version } = match packages {
114        [p] => p.as_str().try_into()?,
115        [] => anyhow::bail!("No package provided"),
116        [..] => anyhow::bail!("Bindings can only be installed for one package at a time"),
117    };
118
119    let url = dataflow::bindings::link_to_package_bindings(name, version, target.language())?;
120
121    let mut cmd = target.command(url.as_str());
122
123    log::debug!("Executing {cmd:?}");
124
125    // Note: We explicitly want to show the command output to users so they can
126    // troubleshoot any failures.
127    let status = cmd
128        .stdin(Stdio::null())
129        .stdout(Stdio::inherit())
130        .stderr(Stdio::inherit())
131        .current_dir(&current_directory)
132        .status()
133        .with_context(|| {
134            format!(
135                "Unable to start \"{}\". Is it installed?",
136                cmd.get_program().to_string_lossy()
137            )
138        })?;
139
140    anyhow::ensure!(status.success(), "Command failed: {:?}", cmd);
141
142    Ok(())
143}
144
145fn wapm_install(options: InstallOpt, current_directory: PathBuf) -> Result<(), anyhow::Error> {
146    match (options.global, options.packages.is_empty()) {
147        (global_flag::GLOBAL_INSTALL, package_args::NO_PACKAGES) => {
148            // install all global packages - unacceptable use case
149            Err(InstallError::MustSupplyPackagesWithGlobalFlag.into())
150        }
151        (global_flag::LOCAL_INSTALL, package_args::NO_PACKAGES) => {
152            local_install_from_lockfile(&current_directory)
153        }
154        (_, package_args::SOME_PACKAGES) => {
155            install_packages(&options.packages, options.global, current_directory)
156        }
157    }
158}
159
160fn install_packages(
161    package_names: &[String],
162    global: bool,
163    current_directory: PathBuf,
164) -> Result<(), anyhow::Error> {
165    let mut packages = vec![];
166    for name in package_names {
167        packages.push(parse_package_and_version(name)?);
168    }
169
170    let installed_packages: Vec<(&str, &str)> = packages
171        .iter()
172        .map(|(name, version)| (name.as_str(), version.as_str()))
173        .collect();
174
175    // the install directory will determine which wapm.lock we are updating. For now, we
176    // look in the local directory, or the global install directory
177    let install_directory: Cow<Path> = match global {
178        true => {
179            let folder = Config::get_globals_directory()?;
180            Cow::Owned(folder)
181        }
182        false => Cow::Borrowed(&current_directory),
183    };
184
185    std::fs::create_dir_all(install_directory.clone())
186        .map_err(InstallError::CannotCreateInstallDirectory)?;
187    let changes_applied = dataflow::update(installed_packages.clone(), vec![], install_directory)
188        .map_err(InstallError::CannotRegenLockFile)?;
189
190    if changes_applied {
191        if global {
192            println!("Global package installed successfully!");
193        } else {
194            println!("Package installed successfully to wapm_packages!");
195        }
196    } else {
197        println!("No packages to install (package already installed?)");
198    }
199
200    Ok(())
201}
202
203#[derive(Debug)]
204struct VersionedPackage<'a> {
205    name: &'a str,
206    version: Option<&'a str>,
207}
208
209impl<'a> TryFrom<&'a str> for VersionedPackage<'a> {
210    type Error = anyhow::Error;
211
212    fn try_from(package_specifier: &'a str) -> Result<Self, Self::Error> {
213        let name_and_version: Vec<_> = package_specifier.split('@').collect();
214
215        match *name_and_version.as_slice() {
216            [name, version] => Ok(VersionedPackage {
217                name,
218                version: Some(version),
219            }),
220            [name] => Ok(VersionedPackage {
221                name,
222                version: None,
223            }),
224            _ => Err(InstallError::InvalidPackageIdentifier {
225                name: package_specifier.to_string(),
226            }
227            .into()),
228        }
229    }
230}
231
232fn parse_package_and_version(package_specifier: &str) -> Result<(String, String), anyhow::Error> {
233    let name_and_version: Vec<_> = package_specifier.split('@').collect();
234    let config = Config::from_file()?;
235    match name_and_version.as_slice() {
236        [name, version] => Ok((name.to_string(), version.to_string())),
237        [name] => {
238            let q = GetPackageQuery::build_query(get_package_query::Variables {
239                name: name.to_string(),
240            });
241            let response: get_package_query::ResponseData = execute_query(&q)?;
242            let package = response.package.ok_or(InstallError::PackageNotFound {
243                name: name.to_string(),
244                registry: config.registry.get_current_registry(),
245            })?;
246            let GetPackageQueryPackageLastVersion { version, .. } =
247                package
248                    .last_version
249                    .ok_or(InstallError::NoVersionsAvailable {
250                        name: name.to_string(),
251                    })?;
252
253            Ok((name.to_string(), version))
254        }
255        _ => Err(InstallError::InvalidPackageIdentifier {
256            name: package_specifier.to_string(),
257        }
258        .into()),
259    }
260}
261
262fn local_install_from_lockfile(current_directory: &Path) -> Result<(), anyhow::Error> {
263    let added_packages = vec![];
264    dataflow::update(added_packages, vec![], current_directory)
265        .map_err(InstallError::FailureInstallingPackages)?;
266    println!("Packages installed to wapm_packages!");
267    Ok(())
268}
269
270#[derive(Debug)]
271enum Target {
272    Npm { dev: bool },
273    Yarn { dev: bool },
274    Pip,
275}
276
277impl Target {
278    fn from_options(options: &InstallOpt) -> Option<Self> {
279        let InstallOpt {
280            yarn,
281            npm,
282            pip,
283            dev,
284            ..
285        } = *options;
286
287        match (yarn, npm, pip) {
288            (true, false, false) => Some(Target::Yarn { dev }),
289            (false, true, false) => Some(Target::Npm { dev }),
290            (false, false, true) => Some(Target::Pip),
291            (false, false, false) => None,
292            _ => unreachable!("Already rejected by clap"),
293        }
294    }
295
296    fn language(&self) -> Language {
297        match self {
298            Target::Npm { .. } | Target::Yarn { .. } => Language::JavaScript,
299            Target::Pip => Language::Python,
300        }
301    }
302
303    fn command(&self, url: &str) -> Command {
304        let shell_command = match self {
305            Target::Npm { dev: false } => format!("npm install {url}"),
306            Target::Npm { dev: true } => format!("npm install --save-dev {url}"),
307            Target::Yarn { dev: false } => format!("yarn add {url}"),
308            Target::Yarn { dev: true } => format!("yarn add --dev {url}"),
309            Target::Pip => format!("pip install {url}"),
310        };
311
312        if cfg!(target_os = "windows") {
313            let mut cmd = Command::new("cmd");
314            cmd.arg("/C").arg(shell_command);
315            cmd
316        } else {
317            let mut cmd = Command::new("sh");
318            cmd.arg("-c").arg(shell_command);
319            cmd
320        }
321    }
322}