use std::{
env::consts,
fs::{self, File},
io::BufReader,
path::{Path, PathBuf},
};
use anyhow::{anyhow, bail, Context, Result};
use serde_json;
use sha2::{Digest, Sha256};
use tempfile::{Builder, NamedTempFile, TempDir};
use crate::{
current::CurrentManager,
dirs::RialoDirs,
manifest::{Artifact, KnownPlatform, Manifest},
remote::{extract_archive, RemoteClient},
shims::ShimManager,
spec::{InstallSpec, ReleaseId},
};
pub struct ReleaseService {
dirs: RialoDirs,
remote: RemoteClient,
}
impl ReleaseService {
pub fn new(dirs: RialoDirs, remote: RemoteClient) -> Self {
Self { dirs, remote }
}
pub fn install(&self, spec: &InstallSpec, set_default: bool) -> Result<ReleaseId> {
eprintln!("Resolving manifest for {spec}");
let manifest = self.remote.fetch_manifest(spec)?;
eprintln!(
"Resolved {spec} -> {}@{}",
manifest.channel, manifest.version
);
let platform = match (consts::OS, consts::ARCH) {
("linux", "x86_64") => KnownPlatform::LinuxAmd64,
("linux", "aarch64") => KnownPlatform::LinuxArm64,
("macos", "aarch64") => KnownPlatform::DarwinArm64,
_ => bail!("no artifact for this platform"),
};
let artifact = manifest
.find_matching(platform)
.ok_or_else(|| anyhow!("no artifact for this platform"))?;
let release_id = ReleaseId::new(manifest.channel, manifest.version.clone());
let target_dir = self.release_dir(&release_id);
let archive_path = self.ensure_cached_artifact(artifact)?;
let staging = self.create_staging_dir()?;
extract_archive(&archive_path, staging.path())?;
self.write_manifest_into(staging.path(), &manifest)?;
self.swap_staging_into_place(staging, &target_dir)?;
if set_default {
let current_mgr = CurrentManager::new(self.dirs.current_file().clone());
let current = current_mgr.load()?;
if current.as_ref().is_some_and(|cur| cur == &release_id) {
eprintln!("{release_id} is already the active release");
} else {
eprintln!("Activating {release_id} as current");
self.update_current(&release_id, &manifest)?;
}
}
Ok(release_id)
}
pub fn use_existing(&self, id: &ReleaseId) -> Result<()> {
if !self.release_dir(id).exists() {
bail!("release {} is not installed", id);
}
let manifest = self.load_manifest(id)?;
self.update_current(id, &manifest)
}
pub fn uninstall(&self, id: &ReleaseId, force: bool) -> Result<()> {
let target_dir = self.release_dir(id);
if !target_dir.exists() {
bail!("release {} is not installed", id);
}
let current_mgr = CurrentManager::new(self.dirs.current_file().clone());
let current = current_mgr.load()?;
let is_current = current.as_ref().is_some_and(|cur| cur == id);
if is_current && !force {
bail!("{} is the active release; pass --force to remove it", id);
}
fs::remove_dir_all(&target_dir)
.with_context(|| format!("failed to remove release dir {}", target_dir.display()))?;
if is_current {
current_mgr.clear()?;
let shim_manager =
ShimManager::new(self.dirs.bin().clone(), self.dirs.releases().clone())?;
shim_manager.clear_all()?;
}
Ok(())
}
fn update_current(&self, id: &ReleaseId, manifest: &Manifest) -> Result<()> {
let current = CurrentManager::new(self.dirs.current_file().clone());
current.save(id)?;
let release_binaries: Vec<_> = manifest
.artifacts
.iter()
.flat_map(|a| &a.binaries)
.map(|x| x.as_str())
.collect();
let release_dir = self.release_dir(id);
let shim_manager = ShimManager::new(self.dirs.bin().clone(), self.dirs.releases().clone())?;
shim_manager.rewrite_shims(&release_binaries, &release_dir)?;
if let Some(tc_version) = manifest.rust_toolchain_version() {
self.update_rustup_link(tc_version)?;
}
Ok(())
}
fn update_rustup_link(&self, version: &str) -> Result<()> {
use rialo_build_lib::{RialoRustToolchain, Toolchain};
let toolchain = RialoRustToolchain::with_version(version)?;
if toolchain.is_installed()? {
eprintln!("Updating rustup link: cargo +rialo -> rialo-rust {version}");
toolchain.register_with_rustup()?;
} else {
eprintln!("Toolchain rialo-rust {version} not installed, skipping rustup link update");
eprintln!("Install it with: rialoman toolchain install rialo-rust --version {version}");
}
Ok(())
}
fn release_dir(&self, id: &ReleaseId) -> PathBuf {
self.dirs
.releases()
.join(id.channel.as_str())
.join(&id.version)
}
pub fn load_manifest(&self, id: &ReleaseId) -> Result<Manifest> {
let path = self.release_dir(id).join("manifest.json");
let contents = fs::read_to_string(&path)
.with_context(|| format!("failed to read manifest at {}", path.display()))?;
serde_json::from_str(&contents).context("failed to parse manifest")
}
fn ensure_cached_artifact(&self, artifact: &Artifact) -> Result<PathBuf> {
let cache_path = self.dirs.downloads().join(&artifact.archive);
if cache_path.exists() {
if Self::verify_sha256(&cache_path, &artifact.sha256)? {
return Ok(cache_path);
}
fs::remove_file(&cache_path).with_context(|| {
format!(
"failed to remove corrupt cached artifact {}",
cache_path.display()
)
})?;
}
self.download_into_cache(artifact, &cache_path)?;
Ok(cache_path)
}
fn download_into_cache(&self, artifact: &Artifact, cache_path: &Path) -> Result<()> {
let temp = NamedTempFile::new_in(self.dirs.downloads())
.context("failed to create temp download file")?;
eprintln!("Downloading {}", artifact.archive);
self.remote
.download_artifact(artifact, temp.path())
.context("artifact download failed")?;
eprintln!("Download complete: {}", artifact.archive);
temp.persist(cache_path).map_err(|err| {
anyhow!(
"failed to persist verified archive into cache {}: {}",
cache_path.display(),
err.error
)
})?;
Ok(())
}
fn verify_sha256(path: &Path, expected: &str) -> Result<bool> {
let file = File::open(path)
.with_context(|| format!("failed to open cached artifact {}", path.display()))?;
let mut file = BufReader::new(file);
let mut hasher = Sha256::new();
std::io::copy(&mut file, &mut hasher)?;
let digest = hex::encode(hasher.finalize());
Ok(digest.eq_ignore_ascii_case(expected))
}
fn create_staging_dir(&self) -> Result<TempDir> {
Builder::new()
.prefix("rialoman-staging-")
.tempdir_in(self.dirs.tmp())
.context("failed to create staging directory")
}
fn write_manifest_into(&self, staging_root: &Path, manifest: &Manifest) -> Result<()> {
let manifest_path = staging_root.join("manifest.json");
let manifest_json = serde_json::to_vec_pretty(manifest)?;
fs::write(&manifest_path, manifest_json).with_context(|| {
format!(
"failed to write manifest into staging dir {}",
manifest_path.display()
)
})
}
fn swap_staging_into_place(&self, staging: TempDir, target_dir: &Path) -> Result<()> {
let staging_path = staging.keep();
if target_dir.exists() {
fs::remove_dir_all(target_dir).with_context(|| {
format!("failed to remove release dir {}", target_dir.display())
})?;
}
fs::create_dir_all(target_dir)
.with_context(|| format!("failed to create a directory {}", target_dir.display()))?;
fs::rename(&staging_path, target_dir)
.with_context(|| format!("failed to move release into {}", target_dir.display()))
}
}