Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TTY aware output to the wasmer package and wasmer container commands #4315

35 changes: 31 additions & 4 deletions lib/cli/src/commands/container/unpack.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

use anyhow::Context;
use dialoguer::console::{style, Emoji};
use indicatif::ProgressBar;
use std::path::PathBuf;

/// Extract contents of a container to a directory.
#[derive(clap::Parser, Debug)]
Expand All @@ -13,13 +14,31 @@ pub struct PackageUnpack {
#[clap(long)]
overwrite: bool,

/// Run the unpack command without any output
#[clap(long)]
pub quiet: bool,

/// Path to the package.
package_path: PathBuf,
}

static PACKAGE_EMOJI: Emoji<'_, '_> = Emoji("📦 ", "");
static EXTRACTED_TO_EMOJI: Emoji<'_, '_> = Emoji("📂 ", "");

impl PackageUnpack {
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> {
eprintln!("Unpacking...");
// Setup the progress bar
let pb = if self.quiet {
ProgressBar::hidden()
} else {
ProgressBar::new_spinner()
};

pb.println(format!(
"{} {}Unpacking...",
style("[1/2]").bold().dim(),
PACKAGE_EMOJI
));

let pkg = webc::compat::Container::from_disk(&self.package_path).with_context(|| {
format!(
Expand All @@ -35,7 +54,14 @@ impl PackageUnpack {
pkg.unpack(outdir, self.overwrite)
.with_context(|| "could not extract package".to_string())?;

eprintln!("Extracted package contents to '{}'", self.out_dir.display());
pb.println(format!(
"{} {}Extracted package contents to '{}'",
style("[2/2]").bold().dim(),
EXTRACTED_TO_EMOJI,
self.out_dir.display()
));

pb.finish();

Ok(())
}
Expand All @@ -61,6 +87,7 @@ mod tests {
out_dir: dir.path().to_owned(),
overwrite: false,
package_path,
quiet: true,
};

cmd.execute().unwrap();
Expand Down
48 changes: 45 additions & 3 deletions lib/cli/src/commands/package/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

use anyhow::Context;
use dialoguer::console::{style, Emoji};
use indicatif::ProgressBar;
use std::path::PathBuf;

/// Build a container from a package manifest.
#[derive(clap::Parser, Debug)]
Expand All @@ -10,17 +11,39 @@ pub struct PackageBuild {
#[clap(short = 'o', long)]
out: Option<PathBuf>,

/// Run the publish command without any output
#[clap(long)]
pub quiet: bool,

/// Path of the package or wasmer.toml manifest.
///
/// Defaults to current directory.
package: Option<PathBuf>,
}

static READING_MANIFEST_EMOJI: Emoji<'_, '_> = Emoji("📖 ", "");
static CREATING_OUTPUT_DIRECTORY_EMOJI: Emoji<'_, '_> = Emoji("📁 ", "");
static WRITING_PACKAGE_EMOJI: Emoji<'_, '_> = Emoji("📦 ", "");
static SPARKLE: Emoji<'_, '_> = Emoji("✨ ", ":-)");

impl PackageBuild {
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> {
let manifest_path = self.manifest_path()?;
let pkg = webc::wasmer_package::Package::from_manifest(manifest_path)?;

// Setup the progress bar
let pb = if self.quiet {
ProgressBar::hidden()
} else {
ProgressBar::new_spinner()
};

pb.println(format!(
"{} {}Reading manifest...",
style("[1/3]").bold().dim(),
READING_MANIFEST_EMOJI
));

let manifest = pkg
.manifest()
.wapm()
Expand All @@ -29,6 +52,13 @@ impl PackageBuild {

let pkgname = manifest.name.replace('/', "-");
let name = format!("{}-{}.webc", pkgname, manifest.version,);

pb.println(format!(
"{} {}Creating output directory...",
style("[2/3]").bold().dim(),
CREATING_OUTPUT_DIRECTORY_EMOJI
));

let out_path = if let Some(p) = &self.out {
if p.is_dir() {
p.join(name)
Expand All @@ -53,10 +83,21 @@ impl PackageBuild {
}

let data = pkg.serialize()?;

pb.println(format!(
"{} {}Writing package...",
style("[3/3]").bold().dim(),
WRITING_PACKAGE_EMOJI
));

std::fs::write(&out_path, &data)
.with_context(|| format!("could not write contents to '{}'", out_path.display()))?;

eprintln!("Package written to '{}'", out_path.display());
pb.finish_with_message(format!(
"{} Package written to '{}'",
SPARKLE,
out_path.display()
));

Ok(())
}
Expand Down Expand Up @@ -126,6 +167,7 @@ description = "hello"
let cmd = PackageBuild {
package: Some(path.to_owned()),
out: Some(path.to_owned()),
quiet: true,
};

cmd.execute().unwrap();
Expand Down
140 changes: 103 additions & 37 deletions lib/cli/src/commands/package/download.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::{io::Write, path::PathBuf};

use anyhow::Context;
use futures::StreamExt;
use dialoguer::console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle};
use std::path::PathBuf;
use tempfile::NamedTempFile;
use wasmer_registry::wasmer_env::WasmerEnv;
use wasmer_wasix::runtime::resolver::PackageSpecifier;

Expand All @@ -20,20 +21,49 @@ pub struct PackageDownload {
#[clap(short = 'o', long)]
out_path: PathBuf,

/// Run the download command without any output
#[clap(long)]
pub quiet: bool,

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

static CREATING_OUTPUT_DIRECTORY_EMOJI: Emoji<'_, '_> = Emoji("📁 ", "");
static DOWNLOADING_PACKAGE_EMOJI: Emoji<'_, '_> = Emoji("🌐 ", "");
static RETRIEVING_PACKAGE_INFORMATION_EMOJI: Emoji<'_, '_> = Emoji("📜 ", "");
static VALIDATING_PACKAGE_EMOJI: Emoji<'_, '_> = Emoji("🔍 ", "");
static WRITING_PACKAGE_EMOJI: Emoji<'_, '_> = Emoji("📦 ", "");

impl PackageDownload {
pub(crate) fn execute(&self) -> Result<(), anyhow::Error> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(self.run())
}
let total_steps = if self.validate { 5 } else { 4 };
let mut step_num = 1;

// Setup the progress bar
let pb = if self.quiet {
ProgressBar::hidden()
} else {
ProgressBar::new_spinner()
};

pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})")
.unwrap()
.progress_chars("#>-"));

pb.println(format!(
"{} {}Creating output directory...",
style(format!("[{}/{}]", step_num, total_steps))
.bold()
.dim(),
CREATING_OUTPUT_DIRECTORY_EMOJI,
));

step_num += 1;

async fn run(&self) -> Result<(), anyhow::Error> {
if let Some(parent) = self.out_path.parent() {
match parent.metadata() {
Ok(m) => {
Expand All @@ -52,6 +82,16 @@ impl PackageDownload {
}
};

pb.println(format!(
"{} {}Retrieving package information...",
style(format!("[{}/{}]", step_num, total_steps))
.bold()
.dim(),
RETRIEVING_PACKAGE_INFORMATION_EMOJI
));

step_num += 1;

let (full_name, version, api_endpoint, token) = match &self.package {
PackageSpecifier::Registry { full_name, version } => {
let endpoint = self.env.registry_endpoint()?;
Expand Down Expand Up @@ -90,30 +130,45 @@ impl PackageDownload {
.pirita_url
.context("registry does provide a container download container download URL")?;

let client = reqwest::Client::new();
let client = reqwest::blocking::Client::new();
let mut b = client
.get(&download_url)
.get(download_url)
.header(http::header::ACCEPT, "application/webc");
if let Some(token) = token {
b = b.header(http::header::AUTHORIZATION, format!("Bearer {token}"));
};

eprintln!("Downloading package...");
pb.println(format!(
"{} {}Downloading package...",
style(format!("[{}/{}]", step_num, total_steps))
.bold()
.dim(),
DOWNLOADING_PACKAGE_EMOJI
));

step_num += 1;

let res = b
.send()
.await
.context("http request failed")?
.error_for_status()
.context("http request failed with non-success status code")?;

let tmp_path = self.out_path.with_extension("webc_tmp");
let mut tmpfile = std::fs::File::create(&tmp_path).with_context(|| {
format!(
"could not create temporary file at '{}'",
tmp_path.display()
)
})?;
let webc_total_size = res
.headers()
.get(http::header::CONTENT_LENGTH)
.and_then(|t| t.to_str().ok())
.and_then(|t| t.parse::<u64>().ok())
.unwrap_or_default();

if webc_total_size == 0 {
anyhow::bail!("Package is empty");
}

// Set the length of the progress bar
pb.set_length(webc_total_size);

let mut tmpfile = NamedTempFile::new_in(self.out_path.parent().unwrap())?;

let ty = res
.headers()
Expand All @@ -127,36 +182,46 @@ impl PackageDownload {
);
}

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.
tmpfile
.write_all(&chunk)
.context("could not write to temporary file")?;
}
std::io::copy(&mut pb.wrap_read(res), &mut tmpfile)
.context("could not write downloaded data to temporary file")?;

tmpfile.sync_all()?;
std::mem::drop(tmpfile);
tmpfile.as_file_mut().sync_all()?;

if self.validate {
eprintln!("Validating package...");
webc::compat::Container::from_disk(&tmp_path)
if !self.quiet {
println!(
"{} {}Validating package...",
style(format!("[{}/{}]", step_num, total_steps))
.bold()
.dim(),
VALIDATING_PACKAGE_EMOJI
);
}

step_num += 1;

webc::compat::Container::from_disk(tmpfile.path())
.context("could not parse downloaded file as a package - invalid download?")?;
eprintln!("Downloaded package is valid!");
}

std::fs::rename(&tmp_path, &self.out_path).with_context(|| {
tmpfile.persist(&self.out_path).with_context(|| {
format!(
"could not move temporary file from '{}' to '{}'",
tmp_path.display(),
"could not persist temporary file to '{}'",
self.out_path.display()
)
})?;

eprintln!("Package downloaded to '{}'", self.out_path.display());
pb.println(format!(
"{} {}Package downloaded to '{}'",
style(format!("[{}/{}]", step_num, total_steps))
.bold()
.dim(),
WRITING_PACKAGE_EMOJI,
self.out_path.display()
));

// We're done, so finish the progress bar
pb.finish();

Ok(())
}
Expand All @@ -180,6 +245,7 @@ mod tests {
validate: true,
out_path: out_path.clone(),
package: "wasmer/[email protected]".parse().unwrap(),
quiet: true,
};

cmd.execute().unwrap();
Expand Down
Loading