Skip to content

Commit 26ea658

Browse files
committed
more cleanup
1 parent 50fad19 commit 26ea658

34 files changed

Lines changed: 353 additions & 467 deletions

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub struct Git<'bin> {
2121
}
2222

2323
impl<'bin> Git<'bin> {
24-
/// Wrap a `git` executable, typically via `SysRuntime::git()`.
24+
/// Wrap a `git` executable, typically via `AppContext::git()`.
2525
#[must_use]
2626
pub const fn new(bin: &'bin Path) -> Self {
2727
Self { bin }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub struct Python<'bin> {
1515
}
1616

1717
impl<'bin> Python<'bin> {
18-
/// Wrap a `python3` executable, typically via `SysRuntime::python()`.
18+
/// Wrap a `python3` executable, typically via `AppContext::python()`.
1919
#[must_use]
2020
pub const fn new(bin: &'bin Path) -> Self {
2121
Self { bin }

‎crates/hm-core/src/app_context.rs‎

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
1-
//! Process-wide application context: resolved toolchain, directories, and the
2-
//! user-scoped config.
3-
//!
4-
//! Built once in `main` via [`AppContext::init`] and threaded through the
5-
//! application by shared reference (typically leaked to `&'static` so it can be
6-
//! held across `await` points).
1+
//! Process-wide application context: resolved toolchain, directories, and user
2+
//! config.
73
84
use std::path::{Path, PathBuf};
95

@@ -32,11 +28,7 @@ pub enum InitError {
3228
UserConfig(#[source] ConfigLoadingError),
3329
}
3430

35-
/// Resolved toolchain, platform directories, and the user config, captured once
36-
/// at startup.
37-
///
38-
/// Accessed by shared reference; not a global. A missing user config is not an
39-
/// error — [`AppContext::user_config`] returns `None`.
31+
/// Resolved toolchain, platform directories, and the user config.
4032
#[derive(Debug)]
4133
pub struct AppContext {
4234
git: PathBuf,
@@ -59,7 +51,7 @@ impl AppContext {
5951
let python3 = pathbin("python3")?;
6052
let cwd = std::env::current_dir().map_err(InitError::Cwd)?;
6153
let dirs = DirProvider::new().ok_or(InitError::Dirs)?;
62-
let user_config = Self::load_user_config(&Self::user_config_path(&dirs))
54+
let user_config = Self::load_user_config(&Self::user_config_file(&dirs))
6355
.await
6456
.map_err(InitError::UserConfig)?;
6557

@@ -73,8 +65,9 @@ impl AppContext {
7365
}
7466

7567
/// The user config path (`~/.hm/config.toml`).
76-
fn user_config_path(dirs: &DirProvider) -> PathBuf {
77-
dirs.home().join(".hm").join("config.toml")
68+
#[must_use]
69+
pub fn user_config_path(&self) -> PathBuf {
70+
Self::user_config_file(&self.dirs)
7871
}
7972

8073
/// Read the user config, treating a missing file as [`None`].
@@ -122,6 +115,11 @@ impl AppContext {
122115
pub fn python(&self) -> Python<'_> {
123116
Python::new(&self.python3)
124117
}
118+
119+
/// The user config path (`~/.hm/config.toml`) under `dirs`.
120+
fn user_config_file(dirs: &DirProvider) -> PathBuf {
121+
dirs.home().join(".hm").join("config.toml")
122+
}
125123
}
126124

127125
#[cfg(test)]

‎crates/hm-core/src/config/mod.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
//! Resolving the user and project config layers into one effective view.
22
//!
3-
//! [`UserConfig`] and [`ProjectConfig`] are sparse, all-optional layers loaded
4-
//! from their respective files. [`ResolvedProjectConfig`] is what the rest of
5-
//! the CLI reads: the two layers merged, project over user, with defaults
3+
//! [`UserConfig`] and [`ProjectConfig`] are sparse file layers;
4+
//! [`ResolvedProjectConfig`] is the two merged — project over user, defaults
65
//! applied.
76
87
pub mod creds;

‎crates/hm-core/src/config/user.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use std::path::Path;
44

5+
use anyhow::Context as _;
56
use serde::{Deserialize, Serialize};
67

78
use super::domain::{BackendConfig, BackendDomain, ConfigLoadingError};
@@ -36,4 +37,15 @@ impl UserConfig {
3637
let contents = tokio::fs::read_to_string(path).await?;
3738
Ok(toml::from_str(&contents)?)
3839
}
40+
41+
/// Serialize to `path`, creating parent directories as needed.
42+
///
43+
/// # Errors
44+
/// [`anyhow::Error`] if serialization or the write fails.
45+
pub async fn save(&self, path: &Path) -> anyhow::Result<()> {
46+
let serialized = toml::to_string_pretty(self).context("serializing user config")?;
47+
hm_common::fs::write_create_all(path, serialized)
48+
.await
49+
.with_context(|| format!("writing {}", path.display()))
50+
}
3951
}

‎crates/hm-core/src/exec/cloud/backend.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ pub struct CloudBackend {
3737
api_base: String,
3838
/// Dashboard (SPA) base used to build the human-clickable watch URL. This
3939
/// is the `app.` host, NOT `api.` — a link built from `api_base` lands on
40-
/// raw JSON. Resolved via [`crate::config::app_url`] at the call site.
40+
/// raw JSON. Derived from the configured domain's `app_url` at the call site.
4141
app_base: String,
4242
org: String,
4343
}
@@ -319,7 +319,7 @@ mod tests {
319319

320320
#[rstest]
321321
fn watch_url_uses_app_host_and_pipelines_path() {
322-
// Mirrors crate::config::app_url(DEFAULT_API_URL) -> https://app.harmont.dev.
322+
// Mirrors BackendDomain::default().app_url() -> https://app.harmont.dev.
323323
assert_eq!(
324324
dashboard_build_url("https://app.harmont.dev", "acme", "web", 42),
325325
"https://app.harmont.dev/acme/pipelines/web/builds/42"

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
//! Core of the `hm` CLI: layered configuration, the pluggable CI execution
2-
//! backends, and the application/project runtime context. Shared by the `hm`
3-
//! binary and `hm-plugin-cloud`.
1+
//! Core of the `hm` CLI.
42
//!
53
//! - [`config`] — user/project config resolution.
6-
//! - [`exec`] — the [`exec::ExecutionBackend`] trait and its local + cloud
7-
//! implementations.
8-
//! - [`app_context`] — the process-wide toolchain/directory/user-config context.
4+
//! - [`exec`] — the [`exec::ExecutionBackend`] trait and its backends.
5+
//! - [`app_context`] — process-wide toolchain, directories, and user config.
96
//! - [`project`] — a workspace and its resolved config.
107
118
pub mod app_context;

‎crates/hm-core/src/project.rs‎

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
//! A Harmont workspace and its resolved configuration.
22
//!
3-
//! A project is a directory tree rooted at a directory containing `.hm/`. A
4-
//! [`ProjectContext`] wraps that root, exposes its `.hm/` paths, and holds the
5-
//! [`ResolvedProjectConfig`] merged from the user and project layers at
6-
//! construction.
3+
//! A project is a directory tree rooted at a directory containing `.hm/`.
74
85
use std::path::{Path, PathBuf};
96

@@ -32,7 +29,7 @@ impl ProjectContext {
3229
start: &Path,
3330
user: Option<&UserConfig>,
3431
) -> Result<Option<Self>, ConfigLoadingError> {
35-
match find_project_root(start) {
32+
match Self::find_root(start) {
3633
Some(root) => Ok(Some(Self::at(root, user).await?)),
3734
None => Ok(None),
3835
}
@@ -45,7 +42,7 @@ impl ProjectContext {
4542
/// [`ConfigLoadingError`] if the project config file is present but
4643
/// unreadable or malformed.
4744
pub async fn at(path: PathBuf, user: Option<&UserConfig>) -> Result<Self, ConfigLoadingError> {
48-
let project = load_project_config(&config_path_for(&path)).await?;
45+
let project = Self::load_config(&Self::config_file(&path)).await?;
4946
let user = user.cloned().unwrap_or_default();
5047
let config = ResolvedProjectConfig::from_user_project(&user, &project);
5148
Ok(Self { path, config })
@@ -70,44 +67,41 @@ impl ProjectContext {
7067
/// The project config file path (`.hm/config.toml`).
7168
#[must_use]
7269
pub fn config_path(&self) -> PathBuf {
73-
config_path_for(&self.path)
70+
Self::config_file(&self.path)
7471
}
7572

7673
/// The resolved configuration for this workspace.
7774
#[must_use]
7875
pub const fn config(&self) -> &ResolvedProjectConfig {
7976
&self.config
8077
}
81-
}
8278

83-
/// The project config file path for a workspace root.
84-
fn config_path_for(root: &Path) -> PathBuf {
85-
root.join(".hm").join("config.toml")
86-
}
79+
/// The project config file path for a workspace root.
80+
fn config_file(root: &Path) -> PathBuf {
81+
root.join(".hm").join("config.toml")
82+
}
8783

88-
/// Walk up from `start` to the first directory containing `.hm/`.
89-
///
90-
/// Returns the directory *containing* `.hm/`, or `None` at the filesystem root.
91-
#[must_use]
92-
pub fn find_project_root(start: &Path) -> Option<PathBuf> {
93-
let mut current = start;
94-
loop {
95-
if current.join(".hm").is_dir() {
96-
return Some(current.to_path_buf());
84+
/// Walk up from `start` to the first directory containing `.hm/`.
85+
///
86+
/// Returns the directory *containing* `.hm/`, or `None` at the filesystem
87+
/// root.
88+
fn find_root(start: &Path) -> Option<PathBuf> {
89+
let mut current = start;
90+
loop {
91+
if current.join(".hm").is_dir() {
92+
return Some(current.to_path_buf());
93+
}
94+
current = current.parent()?;
9795
}
98-
current = current.parent()?;
9996
}
100-
}
10197

102-
/// Read a project config, treating a missing file as defaults.
103-
///
104-
/// # Errors
105-
/// [`ConfigLoadingError`] if the file is present but unreadable or malformed.
106-
async fn load_project_config(path: &Path) -> Result<ProjectConfig, ConfigLoadingError> {
107-
match tokio::fs::read_to_string(path).await {
108-
Ok(contents) => Ok(toml::from_str(&contents)?),
109-
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ProjectConfig::default()),
110-
Err(e) => Err(e.into()),
98+
/// Read a project config, treating a missing file as defaults.
99+
async fn load_config(path: &Path) -> Result<ProjectConfig, ConfigLoadingError> {
100+
match tokio::fs::read_to_string(path).await {
101+
Ok(contents) => Ok(toml::from_str(&contents)?),
102+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ProjectConfig::default()),
103+
Err(e) => Err(e.into()),
104+
}
111105
}
112106
}
113107

@@ -123,13 +117,16 @@ mod tests {
123117
std::fs::create_dir(tmp.path().join(".hm")).unwrap();
124118
let nested = tmp.path().join("src").join("deep");
125119
std::fs::create_dir_all(&nested).unwrap();
126-
assert_eq!(find_project_root(&nested), Some(tmp.path().to_path_buf()));
120+
assert_eq!(
121+
ProjectContext::find_root(&nested),
122+
Some(tmp.path().to_path_buf())
123+
);
127124
}
128125

129126
#[rstest]
130127
fn find_project_root_none_when_absent() {
131128
let tmp = tempfile::tempdir().unwrap();
132-
assert_eq!(find_project_root(tmp.path()), None);
129+
assert_eq!(ProjectContext::find_root(tmp.path()), None);
133130
}
134131

135132
#[rstest]

‎crates/hm-dsl-engine/src/python_engine.rs‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::path::Path;
22

33
use anyhow::{Context, Result};
44
use async_trait::async_trait;
5-
use hm_core::sys_runtime::SysRuntime;
65
use hm_common::process::CapturedStreams as _;
6+
use hm_core::app_context::AppContext;
77
use tracing::debug;
88

99
use crate::bundled_sources;
@@ -59,15 +59,16 @@ if match is None:
5959
print(json.dumps(match['definition']))
6060
";
6161

62-
#[derive(Debug, Default)]
63-
pub struct SubprocessPythonEngine;
62+
#[derive(Debug)]
63+
pub struct SubprocessPythonEngine<'app> {
64+
app: &'app AppContext,
65+
}
6466

65-
impl SubprocessPythonEngine {
66-
/// Create the engine. It runs `python3` through [`SysRuntime::python`], so
67-
/// [`SysRuntime::init`] must have been called first.
67+
impl<'app> SubprocessPythonEngine<'app> {
68+
/// Create the engine bound to `app`, whose resolved `python3` it runs.
6869
#[must_use]
69-
pub const fn new() -> Self {
70-
Self
70+
pub const fn new(app: &'app AppContext) -> Self {
71+
Self { app }
7172
}
7273

7374
async fn run_script(
@@ -80,7 +81,7 @@ impl SubprocessPythonEngine {
8081
let harmont_pkg = tmp.path().join("harmont");
8182
bundled_sources::extract_to(&bundled_sources::HARMONT_PY, &harmont_pkg)?;
8283

83-
let mut py = SysRuntime::python().program(script);
84+
let mut py = self.app.python().program(script);
8485
py.args(extra_args).current_dir(project_dir);
8586
py.pythonpath(tmp.path());
8687

@@ -91,7 +92,7 @@ impl SubprocessPythonEngine {
9192
}
9293

9394
#[async_trait]
94-
impl DslEngine for SubprocessPythonEngine {
95+
impl DslEngine for SubprocessPythonEngine<'_> {
9596
async fn list_pipelines(&self, project_dir: &Path) -> Result<Vec<PipelineMeta>> {
9697
let stdout = self
9798
.run_script(project_dir, LIST_PIPELINES_SCRIPT, &[])

‎crates/hm-dsl-engine/tests/python_engine_test.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ use hm_dsl_engine::DslEngine;
1010
#[tokio::test]
1111
async fn python_roundtrip() {
1212
// Skip if the toolchain (python3 + git) is unavailable.
13-
if hm_core::sys_runtime::SysRuntime::init().is_err() {
13+
let Ok(app) = hm_core::app_context::AppContext::init().await else {
1414
eprintln!("skipping: build toolchain unavailable");
1515
return;
16-
}
16+
};
1717

1818
let dir = tempfile::tempdir().unwrap();
1919
let harmont = dir.path().join(".hm");
@@ -31,7 +31,7 @@ def ci() -> hm.Step:
3131

3232
hm_dsl_engine::detect::check_python(dir.path()).unwrap();
3333

34-
let engine = hm_dsl_engine::SubprocessPythonEngine::new();
34+
let engine = hm_dsl_engine::SubprocessPythonEngine::new(&app);
3535
let metas = engine.list_pipelines(dir.path()).await.unwrap();
3636
assert_eq!(metas.len(), 1);
3737
assert_eq!(metas[0].slug, "ci");
@@ -43,10 +43,10 @@ def ci() -> hm.Step:
4343

4444
#[tokio::test]
4545
async fn python_registry_json_carries_triggers_and_allow_manual() {
46-
if hm_core::sys_runtime::SysRuntime::init().is_err() {
46+
let Ok(app) = hm_core::app_context::AppContext::init().await else {
4747
eprintln!("skipping: build toolchain unavailable");
4848
return;
49-
}
49+
};
5050

5151
let dir = tempfile::tempdir().unwrap();
5252
let harmont = dir.path().join(".hm");
@@ -62,7 +62,7 @@ def ci() -> hm.Step:
6262
)
6363
.unwrap();
6464

65-
let engine = hm_dsl_engine::SubprocessPythonEngine::new();
65+
let engine = hm_dsl_engine::SubprocessPythonEngine::new(&app);
6666
let json = engine.registry_json(dir.path()).await.unwrap();
6767
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
6868

0 commit comments

Comments
 (0)