use std::collections::HashMap;
use anyhow::{bail, Result};
use reqwest::StatusCode;
use tokio::fs::{create_dir_all, metadata, File};
use tokio::process::{Child, Command};
use tokio_stream::StreamExt;
use tokio_util::io::StreamReader;
use tracing::warn;
#[cfg(target_family = "unix")]
use std::os::unix::prelude::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::Stdio;
#[cfg(target_family = "unix")]
use command_group::AsyncCommandGroup;
use super::get_download_client;
const WASMCLOUD_GITHUB_RELEASE_URL: &str =
"https://github.com/wasmCloud/wasmCloud/releases/download";
#[cfg(target_family = "unix")]
pub const WASMCLOUD_HOST_BIN: &str = "wasmcloud_host";
#[cfg(target_family = "windows")]
pub const WASMCLOUD_HOST_BIN: &str = "wasmcloud_host.exe";
const MINIMUM_WASMCLOUD_VERSION: &str = "0.81.0";
pub async fn ensure_wasmcloud<P>(version: &str, dir: P) -> Result<PathBuf>
where
P: AsRef<Path>,
{
ensure_wasmcloud_for_os_arch_pair(version, dir).await
}
pub async fn ensure_wasmcloud_for_os_arch_pair<P>(version: &str, dir: P) -> Result<PathBuf>
where
P: AsRef<Path>,
{
check_version(version)?;
if let Some(dir) = find_wasmcloud_binary(&dir, version).await {
return Ok(dir);
}
download_wasmcloud_for_os_arch_pair(version, dir).await
}
pub async fn download_wasmcloud<P>(version: &str, dir: P) -> Result<PathBuf>
where
P: AsRef<Path>,
{
download_wasmcloud_for_os_arch_pair(version, dir).await
}
pub async fn download_wasmcloud_for_os_arch_pair<P>(version: &str, dir: P) -> Result<PathBuf>
where
P: AsRef<Path>,
{
let url = wasmcloud_url(version);
let download_response = get_download_client()?.get(&url).send().await?;
if download_response.status() != StatusCode::OK {
bail!(
"failed to download wasmCloud host from {}. Status code: {}",
url,
download_response.status()
);
}
let burrito_bites_stream = download_response
.bytes_stream()
.map(|result| result.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)));
let mut wasmcloud_host_burrito = StreamReader::new(burrito_bites_stream);
let version_dir = dir.as_ref().join(version);
let file_path = version_dir.join(WASMCLOUD_HOST_BIN);
if let Some(parent_folder) = file_path.parent() {
create_dir_all(parent_folder).await?;
}
if let Ok(mut wasmcloud_file) = File::create(&file_path).await {
if file_path.file_name().is_some() {
#[cfg(target_family = "unix")]
{
let mut perms = wasmcloud_file.metadata().await?.permissions();
perms.set_mode(0o755);
wasmcloud_file.set_permissions(perms).await?;
}
}
tokio::io::copy(&mut wasmcloud_host_burrito, &mut wasmcloud_file).await?;
}
match find_wasmcloud_binary(&dir, version).await {
Some(path) => Ok(path),
None => bail!("wasmCloud was not installed successfully, please see logs"),
}
}
pub async fn start_wasmcloud_host<P, T, S>(
bin_path: P,
stdout: T,
stderr: S,
env_vars: HashMap<String, String>,
) -> Result<Child>
where
P: AsRef<Path>,
T: Into<Stdio>,
S: Into<Stdio>,
{
let mut cmd = Command::new(bin_path.as_ref());
let cmd = cmd
.stderr(stderr)
.stdout(stdout)
.stdin(Stdio::null())
.envs(&env_vars);
#[cfg(target_family = "unix")]
{
Ok(cmd.group_spawn()?.into_inner())
}
#[cfg(target_family = "windows")]
{
Ok(cmd.spawn()?)
}
}
pub async fn find_wasmcloud_binary<P>(dir: P, version: &str) -> Option<PathBuf>
where
P: AsRef<Path>,
{
let versioned_dir = dir.as_ref().join(version);
let bin_file = versioned_dir.join(WASMCLOUD_HOST_BIN);
metadata(&bin_file).await.is_ok().then_some(bin_file)
}
fn wasmcloud_url(version: &str) -> String {
#[cfg(target_os = "android")]
let os = "linux-android";
#[cfg(target_os = "macos")]
let os = "apple-darwin";
#[cfg(all(target_os = "linux", not(target_arch = "riscv64")))]
let os = "unknown-linux-musl";
#[cfg(all(target_os = "linux", target_arch = "riscv64"))]
let os = "unknown-linux-gnu";
#[cfg(target_os = "windows")]
let os = "pc-windows-msvc.exe";
format!(
"{WASMCLOUD_GITHUB_RELEASE_URL}/{version}/wasmcloud-{arch}-{os}",
arch = std::env::consts::ARCH
)
}
fn check_version(version: &str) -> Result<()> {
let version_req = semver::VersionReq::parse(&format!(">={MINIMUM_WASMCLOUD_VERSION}"))?;
match semver::Version::parse(version.trim_start_matches('v')) {
Ok(parsed_version) if !parsed_version.pre.is_empty() => {
warn!("Using prerelease version {} of wasmCloud", version);
Ok(())
}
Ok(parsed_version) if !version_req.matches(&parsed_version) => bail!(
"wasmCloud version {} is earlier than the minimum supported version of v{}",
version,
MINIMUM_WASMCLOUD_VERSION
),
Ok(_ver) => Ok(()),
Err(_parse_err) => {
warn!("Failed to parse wasmCloud version as a semantic version, download may fail");
Ok(())
}
}
}
#[cfg(test)]
mod test {
use super::{check_version, MINIMUM_WASMCLOUD_VERSION};
#[tokio::test]
async fn can_properly_deny_too_old_hosts() -> anyhow::Result<()> {
assert!(check_version("v0.81.0").is_ok());
assert!(check_version(MINIMUM_WASMCLOUD_VERSION).is_ok());
assert!(check_version("v0.81.0-rc1").is_ok());
assert!(check_version("v0.80.99").is_err());
if let Err(e) = check_version("v0.56.0") {
assert_eq!(e.to_string(), format!("wasmCloud version v0.56.0 is earlier than the minimum supported version of v{MINIMUM_WASMCLOUD_VERSION}"));
} else {
panic!("v0.56.0 should be before the minimum version")
}
assert!(check_version("ungabunga").is_ok());
assert!(check_version("v11.1").is_ok());
Ok(())
}
}