1use 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#[derive(StructOpt, Debug)]
26pub struct InstallOpt {
27 packages: Vec<String>,
28 #[structopt(short = "g", long = "global")]
30 global: bool,
31 #[structopt(long = "force-yes", short = "y")]
33 force_yes: bool,
34 #[structopt(long, groups = &["bindings", "js"], conflicts_with = "global")]
36 yarn: bool,
37 #[structopt(long, groups = &["bindings", "js"], conflicts_with = "global")]
39 npm: bool,
40 #[structopt(long, requires = "js")]
42 dev: bool,
43 #[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 pub const NO_PACKAGES: bool = true;
90 pub const SOME_PACKAGES: bool = false;
91}
92
93pub 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 let status = cmd
128 .stdin(Stdio::null())
129 .stdout(Stdio::inherit())
130 .stderr(Stdio::inherit())
131 .current_dir(¤t_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 Err(InstallError::MustSupplyPackagesWithGlobalFlag.into())
150 }
151 (global_flag::LOCAL_INSTALL, package_args::NO_PACKAGES) => {
152 local_install_from_lockfile(¤t_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 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(¤t_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}