use std::fs;
use std::path::{Path, PathBuf};
use crate::download::download_and_verify;
use crate::extract::extract_hyperd;
use crate::platform::Platform;
use crate::release::PinnedRelease;
use crate::scrape::scrape_latest;
use crate::url::build_download_url;
use crate::Error;
pub const DEFAULT_DEST_ROOT: &str = ".hyperd";
#[derive(Debug, Clone, Default)]
pub enum VersionSource {
#[default]
Builtin,
TomlFile(PathBuf),
Explicit(PinnedRelease),
ScrapeLatest,
}
#[derive(Debug, Clone)]
pub struct InstallOptions {
pub dest_root: PathBuf,
pub version_source: VersionSource,
pub platform: Option<Platform>,
pub force: bool,
}
impl Default for InstallOptions {
fn default() -> Self {
Self {
dest_root: PathBuf::from(DEFAULT_DEST_ROOT),
version_source: VersionSource::Builtin,
platform: None,
force: false,
}
}
}
#[derive(Debug, Clone)]
pub struct InstalledHyperd {
pub binary_path: PathBuf,
pub version: String,
pub build_id: String,
pub platform: Platform,
pub cache_hit: bool,
}
#[expect(
clippy::needless_pass_by_value,
reason = "call-site ergonomics: function consumes logically-owned parameters, refactoring signatures is not worth per-site churn"
)]
pub fn install(opts: InstallOptions) -> Result<InstalledHyperd, Error> {
let platform = match opts.platform {
Some(p) => p,
None => Platform::current()?,
};
let release = resolve_release(&opts.version_source, platform)?;
let version_tag = release.version_tag();
let versioned_dir = opts.dest_root.join(&version_tag);
let current_dir = opts.dest_root.join("current");
let exe_name = platform.executable_name();
let cache_hit = versioned_dir.join(exe_name).exists() && !opts.force;
if cache_hit {
tracing::info!(
dir = %versioned_dir.display(),
"cache hit, skipping download"
);
} else {
download_and_extract(&release, platform, &versioned_dir)?;
}
refresh_current(¤t_dir, &versioned_dir, &version_tag)?;
let binary_path = current_dir.join(exe_name);
Ok(InstalledHyperd {
binary_path,
version: release.version,
build_id: release.build_id,
platform,
cache_hit,
})
}
fn resolve_release(source: &VersionSource, platform: Platform) -> Result<PinnedRelease, Error> {
match source {
VersionSource::Builtin => Ok(PinnedRelease::builtin()),
VersionSource::TomlFile(path) => PinnedRelease::from_toml_file(path),
VersionSource::Explicit(r) => Ok(r.clone()),
VersionSource::ScrapeLatest => scrape_latest(platform),
}
}
fn download_and_extract(
release: &PinnedRelease,
platform: Platform,
versioned_dir: &Path,
) -> Result<(), Error> {
if versioned_dir.exists() {
fs::remove_dir_all(versioned_dir).map_err(|source| Error::Io {
context: format!("clearing {}", versioned_dir.display()),
source,
})?;
}
fs::create_dir_all(versioned_dir).map_err(|source| Error::Io {
context: format!("creating {}", versioned_dir.display()),
source,
})?;
let url = build_download_url(release, platform);
let tmp = tempfile::tempdir().map_err(|source| Error::Io {
context: "creating temp dir".to_string(),
source,
})?;
let zip_path = tmp.path().join("hyperapi-cxx.zip");
download_and_verify(&url, release.sha256_for(platform), &zip_path)?;
extract_hyperd(&zip_path, versioned_dir)?;
Ok(())
}
fn refresh_current(current: &Path, source: &Path, version_tag: &str) -> Result<(), Error> {
if current.exists() {
fs::remove_dir_all(current).map_err(|source| Error::Io {
context: format!("clearing {}", current.display()),
source,
})?;
}
fs::create_dir_all(current).map_err(|source| Error::Io {
context: format!("creating {}", current.display()),
source,
})?;
copy_dir_contents(source, current)?;
fs::write(current.join("VERSION"), version_tag).map_err(|source| Error::Io {
context: format!("writing {}/VERSION", current.display()),
source,
})?;
Ok(())
}
fn copy_dir_contents(from: &Path, to: &Path) -> Result<(), Error> {
for entry in fs::read_dir(from).map_err(|source| Error::Io {
context: format!("reading {}", from.display()),
source,
})? {
let entry = entry.map_err(|source| Error::Io {
context: format!("reading {}", from.display()),
source,
})?;
let src = entry.path();
let dst = to.join(entry.file_name());
let ty = entry.file_type().map_err(|source| Error::Io {
context: format!("stat {}", src.display()),
source,
})?;
if ty.is_dir() {
fs::create_dir_all(&dst).map_err(|source| Error::Io {
context: format!("creating {}", dst.display()),
source,
})?;
copy_dir_contents(&src, &dst)?;
} else {
fs::copy(&src, &dst).map_err(|source| Error::Io {
context: format!("copying {} -> {}", src.display(), dst.display()),
source,
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(meta) = fs::metadata(&src) {
let mode = meta.permissions().mode();
let _ = fs::set_permissions(&dst, fs::Permissions::from_mode(mode));
}
}
}
}
Ok(())
}