Skip to content
This repository has been archived by the owner on Jun 14, 2023. It is now read-only.

Npm and python bindings #262

Merged
merged 7 commits into from
Aug 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions graphql/queries/get_bindings.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
query GetBindingsQuery ($name: String!, $version: String = "latest") {
packageVersion: getPackageVersion(name:$name, version:$version) {
version
bindings {
module
__typename

... on PackageVersionNPMBinding {
npmDefaultInstallPackageName
}
... on PackageVersionPythonBinding {
pythonDefaultInstallPackageName
}
}
}
}
14 changes: 14 additions & 0 deletions graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ type PackageTransferRequestEdge {
}

type PackageVersion implements Node {
bindings: [PackageVersionBinding]!
commands: [Command!]!
createdAt: DateTime!
description: String!
Expand Down Expand Up @@ -721,6 +722,19 @@ type PackageVersion implements Node {
version: String!
}

interface PackageVersionBinding {
# The module these bindings are associated with.
module: String!
}

type PackageVersionNPMBinding implements PackageVersionBinding {
npmDefaultInstallPackageName: String!
}

type PackageVersionPythonBinding implements PackageVersionBinding {
pythonDefaultInstallPackageName: String!
}

type PackageVersionConnection {
# Contains the nodes in this connection.
edges: [PackageVersionEdge]!
Expand Down
311 changes: 242 additions & 69 deletions src/commands/install.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
//! Code pertaining to the `install` subcommand

use crate::graphql::execute_query;
use crate::{
commands::install::get_package_query::GetPackageQueryPackageLastVersion,
dataflow::bindings::Language, graphql::execute_query,
};

use anyhow::Context;
use graphql_client::*;

use crate::config::Config;
use crate::dataflow;
use crate::util;
use std::borrow::Cow;
use std::path::Path;
use std::{
borrow::Cow,
convert::TryInto,
path::PathBuf,
process::{Command, Stdio},
};
use std::{convert::TryFrom, path::Path};
use structopt::StructOpt;
use thiserror::Error;

Expand All @@ -22,6 +31,22 @@ pub struct InstallOpt {
/// Agree to all prompts. Useful for non-interactive uses. (WARNING: this may cause undesired behavior)
#[structopt(long = "force-yes", short = "y")]
force_yes: bool,
/// Add the JavaScript bindings using "yarn add".
#[structopt(long, groups = &["bindings", "js"], conflicts_with = "global")]
yarn: bool,
/// Add the JavaScript bindings using "npm install".
#[structopt(long, groups = &["bindings", "js"], conflicts_with = "global")]
npm: bool,
/// Add the package as a dev dependency (JavaScript only)
#[structopt(long, requires = "js")]
dev: bool,
/// Add the Python bindings using "pip install".
#[structopt(long, group = "bindings", conflicts_with = "global")]
pip: bool,
/// The module to install bindings for (useful if a package contains more
/// than one)
#[structopt(long, requires = "bindings")]
module: Option<String>,
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -78,84 +103,232 @@ pub fn install(options: InstallOpt) -> anyhow::Result<()> {
"this function should only be called once!"
);

match Target::from_options(&options) {
Some(language) => install_bindings(
language,
&options.packages,
options.module.as_deref(),
options.dev,
current_directory,
),
None => wapm_install(options, current_directory),
}
}

fn install_bindings(
target: Target,
packages: &[String],
module: Option<&str>,
dev: bool,
current_directory: PathBuf,
) -> Result<(), anyhow::Error> {
let VersionedPackage { name, version } = match packages {
[p] => p.as_str().try_into()?,
[] => anyhow::bail!("No package provided"),
[..] => anyhow::bail!("Bindings can only be installed for one package at a time"),
};

let url =
dataflow::bindings::link_to_package_bindings(name, version, target.language(), module)?;

let mut cmd = target.command(url.as_str(), dev);

// Note: We explicitly want to show the command output to users so they can
// troubleshoot any failures.
let status = cmd
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&current_directory)
.status()
.with_context(|| {
format!(
"Unable to start \"{}\". Is it installed?",
cmd.get_program().to_string_lossy()
)
})?;

anyhow::ensure!(status.success(), "Command failed: {:?}", cmd);

Ok(())
}

fn wapm_install(options: InstallOpt, current_directory: PathBuf) -> Result<(), anyhow::Error> {
match (options.global, options.packages.is_empty()) {
(global_flag::GLOBAL_INSTALL, package_args::NO_PACKAGES) => {
// install all global packages - unacceptable use case
return Err(InstallError::MustSupplyPackagesWithGlobalFlag.into());
Err(InstallError::MustSupplyPackagesWithGlobalFlag.into())
}
(global_flag::LOCAL_INSTALL, package_args::NO_PACKAGES) => {
// install all packages locally
let added_packages = vec![];
dataflow::update(added_packages, vec![], &current_directory)
.map_err(|err| InstallError::FailureInstallingPackages(err))?;
println!("Packages installed to wapm_packages!");
local_install_from_lockfile(&current_directory)
}
(_, package_args::SOME_PACKAGES) => {
let mut packages = vec![];
for name in options.packages {
let name_with_version: Vec<&str> = name.split("@").collect();

match &name_with_version[..] {
[package_name, package_version] => {
packages.push((package_name.to_string(), package_version.to_string()));
}
[name] => {
let q = GetPackageQuery::build_query(get_package_query::Variables {
name: name.to_string(),
});
let response: get_package_query::ResponseData = execute_query(&q)?;
let package = response.package.ok_or(InstallError::PackageNotFound {
name: name.to_string(),
})?;
let last_version =
package
.last_version
.ok_or(InstallError::NoVersionsAvailable {
name: name.to_string(),
})?;
let package_name = package.name.clone();
let package_version = last_version.version.clone();
packages.push((package_name, package_version));
}
_ => {
return Err(
InstallError::InvalidPackageIdentifier { name: name.clone() }.into(),
);
}
}
install_packages(&options.packages, options.global, current_directory)
}
}
}

fn install_packages(
package_names: &[String],
global: bool,
current_directory: PathBuf,
) -> Result<(), anyhow::Error> {
let mut packages = vec![];
for name in package_names {
packages.push(parse_package_and_version(name)?);
}

let installed_packages: Vec<(&str, &str)> = packages
.iter()
.map(|(name, version)| (name.as_str(), version.as_str()))
.collect();

// the install directory will determine which wapm.lock we are updating. For now, we
// look in the local directory, or the global install directory
let install_directory: Cow<Path> = match global {
true => {
let folder = Config::get_globals_directory()?;
Cow::Owned(folder)
}
false => Cow::Borrowed(&current_directory),
};

std::fs::create_dir_all(install_directory.clone())
.map_err(|err| InstallError::CannotCreateInstallDirectory(err))?;
let changes_applied = dataflow::update(installed_packages.clone(), vec![], install_directory)
.map_err(|err| InstallError::CannotRegenLockFile(err))?;

if changes_applied {
if global {
println!("Global package installed successfully!");
} else {
println!("Package installed successfully to wapm_packages!");
}
} else {
println!("No packages to install");
}

Ok(())
}

#[derive(Debug)]
struct VersionedPackage<'a> {
name: &'a str,
version: Option<&'a str>,
}

impl<'a> TryFrom<&'a str> for VersionedPackage<'a> {
type Error = anyhow::Error;

fn try_from(package_specifier: &'a str) -> Result<Self, Self::Error> {
let name_and_version: Vec<_> = package_specifier.split('@').collect();

match *name_and_version.as_slice() {
[name, version] => Ok(VersionedPackage {
name,
version: Some(version),
}),
[name] => Ok(VersionedPackage {
name,
version: None,
}),
_ => Err(InstallError::InvalidPackageIdentifier {
name: package_specifier.to_string(),
}
.into()),
}
}
}

fn parse_package_and_version(package_specifier: &str) -> Result<(String, String), anyhow::Error> {
let name_and_version: Vec<_> = package_specifier.split('@').collect();

let installed_packages: Vec<(&str, &str)> = packages
.iter()
.map(|(s1, s2)| (s1.as_str(), s2.as_str()))
.collect();

// the install directory will determine which wapm.lock we are updating. For now, we
// look in the local directory, or the global install directory
let install_directory: Cow<Path> = match options.global {
true => {
let folder = Config::get_globals_directory()?;
Cow::Owned(folder)
match name_and_version.as_slice() {
[name, version] => Ok((name.to_string(), version.to_string())),
[name] => {
let q = GetPackageQuery::build_query(get_package_query::Variables {
name: name.to_string(),
});
let response: get_package_query::ResponseData = execute_query(&q)?;
let package = response.package.ok_or(InstallError::PackageNotFound {
name: name.to_string(),
})?;
let GetPackageQueryPackageLastVersion { version, .. } =
package
.last_version
.ok_or(InstallError::NoVersionsAvailable {
name: name.to_string(),
})?;

Ok((name.to_string(), version))
}
_ => Err(InstallError::InvalidPackageIdentifier {
name: package_specifier.to_string(),
}
.into()),
}
}

fn local_install_from_lockfile(current_directory: &Path) -> Result<(), anyhow::Error> {
let added_packages = vec![];
dataflow::update(added_packages, vec![], current_directory)
.map_err(|err| InstallError::FailureInstallingPackages(err))?;
println!("Packages installed to wapm_packages!");
Ok(())
}

#[derive(Debug)]
enum Target {
Npm,
Yarn,
Pip,
}

impl Target {
fn from_options(options: &InstallOpt) -> Option<Self> {
let InstallOpt { yarn, npm, pip, .. } = options;

match (yarn, npm, pip) {
(true, false, false) => Some(Target::Yarn),
(false, true, false) => Some(Target::Npm),
(false, false, true) => Some(Target::Pip),
(false, false, false) => None,
_ => unreachable!("Already rejected by clap"),
}
}

fn language(&self) -> Language {
match self {
Target::Npm | Target::Yarn => Language::JavaScript,
Target::Pip => Language::Python,
}
}

fn command(&self, url: &str, dev: bool) -> Command {
match self {
Target::Npm => {
let mut cmd = Command::new("npm");
cmd.arg("install");
if dev {
cmd.arg("--save-dev");
}
false => Cow::Borrowed(&current_directory),
};
std::fs::create_dir_all(install_directory.clone())
.map_err(|err| InstallError::CannotCreateInstallDirectory(err))?;

let changes_applied =
dataflow::update(installed_packages.clone(), vec![], install_directory)
.map_err(|err| InstallError::CannotRegenLockFile(err))?;

if changes_applied {
if options.global {
println!("Global package installed successfully!");
} else {
println!("Package installed successfully to wapm_packages!");
cmd.arg(url);
cmd
}
Target::Yarn => {
let mut cmd = Command::new("yarn");
cmd.arg("add");
if dev {
cmd.arg("--dev");
}
} else {
println!("No packages to install")
cmd.arg(url);
cmd
}
Target::Pip => {
let mut cmd = Command::new("pip");
cmd.arg("install").arg(url);
cmd
}
}
}
Ok(())
}
Loading