Skip to content

Commit 9937ee7

Browse files
committed
feat(hm-common): add AppRuntime behind app-runtime feature
Add a process-global runtime context, gated behind the opt-in `app-runtime` feature. AppRuntime::init() resolves SystemBins and captures the absolute cwd once, installing them in a OnceLock; the associated bins()/cwd() accessors read the global from anywhere with no threading, panicking only if used before init. init() fails fast with a typed InitError when a required executable is missing.
1 parent a33bca5 commit 9937ee7

3 files changed

Lines changed: 132 additions & 0 deletions

File tree

‎crates/hm-common/Cargo.toml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ categories = ["command-line-utilities"]
1111
[lib]
1212
path = "src/lib.rs"
1313

14+
[features]
15+
# Process-global application runtime (AppRuntime). Opt-in — only the binary
16+
# that owns process startup should enable it.
17+
app-runtime = []
18+
1419
[dependencies]
1520
anyhow = { workspace = true }
1621
async-trait = { workspace = true }
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//! Application runtime context.
2+
3+
use std::path::{Path, PathBuf};
4+
use std::sync::OnceLock;
5+
6+
use crate::process::{ExecutableNotFound, SystemBins};
7+
8+
/// Failure to initialize the [`AppRuntime`].
9+
#[derive(Debug, thiserror::Error)]
10+
pub enum InitError {
11+
/// A required executable was missing from `PATH`.
12+
#[error(transparent)]
13+
Executables(#[from] ExecutableNotFound),
14+
/// The current directory could not be read.
15+
#[error("reading the current directory")]
16+
Cwd(#[source] std::io::Error),
17+
}
18+
19+
/// Process-wide runtime context, resolved once at startup.
20+
///
21+
/// Install it with [`init`](Self::init), then read it from anywhere through the
22+
/// associated accessors — no threading required.
23+
#[derive(Debug)]
24+
pub struct AppRuntime {
25+
bins: SystemBins,
26+
cwd: PathBuf,
27+
}
28+
29+
static RUNTIME: OnceLock<AppRuntime> = OnceLock::new();
30+
31+
impl AppRuntime {
32+
/// Resolve the runtime and install it as the process-wide singleton.
33+
///
34+
/// Call once, early in `main`, before any accessor. A later call is ignored.
35+
///
36+
/// # Errors
37+
/// [`InitError`] if a required executable is missing from `PATH` or the
38+
/// current directory cannot be read.
39+
pub fn init() -> Result<(), InitError> {
40+
let runtime = Self::resolve()?;
41+
let _ = RUNTIME.set(runtime);
42+
Ok(())
43+
}
44+
45+
fn resolve() -> Result<Self, InitError> {
46+
Ok(Self {
47+
bins: SystemBins::resolve()?,
48+
cwd: std::env::current_dir().map_err(InitError::Cwd)?,
49+
})
50+
}
51+
52+
#[allow(
53+
clippy::expect_used,
54+
reason = "accessing the runtime before init is a startup bug, not a runtime error"
55+
)]
56+
fn get() -> &'static Self {
57+
RUNTIME
58+
.get()
59+
.expect("AppRuntime::init must be called before the runtime is accessed")
60+
}
61+
62+
/// The resolved system executables.
63+
///
64+
/// # Panics
65+
/// If [`init`](Self::init) has not been called.
66+
#[must_use]
67+
pub fn bins() -> &'static SystemBins {
68+
&Self::get().bins
69+
}
70+
71+
/// The absolute working directory captured at initialization.
72+
///
73+
/// # Panics
74+
/// If [`init`](Self::init) has not been called.
75+
#[must_use]
76+
pub fn cwd() -> &'static Path {
77+
&Self::get().cwd
78+
}
79+
}
80+
81+
#[cfg(test)]
82+
#[allow(
83+
clippy::unwrap_used,
84+
clippy::print_stderr,
85+
reason = "test setup, assertions, and skip diagnostics"
86+
)]
87+
mod tests {
88+
use super::*;
89+
use crate::process::{git, pathbin, python3};
90+
use rstest::rstest;
91+
92+
#[rstest]
93+
fn resolve_reports_toolchain_availability() {
94+
assert_eq!(
95+
AppRuntime::resolve().is_ok(),
96+
python3().is_ok() && git().is_ok()
97+
);
98+
}
99+
100+
#[rstest]
101+
fn resolve_captures_an_absolute_cwd() {
102+
let Ok(runtime) = AppRuntime::resolve() else {
103+
eprintln!("skipping: toolchain unavailable");
104+
return;
105+
};
106+
assert!(runtime.cwd.is_absolute(), "cwd was {:?}", runtime.cwd);
107+
}
108+
109+
#[rstest]
110+
fn init_installs_a_globally_accessible_runtime() {
111+
if AppRuntime::resolve().is_err() {
112+
eprintln!("skipping: toolchain unavailable");
113+
return;
114+
}
115+
AppRuntime::init().unwrap();
116+
assert!(AppRuntime::cwd().is_absolute());
117+
assert!(AppRuntime::bins().git().is_absolute());
118+
}
119+
120+
#[rstest]
121+
fn init_error_wraps_a_missing_executable() {
122+
let err: InitError = pathbin("hm-common-no-such-binary-xyz").unwrap_err().into();
123+
assert!(matches!(err, InitError::Executables(_)));
124+
}
125+
}

‎crates/hm-common/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
//! Harmont common utilities shared across the `hm` workspace.
22
3+
#[cfg(feature = "app-runtime")]
4+
pub mod app_runtime;
35
pub mod format;
46
pub mod fs;
57
pub mod process;

0 commit comments

Comments
 (0)