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).map_err(|source| Error::Io {
150            context: format!("clearing {}", versioned_dir.display()),
151            source,
152        })?;
153    }
154    fs::create_dir_all(versioned_dir).map_err(|source| Error::Io {
155        context: format!("creating {}", versioned_dir.display()),
156        source,
157    })?;
158
159    let url = build_download_url(release, platform);
160    let tmp = tempfile::tempdir().map_err(|source| Error::Io {
161        context: "creating temp dir".to_string(),
162        source,
163    })?;
164    let zip_path = tmp.path().join("hyperapi-cxx.zip");
165    download_and_verify(&url, release.sha256_for(platform), &zip_path)?;
166    extract_hyperd(&zip_path, versioned_dir)?;
167    Ok(())
168}
169
170fn refresh_current(current: &Path, source: &Path, version_tag: &str) -> Result<(), Error> {
171    // current/ is a fresh file copy every run — avoids Windows symlink
172    // privileges and keeps the Makefile auto-discovery path stable.
173    if current.exists() {
174        fs::remove_dir_all(current).map_err(|source| Error::Io {
175            context: format!("clearing {}", current.display()),
176            source,
177        })?;
178    }
179    fs::create_dir_all(current).map_err(|source| Error::Io {
180        context: format!("creating {}", current.display()),
181        source,
182    })?;
183    copy_dir_contents(source, current)?;
184    fs::write(current.join("VERSION"), version_tag).map_err(|source| Error::Io {
185        context: format!("writing {}/VERSION", current.display()),
186        source,
187    })?;
188    Ok(())
189}
190
191fn copy_dir_contents(from: &Path, to: &Path) -> Result<(), Error> {
192    for entry in fs::read_dir(from).map_err(|source| Error::Io {
193        context: format!("reading {}", from.display()),
194        source,
195    })? {
196        let entry = entry.map_err(|source| Error::Io {
197            context: format!("reading {}", from.display()),
198            source,
199        })?;
200        let src = entry.path();
201        let dst = to.join(entry.file_name());
202        let ty = entry.file_type().map_err(|source| Error::Io {
203            context: format!("stat {}", src.display()),
204            source,
205        })?;
206        if ty.is_dir() {
207            fs::create_dir_all(&dst).map_err(|source| Error::Io {
208                context: format!("creating {}", dst.display()),
209                source,
210            })?;
211            copy_dir_contents(&src, &dst)?;
212        } else {
213            fs::copy(&src, &dst).map_err(|source| Error::Io {
214                context: format!("copying {} -> {}", src.display(), dst.display()),
215                source,
216            })?;
217            #[cfg(unix)]
218            {
219                use std::os::unix::fs::PermissionsExt;
220                if let Ok(meta) = fs::metadata(&src) {
221                    let mode = meta.permissions().mode();
222                    let _ = fs::set_permissions(&dst, fs::Permissions::from_mode(mode));
223                }
224            }
225        }
226    }
227    Ok(())
228}