lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 3+++
Mcrates/identity/Cargo.toml | 3++-
Mcrates/identity/src/error.rs | 4++++
Mcrates/identity/src/identity.rs | 27+++++++++++++++++++++++++--
Mcrates/identity/tests/identity.rs | 44++++++++++++++++++++++++++++++++++----------
Mcrates/net-core/Cargo.toml | 3++-
Mcrates/net-core/src/keys.rs | 71++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Acrates/runtime-paths/src/conventions.rs | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/runtime-paths/src/lib.rs | 6++++++
Mcrates/runtime/Cargo.toml | 1+
Mcrates/runtime/src/cli.rs | 29+++++++++++++++++++++++++----
Mcrates/runtime/src/error.rs | 15+++++++++++++++
Mcrates/runtime/src/lib.rs | 9+++++++--
Mcrates/runtime/src/service.rs | 101++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcrates/runtime/src/tracing.rs | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++------
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()); } }