Skip to main content

hyperdb_bootstrap/
install.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! High-level [`install()`] entry point: resolves a release, downloads and
5//! verifies the archive, extracts the executable into
6//! `<dest_root>/<version_tag>/`, then refreshes the
7//! `<dest_root>/current/` pointer so downstream tooling can always find the
8//! active install at a stable path.
9
10use std::fs;
11use std::path::{Path, PathBuf};
12
13use crate::download::download_and_verify;
14use crate::extract::extract_hyperd;
15use crate::platform::Platform;
16use crate::release::PinnedRelease;
17use crate::scrape::scrape_latest;
18use crate::url::build_download_url;
19use crate::Error;
20
21/// Default directory (relative to CWD) used when [`InstallOptions::dest_root`]
22/// is left at its default. Laid out as
23/// `.hyperd/<version_tag>/hyperd[.exe]` plus a mirror at `.hyperd/current/`.
24pub const DEFAULT_DEST_ROOT: &str = ".hyperd";
25
26/// Which `hyperd` release [`install`] should resolve.
27///
28/// The default is [`VersionSource::Builtin`], which uses the pin baked into
29/// the crate at compile time. Other variants override that pin.
30#[derive(Debug, Clone, Default)]
31pub enum VersionSource {
32    /// Use the release baked into the crate at compile time.
33    #[default]
34    Builtin,
35    /// Load pinned metadata from a specific TOML file.
36    TomlFile(PathBuf),
37    /// Caller-supplied release (e.g. CLI `--version`/`--build-id` flags).
38    Explicit(PinnedRelease),
39    /// Best-effort scrape of the public releases page.
40    ScrapeLatest,
41}
42
43/// Configuration passed to [`install`].
44#[derive(Debug, Clone)]
45pub struct InstallOptions {
46    /// Root directory under which `<version_tag>/` and `current/` are created.
47    pub dest_root: PathBuf,
48    /// Which release to resolve. See [`VersionSource`].
49    pub version_source: VersionSource,
50    /// Override the host-platform auto-detection (useful for cross-platform
51    /// bootstrap in CI). `None` means "detect from the current host".
52    pub platform: Option<Platform>,
53    /// When `true`, re-download even if a cached install already exists.
54    pub force: bool,
55}
56
57impl Default for InstallOptions {
58    fn default() -> Self {
59        Self {
60            dest_root: PathBuf::from(DEFAULT_DEST_ROOT),
61            version_source: VersionSource::Builtin,
62            platform: None,
63            force: false,
64        }
65    }
66}
67
68/// Result of a successful [`install`] call.
69#[derive(Debug, Clone)]
70pub struct InstalledHyperd {
71    /// Absolute or relative path to the installed `hyperd` executable.
72    /// Always under `<dest_root>/current/`.
73    pub binary_path: PathBuf,
74    /// Installed release version (for example, `"0.0.24457"`).
75    pub version: String,
76    /// Installed release build id (for example, `"rc36858b6"`).
77    pub build_id: String,
78    /// Host platform this install targets.
79    pub platform: Platform,
80    /// `true` if the versioned directory already existed and we skipped the
81    /// download; `false` if we downloaded and extracted fresh bytes.
82    pub cache_hit: bool,
83}
84
85#[expect(
86    clippy::needless_pass_by_value,
87    reason = "call-site ergonomics: function consumes logically-owned parameters, refactoring signatures is not worth per-site churn"
88)]
89/// Downloads (if needed), verifies, and installs a `hyperd` executable
90/// according to `opts`.
91///
92/// This function is **blocking**. It performs HTTP I/O, ZIP extraction, and
93/// filesystem operations on the calling thread; it has no dependency on
94/// `tokio`.
95///
96/// # Errors
97///
98/// Returns any [`Error`] variant produced by the phases it drives —
99/// platform detection, release resolution (TOML parsing or scraping),
100/// download, checksum verification, ZIP extraction, or filesystem
101/// operations under `dest_root`.
102pub fn install(opts: InstallOptions) -> Result<InstalledHyperd, Error> {
103    let platform = match opts.platform {
104        Some(p) => p,
105        None => Platform::current()?,
106    };
107    let release = resolve_release(&opts.version_source, platform)?;
108    let version_tag = release.version_tag();
109    let versioned_dir = opts.dest_root.join(&version_tag);
110    let current_dir = opts.dest_root.join("current");
111    let exe_name = platform.executable_name();
112
113    let cache_hit = versioned_dir.join(exe_name).exists() && !opts.force;
114    if cache_hit {
115        tracing::info!(
116            dir = %versioned_dir.display(),
117            "cache hit, skipping download"
118        );
119    } else {
120        download_and_extract(&release, platform, &versioned_dir)?;
121    }
122
123    refresh_current(&current_dir, &versioned_dir, &version_tag)?;
124    let binary_path = current_dir.join(exe_name);
125    Ok(InstalledHyperd {
126        binary_path,
127        version: release.version,
128        build_id: release.build_id,
129        platform,
130        cache_hit,
131    })
132}
133
134fn resolve_release(source: &VersionSource, platform: Platform) -> Result<PinnedRelease, Error> {
135    match source {
136        VersionSource::Builtin => Ok(PinnedRelease::builtin()),
137        VersionSource::TomlFile(path) => PinnedRelease::from_toml_file(path),
138        VersionSource::Explicit(r) => Ok(r.clone()),
139        VersionSource::ScrapeLatest => scrape_latest(platform),
140    }
141}
142
143fn download_and_extract(
144    release: &PinnedRelease,
145    platform: Platform,
146    versioned_dir: &Path,
147) -> Result<(), Error> {
148    if versioned_dir.exists() {
149        fs::remove_dir_all(versioned_dir)
150            .map_err(|source| Error::io(format!("clearing {}", versioned_dir.display()), source))?;
151    }
152    fs::create_dir_all(versioned_dir)
153        .map_err(|source| Error::io(format!("creating {}", versioned_dir.display()), source))?;
154
155    let url = build_download_url(release, platform);
156    let tmp = tempfile::tempdir().map_err(|source| Error::io("creating temp dir", source))?;
157    let zip_path = tmp.path().join("hyperapi-java.zip");
158    download_and_verify(&url, release.sha256_for(platform), &zip_path)?;
159    extract_hyperd(&zip_path, versioned_dir)?;
160    Ok(())
161}
162
163fn refresh_current(current: &Path, source: &Path, version_tag: &str) -> Result<(), Error> {
164    // current/ is a fresh file copy every run — avoids Windows symlink
165    // privileges and keeps the Makefile auto-discovery path stable.
166    if current.exists() {
167        fs::remove_dir_all(current)
168            .map_err(|source| Error::io(format!("clearing {}", current.display()), source))?;
169    }
170    fs::create_dir_all(current)
171        .map_err(|source| Error::io(format!("creating {}", current.display()), source))?;
172    copy_dir_contents(source, current)?;
173    fs::write(current.join("VERSION"), version_tag)
174        .map_err(|source| Error::io(format!("writing {}/VERSION", current.display()), source))?;
175    Ok(())
176}
177
178fn copy_dir_contents(from: &Path, to: &Path) -> Result<(), Error> {
179    for entry in fs::read_dir(from)
180        .map_err(|source| Error::io(format!("reading {}", from.display()), source))?
181    {
182        let entry =
183            entry.map_err(|source| Error::io(format!("reading {}", from.display()), source))?;
184        let src = entry.path();
185        let dst = to.join(entry.file_name());
186        let ty = entry
187            .file_type()
188            .map_err(|source| Error::io(format!("stat {}", src.display()), source))?;
189        if ty.is_dir() {
190            fs::create_dir_all(&dst)
191                .map_err(|source| Error::io(format!("creating {}", dst.display()), source))?;
192            copy_dir_contents(&src, &dst)?;
193        } else {
194            fs::copy(&src, &dst).map_err(|source| {
195                Error::io(
196                    format!("copying {} -> {}", src.display(), dst.display()),
197                    source,
198                )
199            })?;
200            #[cfg(unix)]
201            {
202                use std::os::unix::fs::PermissionsExt;
203                if let Ok(meta) = fs::metadata(&src) {
204                    let mode = meta.permissions().mode();
205                    let _ = fs::set_permissions(&dst, fs::Permissions::from_mode(mode));
206                }
207            }
208        }
209    }
210    Ok(())
211}