use std::io::{Cursor};
use std::path::{Path};
use crate::lock::{FrateLock, LockedPackage};
use crate::shims::create_shim;
use crate::util::{ensure_frate_dirs, get_frate_dir};
use anyhow::{anyhow, bail, Result};
use colored::Colorize;
use sha2::Digest;
use verbosio::verbose;
use crate::{get_binary, is_cached};
use crate::global::cache::{cache_archive, get_cached_archive};
pub fn install_packages<P: AsRef<Path>>(lock: &FrateLock, project_root: P) -> Result<()> {
let frate_dir = ensure_frate_dirs(project_root)?;
for package in &lock.packages {
install_package(package, &frate_dir)?;
}
Ok(())
}
pub fn install_package(package: &LockedPackage, frate_dir: &Path) -> Result<()> {
let bin_dir = frate_dir.join("bin");
let shims_dir = frate_dir.join("shims");
let url = &package.source;
let dest_dir = bin_dir.join(&package.name);
std::fs::create_dir_all(&dest_dir)?;
if let Some(cached_path) = get_cached_archive(&package.source)? {
extract_cached(cached_path, dest_dir, &package.hash)?;
}
else {
download_and_extract(url, &dest_dir.to_string_lossy(), &package.hash)?;
}
let target_path = get_binary(&package.name)?
.ok_or(anyhow!("Binary not found: {}", package.name))?;
let shim_path = shims_dir.join(
target_path
.file_stem()
.ok_or_else(|| anyhow!("Invalid file name: {}", target_path.display()))?
);
create_shim(target_path, shim_path)?;
println!(" {} {}", "Installed".bold().green(), package.name);
Ok(())
}
pub fn uninstall_packages() -> Result<()> {
println!("{} {}", "Uninstalling".bold().yellow(), "all packages");
let frate_dir = get_frate_dir()?;
std::fs::remove_dir_all(frate_dir.join("bin"))?;
std::fs::remove_dir_all(frate_dir.join("shims"))?;
std::fs::create_dir_all(frate_dir.join("bin"))?;
std::fs::create_dir_all(frate_dir.join("shims"))?;
println!(" {}", "Done".bold().green());
Ok(())
}
pub fn uninstall_package(name: &str) -> Result<()> {
println!("{} {}", "Uninstalling".bold().yellow(), name);
let cwd = std::env::current_dir()?;
let frate_dir = cwd.join(".frate");
let bin_dir = frate_dir.join("bin");
let shims_dir = frate_dir.join("shims");
#[cfg(target_os = "windows")]
{
let bin_path = bin_dir.join(name);
if bin_path.exists() {
std::fs::remove_dir_all(bin_path)?;
}
let shim_path = shims_dir.join(format!("{name}.bat"));
if shim_path.exists() {
std::fs::remove_file(shim_path)?;
}
}
#[cfg(not(target_os = "windows"))]
{
let bin_path = bin_dir.join(name);
if bin_path.exists() {
std::fs::remove_dir_all(bin_path)?;
}
let shim_path = shims_dir.join(name);
if shim_path.exists() {
std::fs::remove_file(shim_path)?;
}
}
println!(" {}", "Done".bold().green());
Ok(())
}
pub fn download_and_extract(url: &str, dest_dir: &str, expected_hash: &str) -> Result<()> {
let expected_hash = crate::util::format_hash(expected_hash);
println!(" {} {}", "Downloading".bold().green(), url);
let response = reqwest::blocking::get(url)?;
if !response.status().is_success() {
bail!(" {} {}: {}", "Failed to download".bold().red(), url, response.status());
}
let bytes = response.bytes()?;
let mut hasher = sha2::Sha256::new();
hasher.update(&bytes);
let actual_hash = hex::encode(hasher.finalize());
if actual_hash != expected_hash {
bail!(" {}\n expected: {}\n got: {}", "Hash mismatch:".bold().red(), expected_hash, actual_hash);
}
println!(" {} {} to {}", "Extracting".bold().green(), url, dest_dir);
if url.ends_with(".zip") {
let reader = Cursor::new(&bytes);
let mut zip = zip::ZipArchive::new(reader)?;
zip.extract(dest_dir)?;
}
else if url.ends_with(".tar.gz") {
let tar = flate2::read::GzDecoder::new(Cursor::new(&bytes));
let mut archive = tar::Archive::new(tar);
archive.unpack(dest_dir)?;
}
else {
bail!("Unsupported archive type: {}", url.split(crate::util::PATH_SEPARATOR).last().unwrap_or(url));
}
if !is_cached(url)? {
println!(" {}", "Caching".bold().green());
cache_archive(url, bytes.as_ref())?;
}
Ok(())
}
pub fn extract_cached<P: AsRef<Path>>(
cached_path: P,
dest_dir: P,
expected_hash: &str
) -> Result<()> {
let expected_hash = crate::util::format_hash(expected_hash);
let archive_bytes = std::fs::read(&cached_path)?;
let mut hasher = sha2::Sha256::new();
hasher.update(&archive_bytes);
let actual_hash = hex::encode(hasher.finalize());
if actual_hash != expected_hash {
bail!(
" {}\n expected: {}\n got: {}\n for: {}",
"Hash mismatch:".bold().red(),
expected_hash,
actual_hash,
cached_path.as_ref().display()
);
}
let cached_path_str = cached_path.as_ref().to_string_lossy();
verbose!(" {} FROM CACHE {} to {}", "Extracting".bold().green(), cached_path.as_ref().display(), dest_dir.as_ref().display());
if cached_path_str.ends_with(".zip") {
let reader = Cursor::new(archive_bytes);
let mut zip = zip::ZipArchive::new(reader)?;
zip.extract(dest_dir)?;
}
else if cached_path_str.ends_with(".tar.gz") {
let tar = flate2::read::GzDecoder::new(Cursor::new(archive_bytes));
let mut archive = tar::Archive::new(tar);
archive.unpack(dest_dir)?;
}
else {
bail!("Unsupported archive type: {}",
dest_dir
.as_ref()
.display()
.to_string()
.split(crate::util::PATH_SEPARATOR)
.last()
.unwrap_or(
dest_dir.as_ref().display()
.to_string().split('.').next_back()
.unwrap_or(dest_dir.as_ref().display().to_string().as_str())
)
);
}
Ok(())
}