use chrono::DateTime;
use directories::BaseDirs;
use dirs::{cache_dir, config_dir, data_dir};
use which::which;
use flate2::read::GzDecoder;
use is_executable::IsExecutable;
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};
use std::fs::{create_dir_all, File};
use std::io::{copy, Write};
use std::{env, fs};
use std::path::{PathBuf, Path};
use std::{
env::consts::{ARCH, OS},
process::Command,
};
use tar::Archive;
use std::time::Duration;
use indicatif::{ProgressBar, ProgressStyle};
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ReleaseAsset {
pub name: String,
pub browser_download_url: String,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Release {
pub tag_name: String,
pub prerelease: bool,
pub published_at: String,
pub assets: Vec<ReleaseAsset>,
}
impl Release {
pub fn version(&self) -> Version {
let tag_name = self.tag_name.trim_start_matches('v');
Version::parse(tag_name).unwrap()
}
pub fn download_url(&self) -> Option<&str> {
let filename = get_filename_for_system_architecture(OS, ARCH);
self.assets.iter().find(|asset| asset.name == filename).map(|asset| &asset.browser_download_url).map(|x| x.as_str())
}
pub fn filename(&self) -> Option<String> {
let filename = get_filename_for_system_architecture(OS, ARCH);
self.assets.iter().find(|asset| asset.name == filename).map(|asset| format!("{}-{}", self.version(), asset.name))
}
pub fn published_time(&self) -> String {
let date_time = DateTime::parse_from_rfc3339(&self.published_at).unwrap();
date_time.format("%B %e %Y %r").to_string()
}
pub fn tags(&self) -> Vec<&str> {
let mut tags: Vec<&str> = Vec::new();
if self.prerelease {
tags.push("prerelease");
}
if let Some(system_wasmer_version) = find_system_wasmer() {
if system_wasmer_version == self.version() {
tags.push("system")
}
}
tags
}
}
pub fn list_releases() -> Result<Vec<Release>, reqwest::Error> {
let url = "https://api.github.com/repos/wasmerio/wasmer/releases";
let client = reqwest::blocking::Client::new();
let response = client.get(url).header("User-Agent", "wasmenv").send()?;
response.json()
}
pub fn list_releases_interactively() -> Result<Vec<Release>, reqwest::Error> {
let progress_bar = create_progress_bar(String::from("Fetching wasmer releases..."));
let releases = list_releases().expect("A list of wasmer releases from github.");
progress_bar.finish_and_clear();
Ok(releases)
}
pub fn get_filename_for_system_architecture(target_os: &str, target_arch: &str) -> String {
let filename = match (target_os, target_arch) {
("linux", "x86_64") => "wasmer-linux-amd64.tar.gz",
("linux", "aarch64") => "wasmer-linux-aarch64.tar.gz",
("linux", "mips64") => "wasmer-linux-mips64.tar.gz",
("linux", "riscv64") => "wasmer-linux-riscv64.tar.gz",
("macos", "x86_64") => "wasmer-darwin-amd64.tar.gz",
("macos", "aarch64") => "wasmer-darwin-arm64.tar.gz",
("windows", "x86_64") => "wasmer-windows-amd64.tar.gz",
("windows", "gnu") => "wasmer-windows-gnu64.tar.gz",
("windows", _) => "wasmer-windows.exe",
_ => panic!("Unsupported architecture: {}-{}", target_os, target_arch),
};
filename.to_string()
}
fn version_from_version_string(version_string: String) -> anyhow::Result<Version> {
match version_string
.trim()
.trim_start_matches("wasmer ")
.parse::<Version>() {
Ok(version) => {
Ok(version)
},
Err(_) => {
Err(anyhow::anyhow!("Could not get wasmer version form the version string"))
}
}
}
pub fn find_system_wasmer() -> Option<Version> {
let wasmer_path = BaseDirs::new().map(|base_dirs| {
let wasmer_path = base_dirs.home_dir().join(".wasmer/bin/wasmer");
if wasmer_path.is_executable() {
Some(wasmer_path)
} else {
None
}
})?;
if let Some(wasmer_path) = wasmer_path {
let output = Command::new(wasmer_path).arg("--version").output().ok()?;
if output.status.success() {
let version_str = String::from_utf8_lossy(&output.stdout).to_string();
if let Ok(version) = version_from_version_string(version_str) {
return Some(version);
} else {
return None;
}
}
}
None
}
pub fn find_current_wasmer() -> Option<Version> {
let output = Command::new("wasmer").arg("--version").output().ok()?;
if output.status.success() {
let version_str = String::from_utf8_lossy(&output.stdout).to_string();
if let Ok(version) = version_from_version_string(version_str) {
return Some(version);
}
}
None
}
pub fn find_current_wasmer_dir() -> anyhow::Result<PathBuf> {
Ok(which("wasmer")?.parent().expect("path to wasmer executable").to_path_buf())
}
pub fn download_wasmer_to_cache(release: &Release) -> anyhow::Result<PathBuf> {
let url = release
.download_url()
.expect("Download url for wasmer release");
let filename = release.filename().expect("Filename for wasmer release");
let filepath = cache_dir()
.ok_or(anyhow::anyhow!("Could not get cache directory"))?
.join("wasmenv")
.join(filename);
if filepath.exists() {
return Ok(filepath);
}
println!("downloading to {}", filepath.to_str().unwrap());
create_dir_all(filepath.parent().unwrap())?;
let client = reqwest::blocking::Client::new();
let progress_bar = create_progress_bar(format!("Downloading wasmer {}...", release.version()));
let mut response = &mut client.get(url).send()?;
let mut tmp_file = File::create(&filepath)?;
copy(&mut response, &mut tmp_file)?;
progress_bar.finish_and_clear();
Ok(filepath)
}
pub fn download_and_install_wasmer(release: &Release, dest_dir: &PathBuf) -> anyhow::Result<()> {
let filepath = download_wasmer_to_cache(release)?;
let progress_bar = create_progress_bar(format!("Installing wasmer {}...", release.version()));
if !dest_dir.exists() {
std::fs::create_dir_all(dest_dir)?;
}
let file = File::open(filepath)?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
archive.unpack(dest_dir)?;
progress_bar.finish_and_clear();
Ok(())
}
fn create_progress_bar(message: String) -> ProgressBar {
let progress_bar = ProgressBar::new_spinner();
progress_bar.set_style(ProgressStyle::default_spinner().tick_strings(&[
"( ● )",
"( ● )",
"( ● )",
"( ● )",
"( ●)",
"( ● )",
"( ● )",
"( ● )",
]));
progress_bar.set_message(message);
progress_bar.enable_steady_tick(Duration::from_millis(100));
progress_bar
}
fn create_config_files(config_dir: &Path, wasmer_current_dir: &str) -> anyhow::Result<()> {
let filepath = config_dir.join("wasmenv.sh");
if !filepath.exists() {
fs::create_dir_all(config_dir)?;
let mut wasmenv_sh = File::create(filepath)?;
let wasmenv_sh_contents = format!(
"\
# wasmer config\n\
export WASMER_DIR=\"{}\"\n\
export PATH=\"$WASMER_DIR/bin\":$PATH\n",
wasmer_current_dir
);
wasmenv_sh.write_all(wasmenv_sh_contents.as_bytes())?;
}
let filepath = config_dir.join("wasmenv.fish");
if !filepath.exists() {
fs::create_dir_all(config_dir)?;
let mut wasmenv_sh = File::create(filepath)?;
let wasmenv_sh_contents = format!(
"\
# wasmer config for fish\n\
set -x WASMER_DIR \"{}\"\n\
set -x PATH $WASMER_DIR/bin $PATH\n",
wasmer_current_dir
);
wasmenv_sh.write_all(wasmenv_sh_contents.as_bytes())?;
}
Ok(())
}
pub fn wasmenv_config_dir() -> anyhow::Result<PathBuf> {
let (config_dir, _) = setup_config_directory()?;
Ok(config_dir)
}
fn setup_config_directory() -> anyhow::Result<(PathBuf, PathBuf)> {
let config_dir = config_dir()
.expect("Config directory should be present")
.join("wasmenv");
if !config_dir.exists() {
fs::create_dir_all(&config_dir)?;
}
let data_dir = data_dir().expect("Data directory should be present");
let wasmer_current_dir = data_dir.join("wasmenv/current");
if !wasmer_current_dir.exists() {
fs::create_dir_all(&wasmer_current_dir)?;
}
create_config_files(&config_dir, wasmer_current_dir.to_str().expect("String containing wasmer current path"))?;
Ok((config_dir, wasmer_current_dir))
}
pub fn verify_wasmenv_is_in_path() -> anyhow::Result<()> {
match env::var("WASMENV_DIR") {
Ok(_) => {
Ok(())
}
Err(_) => {
Err(anyhow::anyhow!(
"Looks like you haven't initialized wasmenv.\n\
run `wasmenv shell | source` to initialize it.\n"
))
}
}
}
pub fn release_to_install(version: &Option<VersionReq>, install_prerelease: bool) -> anyhow::Result<Option<Release>> {
let mut releases = list_releases_interactively()?;
if !install_prerelease {
releases.retain(|rel| !rel.prerelease);
}
let release = if let Some(req) = version {
releases.into_iter().find(|rel| req.matches(&rel.version()))
} else {
releases.first().cloned()
};
Ok(release)
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_wasmenv_config_dir() -> anyhow::Result<()> {
let result = wasmenv_config_dir()?;
assert!(result.exists());
assert!(result.ends_with("wasmenv"));
Ok(())
}
#[test]
fn test_list_releases() -> anyhow::Result<()> {
let releases = list_releases()?;
assert!(!releases.is_empty());
Ok(())
}
#[test]
fn test_verify_wasmenv_is_in_path() {
env::set_var("WASMENV_DIR", "/path/to/wasmenv");
assert!(verify_wasmenv_is_in_path().is_ok());
env::remove_var("WASMENV_DIR");
let result = verify_wasmenv_is_in_path();
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Looks like you haven't initialized wasmenv.\n\
run `wasmenv shell | source` to initialize it.\n"
);
}
#[test]
fn test_version_from_version_string() {
let version_string = "wasmer 1.0.0".to_string();
let version = version_from_version_string(version_string).unwrap();
assert_eq!(version.to_string(), "1.0.0");
let version_string = "invalid version string".to_string();
let result = version_from_version_string(version_string);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
"Could not get wasmer version form the version string"
);
}
}