Skip to content

Commit

Permalink
feat(cli): Add "package dowload" command
Browse files Browse the repository at this point in the history
Adds a new CLI command for downloading packages.
  • Loading branch information
theduke committed Oct 31, 2023
1 parent bfeab2b commit 82152ca
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 53 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 10 additions & 50 deletions lib/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ once_cell = "1.17.1"
indicatif = "0.17.5"
opener = "0.6.1"
hyper = { version = "0.14.27", features = ["server"] }
http = "0.2.9"
futures = "0.3.29"

# NOTE: Must use different features for clap because the "color" feature does not
# work on wasi due to the anstream dependency not compiling.
Expand Down Expand Up @@ -155,71 +157,29 @@ unix_mode = "0.1.3"
[features]
# Don't add the compiler features in default, please add them on the Makefile
# since we might want to autoconfigure them depending on the availability on the host.
default = [
"sys",
"wat",
"wast",
"compiler",
"wasmer-artifact-create",
"static-artifact-create",
]
default = ["sys", "wat", "wast", "compiler", "wasmer-artifact-create", "static-artifact-create"]
backend = []
coredump = ["wasm-coredump-builder"]
sys = ["compiler", "wasmer-vm"]
jsc = ["backend", "wasmer/jsc", "wasmer/std"]
wast = ["wasmer-wast"]
host-net = ["virtual-net/host-net"]
wat = ["wasmer/wat"]
compiler = [
"backend",
"wasmer/compiler",
"wasmer-compiler/translator",
"wasmer-compiler/compiler",
]
wasmer-artifact-create = [
"compiler",
"wasmer/wasmer-artifact-load",
"wasmer/wasmer-artifact-create",
"wasmer-compiler/wasmer-artifact-load",
"wasmer-compiler/wasmer-artifact-create",
"wasmer-object",
]
static-artifact-create = [
"compiler",
"wasmer/static-artifact-load",
"wasmer/static-artifact-create",
"wasmer-compiler/static-artifact-load",
"wasmer-compiler/static-artifact-create",
"wasmer-object",
]
wasmer-artifact-load = [
"compiler",
"wasmer/wasmer-artifact-load",
"wasmer-compiler/wasmer-artifact-load",
]
static-artifact-load = [
"compiler",
"wasmer/static-artifact-load",
"wasmer-compiler/static-artifact-load",
]
compiler = ["backend", "wasmer/compiler", "wasmer-compiler/translator", "wasmer-compiler/compiler"]
wasmer-artifact-create = ["compiler", "wasmer/wasmer-artifact-load", "wasmer/wasmer-artifact-create", "wasmer-compiler/wasmer-artifact-load", "wasmer-compiler/wasmer-artifact-create", "wasmer-object"]
static-artifact-create = ["compiler", "wasmer/static-artifact-load", "wasmer/static-artifact-create", "wasmer-compiler/static-artifact-load", "wasmer-compiler/static-artifact-create", "wasmer-object"]
wasmer-artifact-load = ["compiler", "wasmer/wasmer-artifact-load", "wasmer-compiler/wasmer-artifact-load"]
static-artifact-load = ["compiler", "wasmer/static-artifact-load", "wasmer-compiler/static-artifact-load"]
experimental-io-devices = ["wasmer-wasix-experimental-io-devices"]
singlepass = ["wasmer-compiler-singlepass", "compiler"]
cranelift = ["wasmer-compiler-cranelift", "compiler"]
llvm = ["wasmer-compiler-llvm", "compiler"]
disable-all-logging = [
"wasmer-wasix/disable-all-logging",
"log/release_max_level_off",
]
disable-all-logging = ["wasmer-wasix/disable-all-logging", "log/release_max_level_off"]
headless = []
headless-minimal = ["headless", "disable-all-logging"]

# Optional
enable-serde = [
"wasmer/enable-serde",
"wasmer-vm/enable-serde",
"wasmer-compiler/enable-serde",
"wasmer-wasix/enable-serde",
]
enable-serde = ["wasmer/enable-serde", "wasmer-vm/enable-serde", "wasmer-compiler/enable-serde", "wasmer-wasix/enable-serde"]

[dev-dependencies]
assert_cmd = "2.0.11"
Expand Down
11 changes: 9 additions & 2 deletions lib/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use crate::commands::CreateExe;
#[cfg(feature = "wast")]
use crate::commands::Wast;
use crate::commands::{
Add, Cache, Config, Init, Inspect, Login, Publish, Run, SelfUpdate, Validate, Whoami,
Add, Cache, CmdPackage, Config, Init, Inspect, Login, Publish, Run, SelfUpdate, Validate,
Whoami,
};
#[cfg(feature = "static-artifact-create")]
use crate::commands::{CreateObj, GenCHeader};
Expand Down Expand Up @@ -108,6 +109,9 @@ impl Args {
Some(Cmd::Init(init)) => init.execute(),
Some(Cmd::Login(login)) => login.execute(),
Some(Cmd::Publish(publish)) => publish.execute(),
Some(Cmd::Package(cmd)) => match cmd {
CmdPackage::Download(cmd) => cmd.execute(),
},
/*
Some(Cmd::Connect(connect)) => connect.execute(),
*/
Expand Down Expand Up @@ -258,7 +262,10 @@ enum Cmd {
#[clap(alias = "run-unstable")]
Run(Run),

// DEPLOY commands
#[clap(subcommand)]
Package(crate::commands::CmdPackage),

// Edge commands
/// Deploy apps to the Wasmer Edge.
Deploy(wasmer_deploy_cli::cmd::deploy::CmdDeploy),

Expand Down
3 changes: 2 additions & 1 deletion lib/cli/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod gen_c_header;
mod init;
mod inspect;
mod login;
mod package;
mod publish;
mod run;
mod self_update;
Expand All @@ -32,7 +33,7 @@ pub use create_exe::*;
#[cfg(feature = "wast")]
pub use wast::*;
pub use {
add::*, cache::*, config::*, init::*, inspect::*, login::*, publish::*, run::Run,
add::*, cache::*, config::*, init::*, inspect::*, login::*, package::*, publish::*, run::Run,
self_update::*, validate::*, whoami::*,
};
#[cfg(feature = "static-artifact-create")]
Expand Down
166 changes: 166 additions & 0 deletions lib/cli/src/commands/package/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use std::path::PathBuf;

use anyhow::Context;
use futures::StreamExt;
use tokio::io::AsyncWriteExt;
use wasmer_registry::wasmer_env::WasmerEnv;
use wasmer_wasix::runtime::resolver::PackageSpecifier;

/// Download a package from the registry.
#[derive(clap::Parser, Debug)]
pub struct CmdPackageDownload {
#[clap(flatten)]
env: WasmerEnv,

/// Verify that the downloaded file is a valid package.
#[clap(long)]
validate: bool,

/// Path where the package file should be written to.
/// If not specified, the data will be written to stdout.
#[clap(short = 'o', long)]
out_path: PathBuf,

/// The package to download.
/// Can be:
/// * a pakage specifier: `namespace/package[@vesion]`
/// * a URL
package: PackageSpecifier,
}

impl CmdPackageDownload {
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(self.run())
}

async fn run(&self) -> Result<(), anyhow::Error> {
if let Some(parent) = self.out_path.parent() {
match parent.metadata() {
Ok(m) => {
if !m.is_dir() {
anyhow::bail!(
"parent of output file is not a directory: '{}'",
parent.display()
);
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
std::fs::create_dir_all(parent)
.context("could not create parent directory of output file")?;
}
Err(err) => return Err(err.into()),
}
};

let (url, token) = match &self.package {
PackageSpecifier::Registry { full_name, version } => {
let mut url = self.env.registry_public_url()?;
let p = format!("/{full_name}@{version}");
url.set_path(&p);
(url, self.env.token())
}
PackageSpecifier::Url(url) => {
(url.clone(), self.env.get_token_opt().map(|x| x.to_string()))
}
PackageSpecifier::Path(_) => {
anyhow::bail!("cannot download a package from a local path");
}
};

let client = reqwest::Client::new();
let mut b = client
.get(url.clone())
.header(http::header::ACCEPT, "application/webc");
if let Some(token) = token {
b = b.header(http::header::AUTHORIZATION, format!("Bearer {token}"));
};
let res = b
.send()
.await
.context("http request failed")?
.error_for_status()
.context("http request failed with non-success status code")?;

let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs();

let tmp_name = url
.as_str()
.chars()
.map(|x| match x {
'a'..='z' | 'A'..='Z' | '0'..='9' | '-' => x,
_ => '_',
})
.collect::<String>();
let tmp_path = std::env::temp_dir().join(format!("wasmer-download-{}-{}", tmp_name, time));
let mut file = tokio::fs::File::create(&tmp_path).await.with_context(|| {
format!(
"could not create temporary file at '{}'",
tmp_path.display()
)
})?;

let ty = res
.headers()
.get(http::header::CONTENT_TYPE)
.and_then(|t| t.to_str().ok())
.unwrap_or_default();

if !(ty == "application/webc" || ty == "application/octet-stream") {
eprintln!(
"Warning: response has invalid content type - expected \
'application/webc' or 'application/octet-stream', got {ty}"
);
}

let mut body = res.bytes_stream();

while let Some(res) = body.next().await {
let chunk = res.context("could not read response body")?;
// Yes, we are mixing async and sync code here, but since this is
// a top-level command, this can't interfere with other tasks.
file.write_all(&chunk)
.await
.context("could not write to temporary file")?;
}

file.sync_all()
.await
.context("could not sync temporary file")?;
std::mem::drop(file);

if self.validate {
webc::compat::Container::from_disk(&tmp_path)
.context("could not parse downloaded file as a package - invalid download?")?;
}

std::fs::rename(&tmp_path, &self.out_path)
.context("could not move temporary file to output path")?;

Ok(())
}
}

#[cfg(test)]
mod tests {
use wasmer_registry::wasmer_env::WASMER_DIR;

use super::*;

/// Download a package from the dev registry.
#[test]
fn test_cmd_package_download() {
let dir = tempfile::tempdir().unwrap();

let cmd = CmdPackageDownload {
env: WasmerEnv::new(WASMER_DIR.clone(), Some("wasmer.wtf".into()), None, None),
validate: true,
out_path: dir.path().join("hello.webc"),
package: "wasmer/[email protected]".parse().unwrap(),
};

cmd.execute().unwrap();
}
}
11 changes: 11 additions & 0 deletions lib/cli/src/commands/package/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mod download;
pub use download::CmdPackageDownload;

/// Package related commands.
#[derive(clap::Subcommand, Debug)]
// Allowing missing_docs because the comment would override the doc comment on
// the command struct.
#[allow(missing_docs)]
pub enum CmdPackage {
Download(CmdPackageDownload),
}
Empty file removed lib/registry/src/queries.rs
Empty file.
24 changes: 24 additions & 0 deletions lib/registry/src/wasmer_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ impl WasmerEnv {
}
}

pub fn registry_public_url(&self) -> Result<Url, Error> {
let mut url = self.registry_endpoint()?;
url.set_path("");

let domain = url
.host_str()
.context("url has no host")?
.strip_prefix("registry.")
.context("could not derive registry public url")?
.to_string();
url.set_host(Some(&domain))
.context("could not derive registry public url")?;

Ok(url)
}

/// Get the GraphQL endpoint used to query the registry.
pub fn registry_endpoint(&self) -> Result<Url, Error> {
if let Some(registry) = &self.registry {
Expand Down Expand Up @@ -79,6 +95,14 @@ impl WasmerEnv {
}
}

/// Retrieve the specified token.
///
/// NOTE: In contrast to [`Self::token`], this will not fall back to loading
/// the token from the confg file.
pub fn get_token_opt(&self) -> Option<&str> {
self.token.as_deref()
}

/// The API token for the active registry.
pub fn token(&self) -> Option<String> {
if let Some(token) = &self.token {
Expand Down

0 comments on commit 82152ca

Please sign in to comment.