Add downloading .tar.gz URLs and new caching in checkouts dir

This commit is contained in:
Felix Schütt
2022-12-09 13:09:57 +01:00
parent ef8d2f651e
commit bc22701cae
10 changed files with 517 additions and 800 deletions

15
Cargo.lock generated
View File

@@ -1086,9 +1086,9 @@ dependencies = [
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.18" version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
@@ -1130,6 +1130,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "fs_extra"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394"
[[package]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@@ -4059,6 +4065,7 @@ dependencies = [
"dirs", "dirs",
"distance", "distance",
"fern", "fern",
"hex",
"http_req", "http_req",
"isatty", "isatty",
"libc", "libc",
@@ -4299,13 +4306,16 @@ version = "3.0.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dirs", "dirs",
"filetime",
"flate2", "flate2",
"fs_extra",
"futures-util", "futures-util",
"graphql_client", "graphql_client",
"hex", "hex",
"log", "log",
"lzma-rs", "lzma-rs",
"rand 0.8.5", "rand 0.8.5",
"regex",
"reqwest", "reqwest",
"semver 1.0.14", "semver 1.0.14",
"serde", "serde",
@@ -4313,6 +4323,7 @@ dependencies = [
"tar", "tar",
"tempdir", "tempdir",
"thiserror", "thiserror",
"tldextract",
"tokio", "tokio",
"toml", "toml",
"url", "url",

View File

@@ -73,6 +73,7 @@ webc = { version = "3.0.1", optional = true }
isatty = "0.1.9" isatty = "0.1.9"
dialoguer = "0.10.2" dialoguer = "0.10.2"
tldextract = "0.6.0" tldextract = "0.6.0"
hex = "0.4.3"
[build-dependencies] [build-dependencies]
chrono = { version = "^0.4", default-features = false, features = [ "std", "clock" ] } chrono = { version = "^0.4", default-features = false, features = [ "std", "clock" ] }

View File

@@ -15,7 +15,6 @@ use crate::commands::{
}; };
use crate::error::PrettyError; use crate::error::PrettyError;
use clap::{CommandFactory, ErrorKind, Parser}; use clap::{CommandFactory, ErrorKind, Parser};
use std::{fmt, str::FromStr};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[cfg_attr( #[cfg_attr(
@@ -243,218 +242,9 @@ fn wasmer_main_inner() -> Result<(), anyhow::Error> {
} }
}; };
// Check if the file is a package name
if let WasmerCLIOptions::Run(r) = &options {
#[cfg(not(feature = "debug"))]
let debug = false;
#[cfg(feature = "debug")]
let debug = r.options.debug;
return crate::commands::try_run_package_or_file(&args, r, debug);
}
options.execute() options.execute()
} }
#[derive(Debug, Clone, PartialEq, Default)]
pub(crate) struct SplitVersion {
pub(crate) original: String,
pub(crate) registry: Option<String>,
pub(crate) package: String,
pub(crate) version: Option<String>,
pub(crate) command: Option<String>,
}
impl fmt::Display for SplitVersion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let version = self.version.as_deref().unwrap_or("latest");
let command = self
.command
.as_ref()
.map(|s| format!(":{s}"))
.unwrap_or_default();
write!(f, "{}@{version}{command}", self.package)
}
}
#[test]
fn test_split_version() {
assert_eq!(
SplitVersion::parse("registry.wapm.io/graphql/python/python").unwrap(),
SplitVersion {
original: "registry.wapm.io/graphql/python/python".to_string(),
registry: Some("https://registry.wapm.io/graphql".to_string()),
package: "python/python".to_string(),
version: None,
command: None,
}
);
assert_eq!(
SplitVersion::parse("registry.wapm.io/python/python").unwrap(),
SplitVersion {
original: "registry.wapm.io/python/python".to_string(),
registry: Some("https://registry.wapm.io/graphql".to_string()),
package: "python/python".to_string(),
version: None,
command: None,
}
);
assert_eq!(
SplitVersion::parse("namespace/name@version:command").unwrap(),
SplitVersion {
original: "namespace/name@version:command".to_string(),
registry: None,
package: "namespace/name".to_string(),
version: Some("version".to_string()),
command: Some("command".to_string()),
}
);
assert_eq!(
SplitVersion::parse("namespace/name@version").unwrap(),
SplitVersion {
original: "namespace/name@version".to_string(),
registry: None,
package: "namespace/name".to_string(),
version: Some("version".to_string()),
command: None,
}
);
assert_eq!(
SplitVersion::parse("namespace/name").unwrap(),
SplitVersion {
original: "namespace/name".to_string(),
registry: None,
package: "namespace/name".to_string(),
version: None,
command: None,
}
);
assert_eq!(
SplitVersion::parse("registry.wapm.io/namespace/name").unwrap(),
SplitVersion {
original: "registry.wapm.io/namespace/name".to_string(),
registry: Some("https://registry.wapm.io/graphql".to_string()),
package: "namespace/name".to_string(),
version: None,
command: None,
}
);
assert_eq!(
format!("{}", SplitVersion::parse("namespace").unwrap_err()),
"Invalid package version: \"namespace\"".to_string(),
);
}
impl SplitVersion {
pub fn parse(s: &str) -> Result<SplitVersion, anyhow::Error> {
s.parse()
}
}
impl FromStr for SplitVersion {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let command = WasmerCLIOptions::command();
let mut prohibited_package_names = command.get_subcommands().map(|s| s.get_name());
let re1 = regex::Regex::new(r#"(.*)/(.*)@(.*):(.*)"#).unwrap();
let re2 = regex::Regex::new(r#"(.*)/(.*)@(.*)"#).unwrap();
let re3 = regex::Regex::new(r#"(.*)/(.*)"#).unwrap();
let re4 = regex::Regex::new(r#"(.*)/(.*):(.*)"#).unwrap();
let mut no_version = false;
let captures = if re1.is_match(s) {
re1.captures(s)
.map(|c| {
c.iter()
.flatten()
.map(|m| m.as_str().to_owned())
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else if re2.is_match(s) {
re2.captures(s)
.map(|c| {
c.iter()
.flatten()
.map(|m| m.as_str().to_owned())
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else if re4.is_match(s) {
no_version = true;
re4.captures(s)
.map(|c| {
c.iter()
.flatten()
.map(|m| m.as_str().to_owned())
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else if re3.is_match(s) {
re3.captures(s)
.map(|c| {
c.iter()
.flatten()
.map(|m| m.as_str().to_owned())
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else {
return Err(anyhow::anyhow!("Invalid package version: {s:?}"));
};
let mut namespace = match captures.get(1).cloned() {
Some(s) => s,
None => {
return Err(anyhow::anyhow!(
"Invalid package version: {s:?}: no namespace"
))
}
};
let name = match captures.get(2).cloned() {
Some(s) => s,
None => return Err(anyhow::anyhow!("Invalid package version: {s:?}: no name")),
};
let mut registry = None;
if namespace.contains('/') {
let (r, n) = namespace.rsplit_once('/').unwrap();
let mut real_registry = r.to_string();
if !real_registry.ends_with("graphql") {
real_registry = format!("{real_registry}/graphql");
}
if !real_registry.contains("://") {
real_registry = format!("https://{real_registry}");
}
registry = Some(real_registry);
namespace = n.to_string();
}
let sv = SplitVersion {
original: s.to_string(),
registry,
package: format!("{namespace}/{name}"),
version: if no_version {
None
} else {
captures.get(3).cloned()
},
command: captures.get(if no_version { 3 } else { 4 }).cloned(),
};
let svp = sv.package.clone();
anyhow::ensure!(
!prohibited_package_names.any(|s| s == sv.package.trim()),
"Invalid package name {svp:?}"
);
Ok(sv)
}
}
fn print_help(verbose: bool) -> Result<(), anyhow::Error> { fn print_help(verbose: bool) -> Result<(), anyhow::Error> {
let mut cmd = WasmerCLIOptions::command(); let mut cmd = WasmerCLIOptions::command();
if verbose { if verbose {

View File

@@ -4,8 +4,6 @@ use anyhow::{Context, Error};
use clap::Parser; use clap::Parser;
use wasmer_registry::{Bindings, PartialWapmConfig, ProgrammingLanguage}; use wasmer_registry::{Bindings, PartialWapmConfig, ProgrammingLanguage};
use crate::cli::SplitVersion;
/// Add a WAPM package's bindings to your application. /// Add a WAPM package's bindings to your application.
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
pub struct Add { pub struct Add {
@@ -26,7 +24,7 @@ pub struct Add {
pip: bool, pip: bool,
/// The packages to add (e.g. "wasmer/wasmer-pack@0.5.0" or "python/python") /// The packages to add (e.g. "wasmer/wasmer-pack@0.5.0" or "python/python")
#[clap(parse(try_from_str))] #[clap(parse(try_from_str))]
packages: Vec<SplitVersion>, packages: Vec<wasmer_registry::Package>,
} }
impl Add { impl Add {
@@ -103,11 +101,11 @@ impl Add {
fn lookup_bindings_for_package( fn lookup_bindings_for_package(
registry: &str, registry: &str,
pkg: &SplitVersion, pkg: &wasmer_registry::Package,
language: &ProgrammingLanguage, language: &ProgrammingLanguage,
) -> Result<Bindings, Error> { ) -> Result<Bindings, Error> {
let all_bindings = let all_bindings =
wasmer_registry::list_bindings(registry, &pkg.package, pkg.version.as_deref())?; wasmer_registry::list_bindings(registry, &pkg.package(), pkg.version.as_deref())?;
match all_bindings.iter().find(|b| b.language == *language) { match all_bindings.iter().find(|b| b.language == *language) {
Some(b) => { Some(b) => {

View File

@@ -1,7 +1,7 @@
use crate::cli::SplitVersion;
use crate::common::get_cache_dir; use crate::common::get_cache_dir;
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
use crate::logging; use crate::logging;
use crate::package_source::PackageSource;
use crate::store::{CompilerType, StoreOptions}; use crate::store::{CompilerType, StoreOptions};
use crate::suggestions::suggest_function_exports; use crate::suggestions::suggest_function_exports;
use crate::warning; use crate::warning;
@@ -11,12 +11,10 @@ use std::collections::HashMap;
use std::ops::Deref; use std::ops::Deref;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use url::Url;
use wasmer::FunctionEnv; use wasmer::FunctionEnv;
use wasmer::*; use wasmer::*;
#[cfg(feature = "cache")] #[cfg(feature = "cache")]
use wasmer_cache::{Cache, FileSystemCache, Hash}; use wasmer_cache::{Cache, FileSystemCache, Hash};
use wasmer_registry::PackageDownloadInfo;
use wasmer_types::Type as ValueType; use wasmer_types::Type as ValueType;
#[cfg(feature = "webc_runner")] #[cfg(feature = "webc_runner")]
use wasmer_wasi::runners::{Runner, WapmContainer}; use wasmer_wasi::runners::{Runner, WapmContainer};
@@ -27,6 +25,17 @@ mod wasi;
#[cfg(feature = "wasi")] #[cfg(feature = "wasi")]
use wasi::Wasi; use wasi::Wasi;
/// The options for the `wasmer run` subcommand, runs either a package, URL or a file
#[derive(Debug, Parser, Clone, Default)]
pub struct Run {
/// File to run
#[clap(name = "SOURCE", parse(try_from_str))]
pub(crate) path: PackageSource,
/// Options to run the file / package / URL with
#[clap(flatten)]
pub(crate) options: RunWithoutFile,
}
/// Same as `wasmer run`, but without the required `path` argument (injected previously) /// Same as `wasmer run`, but without the required `path` argument (injected previously)
#[derive(Debug, Parser, Clone, Default)] #[derive(Debug, Parser, Clone, Default)]
pub struct RunWithoutFile { pub struct RunWithoutFile {
@@ -83,103 +92,65 @@ pub struct RunWithoutFile {
pub(crate) args: Vec<String>, pub(crate) args: Vec<String>,
} }
#[allow(dead_code)] /// Same as `Run`, but uses a resolved local file path.
fn is_dir(e: &walkdir::DirEntry) -> bool { #[derive(Debug, Clone, Default)]
let meta = match e.metadata() { pub struct RunWithPathBuf {
Ok(o) => o,
Err(_) => return false,
};
meta.is_dir()
}
impl RunWithoutFile {
/// Given a local path, returns the `Run` command (overriding the `--path` argument).
pub fn into_run_args(
mut self,
package_root_dir: PathBuf, // <- package dir
command: Option<&str>,
_debug_output_allowed: bool,
) -> Result<Run, anyhow::Error> {
let (manifest, pathbuf) =
wasmer_registry::get_executable_file_from_path(&package_root_dir, command)?;
#[cfg(feature = "wasi")]
{
let default = HashMap::default();
let fs = manifest.fs.as_ref().unwrap_or(&default);
for (alias, real_dir) in fs.iter() {
let real_dir = package_root_dir.join(&real_dir);
if !real_dir.exists() {
if _debug_output_allowed {
println!(
"warning: cannot map {alias:?} to {}: directory does not exist",
real_dir.display()
);
}
continue;
}
self.wasi.map_dir(alias, real_dir.clone());
}
}
Ok(Run {
path: pathbuf,
options: RunWithoutFile {
force_install: self.force_install,
#[cfg(feature = "cache")]
disable_cache: self.disable_cache,
invoke: self.invoke,
// If the RunWithoutFile was constructed via a package name,
// the correct syntax is "package:command-name" (--command-name would be
// interpreted as a CLI argument for the .wasm file)
command_name: None,
#[cfg(feature = "cache")]
cache_key: self.cache_key,
store: self.store,
#[cfg(feature = "wasi")]
wasi: self.wasi,
#[cfg(feature = "io-devices")]
enable_experimental_io_devices: self.enable_experimental_io_devices,
#[cfg(feature = "debug")]
debug: self.debug,
#[cfg(feature = "debug")]
verbose: self.verbose,
args: self.args,
},
})
}
}
#[derive(Debug, Parser, Clone, Default)]
/// The options for the `wasmer run` subcommand
pub struct Run {
/// File to run /// File to run
#[clap(name = "FILE", parse(from_os_str))]
pub(crate) path: PathBuf, pub(crate) path: PathBuf,
/// Options for running the file
#[clap(flatten)]
pub(crate) options: RunWithoutFile, pub(crate) options: RunWithoutFile,
} }
impl Deref for Run { impl Deref for RunWithPathBuf {
type Target = RunWithoutFile; type Target = RunWithoutFile;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.options &self.options
} }
} }
impl Run { impl RunWithPathBuf {
/// Execute the run command /// Execute the run command
pub fn execute(&self) -> Result<()> { pub fn execute(&self) -> Result<()> {
let mut self_clone = self.clone();
if self_clone.path.is_dir() {
let (manifest, pathbuf) = wasmer_registry::get_executable_file_from_path(
&self_clone.path,
self_clone.command_name.as_deref(),
)?;
#[cfg(feature = "wasi")]
{
let default = HashMap::default();
let fs = manifest.fs.as_ref().unwrap_or(&default);
for (alias, real_dir) in fs.iter() {
let real_dir = self_clone.path.join(&real_dir);
if !real_dir.exists() {
#[cfg(feature = "debug")]
if self_clone.debug {
println!(
"warning: cannot map {alias:?} to {}: directory does not exist",
real_dir.display()
);
}
continue;
}
self_clone.options.wasi.map_dir(alias, real_dir.clone());
}
}
self_clone.path = pathbuf;
}
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
if self.debug { if self.debug {
logging::set_up_logging(self.verbose.unwrap_or(0)).unwrap(); logging::set_up_logging(self_clone.verbose.unwrap_or(0)).unwrap();
} }
self.inner_execute().with_context(|| { self_clone.inner_execute().with_context(|| {
format!( format!(
"failed to run `{}`{}", "failed to run `{}`{}",
self.path.display(), self_clone.path.display(),
if CompilerType::enabled().is_empty() { if CompilerType::enabled().is_empty() {
" (no compilers enabled)" " (no compilers enabled)"
} else { } else {
@@ -586,6 +557,19 @@ impl Run {
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
Ok(func.call(ctx, &invoke_args)?) Ok(func.call(ctx, &invoke_args)?)
} }
}
impl Run {
/// Executes the `wasmer run` command
pub fn execute(&self) -> Result<(), anyhow::Error> {
// downloads and installs the package if necessary
let path_to_run = self.path.download_and_get_filepath()?;
RunWithPathBuf {
path: path_to_run,
options: self.options.clone(),
}
.execute()
}
/// Create Run instance for arguments/env, /// Create Run instance for arguments/env,
/// assuming we're being run from a CFP binfmt interpreter. /// assuming we're being run from a CFP binfmt interpreter.
@@ -599,37 +583,17 @@ impl Run {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn from_binfmt_args_fallible() -> Result<Run> { fn from_binfmt_args_fallible() -> Result<Run> {
let argv = std::env::args_os().collect::<Vec<_>>(); let argv = std::env::args().collect::<Vec<_>>();
let (_interpreter, executable, original_executable, args) = match &argv[..] { let (_interpreter, executable, original_executable, args) = match &argv[..] {
[a, b, c, d @ ..] => (a, b, c, d), [a, b, c, d @ ..] => (a, b, c, d),
_ => { _ => {
bail!("Wasmer binfmt interpreter needs at least three arguments (including $0) - must be registered as binfmt interpreter with the CFP flags. (Got arguments: {:?})", argv); bail!("Wasmer binfmt interpreter needs at least three arguments (including $0) - must be registered as binfmt interpreter with the CFP flags. (Got arguments: {:?})", argv);
} }
}; };
// TODO: Optimally, args and env would be passed as an UTF-8 Vec.
// (Can be pulled out of std::os::unix::ffi::OsStrExt)
// But I don't want to duplicate or rewrite run.rs today.
let args = args
.iter()
.enumerate()
.map(|(i, s)| {
s.clone().into_string().map_err(|s| {
anyhow!(
"Cannot convert argument {} ({:?}) to UTF-8 string",
i + 1,
s
)
})
})
.collect::<Result<Vec<_>>>()?;
let original_executable = original_executable
.clone()
.into_string()
.map_err(|s| anyhow!("Cannot convert executable name {:?} to UTF-8 string", s))?;
let store = StoreOptions::default(); let store = StoreOptions::default();
// TODO: store.compiler.features.all = true; ? // TODO: store.compiler.features.all = true; ?
Ok(Self { Ok(Self {
path: executable.into(), path: PackageSource::parse(executable),
options: RunWithoutFile { options: RunWithoutFile {
args, args,
command_name: Some(original_executable), command_name: Some(original_executable),
@@ -639,324 +603,9 @@ impl Run {
}, },
}) })
} }
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
fn from_binfmt_args_fallible() -> Result<Run> { fn from_binfmt_args_fallible() -> Result<Run> {
bail!("binfmt_misc is only available on linux.") bail!("binfmt_misc is only available on linux.")
} }
} }
fn start_spinner(msg: String) -> Option<spinoff::Spinner> {
if !isatty::stdout_isatty() {
return None;
}
#[cfg(target_os = "windows")]
{
use colored::control;
let _ = control::set_virtual_terminal(true);
}
Some(spinoff::Spinner::new(
spinoff::Spinners::Dots,
msg,
spinoff::Color::White,
))
}
/// Before looking up a command from the registry, try to see if we have
/// the command already installed
fn try_run_local_command(
args: &[String],
sv: &SplitVersion,
debug_msgs_allowed: bool,
) -> Result<(), ExecuteLocalPackageError> {
let result = wasmer_registry::try_finding_local_command(&sv.original).ok_or_else(|| {
ExecuteLocalPackageError::BeforeExec(anyhow::anyhow!(
"could not find command {} locally",
sv.original
))
})?;
let package_dir = result
.get_path()
.map_err(|e| ExecuteLocalPackageError::BeforeExec(anyhow::anyhow!("{e}")))?;
// Try auto-installing the remote package
let args_without_package = fixup_args(args, &sv.original);
let mut run_args = RunWithoutFile::try_parse_from(args_without_package.iter())
.map_err(|e| ExecuteLocalPackageError::DuringExec(e.into()))?;
run_args.command_name = sv.command.clone();
run_args
.into_run_args(package_dir, sv.command.as_deref(), debug_msgs_allowed)
.map_err(ExecuteLocalPackageError::DuringExec)?
.execute()
.map_err(ExecuteLocalPackageError::DuringExec)
}
pub(crate) fn try_autoinstall_package(
args: &[String],
sv: &SplitVersion,
package: Option<PackageDownloadInfo>,
force_install: bool,
) -> Result<(), anyhow::Error> {
use std::io::Write;
let mut sp = start_spinner(format!("Installing package {} ...", sv.package));
let debug_msgs_allowed = sp.is_some();
let v = sv.version.as_deref();
let result = wasmer_registry::install_package(
sv.registry.as_deref(),
&sv.package,
v,
package,
force_install,
);
if let Some(sp) = sp.take() {
sp.clear();
}
let _ = std::io::stdout().flush();
let (_, package_dir) = match result {
Ok(o) => o,
Err(e) => {
return Err(anyhow::anyhow!("{e}"));
}
};
// Try auto-installing the remote package
let args_without_package = fixup_args(args, &sv.original);
let mut run_args = RunWithoutFile::try_parse_from(args_without_package.iter())?;
run_args.command_name = sv.command.clone();
run_args
.into_run_args(package_dir, sv.command.as_deref(), debug_msgs_allowed)?
.execute()
}
// We need to distinguish between errors that happen
// before vs. during execution
enum ExecuteLocalPackageError {
BeforeExec(anyhow::Error),
DuringExec(anyhow::Error),
}
fn try_execute_local_package(
args: &[String],
sv: &SplitVersion,
debug_msgs_allowed: bool,
) -> Result<(), ExecuteLocalPackageError> {
let package = wasmer_registry::get_local_package(None, &sv.package, sv.version.as_deref())
.ok_or_else(|| {
ExecuteLocalPackageError::BeforeExec(anyhow::anyhow!("no local package {sv:?} found"))
})?;
let package_dir = package
.get_path()
.map_err(|e| ExecuteLocalPackageError::BeforeExec(anyhow::anyhow!("{e}")))?;
// Try finding the local package
let args_without_package = fixup_args(args, &sv.original);
RunWithoutFile::try_parse_from(args_without_package.iter())
.map_err(|e| ExecuteLocalPackageError::DuringExec(e.into()))?
.into_run_args(package_dir, sv.command.as_deref(), debug_msgs_allowed)
.map_err(ExecuteLocalPackageError::DuringExec)?
.execute()
.map_err(|e| ExecuteLocalPackageError::DuringExec(e.context(anyhow::anyhow!("{}", sv))))
}
fn try_lookup_command(sv: &mut SplitVersion) -> Result<PackageDownloadInfo, anyhow::Error> {
use std::io::Write;
let mut sp = start_spinner(format!("Looking up command {} ...", sv.package));
for registry in wasmer_registry::get_all_available_registries().unwrap_or_default() {
let result = wasmer_registry::query_command_from_registry(&registry, &sv.package);
if let Some(s) = sp.take() {
s.clear();
}
let _ = std::io::stdout().flush();
let command = sv.package.clone();
if let Ok(o) = result {
sv.package = o.package.clone();
sv.version = Some(o.version.clone());
sv.command = Some(command);
return Ok(o);
}
}
if let Some(sp) = sp.take() {
sp.clear();
}
let _ = std::io::stdout().flush();
Err(anyhow::anyhow!("command {sv} not found"))
}
/// Removes the difference between "wasmer run {file} arg1 arg2" and "wasmer {file} arg1 arg2"
fn fixup_args(args: &[String], command: &str) -> Vec<String> {
let mut args_without_package = args.to_vec();
if args_without_package.get(1).map(|s| s.as_str()) == Some(command) {
let _ = args_without_package.remove(1);
} else if args_without_package.get(2).map(|s| s.as_str()) == Some(command) {
let _ = args_without_package.remove(1);
let _ = args_without_package.remove(1);
}
args_without_package
}
#[test]
fn test_fixup_args() {
let first_args = vec![
format!("wasmer"),
format!("run"),
format!("python/python"),
format!("--arg1"),
format!("--arg2"),
];
let second_args = vec![
format!("wasmer"), // no "run"
format!("python/python"),
format!("--arg1"),
format!("--arg2"),
];
let arg1_transformed = fixup_args(&first_args, "python/python");
let arg2_transformed = fixup_args(&second_args, "python/python");
assert_eq!(arg1_transformed, arg2_transformed);
}
pub(crate) fn try_run_package_or_file(
args: &[String],
r: &Run,
debug: bool,
) -> Result<(), anyhow::Error> {
let debug_msgs_allowed = isatty::stdout_isatty();
// Check "r.path" is a file or a package / command name
if r.path.exists() {
if r.path.is_dir() && r.path.join("wapm.toml").exists() {
let args_without_package = fixup_args(args, &format!("{}", r.path.display()));
return RunWithoutFile::try_parse_from(args_without_package.iter())?
.into_run_args(
r.path.clone(),
r.command_name.as_deref(),
debug_msgs_allowed,
)?
.execute();
}
return r.execute();
}
// c:// might be parsed as a URL on Windows
let url_string = format!("{}", r.path.display());
if let Ok(url) = url::Url::parse(&url_string) {
if url.scheme() == "http" || url.scheme() == "https" {
match try_run_url(&url, args, r, debug) {
Err(ExecuteLocalPackageError::BeforeExec(_)) => {}
Err(ExecuteLocalPackageError::DuringExec(e)) => return Err(e),
Ok(o) => return Ok(o),
}
}
}
let package = format!("{}", r.path.display());
let mut is_fake_sv = false;
let mut sv = match SplitVersion::parse(&package) {
Ok(o) => o,
Err(_) => {
let mut fake_sv = SplitVersion {
original: package.to_string(),
registry: None,
package: package.to_string(),
version: None,
command: None,
};
is_fake_sv = true;
match try_run_local_command(args, &fake_sv, debug) {
Ok(()) => return Ok(()),
Err(ExecuteLocalPackageError::DuringExec(e)) => return Err(e),
_ => {}
}
match try_lookup_command(&mut fake_sv) {
Ok(o) => SplitVersion {
original: package.to_string(),
registry: None,
package: o.package,
version: Some(o.version),
command: r.command_name.clone(),
},
Err(e) => {
return Err(
anyhow::anyhow!("No package for command {package:?} found, file {package:?} not found either")
.context(e)
.context(anyhow::anyhow!("{}", r.path.display()))
);
}
}
}
};
if sv.command.is_none() {
sv.command = r.command_name.clone();
}
if sv.command.is_none() && is_fake_sv {
sv.command = Some(package);
}
let mut package_download_info = None;
if !sv.package.contains('/') {
if let Ok(o) = try_lookup_command(&mut sv) {
package_download_info = Some(o);
}
}
match try_execute_local_package(args, &sv, debug_msgs_allowed) {
Ok(o) => return Ok(o),
Err(ExecuteLocalPackageError::DuringExec(e)) => return Err(e),
_ => {}
}
if debug && isatty::stdout_isatty() {
eprintln!("finding local package {} failed", sv);
}
// else: local package not found - try to download and install package
try_autoinstall_package(args, &sv, package_download_info, r.force_install)
}
fn try_run_url(
url: &Url,
_args: &[String],
r: &Run,
_debug: bool,
) -> Result<(), ExecuteLocalPackageError> {
let checksum = wasmer_registry::get_remote_webc_checksum(url).map_err(|e| {
ExecuteLocalPackageError::BeforeExec(anyhow::anyhow!("error fetching {url}: {e}"))
})?;
let packages = wasmer_registry::get_all_installed_webc_packages();
if !packages.iter().any(|p| p.checksum == checksum) {
let sp = start_spinner(format!("Installing {}", url));
let result = wasmer_registry::install_webc_package(url, &checksum);
result.map_err(|e| {
ExecuteLocalPackageError::BeforeExec(anyhow::anyhow!("error fetching {url}: {e}"))
})?;
if let Some(sp) = sp {
sp.clear();
}
}
let webc_dir = wasmer_registry::get_webc_dir();
let webc_install_path = webc_dir
.context("Error installing package: no webc dir")
.map_err(ExecuteLocalPackageError::BeforeExec)?
.join(checksum);
let mut r = r.clone();
r.path = webc_install_path;
r.execute().map_err(ExecuteLocalPackageError::DuringExec)
}

View File

@@ -24,6 +24,7 @@ pub mod c_gen;
pub mod cli; pub mod cli;
#[cfg(feature = "debug")] #[cfg(feature = "debug")]
pub mod logging; pub mod logging;
pub mod package_source;
pub mod store; pub mod store;
pub mod suggestions; pub mod suggestions;
pub mod utils; pub mod utils;

View File

@@ -0,0 +1,114 @@
//! Module for parsing and installing packages
use anyhow::Context;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use url::Url;
/// Source of a package
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PackageSource {
/// Download from a URL
Url(Url),
/// Run a local file
File(String),
/// Download from a package
Package(wasmer_registry::Package),
}
impl Default for PackageSource {
fn default() -> Self {
PackageSource::File(String::new())
}
}
impl FromStr for PackageSource {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl PackageSource {
/// Parses a package source and transforms it to a URL or a File
pub fn parse(s: &str) -> Result<Self, String> {
// If the file is a http:// URL, run the URL
if let Ok(url) = url::Url::parse(s) {
if url.scheme() == "http" || url.scheme() == "https" {
return Ok(Self::Url(url));
}
}
Ok(match wasmer_registry::Package::from_str(s) {
Ok(o) => Self::Package(o),
Err(_) => Self::File(s.to_string()),
})
}
/// Downloads the package (if any) to the installation directory, returns the path
/// of the package directory (containing the wapm.toml)
pub fn download_and_get_filepath(&self) -> Result<PathBuf, anyhow::Error> {
let url = match self {
Self::File(f) => {
let path = Path::new(&f).to_path_buf();
return if path.exists() {
Ok(path)
} else {
Err(anyhow::anyhow!(
"invalid package name, could not find file {f}"
))
};
}
Self::Url(u) => {
if let Some(path) = wasmer_registry::Package::is_url_already_installed(u) {
return Ok(path);
} else {
u.clone()
}
}
Self::Package(p) => {
if let Some(path) = p.already_installed() {
return Ok(path);
} else {
p.url()?
}
}
};
let extra = if let Self::Package(p) = self {
format!(", file {} does not exist either", p.file())
} else {
String::new()
};
let mut sp = start_spinner(format!("Installing package {url} ..."));
let opt_path = wasmer_registry::install_package(&url);
if let Some(sp) = sp.take() {
use std::io::Write;
sp.clear();
let _ = std::io::stdout().flush();
}
let (_, path) = opt_path
.with_context(|| anyhow::anyhow!("could not install package from URL {url}{extra}"))?;
Ok(path)
}
}
fn start_spinner(msg: String) -> Option<spinoff::Spinner> {
if !isatty::stdout_isatty() {
return None;
}
#[cfg(target_os = "windows")]
{
use colored::control;
let _ = control::set_virtual_terminal(true);
}
Some(spinoff::Spinner::new(
spinoff::Spinners::Dots,
msg,
spinoff::Color::White,
))
}

View File

@@ -30,3 +30,7 @@ hex = "0.4.3"
tokio = "1.21.2" tokio = "1.21.2"
tempdir = "0.3.7" tempdir = "0.3.7"
log = "0.4.17" log = "0.4.17"
regex = "1.7.0"
fs_extra = "1.2.0"
filetime = "0.2.19"
tldextract = "0.6.0"

View File

@@ -16,20 +16,18 @@ use std::fmt;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
use std::{
collections::BTreeMap,
fmt::{Display, Formatter},
};
use url::Url; use url::Url;
pub mod config; pub mod config;
pub mod graphql; pub mod graphql;
pub mod login; pub mod login;
pub mod package;
pub mod queries; pub mod queries;
pub mod utils; pub mod utils;
pub use crate::{ pub use crate::{
config::{format_graphql, PartialWapmConfig}, config::{format_graphql, PartialWapmConfig},
package::Package,
queries::get_bindings_query::ProgrammingLanguage, queries::get_bindings_query::ProgrammingLanguage,
}; };
@@ -157,9 +155,9 @@ pub fn get_executable_file_from_path(
} else if commands.len() == 1 { } else if commands.len() == 1 {
Ok(&commands[0]) Ok(&commands[0])
} else { } else {
Err(anyhow::anyhow!(" -> wasmer run {name}@{version} --command-name={0} OR wasmer run {name}@{version}:{0}", commands.first().map(|f| f.get_name()).unwrap())) Err(anyhow::anyhow!(" -> wasmer run {name}@{version} --command-name={0}", commands.first().map(|f| f.get_name()).unwrap()))
.context(anyhow::anyhow!("{}", commands.iter().map(|c| format!("`{}`", c.get_name())).collect::<Vec<_>>().join(", "))) .context(anyhow::anyhow!("{}", commands.iter().map(|c| format!("`{}`", c.get_name())).collect::<Vec<_>>().join(", ")))
.context(anyhow::anyhow!("You can run any of those by using the --command-name=COMMAND flag or : postfix")) .context(anyhow::anyhow!("You can run any of those by using the --command-name=COMMAND flag"))
.context(anyhow::anyhow!("The `{name}@{version}` package doesn't have a default entrypoint, but has multiple available commands:")) .context(anyhow::anyhow!("The `{name}@{version}` package doesn't have a default entrypoint, but has multiple available commands:"))
}? }?
} }
@@ -320,7 +318,7 @@ pub fn query_command_from_registry(
}); });
let response: get_package_by_command_query::ResponseData = execute_query(registry_url, "", &q) let response: get_package_by_command_query::ResponseData = execute_query(registry_url, "", &q)
.map_err(|e| format!("Error sending GetPackageByCommandQuery:  {e}"))?; .map_err(|e| format!("Error sending GetPackageByCommandQuery: {e}"))?;
let command = response let command = response
.get_command .get_command
@@ -353,7 +351,7 @@ pub enum QueryPackageError {
} }
impl fmt::Display for QueryPackageError { impl fmt::Display for QueryPackageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
QueryPackageError::ErrorSendingQuery(q) => write!(f, "error sending query: {q}"), QueryPackageError::ErrorSendingQuery(q) => write!(f, "error sending query: {q}"),
QueryPackageError::NoPackageFound { name, version } => { QueryPackageError::NoPackageFound { name, version } => {
@@ -450,6 +448,7 @@ fn test_get_if_package_has_new_version() {
pub fn get_if_package_has_new_version( pub fn get_if_package_has_new_version(
#[cfg(test)] test_name: &str, #[cfg(test)] test_name: &str,
registry_url: &str, registry_url: &str,
namespace: &str,
name: &str, name: &str,
version: Option<String>, version: Option<String>,
max_timeout: Duration, max_timeout: Duration,
@@ -462,10 +461,6 @@ pub fn get_if_package_has_new_version(
Err(_) => return Err(format!("invalid host: {registry_url}")), Err(_) => return Err(format!("invalid host: {registry_url}")),
}; };
let (namespace, name) = name
.split_once('/')
.ok_or_else(|| format!("missing namespace / name for {name:?}"))?;
#[cfg(not(test))] #[cfg(not(test))]
let global_install_dir = get_global_install_dir(&host); let global_install_dir = get_global_install_dir(&host);
#[cfg(test)] #[cfg(test)]
@@ -524,7 +519,13 @@ pub fn get_if_package_has_new_version(
.filter_map(|entry| { .filter_map(|entry| {
let entry = entry.ok()?; let entry = entry.ok()?;
let version = semver::Version::parse(entry.file_name().to_str()?).ok()?; let version = semver::Version::parse(entry.file_name().to_str()?).ok()?;
let modified = entry.metadata().ok()?.modified().ok()?; let modified = entry
.path()
.join("wapm.toml")
.metadata()
.ok()?
.modified()
.ok()?;
let older_than_timeout = modified.elapsed().ok()? > max_timeout; let older_than_timeout = modified.elapsed().ok()? > max_timeout;
Some((version, older_than_timeout)) Some((version, older_than_timeout))
}) })
@@ -603,9 +604,7 @@ pub fn query_package_from_registry(
})?; })?;
let v = response.package_version.as_ref().ok_or_else(|| { let v = response.package_version.as_ref().ok_or_else(|| {
QueryPackageError::ErrorSendingQuery(format!( QueryPackageError::ErrorSendingQuery(format!("no package version for {name:?}"))
"Invalid response for crate {name:?}: no package version: {response:#?}"
))
})?; })?;
let manifest = toml::from_str::<wapm_toml::Manifest>(&v.manifest).map_err(|e| { let manifest = toml::from_str::<wapm_toml::Manifest>(&v.manifest).map_err(|e| {
@@ -777,139 +776,80 @@ where
Ok(()) Ok(())
} }
/// Given a triple of [registry, name, version], downloads and installs the /// Installs the .tar.gz if it doesn't yet exist, returns the
/// .tar.gz if it doesn't yet exist, returns the (package dir, entrypoint .wasm file path) /// (package dir, entrypoint .wasm file path)
pub fn install_package( pub fn install_package(
#[cfg(test)] test_name: &str, #[cfg(test)] test_name: &str,
registry: Option<&str>, url: &Url,
name: &str, ) -> Result<(LocalPackage, PathBuf), anyhow::Error> {
version: Option<&str>, use fs_extra::dir::copy;
package_download_info: Option<PackageDownloadInfo>,
force_install: bool,
) -> Result<(LocalPackage, PathBuf), String> {
let package_info = match package_download_info {
Some(s) => s,
None => {
let registries = match registry {
Some(s) => vec![s.to_string()],
None => {
#[cfg(test)]
{
get_all_available_registries(test_name)?
}
#[cfg(not(test))]
{
get_all_available_registries()?
}
}
};
let mut url_of_package = None;
let version_str = match version { let host = url
None => name.to_string(),
Some(v) => format!("{name}@{v}"),
};
let registries_searched = registries
.iter()
.filter_map(|s| url::Url::parse(s).ok())
.filter_map(|s| Some(s.host_str()?.to_string()))
.collect::<Vec<_>>();
let mut errors = BTreeMap::new();
for r in registries.iter() {
if !force_install {
#[cfg(not(test))]
let package_has_new_version = get_if_package_has_new_version(
r,
name,
version.map(|s| s.to_string()),
Duration::from_secs(60 * 5),
)?;
#[cfg(test)]
let package_has_new_version = get_if_package_has_new_version(
test_name,
r,
name,
version.map(|s| s.to_string()),
Duration::from_secs(60 * 5),
)?;
if let GetIfPackageHasNewVersionResult::UseLocalAlreadyInstalled {
registry_host,
namespace,
name,
version,
path,
} = package_has_new_version
{
return Ok((
LocalPackage {
registry: registry_host,
name: format!("{namespace}/{name}"),
version,
},
path,
));
}
}
match query_package_from_registry(r, name, version) {
Ok(o) => {
url_of_package = Some((r, o));
break;
}
Err(e) => {
errors.insert(r.clone(), e);
}
}
}
let errors = errors
.into_iter()
.map(|(registry, e)| format!(" {registry}: {e}"))
.collect::<Vec<_>>()
.join("\r\n");
let (_, package_info) = url_of_package.ok_or_else(|| {
format!("Package {version_str} not found in registries {registries_searched:?}.\r\n\r\nErrors:\r\n\r\n{errors}")
})?;
package_info
}
};
let host = url::Url::parse(&package_info.registry)
.map_err(|e| format!("invalid url: {}: {e}", package_info.registry))?
.host_str() .host_str()
.ok_or_else(|| format!("invalid url: {}", package_info.registry))? .ok_or_else(|| anyhow::anyhow!("invalid url: {}", url))?
.to_string(); .to_string();
#[cfg(test)] let tempdir = tempdir::TempDir::new(&format!("download-{host}"))
let dir = get_package_local_dir( .map_err(|e| anyhow::anyhow!("could not create download temp dir: {e}"))?;
test_name,
&host,
&package_info.package,
&package_info.version,
)?;
#[cfg(not(test))]
let dir = get_package_local_dir(&host, &package_info.package, &package_info.version)?;
let version = package_info.version; let target_targz_path = tempdir.path().join("package.tar.gz");
let name = package_info.package; let unpacked_targz_path = tempdir.path().join("package");
std::fs::create_dir_all(&unpacked_targz_path).map_err(|e| {
anyhow::anyhow!(
"could not create dir {}: {e}",
unpacked_targz_path.display()
)
})?;
if !dir.join("wapm.toml").exists() || force_install { get_targz_bytes(url, None, Some(target_targz_path.clone()))
download_and_unpack_targz(&package_info.url, &dir, false).map_err(|e| format!("{e}"))?; .map_err(|e| anyhow::anyhow!("failed to download {url}: {e}"))?;
}
Ok(( try_unpack_targz(
LocalPackage { target_targz_path.as_path(),
registry: package_info.registry, unpacked_targz_path.as_path(),
name, false,
version, )
}, .with_context(|| anyhow::anyhow!("Could not unpack file downloaded from {url}"))?;
dir,
)) // read {unpacked}/wapm.toml to get the name + version number
let toml_path = unpacked_targz_path.join("wapm.toml");
let toml = std::fs::read_to_string(&toml_path)
.map_err(|e| anyhow::anyhow!("error reading {}: {e}", toml_path.display()))?;
let toml_parsed = toml::from_str::<wapm_toml::Manifest>(&toml)
.map_err(|e| anyhow::anyhow!("error parsing {}: {e}", toml_path.display()))?;
let package = LocalPackage {
registry: host,
name: toml_parsed.package.name,
version: toml_parsed.package.version.to_string(),
};
let installation_path = package.get_path().map_err(|e| {
anyhow::anyhow!(
"could not determine installation path for {}: {e}",
package.name
)
})?;
std::fs::create_dir_all(&installation_path).map_err(|e| {
anyhow::anyhow!(
"could not create installation path for {}: {e}",
package.name
)
})?;
let mut options = fs_extra::dir::CopyOptions::new();
options.content_only = true;
options.overwrite = true;
copy(&unpacked_targz_path, &installation_path, &options)?;
#[cfg(not(target_os = "wasi"))]
let _ = filetime::set_file_mtime(
installation_path.join("wapm.toml"),
filetime::FileTime::now(),
);
Ok((package, installation_path))
} }
pub fn whoami( pub fn whoami(
@@ -1043,6 +983,7 @@ async fn install_webc_package_inner(
let builder = reqwest::Client::builder(); let builder = reqwest::Client::builder();
let builder = crate::graphql::proxy::maybe_set_up_proxy(builder)?; let builder = crate::graphql::proxy::maybe_set_up_proxy(builder)?;
builder builder
.redirect(reqwest::redirect::Policy::limited(10))
.build() .build()
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
.context("install_webc_package: failed to build reqwest Client")? .context("install_webc_package: failed to build reqwest Client")?
@@ -1140,18 +1081,18 @@ pub fn get_checksum_hash(bytes: &[u8]) -> String {
while checksum.last().copied() == Some(0) { while checksum.last().copied() == Some(0) {
checksum.pop(); checksum.pop();
} }
hex::encode(&checksum) hex::encode(&checksum).chars().take(64).collect()
} }
/// Returns the checksum of the .webc file, so that we can check whether the /// Returns the checksum of the .webc file, so that we can check whether the
/// file is already installed before downloading it /// file is already installed before downloading it
pub fn get_remote_webc_checksum(url: &Url) -> Result<String, anyhow::Error> { pub fn get_remote_webc_checksum(url: &Url) -> Result<String, anyhow::Error> {
let request_max_bytes = webc::WebC::get_signature_offset_start() + 4 + 1024 + 8 + 8; let request_max_bytes = webc::WebC::get_signature_offset_start() + 4 + 1024 + 8 + 8;
let data = get_webc_bytes(url, Some(0..request_max_bytes)) let data = get_webc_bytes(url, Some(0..request_max_bytes), None)
.with_context(|| format!("get_webc_bytes failed on {url}"))?; .with_context(|| anyhow::anyhow!("note: use --registry to change the registry URL"))?
.unwrap();
let checksum = webc::WebC::get_checksum_bytes(&data) let checksum = webc::WebC::get_checksum_bytes(&data)
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))?
.context("get_checksum_bytes failed")?
.to_vec(); .to_vec();
Ok(get_checksum_hash(&checksum)) Ok(get_checksum_hash(&checksum))
} }
@@ -1161,7 +1102,7 @@ pub fn get_remote_webc_checksum(url: &Url) -> Result<String, anyhow::Error> {
pub fn get_remote_webc_manifest(url: &Url) -> Result<RemoteWebcInfo, anyhow::Error> { pub fn get_remote_webc_manifest(url: &Url) -> Result<RemoteWebcInfo, anyhow::Error> {
// Request up unti manifest size / manifest len // Request up unti manifest size / manifest len
let request_max_bytes = webc::WebC::get_signature_offset_start() + 4 + 1024 + 8 + 8; let request_max_bytes = webc::WebC::get_signature_offset_start() + 4 + 1024 + 8 + 8;
let data = get_webc_bytes(url, Some(0..request_max_bytes))?; let data = get_webc_bytes(url, Some(0..request_max_bytes), None)?.unwrap();
let checksum = webc::WebC::get_checksum_bytes(&data) let checksum = webc::WebC::get_checksum_bytes(&data)
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
.context("WebC::get_checksum_bytes failed")? .context("WebC::get_checksum_bytes failed")?
@@ -1171,7 +1112,8 @@ pub fn get_remote_webc_manifest(url: &Url) -> Result<RemoteWebcInfo, anyhow::Err
let (manifest_start, manifest_len) = webc::WebC::get_manifest_offset_size(&data) let (manifest_start, manifest_len) = webc::WebC::get_manifest_offset_size(&data)
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
.context("WebC::get_manifest_offset_size failed")?; .context("WebC::get_manifest_offset_size failed")?;
let data_with_manifest = get_webc_bytes(url, Some(0..manifest_start + manifest_len))?; let data_with_manifest =
get_webc_bytes(url, Some(0..manifest_start + manifest_len), None)?.unwrap();
let manifest = webc::WebC::get_manifest(&data_with_manifest) let manifest = webc::WebC::get_manifest(&data_with_manifest)
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
.context("WebC::get_manifest failed")?; .context("WebC::get_manifest failed")?;
@@ -1181,39 +1123,99 @@ pub fn get_remote_webc_manifest(url: &Url) -> Result<RemoteWebcInfo, anyhow::Err
}) })
} }
fn setup_webc_client(url: &Url) -> Result<reqwest::blocking::RequestBuilder, anyhow::Error> { fn setup_client(
url: &Url,
application_type: &'static str,
) -> Result<reqwest::blocking::RequestBuilder, anyhow::Error> {
let client = { let client = {
let builder = reqwest::blocking::Client::builder(); let builder = reqwest::blocking::Client::builder();
let builder = crate::graphql::proxy::maybe_set_up_proxy_blocking(builder) let builder = crate::graphql::proxy::maybe_set_up_proxy_blocking(builder)
.context("setup_webc_client")?; .context("setup_webc_client")?;
builder builder
.redirect(reqwest::redirect::Policy::limited(10))
.build() .build()
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
.context("setup_webc_client: builder.build() failed")? .context("setup_webc_client: builder.build() failed")?
}; };
Ok(client.get(url.clone()).header(ACCEPT, "application/webc")) Ok(client.get(url.clone()).header(ACCEPT, application_type))
} }
fn get_webc_bytes(url: &Url, range: Option<Range<usize>>) -> Result<Vec<u8>, anyhow::Error> { fn get_webc_bytes(
url: &Url,
range: Option<Range<usize>>,
stream_response_into: Option<PathBuf>,
) -> Result<Option<Vec<u8>>, anyhow::Error> {
get_bytes(url, range, "application/webc", stream_response_into)
}
fn get_targz_bytes(
url: &Url,
range: Option<Range<usize>>,
stream_response_into: Option<PathBuf>,
) -> Result<Option<Vec<u8>>, anyhow::Error> {
get_bytes(url, range, "application/tar+gzip", stream_response_into)
}
fn get_bytes(
url: &Url,
range: Option<Range<usize>>,
application_type: &'static str,
stream_response_into: Option<PathBuf>,
) -> Result<Option<Vec<u8>>, anyhow::Error> {
// curl -r 0-500 -L https://wapm.dev/syrusakbary/python -H "Accept: application/webc" --output python.webc // curl -r 0-500 -L https://wapm.dev/syrusakbary/python -H "Accept: application/webc" --output python.webc
let mut res = setup_webc_client(url)?; let mut res = setup_client(url, application_type)?;
if let Some(range) = range.as_ref() { if let Some(range) = range.as_ref() {
res = res.header(RANGE, format!("bytes={}-{}", range.start, range.end)); res = res.header(RANGE, format!("bytes={}-{}", range.start, range.end));
} }
let res = res let mut res = res
.send() .send()
.map_err(|e| anyhow::anyhow!("{e}")) .map_err(|e| anyhow::anyhow!("{e}"))
.context("send() failed")?; .context("send() failed")?;
let bytes = res
.bytes()
.map_err(|e| anyhow::anyhow!("{e}"))
.context("bytes() failed")?;
Ok(bytes.to_vec()) if res.status().is_redirection() {
return Err(anyhow::anyhow!("redirect: {:?}", res.status()));
}
if res.status().is_server_error() {
return Err(anyhow::anyhow!("server error: {:?}", res.status()));
}
if res.status().is_client_error() {
return Err(anyhow::anyhow!("client error: {:?}", res.status()));
}
if let Some(path) = stream_response_into.as_ref() {
let mut file = std::fs::File::create(&path).map_err(|e| {
anyhow::anyhow!("failed to download {url} into {}: {e}", path.display())
})?;
res.copy_to(&mut file)
.map_err(|e| anyhow::anyhow!("{e}"))
.map_err(|e| {
anyhow::anyhow!("failed to download {url} into {}: {e}", path.display())
})?;
Ok(None)
} else {
let bytes = res
.bytes()
.map_err(|e| anyhow::anyhow!("{e}"))
.context("bytes() failed")?;
if (range.is_none() || range.unwrap().start == 0)
&& bytes[0..webc::MAGIC.len()] != webc::MAGIC[..]
{
let bytes = bytes.iter().copied().take(100).collect::<Vec<_>>();
let first_50_bytes = String::from_utf8_lossy(&bytes);
return Err(anyhow::anyhow!("invalid webc bytes: {first_50_bytes:?}"));
}
Ok(Some(bytes.to_vec()))
}
} }
// TODO: this test is segfaulting only on linux-musl, no other OS // TODO: this test is segfaulting only on linux-musl, no other OS
@@ -1269,14 +1271,10 @@ fn test_install_package() {
let all_installed_packages = get_all_local_packages(TEST_NAME, Some(registry)); let all_installed_packages = get_all_local_packages(TEST_NAME, Some(registry));
println!("all_installed_packages: {all_installed_packages:#?}");
let is_installed = all_installed_packages let is_installed = all_installed_packages
.iter() .iter()
.any(|p| p.name == "wasmer/wabt" && p.version == "1.0.29"); .any(|p| p.name == "wasmer/wabt" && p.version == "1.0.29");
println!("is_installed: {is_installed:#?}");
if !is_installed { if !is_installed {
let panic_str = get_all_local_packages(TEST_NAME, Some(registry)) let panic_str = get_all_local_packages(TEST_NAME, Some(registry))
.iter() .iter()
@@ -1316,8 +1314,8 @@ pub struct BindingsGenerator {
pub command: String, pub command: String,
} }
impl Display for BindingsGenerator { impl fmt::Display for BindingsGenerator {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let BindingsGenerator { let BindingsGenerator {
package_name, package_name,
version, version,

151
lib/registry/src/package.rs Normal file
View File

@@ -0,0 +1,151 @@
use crate::PartialWapmConfig;
use std::path::PathBuf;
use std::{fmt, str::FromStr};
use url::Url;
#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Package {
pub namespace: String,
pub name: String,
pub version: Option<String>,
}
impl fmt::Display for Package {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.file())
}
}
impl Package {
/// Checks whether the package is already installed, if yes, returns the path to the root dir
pub fn already_installed(&self) -> Option<PathBuf> {
let checkouts_dir = crate::get_checkouts_dir()?;
let hash = self.get_hash();
let found = std::fs::read_dir(&checkouts_dir)
.ok()?
.filter_map(|e| Some(e.ok()?.file_name().to_str()?.to_string()))
.find(|s| match self.version.as_ref() {
None => s.contains(&hash),
Some(v) => s.contains(&hash) && s.ends_with(v),
})?;
Some(checkouts_dir.join(found))
}
pub fn is_url_already_installed(_url: &Url) -> Option<PathBuf> {
None // TODO
}
pub fn get_hash(&self) -> String {
hex::encode(&format!(
"{}",
self.get_url_without_version().unwrap_or_default()
))
.chars()
.take(64)
.collect()
}
fn get_url_without_version(&self) -> Result<String, anyhow::Error> {
Ok(format!(
"{}/{}/{}",
self.url()?.origin().ascii_serialization(),
self.namespace,
self.name
))
}
/// Returns the filename for this package
pub fn file(&self) -> String {
let version = self
.version
.as_ref()
.map(|f| format!("@{f}"))
.unwrap_or_default();
format!("{}/{}{version}", self.namespace, self.name)
}
/// Returns the {namespace}/{name} package name
pub fn package(&self) -> String {
format!("{}/{}", self.namespace, self.name)
}
pub fn url(&self) -> Result<Url, anyhow::Error> {
let config = PartialWapmConfig::from_file()
.map_err(|e| anyhow::anyhow!("could not read wapm config: {e}"))?;
let registry = config.registry.get_current_registry();
let registry_tld = tldextract::TldExtractor::new(tldextract::TldOption::default())
.extract(&registry)
.map_err(|e| anyhow::anyhow!("Invalid registry: {}: {e}", registry))?;
let registry_tld = format!(
"{}.{}",
registry_tld.domain.as_deref().unwrap_or(""),
registry_tld.suffix.as_deref().unwrap_or(""),
);
let version = self
.version
.as_ref()
.map(|f| format!("@{f}"))
.unwrap_or_default();
let url = format!(
"https://{registry_tld}/{}/{}{version}",
self.namespace, self.name
);
url::Url::parse(&url).map_err(|e| anyhow::anyhow!("error parsing {url}: {e}"))
}
/// Returns the path to the installation directory.
/// Does not check whether the installation directory already exists.
pub fn get_path(&self) -> Result<PathBuf, anyhow::Error> {
let checkouts_dir =
crate::get_checkouts_dir().ok_or_else(|| anyhow::anyhow!("no checkouts dir"))?;
match self.version.as_ref() {
Some(v) => Ok(checkouts_dir.join(format!("{}@{}", self.get_hash(), v))),
None => Ok(checkouts_dir.join(&self.get_hash())),
}
}
}
impl FromStr for Package {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let regex = regex::Regex::new(r#"^([a-zA-Z0-9\-_]+)/([a-zA-Z0-9\-_]+)(@(.*))?$"#).unwrap();
let captures = regex
.captures(s.trim())
.map(|c| {
c.iter()
.flatten()
.map(|m| m.as_str().to_owned())
.collect::<Vec<_>>()
})
.unwrap_or_default();
match captures.len() {
// namespace/package
3 => {
let namespace = captures[1].to_string();
let name = captures[2].to_string();
Ok(Package {
namespace,
name,
version: None,
})
}
// namespace/package@version
5 => {
let namespace = captures[1].to_string();
let name = captures[2].to_string();
let version = captures[4].to_string();
Ok(Package {
namespace,
name,
version: Some(version),
})
}
other => Err(anyhow::anyhow!("invalid package {other}")),
}
}
}