commit baf73786aa77cca7a2d8f6bff7910d36a4323928
parent cb56b30efec4e2a52b2631f09d2a940e029a1db3
Author: triesap <tyson@radroots.org>
Date: Tue, 7 Apr 2026 23:25:42 +0000
paths: align rr-rs bootstrap defaults with runtime-paths
Diffstat:
15 files changed, 470 insertions(+), 41 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -2363,6 +2363,7 @@ dependencies = [
"nostr",
"radroots-events",
"radroots-runtime",
+ "radroots-runtime-paths",
"radroots-test-fixtures",
"secrecy",
"serde",
@@ -2406,6 +2407,7 @@ dependencies = [
"radroots-nostr",
"radroots-nostr-accounts",
"radroots-nostr-signer",
+ "radroots-runtime-paths",
"secrecy",
"serde",
"serde_json",
@@ -2604,6 +2606,7 @@ dependencies = [
"getrandom 0.2.17",
"radroots-log",
"radroots-protected-store",
+ "radroots-runtime-paths",
"radroots-secret-vault",
"serde",
"serde_json",
diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml
@@ -16,7 +16,7 @@ build = "build.rs"
[features]
default = ["std", "profile", "json-file", "nip49"]
-std = []
+std = ["dep:radroots-runtime-paths"]
profile = ["dep:radroots-events"]
json-file = ["std", "dep:radroots-runtime"]
nip49 = ["std", "nostr/nip49"]
@@ -26,6 +26,7 @@ ts-rs = ["dep:ts-rs"]
[dependencies]
radroots-runtime = { workspace = true, optional = true }
+radroots-runtime-paths = { workspace = true, optional = true }
radroots-events = { workspace = true, optional = true, default-features = false, features = [
"serde",
] }
diff --git a/crates/identity/src/error.rs b/crates/identity/src/error.rs
@@ -55,4 +55,8 @@ pub enum IdentityError {
#[cfg(all(feature = "std", feature = "json-file"))]
#[error(transparent)]
Store(#[from] RuntimeJsonError),
+
+ #[cfg(feature = "std")]
+ #[error(transparent)]
+ Paths(#[from] radroots_runtime_paths::RadrootsRuntimePathsError),
}
diff --git a/crates/identity/src/identity.rs b/crates/identity/src/identity.rs
@@ -15,12 +15,16 @@ use serde::{Deserialize, Serialize};
use alloc::string::String;
#[cfg(all(feature = "std", feature = "json-file"))]
use radroots_runtime::JsonFile;
+#[cfg(feature = "std")]
+use radroots_runtime_paths::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, default_shared_identity_path,
+};
#[cfg(all(feature = "std", feature = "json-file"))]
use std::path::PathBuf;
#[cfg(feature = "std")]
use std::{fs, path::Path};
-pub const DEFAULT_IDENTITY_PATH: &str = "identity.json";
+pub const DEFAULT_IDENTITY_PATH: &str = "default.json";
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RadrootsIdentityId(String);
@@ -438,6 +442,24 @@ impl RadrootsIdentity {
parse_identity_bytes(&bytes)
}
+ #[cfg(feature = "std")]
+ pub fn default_path() -> Result<PathBuf, IdentityError> {
+ Self::default_path_for(
+ &RadrootsPathResolver::current(),
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+ }
+
+ #[cfg(feature = "std")]
+ pub fn default_path_for(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+ ) -> Result<PathBuf, IdentityError> {
+ Ok(default_shared_identity_path(resolver, profile, overrides)?)
+ }
+
#[cfg(all(feature = "std", feature = "json-file"))]
pub fn load_or_generate<P: AsRef<Path>>(
path: Option<P>,
@@ -445,7 +467,8 @@ impl RadrootsIdentity {
) -> Result<Self, IdentityError> {
let path = path
.map(|p| p.as_ref().to_path_buf())
- .unwrap_or_else(|| PathBuf::from(DEFAULT_IDENTITY_PATH));
+ .map(Ok)
+ .unwrap_or_else(Self::default_path)?;
if path.exists() {
return Self::load_from_path_auto(&path);
}
diff --git a/crates/identity/tests/identity.rs b/crates/identity/tests/identity.rs
@@ -7,6 +7,10 @@ use radroots_identity::{
use radroots_identity::{
RadrootsIdentityEncryptedSecretKeyOptions, RadrootsIdentityEncryptedSecretKeySecurity,
};
+use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform,
+};
use radroots_test_fixtures::{ApprovedFixtureIdentity, FIXTURE_ALICE, FIXTURE_BOB};
use std::path::PathBuf;
@@ -495,23 +499,43 @@ fn load_or_generate_reports_save_failure_when_parent_not_writable() {
#[test]
fn load_or_generate_uses_default_path_when_missing() {
- let original = std::env::current_dir().unwrap();
- let dir = tempfile::tempdir().unwrap();
- std::env::set_current_dir(dir.path()).unwrap();
-
- let denied = RadrootsIdentity::load_or_generate::<&std::path::Path>(None, false).unwrap_err();
- assert!(
- matches!(denied, IdentityError::GenerationNotAllowed(path) if path == PathBuf::from(DEFAULT_IDENTITY_PATH))
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
);
+ let default_path = RadrootsIdentity::default_path_for(
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+ .unwrap();
+
+ let denied = RadrootsIdentity::load_or_generate::<&std::path::Path>(Some(&default_path), false)
+ .unwrap_err();
+ assert!(matches!(denied, IdentityError::GenerationNotAllowed(path) if path == default_path));
+ assert_eq!(
+ default_path.file_name().and_then(std::ffi::OsStr::to_str),
+ Some(DEFAULT_IDENTITY_PATH)
+ );
+ assert_eq!(
+ default_path,
+ PathBuf::from("/home/treesap/.radroots/secrets/shared/identities/default.json")
+ );
+}
- let generated = RadrootsIdentity::load_or_generate::<&std::path::Path>(None, true).unwrap();
+#[test]
+fn load_or_generate_creates_at_explicit_default_path() {
+ let dir = tempfile::tempdir().unwrap();
let default_path = dir.path().join(DEFAULT_IDENTITY_PATH);
+ let generated =
+ RadrootsIdentity::load_or_generate::<&std::path::Path>(Some(&default_path), true).unwrap();
assert!(default_path.exists());
let loaded = RadrootsIdentity::load_from_path_auto(&default_path).unwrap();
assert_eq!(generated.public_key(), loaded.public_key());
-
- std::env::set_current_dir(original).unwrap();
}
#[test]
diff --git a/crates/net-core/Cargo.toml b/crates/net-core/Cargo.toml
@@ -31,7 +31,7 @@ nostr-client = [
"dep:radroots-nostr",
]
directories = ["std", "dep:directories"]
-fs-persistence = ["std"]
+fs-persistence = ["std", "dep:radroots-runtime-paths"]
[dependencies]
radroots-events = { workspace = true, optional = true, default-features = true, features = [
@@ -52,6 +52,7 @@ radroots-nostr = { workspace = true, optional = true, default-features = true, f
] }
directories = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
+radroots-runtime-paths = { workspace = true, optional = true }
secrecy = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }
diff --git a/crates/net-core/src/keys.rs b/crates/net-core/src/keys.rs
@@ -7,6 +7,10 @@ use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecp256k1SecretKey, RadrootsNostrSecretKey,
RadrootsNostrToBech32,
};
+#[cfg(all(feature = "nostr-client", feature = "fs-persistence"))]
+use radroots_runtime_paths::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, default_shared_identity_path,
+};
#[cfg(feature = "nostr-client")]
use serde::Deserialize;
#[cfg(feature = "nostr-client")]
@@ -262,13 +266,16 @@ impl KeysManager {
self.state.npub.clone()
}
- #[cfg(all(feature = "directories", feature = "fs-persistence"))]
+ #[cfg(feature = "fs-persistence")]
pub fn default_key_path() -> Option<PathBuf> {
- directories::ProjectDirs::from("com", "Radroots", "radroots")
- .map(|d| d.config_dir().join("identity.json"))
+ default_key_path_for(
+ &RadrootsPathResolver::current(),
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
}
- #[cfg(all(feature = "directories", feature = "fs-persistence"))]
+ #[cfg(feature = "fs-persistence")]
pub fn persist_best_practice(&self) -> Result<PathBuf> {
let path = Self::default_key_path().ok_or(NetError::PersistenceUnsupported)?;
if path.exists() {
@@ -278,7 +285,7 @@ impl KeysManager {
Ok(path)
}
- #[cfg(not(all(feature = "directories", feature = "fs-persistence")))]
+ #[cfg(not(feature = "fs-persistence"))]
pub fn persist_best_practice(&self) -> Result<PathBuf> {
Err(NetError::PersistenceUnsupported)
}
@@ -288,11 +295,11 @@ impl KeysManager {
let path = if let Some(p) = &cfg.path {
p.clone()
} else {
- #[cfg(all(feature = "directories", feature = "fs-persistence"))]
+ #[cfg(feature = "fs-persistence")]
{
Self::default_key_path().ok_or(NetError::PersistenceUnsupported)?
}
- #[cfg(not(all(feature = "directories", feature = "fs-persistence")))]
+ #[cfg(not(feature = "fs-persistence"))]
{
return Err(NetError::PersistenceUnsupported);
}
@@ -307,6 +314,15 @@ impl KeysManager {
}
}
+#[cfg(all(feature = "nostr-client", feature = "fs-persistence"))]
+fn default_key_path_for(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+) -> Option<PathBuf> {
+ default_shared_identity_path(resolver, profile, overrides).ok()
+}
+
#[cfg(feature = "nostr-client")]
fn write_secret_atomically_noclobber(path: &Path, data: &[u8]) -> crate::error::Result<()> {
use std::io::Write;
@@ -336,3 +352,44 @@ fn write_secret_atomically_noclobber(path: &Path, data: &[u8]) -> crate::error::
Ok(())
}
+
+#[cfg(all(test, feature = "nostr-client", feature = "fs-persistence"))]
+mod tests {
+ use std::path::PathBuf;
+
+ use radroots_identity::RadrootsIdentity;
+ use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform,
+ };
+
+ use super::default_key_path_for;
+
+ #[test]
+ fn default_key_path_matches_identity_default_path() {
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+ let overrides = RadrootsPathOverrides::default();
+
+ let net_core_path =
+ default_key_path_for(&resolver, RadrootsPathProfile::InteractiveUser, &overrides)
+ .expect("net-core default key path should resolve");
+ let identity_path = RadrootsIdentity::default_path_for(
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &overrides,
+ )
+ .expect("identity default path should resolve");
+
+ assert_eq!(net_core_path, identity_path);
+ assert_eq!(
+ net_core_path,
+ PathBuf::from("/home/treesap/.radroots/secrets/shared/identities/default.json")
+ );
+ }
+}
diff --git a/crates/runtime-paths/src/conventions.rs b/crates/runtime-paths/src/conventions.rs
@@ -0,0 +1,136 @@
+use std::path::PathBuf;
+
+use crate::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace,
+ RadrootsRuntimePathsError,
+};
+
+pub const DEFAULT_CONFIG_FILE_NAME: &str = "config.toml";
+pub const DEFAULT_SERVICE_IDENTITY_FILE_NAME: &str = "identity.secret.json";
+pub const DEFAULT_SHARED_IDENTITY_FILE_NAME: &str = "default.json";
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct RadrootsBootstrapPaths {
+ pub config_path: PathBuf,
+ pub logs_dir: PathBuf,
+ pub identity_path: PathBuf,
+}
+
+pub fn default_namespaced_bootstrap_paths(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+ namespace: &RadrootsRuntimeNamespace,
+ identity_file_name: &str,
+) -> Result<RadrootsBootstrapPaths, RadrootsRuntimePathsError> {
+ let namespaced = resolver.resolve(profile, overrides)?.namespaced(namespace);
+ Ok(RadrootsBootstrapPaths {
+ config_path: namespaced.config.join(DEFAULT_CONFIG_FILE_NAME),
+ logs_dir: namespaced.logs,
+ identity_path: namespaced.secrets.join(identity_file_name),
+ })
+}
+
+pub fn default_shared_identity_path(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+) -> Result<PathBuf, RadrootsRuntimePathsError> {
+ let namespace = RadrootsRuntimeNamespace::shared("identities")?;
+ let namespaced = resolver.resolve(profile, overrides)?.namespaced(&namespace);
+ Ok(namespaced.secrets.join(DEFAULT_SHARED_IDENTITY_FILE_NAME))
+}
+
+pub fn default_shared_runtime_logs_dir(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+) -> Result<PathBuf, RadrootsRuntimePathsError> {
+ let namespace = RadrootsRuntimeNamespace::shared("runtime")?;
+ let namespaced = resolver.resolve(profile, overrides)?.namespaced(&namespace);
+ Ok(namespaced.logs)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use crate::{RadrootsHostEnvironment, RadrootsPlatform, RadrootsRuntimeNamespace};
+
+ use super::{
+ DEFAULT_SERVICE_IDENTITY_FILE_NAME, DEFAULT_SHARED_IDENTITY_FILE_NAME,
+ default_namespaced_bootstrap_paths, default_shared_identity_path,
+ default_shared_runtime_logs_dir,
+ };
+
+ #[test]
+ fn namespaced_bootstrap_paths_use_canonical_interactive_roots() {
+ let resolver = crate::RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+ let namespace =
+ RadrootsRuntimeNamespace::service("radrootsd").expect("service namespace should parse");
+
+ let paths = default_namespaced_bootstrap_paths(
+ &resolver,
+ crate::RadrootsPathProfile::InteractiveUser,
+ &crate::RadrootsPathOverrides::default(),
+ &namespace,
+ DEFAULT_SERVICE_IDENTITY_FILE_NAME,
+ )
+ .expect("service bootstrap paths should resolve");
+
+ assert_eq!(
+ paths.config_path,
+ PathBuf::from("/home/treesap/.radroots/config/services/radrootsd/config.toml")
+ );
+ assert_eq!(
+ paths.logs_dir,
+ PathBuf::from("/home/treesap/.radroots/logs/services/radrootsd")
+ );
+ assert_eq!(
+ paths.identity_path,
+ PathBuf::from(
+ "/home/treesap/.radroots/secrets/services/radrootsd/identity.secret.json"
+ )
+ );
+ }
+
+ #[test]
+ fn shared_defaults_use_shared_namespaces() {
+ let resolver = crate::RadrootsPathResolver::new(
+ RadrootsPlatform::Macos,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/Users/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+
+ let identity_path = default_shared_identity_path(
+ &resolver,
+ crate::RadrootsPathProfile::InteractiveUser,
+ &crate::RadrootsPathOverrides::default(),
+ )
+ .expect("shared identity path should resolve");
+ assert_eq!(
+ identity_path,
+ PathBuf::from("/Users/treesap/.radroots/secrets/shared/identities")
+ .join(DEFAULT_SHARED_IDENTITY_FILE_NAME)
+ );
+
+ let logs_dir = default_shared_runtime_logs_dir(
+ &resolver,
+ crate::RadrootsPathProfile::InteractiveUser,
+ &crate::RadrootsPathOverrides::default(),
+ )
+ .expect("shared runtime logs dir should resolve");
+ assert_eq!(
+ logs_dir,
+ PathBuf::from("/Users/treesap/.radroots/logs/shared/runtime")
+ );
+ }
+}
diff --git a/crates/runtime-paths/src/lib.rs b/crates/runtime-paths/src/lib.rs
@@ -1,10 +1,16 @@
#![forbid(unsafe_code)]
+pub mod conventions;
pub mod error;
pub mod namespace;
pub mod platform;
pub mod roots;
+pub use conventions::{
+ DEFAULT_CONFIG_FILE_NAME, DEFAULT_SERVICE_IDENTITY_FILE_NAME,
+ DEFAULT_SHARED_IDENTITY_FILE_NAME, RadrootsBootstrapPaths, default_namespaced_bootstrap_paths,
+ default_shared_identity_path, default_shared_runtime_logs_dir,
+};
pub use error::RadrootsRuntimePathsError;
pub use namespace::{RadrootsRuntimeNamespace, RadrootsRuntimeNamespaceKind};
pub use platform::{RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPlatform};
diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml
@@ -25,6 +25,7 @@ config = { workspace = true }
getrandom = { workspace = true }
radroots-log = { workspace = true, features = ["std"] }
radroots-protected-store = { workspace = true, features = ["std"] }
+radroots-runtime-paths = { workspace = true }
radroots-secret-vault = { workspace = true, features = ["std"] }
serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/crates/runtime/src/cli.rs b/crates/runtime/src/cli.rs
@@ -14,7 +14,7 @@ where
FP: Fn(&Args) -> Option<&Path>,
{
let args = Args::try_parse().map_err(RuntimeCliError::from)?;
- let path = resolve_path(path_of(&args));
+ let path = resolve_path(path_of(&args))?;
let cfg = crate::config::load_required_file::<C>(&path)?;
Ok((args, cfg))
}
@@ -31,7 +31,7 @@ where
FO: Fn(&Args) -> Option<Map<String, Value>>,
{
let args = Args::try_parse().map_err(RuntimeCliError::from)?;
- let path = resolve_path(path_of(&args));
+ let path = resolve_path(path_of(&args))?;
let cfg = crate::config::load_required_file_with_env_and_overrides::<C>(
&path,
env_prefix,
@@ -80,6 +80,27 @@ where
}
#[inline]
-fn resolve_path(p: Option<&Path>) -> PathBuf {
- p.unwrap_or_else(|| Path::new("config.toml")).to_path_buf()
+fn resolve_path(p: Option<&Path>) -> Result<PathBuf, RuntimeCliError> {
+ p.map(Path::to_path_buf)
+ .ok_or(RuntimeCliError::MissingConfigPath)
+}
+
+#[cfg(test)]
+mod tests {
+ use std::path::PathBuf;
+
+ use super::resolve_path;
+ use crate::RuntimeCliError;
+
+ #[test]
+ fn resolve_path_requires_explicit_path() {
+ let err = resolve_path(None).expect_err("missing path should error");
+ assert!(matches!(err, RuntimeCliError::MissingConfigPath));
+
+ let path = PathBuf::from("/tmp/config.toml");
+ assert_eq!(
+ resolve_path(Some(path.as_path())).expect("path should resolve"),
+ path
+ );
+ }
}
diff --git a/crates/runtime/src/error.rs b/crates/runtime/src/error.rs
@@ -15,12 +15,18 @@ pub enum RuntimeConfigError {
pub enum RuntimeCliError {
#[error(transparent)]
Parse(#[from] clap::Error),
+
+ #[error("configuration path is required; no implicit cwd-rooted default is used")]
+ MissingConfigPath,
}
#[derive(Debug, Error)]
pub enum RuntimeTracingError {
#[error(transparent)]
Log(#[from] radroots_log::Error),
+
+ #[error(transparent)]
+ Paths(#[from] radroots_runtime_paths::RadrootsRuntimePathsError),
}
#[derive(Debug, Error)]
@@ -105,6 +111,15 @@ mod tests {
let runtime_from_tracing: RuntimeError = tracing.into();
assert!(runtime_from_tracing.to_string().contains("log-failure"));
assert!(runtime_from_tracing.source().is_none());
+
+ let paths = RuntimeTracingError::from(
+ radroots_runtime_paths::RadrootsRuntimePathsError::MissingHomeDir {
+ platform: radroots_runtime_paths::RadrootsPlatform::Linux,
+ },
+ );
+ let runtime_from_paths: RuntimeError = paths.into();
+ assert!(runtime_from_paths.to_string().contains("home directory"));
+ assert!(runtime_from_paths.source().is_none());
}
#[test]
diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs
@@ -27,9 +27,14 @@ pub use error::{RuntimeConfigError, RuntimeError, RuntimeTracingError};
pub use json::{JsonFile, JsonWriteOptions, RuntimeJsonError};
pub use secret_file::{local_wrapping_key_path, open_local_secret_file, seal_local_secret_file};
-pub use service::DEFAULT_SERVICE_IDENTITY_PATH;
pub use service::RadrootsNostrServiceConfig;
#[cfg(feature = "cli")]
pub use service::RadrootsServiceCliArgs;
+pub use service::{
+ DEFAULT_SERVICE_IDENTITY_PATH, default_service_bootstrap_paths, default_service_config_path,
+ default_service_identity_path, default_service_logs_dir, service_bootstrap_paths_for,
+};
pub use signals::shutdown_signal;
-pub use tracing::{init, init_with};
+pub use tracing::{
+ default_shared_runtime_logs_dir, default_shared_runtime_logs_dir_for, init, init_with,
+};
diff --git a/crates/runtime/src/service.rs b/crates/runtime/src/service.rs
@@ -1,11 +1,56 @@
use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
#[cfg(feature = "cli")]
use clap::{ArgAction, Args, ValueHint};
-#[cfg(feature = "cli")]
-use std::path::PathBuf;
+use radroots_runtime_paths::{
+ DEFAULT_SERVICE_IDENTITY_FILE_NAME, RadrootsBootstrapPaths, RadrootsPathOverrides,
+ RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimeNamespace, RadrootsRuntimePathsError,
+ default_namespaced_bootstrap_paths,
+};
+
+pub const DEFAULT_SERVICE_IDENTITY_PATH: &str = DEFAULT_SERVICE_IDENTITY_FILE_NAME;
+
+pub fn service_bootstrap_paths_for(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+ runtime_id: &str,
+) -> Result<RadrootsBootstrapPaths, RadrootsRuntimePathsError> {
+ let namespace = RadrootsRuntimeNamespace::service(runtime_id)?;
+ default_namespaced_bootstrap_paths(
+ resolver,
+ profile,
+ overrides,
+ &namespace,
+ DEFAULT_SERVICE_IDENTITY_PATH,
+ )
+}
-pub const DEFAULT_SERVICE_IDENTITY_PATH: &str = "identity.secret.json";
+pub fn default_service_bootstrap_paths(
+ runtime_id: &str,
+) -> Result<RadrootsBootstrapPaths, RadrootsRuntimePathsError> {
+ service_bootstrap_paths_for(
+ &RadrootsPathResolver::current(),
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ runtime_id,
+ )
+}
+
+pub fn default_service_config_path(runtime_id: &str) -> Result<PathBuf, RadrootsRuntimePathsError> {
+ Ok(default_service_bootstrap_paths(runtime_id)?.config_path)
+}
+
+pub fn default_service_logs_dir(runtime_id: &str) -> Result<PathBuf, RadrootsRuntimePathsError> {
+ Ok(default_service_bootstrap_paths(runtime_id)?.logs_dir)
+}
+
+pub fn default_service_identity_path(
+ runtime_id: &str,
+) -> Result<PathBuf, RadrootsRuntimePathsError> {
+ Ok(default_service_bootstrap_paths(runtime_id)?.identity_path)
+}
#[cfg(feature = "cli")]
#[derive(Args, Debug, Clone)]
@@ -14,16 +59,15 @@ pub struct RadrootsServiceCliArgs {
long,
value_name = "PATH",
value_hint = ValueHint::FilePath,
- default_value = "config.toml",
- help = "Path to the daemon configuration file (defaults to config.toml)"
+ help = "Path to the daemon configuration file; no implicit cwd-rooted default is used"
)]
- pub config: PathBuf,
+ pub config: Option<PathBuf>,
#[arg(
long,
value_name = "PATH",
value_hint = ValueHint::FilePath,
- help = "Path to the daemon encrypted identity envelope; generated identities default to identity.secret.json with a sibling .key wrapping key file"
+ help = "Path to the daemon encrypted identity envelope; callers may resolve a canonical namespaced default ending in identity.secret.json with a sibling .key wrapping key file"
)]
pub identity: Option<PathBuf>,
@@ -48,7 +92,14 @@ pub struct RadrootsNostrServiceConfig {
#[cfg(test)]
mod tests {
- use super::RadrootsNostrServiceConfig;
+ use std::path::PathBuf;
+
+ use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform,
+ };
+
+ use super::{RadrootsNostrServiceConfig, service_bootstrap_paths_for};
#[test]
fn service_config_defaults_optional_fields() {
@@ -64,4 +115,38 @@ logs_dir = "logs"
assert_eq!(cfg.nip89_identifier, None);
assert!(cfg.nip89_extra_tags.is_empty());
}
+
+ #[test]
+ fn service_bootstrap_paths_follow_runtime_paths_contract() {
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Linux,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/home/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+
+ let paths = service_bootstrap_paths_for(
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ "radrootsd",
+ )
+ .expect("service bootstrap paths should resolve");
+
+ assert_eq!(
+ paths.config_path,
+ PathBuf::from("/home/treesap/.radroots/config/services/radrootsd/config.toml")
+ );
+ assert_eq!(
+ paths.logs_dir,
+ PathBuf::from("/home/treesap/.radroots/logs/services/radrootsd")
+ );
+ assert_eq!(
+ paths.identity_path,
+ PathBuf::from(
+ "/home/treesap/.radroots/secrets/services/radrootsd/identity.secret.json"
+ )
+ );
+ }
}
diff --git a/crates/runtime/src/tracing.rs b/crates/runtime/src/tracing.rs
@@ -1,10 +1,31 @@
use radroots_log::{LogFileLayout, LoggingOptions};
+use radroots_runtime_paths::{
+ RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimePathsError,
+ default_shared_runtime_logs_dir as resolve_shared_runtime_logs_dir,
+};
use std::path::{Path, PathBuf};
use crate::error::RuntimeTracingError;
pub fn init() -> Result<(), RuntimeTracingError> {
- init_with("logs", None)
+ let logs_dir = default_shared_runtime_logs_dir()?;
+ init_with(logs_dir, None)
+}
+
+pub fn default_shared_runtime_logs_dir() -> Result<PathBuf, RadrootsRuntimePathsError> {
+ default_shared_runtime_logs_dir_for(
+ &RadrootsPathResolver::current(),
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+}
+
+pub fn default_shared_runtime_logs_dir_for(
+ resolver: &RadrootsPathResolver,
+ profile: RadrootsPathProfile,
+ overrides: &RadrootsPathOverrides,
+) -> Result<PathBuf, RadrootsRuntimePathsError> {
+ resolve_shared_runtime_logs_dir(resolver, profile, overrides)
}
pub fn init_with(
@@ -113,9 +134,14 @@ fn resolve_default_level(env_level: Option<String>, default_level: Option<&str>)
#[cfg(test)]
mod tests {
use super::{
- default_log_file_name, default_log_file_name_from_exe_name, env_path, env_value, init,
- init_with, log_name_from_path, log_name_from_stem, normalize_env_value,
- resolve_default_level, resolve_log_dir, test_hooks,
+ default_log_file_name, default_log_file_name_from_exe_name,
+ default_shared_runtime_logs_dir_for, env_path, env_value, init_with, log_name_from_path,
+ log_name_from_stem, normalize_env_value, resolve_default_level, resolve_log_dir,
+ test_hooks,
+ };
+ use radroots_runtime_paths::{
+ RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
+ RadrootsPlatform,
};
use std::path::{Path, PathBuf};
use tempfile::tempdir;
@@ -192,6 +218,29 @@ mod tests {
}
#[test]
+ fn default_shared_runtime_logs_dir_uses_shared_namespace() {
+ let resolver = RadrootsPathResolver::new(
+ RadrootsPlatform::Macos,
+ RadrootsHostEnvironment {
+ home_dir: Some(PathBuf::from("/Users/treesap")),
+ ..RadrootsHostEnvironment::default()
+ },
+ );
+
+ let logs_dir = default_shared_runtime_logs_dir_for(
+ &resolver,
+ RadrootsPathProfile::InteractiveUser,
+ &RadrootsPathOverrides::default(),
+ )
+ .expect("default shared runtime logs dir should resolve");
+
+ assert_eq!(
+ logs_dir,
+ PathBuf::from("/Users/treesap/.radroots/logs/shared/runtime")
+ );
+ }
+
+ #[test]
fn init_paths_execute() {
let dir = tempdir().expect("tempdir");
test_hooks::set_ignore_env(true);
@@ -210,7 +259,5 @@ mod tests {
assert!(third.is_ok());
let fourth = init_with("logs", Some("debug"));
assert!(fourth.is_ok());
- let second = init();
- assert!(second.is_ok());
}
}