Skip to content

Commit

Permalink
Added an install command
Browse files Browse the repository at this point in the history
  • Loading branch information
Michael-F-Bryan committed Nov 17, 2022
1 parent 60f4ffa commit e630c50
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 11 deletions.
2 changes: 1 addition & 1 deletion lib/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ atty = "0.2"
colored = "2.0"
anyhow = "1.0"
spinner = "0.5.0"
clap = { version = "3.2.22", features = ["derive"] }
clap = { version = "3.2.22", features = ["derive", "env"] }
# For the function names autosuggestion
distance = "0.4"
# For the inspect subcommand
Expand Down
6 changes: 5 additions & 1 deletion lib/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::commands::CreateExe;
use crate::commands::CreateObj;
#[cfg(feature = "wast")]
use crate::commands::Wast;
use crate::commands::{Cache, Config, Inspect, List, Login, Run, SelfUpdate, Validate};
use crate::commands::{Cache, Config, Inspect, Install, List, Login, Run, SelfUpdate, Validate};
use crate::error::PrettyError;
use clap::{CommandFactory, ErrorKind, Parser};
use std::fmt;
Expand Down Expand Up @@ -150,6 +150,9 @@ enum WasmerCLIOptions {
#[cfg(target_os = "linux")]
#[clap(name = "binfmt")]
Binfmt(Binfmt),

/// Add a WAPM package's bindings to your application.
Install(Install),
}

impl WasmerCLIOptions {
Expand All @@ -173,6 +176,7 @@ impl WasmerCLIOptions {
Self::Wast(wast) => wast.execute(),
#[cfg(target_os = "linux")]
Self::Binfmt(binfmt) => binfmt.execute(),
Self::Install(install) => install.execute(),
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion lib/cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod create_exe;
#[cfg(feature = "static-artifact-create")]
mod create_obj;
mod inspect;
mod install;
mod list;
mod login;
mod run;
Expand All @@ -28,7 +29,10 @@ pub use create_exe::*;
pub use create_obj::*;
#[cfg(feature = "wast")]
pub use wast::*;
pub use {cache::*, config::*, inspect::*, list::*, login::*, run::*, self_update::*, validate::*};
pub use {
cache::*, config::*, inspect::*, install::*, list::*, login::*, run::*, self_update::*,
validate::*,
};

/// The kind of object format to emit.
#[derive(Debug, Copy, Clone, clap::Parser)]
Expand Down
228 changes: 228 additions & 0 deletions lib/cli/src/commands/install.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
use std::{
fmt::{self, Display, Formatter},
process::{Command, Stdio},
str::FromStr,
};

use anyhow::{Context, Error};
use clap::Parser;
use wasmer_registry::{Bindings, PartialWapmConfig, ProgrammingLanguage};

/// Add a WAPM package's bindings to your application.
#[derive(Debug, Parser)]
pub struct Install {
/// The registry to install bindings from.
#[clap(long, env = "WAPM_REGISTRY")]
registry: Option<String>,
/// Add the JavaScript bindings using "npm install".
#[clap(long, groups = &["bindings", "js"])]
npm: bool,
/// Add the JavaScript bindings using "yarn add".
#[clap(long, groups = &["bindings", "js"])]
yarn: bool,
/// Install the package as a dev-dependency.
#[clap(long, requires = "js")]
dev: bool,
/// Add the Python bindings using "pip install".
#[clap(long, groups = &["bindings", "py"])]
pip: bool,
/// The packages to install (e.g. "wasmer/[email protected]" or "python/python")
#[clap(parse(try_from_str))]
packages: Vec<PackageSpecifier>,
}

impl Install {
/// Execute [`Install`].
pub fn execute(&self) -> Result<(), Error> {
anyhow::ensure!(!self.packages.is_empty(), "No packages specified");

let registry = self
.registry()
.context("Unable to determine which registry to use")?;

let bindings = self.lookup_bindings(&registry)?;

let mut cmd = self.target().command(&bindings);

#[cfg(feature = "debug")]
log::debug!("Running {cmd:?}");

let status = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.status()
.with_context(|| {
format!(
"Unable to start \"{:?}\". Is it installed?",
cmd.get_program()
)
})?;

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

Ok(())
}

fn lookup_bindings(&self, registry: &str) -> Result<Vec<Bindings>, Error> {
#[cfg(feature = "debug")]
log::debug!("Querying WAPM for the bindings packages");

let mut bindings_to_install = Vec::new();
let language = self.target().language();

for pkg in &self.packages {
let bindings = lookup_bindings_for_package(registry, pkg, &language)
.with_context(|| format!("Unable to find bindings for {pkg}"))?;
bindings_to_install.push(bindings);
}

Ok(bindings_to_install)
}

fn registry(&self) -> Result<String, Error> {
match &self.registry {
Some(r) => Ok(r.clone()),
None => {
let cfg = PartialWapmConfig::from_file()
.map_err(Error::msg)
.context("Unable to load WAPM's config file")?;
Ok(cfg.registry.get_current_registry())
}
}
}

fn target(&self) -> Target {
match (self.pip, self.npm, self.yarn) {
(true, false, false) => Target::Pip,
(false, true, false) => Target::Npm { dev: self.dev },
(false, false, true) => Target::Yarn { dev: self.dev },
_ => unreachable!(
"Clap should ensure at least one item in the \"bindings\" group is specified"
),
}
}
}

fn lookup_bindings_for_package(
registry: &str,
pkg: &PackageSpecifier,
language: &ProgrammingLanguage,
) -> Result<Bindings, Error> {
let all_bindings =
wasmer_registry::list_bindings(&registry, &pkg.name, pkg.version.as_deref())?;

match all_bindings.iter().find(|b| b.language == *language) {
Some(b) => {
#[cfg(feature = "debug")]
{
let Bindings { url, generator, .. } = b;
log::debug!("Found {pkg} bindings generated by {generator} at {url}");
}

Ok(b.clone())
}
None => {
if all_bindings.is_empty() {
anyhow::bail!("The package doesn't contain any bindings");
} else {
todo!();
}
}
}
}

#[derive(Debug, Copy, Clone)]
enum Target {
Pip,
Yarn { dev: bool },
Npm { dev: bool },
}

impl Target {
fn language(self) -> ProgrammingLanguage {
match self {
Target::Pip => ProgrammingLanguage::PYTHON,
Target::Yarn { .. } | Target::Npm { .. } => ProgrammingLanguage::JAVASCRIPT,
}
}

/// Construct a command which we can run to install the packages.
///
/// This deliberately runs the command using the OS shell instead of
/// invoking the tool directly. That way we can handle when a version
/// manager (e.g. `nvm` or `asdf`) replaces the tool with a script (e.g.
/// `npm.cmd` or `yarn.ps1`).
///
/// See <https://github.com/wasmerio/wapm-cli/issues/291> for more.
fn command(self, packages: &[Bindings]) -> Command {
let command_line = match self {
Target::Pip => "pip install",
Target::Yarn { dev: true } => "yarn add --dev",
Target::Yarn { dev: false } => "yarn add",
Target::Npm { dev: true } => "npm install --dev",
Target::Npm { dev: false } => "npm install",
};
let mut command_line = command_line.to_string();

for pkg in packages {
command_line.push(' ');
command_line.push_str(&pkg.url);
}

if cfg!(windows) {
let mut cmd = Command::new("cmd");
cmd.arg("/C").arg(command_line);
cmd
} else {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg(command_line);
cmd
}
}
}

/// The full name and optional version number for a WAPM package.
#[derive(Debug)]
struct PackageSpecifier {
/// The package's full name (i.e. `wasmer/wasmer-pack` in
/// `wasmer/[email protected]`).
name: String,
version: Option<String>,
}

impl FromStr for PackageSpecifier {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let (name, version) = match s.split_once('@') {
Some((name, version)) => (name, Some(version)),
None => (s, None),
};

if !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || "/_-".contains(c))
{
anyhow::bail!("Invalid package name");
}

Ok(PackageSpecifier {
name: name.to_string(),
version: version.map(|s| s.to_string()),
})
}
}

impl Display for PackageSpecifier {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let PackageSpecifier { name, version } = self;

write!(f, "{name}")?;
if let Some(version) = version {
write!(f, "@{version}")?;
}

Ok(())
}
}
45 changes: 37 additions & 8 deletions lib/registry/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@
//! curl -sSfL https://registry.wapm.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql
//! ```
use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{
collections::BTreeMap,
fmt::{Display, Formatter},
};

pub mod config;
pub mod graphql;
pub mod login;
pub mod utils;

pub use crate::config::format_graphql;
pub use config::PartialWapmConfig;
pub use crate::{
config::{format_graphql, PartialWapmConfig},
graphql::get_bindings_query::ProgrammingLanguage,
};

use crate::config::Registries;
use anyhow::Context;
Expand Down Expand Up @@ -331,7 +336,7 @@ pub enum QueryPackageError {
}

impl fmt::Display for QueryPackageError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
QueryPackageError::ErrorSendingQuery(q) => write!(f, "error sending query: {q}"),
QueryPackageError::NoPackageFound { name, version } => {
Expand Down Expand Up @@ -977,9 +982,9 @@ fn test_install_package() {
println!("ok, done");
}

/// A package which can be added as a dependency.
/// A library that exposes bindings to a WAPM package.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingsPackage {
pub struct Bindings {
/// A unique ID specifying this set of bindings.
pub id: String,
/// The URL which can be used to download the files that were generated
Expand Down Expand Up @@ -1008,6 +1013,30 @@ pub struct BindingsGenerator {
pub command: String,
}

impl Display for BindingsGenerator {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let BindingsGenerator {
name,
namespace,
version,
command,
..
} = self;

if let Some(namespace) = namespace {
write!(f, "{namespace}/")?;
}

write!(f, "{name}@{version}")?;

if command != name {
write!(f, ":{command}")?;
}

Ok(())
}
}

/// List all bindings associated with a particular package.
///
/// If a version number isn't provided, this will default to the most recently
Expand All @@ -1016,7 +1045,7 @@ pub fn list_bindings(
registry: &str,
name: &str,
version: Option<&str>,
) -> Result<Vec<BindingsPackage>, anyhow::Error> {
) -> Result<Vec<Bindings>, anyhow::Error> {
use crate::graphql::{
get_bindings_query::{ResponseData, Variables},
GetBindingsQuery,
Expand All @@ -1036,7 +1065,7 @@ pub fn list_bindings(
let mut bindings_packages = Vec::new();

for b in package_version.bindings.into_iter().flatten() {
let pkg = BindingsPackage {
let pkg = Bindings {
id: b.id,
url: b.url,
language: b.language,
Expand Down

0 comments on commit e630c50

Please sign in to comment.