mod error;
use futures::StreamExt;
use starbase_archive::Archiver;
use starbase_styles::color;
use starbase_utils::fs::{self, FsError};
use std::cmp;
use std::env;
use std::env::consts;
use std::fmt::Debug;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use system_env::SystemLibc;
use tracing::{instrument, trace};
pub use error::ProtoInstallerError;
#[instrument]
pub fn determine_triple() -> miette::Result<String> {
let target = match (consts::OS, consts::ARCH) {
("linux", arch) => format!(
"{arch}-unknown-linux-{}",
if SystemLibc::is_musl() { "musl" } else { "gnu" }
),
("macos", arch) => format!("{arch}-apple-darwin"),
("windows", "x86_64") => "x86_64-pc-windows-msvc".to_owned(),
(os, arch) => {
return Err(ProtoInstallerError::InvalidPlatform {
arch: arch.to_owned(),
os: os.to_owned(),
}
.into());
}
};
Ok(target)
}
#[derive(Debug)]
pub struct DownloadResult {
pub archive_file: PathBuf,
pub file: String,
pub file_stem: String,
pub url: String,
}
#[instrument(skip(on_chunk))]
pub async fn download_release(
triple: &str,
version: &str,
temp_dir: impl AsRef<Path> + Debug,
on_chunk: impl Fn(u64, u64),
) -> miette::Result<DownloadResult> {
let target_ext = if cfg!(windows) { "zip" } else { "tar.xz" };
let target_file = format!("proto_cli-{triple}");
let download_file = format!("{target_file}.{target_ext}");
let download_url =
format!("https://github.com/moonrepo/proto/releases/download/v{version}/{download_file}");
trace!(
version,
triple,
"Downloading proto release from {}",
color::url(&download_url)
);
let handle_error = |error: reqwest::Error| ProtoInstallerError::DownloadFailed {
url: download_url.clone(),
error: Box::new(error),
};
let response = reqwest::Client::new()
.get(&download_url)
.send()
.await
.map_err(handle_error)?;
if !response.status().is_success() {
return Err(ProtoInstallerError::DownloadNotAvailable {
version: version.to_owned(),
status: Box::new(response.status()),
}
.into());
}
let total_size = response.content_length().unwrap_or(0);
on_chunk(0, total_size);
let archive_file = temp_dir.as_ref().join(&download_file);
let mut file = fs::create_file(&archive_file)?;
let mut stream = response.bytes_stream();
let mut downloaded: u64 = 0;
while let Some(item) = stream.next().await {
let chunk = item.map_err(handle_error)?;
file.write_all(&chunk).map_err(|error| FsError::Write {
path: archive_file.to_path_buf(),
error: Box::new(error),
})?;
downloaded = cmp::min(downloaded + (chunk.len() as u64), total_size);
on_chunk(downloaded, total_size);
}
Ok(DownloadResult {
archive_file,
file: download_file,
file_stem: target_file,
url: download_url,
})
}
#[instrument]
pub fn unpack_release(
download: DownloadResult,
install_dir: impl AsRef<Path> + Debug,
relocate_dir: impl AsRef<Path> + Debug,
relocate_current: bool,
) -> miette::Result<bool> {
let temp_dir = download
.archive_file
.parent()
.unwrap()
.join(&download.file_stem);
let install_dir = install_dir.as_ref();
let bin_names = if cfg!(windows) {
vec!["proto.exe", "proto-shim.exe"]
} else {
vec!["proto", "proto-shim"]
};
trace!(
source = ?download.archive_file,
target = ?temp_dir,
"Unpacking downloaded proto release"
);
Archiver::new(&temp_dir, &download.archive_file).unpack_from_ext()?;
let relocate = |current_path: &Path, relocate_path: &Path| -> miette::Result<()> {
trace!(
source = ?current_path,
target = ?relocate_path,
"Relocating old proto binary to a new versioned location",
);
fs::rename(current_path, relocate_path)?;
fs::write_file(
relocate_path.parent().unwrap().join(".last-used"),
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
.to_string(),
)?;
Ok(())
};
for bin_name in &bin_names {
let output_path = install_dir.join(bin_name);
let relocate_path = relocate_dir.as_ref().join(bin_name);
if output_path.exists() && output_path != relocate_path {
relocate(&output_path, &relocate_path)?;
}
if relocate_current {
if let Ok(current_exe) = env::current_exe() {
if current_exe != output_path
&& current_exe
.file_name()
.is_some_and(|name| name == *bin_name)
{
relocate(¤t_exe, &relocate_path)?;
}
}
}
}
let mut unpacked = false;
trace!(bin_dir = ?install_dir, "Moving unpacked proto binaries to the bin directory");
for bin_name in &bin_names {
let output_path = install_dir.join(bin_name);
let input_paths = vec![
temp_dir.join(&download.file_stem).join(bin_name),
temp_dir.join(bin_name),
];
for input_path in input_paths {
if input_path.exists() {
fs::copy_file(input_path, &output_path)?;
fs::update_perms(&output_path, None)?;
unpacked = true;
break;
}
}
}
fs::remove(temp_dir)?;
fs::remove(download.archive_file)?;
Ok(unpacked)
}