commit 4c9054172553e738e769c741f1b29add17bf6f6f
parent dcf0549ff2247785f7edd5b56daaead06314ebea
Author: triesap <tyson@radroots.org>
Date: Sat, 18 Apr 2026 00:02:31 +0000
app: add runtime config and logging bootstrap
Diffstat:
13 files changed, 1092 insertions(+), 32 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1215,6 +1215,15 @@ dependencies = [
]
[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1331,6 +1340,15 @@ dependencies = [
]
[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
name = "derive_more"
version = "0.99.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3462,6 +3480,15 @@ dependencies = [
]
[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
name = "maybe-rayon"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3794,6 +3821,15 @@ dependencies = [
]
[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3844,6 +3880,12 @@ dependencies = [
]
[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
name = "num-derive"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4389,6 +4431,12 @@ dependencies = [
]
[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4618,11 +4666,21 @@ dependencies = [
"radroots_app_sync",
"radroots_app_ui",
"thiserror 2.0.18",
+ "tracing",
]
[[package]]
name = "radroots_app_core"
version = "0.1.0"
+dependencies = [
+ "chrono",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tracing",
+ "tracing-appender",
+ "tracing-subscriber",
+]
[[package]]
name = "radroots_app_i18n"
@@ -5506,6 +5564,15 @@ dependencies = [
]
[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
name = "shellexpand"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5908,6 +5975,12 @@ dependencies = [
]
[[package]]
+name = "symlink"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
+
+[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6097,6 +6170,15 @@ dependencies = [
]
[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
name = "tiff"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6111,6 +6193,37 @@ dependencies = [
]
[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6360,6 +6473,19 @@ dependencies = [
]
[[package]]
+name = "tracing-appender"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
+dependencies = [
+ "crossbeam-channel",
+ "symlink",
+ "thiserror 2.0.18",
+ "time",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6377,6 +6503,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
]
[[package]]
@@ -6667,6 +6823,12 @@ dependencies = [
]
[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
name = "value-bag"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
@@ -22,6 +22,7 @@ homepage = "https://radroots.org"
readme = "README.md"
[workspace.dependencies]
+chrono = { version = "0.4", default-features = false, features = ["clock"] }
gpui = "0.2.2"
gpui-component = "0.5.1"
gpui-component-assets = "0.5.1"
@@ -41,6 +42,9 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2"
toml = "0.8"
+tracing = "0.1"
+tracing-appender = "0.2"
+tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1", features = ["serde", "v7"] }
[workspace.lints.rust]
diff --git a/crates/launchers/desktop/Cargo.toml b/crates/launchers/desktop/Cargo.toml
@@ -19,6 +19,7 @@ radroots_app_state.workspace = true
radroots_app_sync.workspace = true
radroots_app_ui.workspace = true
thiserror.workspace = true
+tracing.workspace = true
[lints]
workspace = true
diff --git a/crates/launchers/desktop/src/app.rs b/crates/launchers/desktop/src/app.rs
@@ -1,15 +1,43 @@
use gpui::{Application, WindowOptions, px, size};
-use radroots_app_core::{APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeSnapshot};
+use radroots_app_core::{
+ APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeConfig, AppRuntimeConfigError,
+ AppRuntimeSnapshot, bootstrap_logging, install_panic_hook, launch_startup_event,
+};
use radroots_app_i18n::select_locale_from_host;
use radroots_app_ui::APP_UI_THEME;
+use thiserror::Error;
+use tracing::{error, info};
use crate::menus::install_native_app_menu;
use crate::runtime::DesktopAppRuntime;
use crate::window::{home_titlebar_options, open_home_window};
-pub fn launch() {
- let snapshot = AppRuntimeSnapshot::capture(build_identity());
+#[derive(Debug, Error)]
+pub enum AppLaunchError {
+ #[error(transparent)]
+ RuntimeConfig(#[from] AppRuntimeConfigError),
+ #[error(transparent)]
+ Logging(#[from] radroots_app_core::AppLoggingError),
+}
+
+pub fn launch() -> Result<(), AppLaunchError> {
+ let build = build_identity();
+ let runtime_config = AppRuntimeConfig::from_env()?;
+ let snapshot = AppRuntimeSnapshot::from_config(build, &runtime_config);
+ bootstrap_logging(&snapshot, runtime_config.local_log_root.as_path())?;
+ install_panic_hook();
+ emit_launch_event(&snapshot);
+
let runtime = DesktopAppRuntime::bootstrap();
+ if let Some(startup_issue) = runtime.summary().startup_issue {
+ error!(
+ target: "runtime",
+ event = "runtime.degraded",
+ startup_issue = %startup_issue,
+ "desktop runtime degraded"
+ );
+ }
+
let app = Application::new().with_assets(gpui_component_assets::Assets);
app.run(move |cx| {
@@ -27,7 +55,7 @@ pub fn launch() {
let snapshot = snapshot.clone();
let runtime = runtime.clone();
cx.spawn(async move |cx| {
- cx.open_window(
+ if let Err(error) = cx.open_window(
WindowOptions {
app_id: Some(snapshot.host.app_identifier.clone()),
window_min_size: Some(size(
@@ -41,14 +69,37 @@ pub fn launch() {
window.activate_window();
open_home_window(window, cx, runtime.clone())
},
- )
- .expect("main radroots app window should open");
+ ) {
+ error!(
+ target: "window",
+ event = "window.home_open_failed",
+ error = %error,
+ "failed to open home window"
+ );
+ let _ = cx.update(|cx| cx.quit());
+ return;
+ }
- cx.update(|cx| cx.activate(true))
- .expect("radroots app activation should succeed");
+ info!(
+ target: "window",
+ event = "window.home_opened",
+ app_id = %snapshot.host.app_identifier,
+ "home window opened"
+ );
+
+ if let Err(error) = cx.update(|cx| cx.activate(true)) {
+ error!(
+ target: "window",
+ event = "window.app_activation_failed",
+ error = %error,
+ "failed to activate app"
+ );
+ }
})
.detach();
});
+
+ Ok(())
}
fn build_identity() -> AppBuildIdentity {
@@ -61,3 +112,17 @@ fn build_identity() -> AppBuildIdentity {
git_commit: option_env!("RADROOTS_GIT_COMMIT").map(str::to_owned),
}
}
+
+fn emit_launch_event(snapshot: &AppRuntimeSnapshot) {
+ let launch_event = launch_startup_event(snapshot);
+ info!(
+ target: "bootstrap",
+ event = launch_event.name,
+ home_screen = %launch_event.metadata.home_screen,
+ core_package = %launch_event.metadata.core_package,
+ host_surface = %launch_event.metadata.host_surface,
+ runtime_mode = %launch_event.metadata.runtime_mode,
+ "{}",
+ launch_event.message
+ );
+}
diff --git a/crates/launchers/desktop/src/lib.rs b/crates/launchers/desktop/src/lib.rs
@@ -7,6 +7,8 @@ mod runtime;
mod source_guards;
mod window;
-pub fn run() {
- app::launch();
+pub use app::AppLaunchError;
+
+pub fn run() -> Result<(), AppLaunchError> {
+ app::launch()
}
diff --git a/crates/launchers/desktop/src/main.rs b/crates/launchers/desktop/src/main.rs
@@ -1,5 +1,5 @@
#![forbid(unsafe_code)]
-fn main() {
- radroots_app::run();
+fn main() -> Result<(), radroots_app::AppLaunchError> {
+ radroots_app::run()
}
diff --git a/crates/launchers/desktop/src/menus.rs b/crates/launchers/desktop/src/menus.rs
@@ -1,6 +1,6 @@
use gpui::{
- App, Bounds, KeyBinding, Menu, MenuItem, SystemMenuType, WindowBounds, WindowOptions,
- WindowBackgroundAppearance, actions, px, size,
+ App, Bounds, KeyBinding, Menu, MenuItem, SystemMenuType, WindowBackgroundAppearance,
+ WindowBounds, WindowOptions, actions, px, size,
};
use radroots_app_i18n::{AppTextKey, app_text};
use radroots_app_ui::APP_UI_THEME;
diff --git a/crates/shared/core/Cargo.toml b/crates/shared/core/Cargo.toml
@@ -7,5 +7,14 @@ rust-version.workspace = true
license.workspace = true
publish = false
+[dependencies]
+chrono.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+thiserror.workspace = true
+tracing.workspace = true
+tracing-appender.workspace = true
+tracing-subscriber.workspace = true
+
[lints]
workspace = true
diff --git a/crates/shared/core/src/lib.rs b/crates/shared/core/src/lib.rs
@@ -1,16 +1,22 @@
#![forbid(unsafe_code)]
+mod logging;
mod paths;
mod runtime;
mod startup;
+pub use logging::{
+ APP_LOG_PRODUCT, APP_LOG_SCHEMA_VERSION, AppLoggingError, AppLoggingOptions,
+ app_runtime_log_dir, bootstrap_logging, init_logging, install_panic_hook,
+};
pub use paths::{
APP_RUNTIME_NAMESPACE, APP_RUNTIME_NAMESPACE_KIND, APP_RUNTIME_NAMESPACE_VALUE,
AppRuntimeHostEnvironment, AppRuntimePathsError, AppRuntimePlatform, AppRuntimeRoots,
};
pub use runtime::{
- APP_ID, APP_NAME, APP_PLATFORM_RUNTIME, APP_PROJECTION_SOURCE, APP_RUNTIME_ORIGIN,
- AppBuildIdentity, AppCoreRuntimeMetadata, AppHostRuntimeMetadata, AppRuntimeCapture,
- AppRuntimeMode, AppRuntimeSnapshot, runtime_mode_label,
+ APP_HOST_PLATFORM, APP_ID, APP_NAME, APP_PLATFORM_RUNTIME, APP_PROJECTION_SOURCE,
+ APP_RUNTIME_CONFIG_ENV, APP_RUNTIME_CONFIG_SCHEMA, APP_RUNTIME_ORIGIN, AppBuildIdentity,
+ AppCoreRuntimeMetadata, AppHostRuntimeMetadata, AppRuntimeCapture, AppRuntimeConfig,
+ AppRuntimeConfigError, AppRuntimeMode, AppRuntimeSnapshot, runtime_mode_label,
};
pub use startup::{AppStartupEvent, AppStartupEventMetadata, launch_startup_event};
diff --git a/crates/shared/core/src/logging.rs b/crates/shared/core/src/logging.rs
@@ -0,0 +1,462 @@
+use std::{
+ fmt, fs, io,
+ path::{Path, PathBuf},
+ sync::OnceLock,
+};
+
+use chrono::{SecondsFormat, Utc};
+use serde::Serialize;
+use serde_json::{Map, Value};
+use thiserror::Error;
+use tracing::field::{Field, Visit};
+use tracing::{Event, Level, Subscriber, info};
+use tracing_appender::{
+ non_blocking::WorkerGuard,
+ rolling::{RollingFileAppender, Rotation},
+};
+use tracing_subscriber::{
+ EnvFilter, fmt as tracing_fmt,
+ fmt::{FmtContext, FormatEvent, FormatFields, format::Writer},
+ prelude::*,
+ registry::LookupSpan,
+};
+
+use crate::{
+ APP_PLATFORM_RUNTIME, AppBuildIdentity, AppCoreRuntimeMetadata, AppHostRuntimeMetadata,
+};
+use crate::{AppRuntimeSnapshot, runtime_mode_label};
+
+pub const APP_LOG_SCHEMA_VERSION: &str = "radroots.app.log.v1";
+pub const APP_LOG_PRODUCT: &str = "app";
+
+static LOG_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
+static TRACING_INSTALLED: OnceLock<()> = OnceLock::new();
+static PANIC_HOOK_INSTALLED: OnceLock<()> = OnceLock::new();
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AppLoggingOptions {
+ pub log_dir: PathBuf,
+ pub snapshot: AppRuntimeSnapshot,
+ pub stdout: bool,
+ pub default_level: String,
+}
+
+#[derive(Debug, Error)]
+pub enum AppLoggingError {
+ #[error(transparent)]
+ Io(#[from] io::Error),
+ #[error(transparent)]
+ Json(#[from] serde_json::Error),
+ #[error(transparent)]
+ AppenderInit(#[from] tracing_appender::rolling::InitError),
+ #[error(transparent)]
+ TracingInit(#[from] tracing_subscriber::util::TryInitError),
+}
+
+#[derive(Clone, Debug)]
+struct StructuredLogFormatter {
+ snapshot: AppRuntimeSnapshot,
+}
+
+#[derive(Debug, Default)]
+struct StructuredFieldVisitor {
+ event_name: Option<String>,
+ message: Option<String>,
+ metadata: Map<String, Value>,
+}
+
+#[derive(Serialize)]
+struct AppLogRecord<'a> {
+ timestamp: String,
+ schema_version: &'static str,
+ product: &'static str,
+ category: String,
+ event: String,
+ level: &'static str,
+ message: String,
+ runtime_mode: &'static str,
+ run_id: &'a str,
+ platform_runtime: &'static str,
+ core: &'a AppCoreRuntimeMetadata,
+ build: &'a AppBuildIdentity,
+ host: &'a AppHostRuntimeMetadata,
+ metadata: Map<String, Value>,
+}
+
+impl AppLoggingOptions {
+ pub fn localhost_dev(snapshot: AppRuntimeSnapshot, local_log_root: &Path) -> Self {
+ Self {
+ log_dir: app_runtime_log_dir(local_log_root),
+ snapshot,
+ stdout: true,
+ default_level: "info".to_owned(),
+ }
+ }
+}
+
+impl StructuredFieldVisitor {
+ fn record_value(&mut self, field: &Field, value: Value) {
+ match field.name() {
+ "message" => {
+ self.message = match value {
+ Value::String(message) => Some(message),
+ other => Some(other.to_string()),
+ };
+ }
+ "event" => {
+ self.event_name = match value {
+ Value::String(event_name) => Some(event_name),
+ other => Some(other.to_string()),
+ };
+ }
+ _ => {
+ self.metadata.insert(field.name().to_owned(), value);
+ }
+ }
+ }
+}
+
+impl Visit for StructuredFieldVisitor {
+ fn record_bool(&mut self, field: &Field, value: bool) {
+ self.record_value(field, Value::Bool(value));
+ }
+
+ fn record_i64(&mut self, field: &Field, value: i64) {
+ self.record_value(field, Value::from(value));
+ }
+
+ fn record_u64(&mut self, field: &Field, value: u64) {
+ self.record_value(field, Value::from(value));
+ }
+
+ fn record_f64(&mut self, field: &Field, value: f64) {
+ self.record_value(field, Value::from(value));
+ }
+
+ fn record_str(&mut self, field: &Field, value: &str) {
+ self.record_value(field, Value::String(value.to_owned()));
+ }
+
+ fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
+ self.record_value(field, Value::String(value.to_string()));
+ }
+
+ fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
+ self.record_value(field, Value::String(format!("{value:?}")));
+ }
+}
+
+impl<S, N> FormatEvent<S, N> for StructuredLogFormatter
+where
+ S: Subscriber + for<'lookup> LookupSpan<'lookup>,
+ N: for<'writer> FormatFields<'writer> + 'static,
+{
+ fn format_event(
+ &self,
+ _ctx: &FmtContext<'_, S, N>,
+ mut writer: Writer<'_>,
+ event: &Event<'_>,
+ ) -> fmt::Result {
+ let mut visitor = StructuredFieldVisitor::default();
+ event.record(&mut visitor);
+
+ let record = AppLogRecord {
+ timestamp: structured_timestamp(),
+ schema_version: APP_LOG_SCHEMA_VERSION,
+ product: APP_LOG_PRODUCT,
+ category: target_category(event.metadata().target()),
+ event: visitor
+ .event_name
+ .unwrap_or_else(|| format!("{}.log", target_category(event.metadata().target()))),
+ level: level_label(event.metadata().level()),
+ message: visitor.message.unwrap_or_default(),
+ runtime_mode: runtime_mode_label(&self.snapshot.runtime_mode),
+ run_id: &self.snapshot.run_id,
+ platform_runtime: APP_PLATFORM_RUNTIME,
+ core: &self.snapshot.core,
+ build: &self.snapshot.build,
+ host: &self.snapshot.host,
+ metadata: visitor.metadata,
+ };
+ let payload = serde_json::to_string(&record).map_err(|_| fmt::Error)?;
+
+ writeln!(writer, "{payload}")
+ }
+}
+
+pub fn app_runtime_log_dir(local_log_root: &Path) -> PathBuf {
+ local_log_root
+ .join("apps")
+ .join("local")
+ .join(APP_LOG_PRODUCT)
+ .join(APP_PLATFORM_RUNTIME)
+}
+
+pub fn bootstrap_logging(
+ snapshot: &AppRuntimeSnapshot,
+ local_log_root: &Path,
+) -> Result<PathBuf, AppLoggingError> {
+ let options = AppLoggingOptions::localhost_dev(snapshot.clone(), local_log_root);
+ let log_dir = options.log_dir.clone();
+ init_logging(options)?;
+ Ok(log_dir)
+}
+
+pub fn init_logging(options: AppLoggingOptions) -> Result<(), AppLoggingError> {
+ if TRACING_INSTALLED.get().is_some() {
+ return Ok(());
+ }
+
+ fs::create_dir_all(&options.log_dir)?;
+ prepare_latest_alias(&options.log_dir)?;
+
+ let file_appender = build_file_appender(&options.log_dir)?;
+ let (file_writer, guard) = tracing_appender::non_blocking(file_appender);
+ let _ = LOG_GUARD.set(guard);
+
+ let formatter = StructuredLogFormatter {
+ snapshot: options.snapshot.clone(),
+ };
+ let file_layer = tracing_fmt::layer()
+ .with_writer(file_writer)
+ .with_ansi(false)
+ .event_format(formatter.clone());
+ let stdout_layer = options.stdout.then(|| {
+ tracing_fmt::layer()
+ .with_writer(std::io::stdout)
+ .with_ansi(false)
+ .event_format(formatter)
+ });
+
+ tracing_subscriber::registry()
+ .with(resolve_env_filter(options.default_level.as_str()))
+ .with(file_layer)
+ .with(stdout_layer)
+ .try_init()?;
+ let _ = TRACING_INSTALLED.set(());
+
+ info!(
+ target: "runtime",
+ event = "logging.initialized",
+ file = %options.log_dir.join(format!("{}.jsonl", current_utc_day())).display(),
+ stdout = options.stdout,
+ "logging initialized"
+ );
+
+ Ok(())
+}
+
+pub fn install_panic_hook() {
+ if PANIC_HOOK_INSTALLED.set(()).is_err() {
+ return;
+ }
+
+ let default_hook = std::panic::take_hook();
+ std::panic::set_hook(Box::new(move |panic_info| {
+ tracing::error!(
+ target: "panic",
+ event = "runtime.panic",
+ panic = %render_panic_payload(panic_info),
+ location = %render_panic_location(panic_info),
+ "panic captured"
+ );
+ default_hook(panic_info);
+ }));
+}
+
+fn build_file_appender(log_dir: &Path) -> Result<RollingFileAppender, AppLoggingError> {
+ Ok(RollingFileAppender::builder()
+ .rotation(Rotation::DAILY)
+ .filename_suffix("jsonl")
+ .build(log_dir)?)
+}
+
+fn current_utc_day() -> String {
+ Utc::now().format("%Y-%m-%d").to_string()
+}
+
+fn level_label(level: &Level) -> &'static str {
+ match *level {
+ Level::ERROR => "error",
+ Level::WARN => "warning",
+ Level::INFO => "info",
+ Level::DEBUG => "debug",
+ Level::TRACE => "debug",
+ }
+}
+
+fn prepare_latest_alias(log_dir: &Path) -> Result<(), AppLoggingError> {
+ let latest_path = log_dir.join("latest.jsonl");
+ match fs::symlink_metadata(&latest_path) {
+ Ok(metadata) => {
+ if metadata.file_type().is_symlink() || metadata.is_file() {
+ fs::remove_file(&latest_path)?;
+ }
+ }
+ Err(error) if error.kind() == io::ErrorKind::NotFound => {}
+ Err(error) => return Err(error.into()),
+ }
+
+ #[cfg(unix)]
+ std::os::unix::fs::symlink(format!("{}.jsonl", current_utc_day()), &latest_path)?;
+
+ #[cfg(not(unix))]
+ fs::write(&latest_path, [])?;
+
+ Ok(())
+}
+
+fn render_panic_location(panic_info: &std::panic::PanicHookInfo<'_>) -> String {
+ panic_info
+ .location()
+ .map(|location| {
+ format!(
+ "{}:{}:{}",
+ location.file(),
+ location.line(),
+ location.column()
+ )
+ })
+ .unwrap_or_else(|| "<unknown>".to_owned())
+}
+
+fn render_panic_payload(panic_info: &std::panic::PanicHookInfo<'_>) -> String {
+ if let Some(payload) = panic_info.payload().downcast_ref::<&str>() {
+ (*payload).to_owned()
+ } else if let Some(payload) = panic_info.payload().downcast_ref::<String>() {
+ payload.clone()
+ } else {
+ "non-string panic payload".to_owned()
+ }
+}
+
+fn resolve_env_filter(default_level: &str) -> EnvFilter {
+ EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level))
+}
+
+fn structured_timestamp() -> String {
+ Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true)
+}
+
+fn target_category(target: &str) -> String {
+ if target.is_empty() {
+ return "runtime".to_owned();
+ }
+
+ target
+ .rsplit("::")
+ .next()
+ .unwrap_or(target)
+ .trim()
+ .trim_matches(':')
+ .to_owned()
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{
+ fs,
+ path::{Path, PathBuf},
+ time::{SystemTime, UNIX_EPOCH},
+ };
+
+ use chrono::{SecondsFormat, Utc};
+ use serde_json::json;
+
+ use crate::{
+ APP_PROJECTION_SOURCE, AppBuildIdentity, AppRuntimeCapture, AppRuntimeMode,
+ AppRuntimeSnapshot,
+ };
+
+ use super::{
+ APP_LOG_PRODUCT, APP_LOG_SCHEMA_VERSION, app_runtime_log_dir, prepare_latest_alias,
+ };
+
+ fn temp_dir(name: &str) -> PathBuf {
+ let nanos = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("system time")
+ .as_nanos();
+ let path = std::env::temp_dir().join(format!("radroots-app-log-{name}-{nanos}"));
+ let _ = fs::remove_dir_all(&path);
+ path
+ }
+
+ fn test_snapshot() -> AppRuntimeSnapshot {
+ AppRuntimeSnapshot::from_capture(
+ AppBuildIdentity {
+ package_name: "radroots_app".to_owned(),
+ package_version: "0.1.0".to_owned(),
+ build_profile: "debug".to_owned(),
+ target_triple: "aarch64-apple-darwin".to_owned(),
+ projection_source: APP_PROJECTION_SOURCE.to_owned(),
+ git_commit: None,
+ },
+ AppRuntimeMode::LocalhostDev,
+ AppRuntimeCapture {
+ host_locale: "en_US.UTF-8".to_owned(),
+ operating_system: "macos".to_owned(),
+ run_id: "run-localhost-dev-123-pid456".to_owned(),
+ },
+ )
+ }
+
+ #[test]
+ fn app_runtime_log_dir_uses_canonical_local_layout() {
+ let dir = app_runtime_log_dir(Path::new("/tmp/repo/logs"));
+
+ assert_eq!(
+ dir,
+ PathBuf::from("/tmp/repo/logs")
+ .join("apps")
+ .join("local")
+ .join(APP_LOG_PRODUCT)
+ .join("app-macos-native")
+ );
+ }
+
+ #[cfg(unix)]
+ #[test]
+ fn prepare_latest_alias_tracks_current_day_log_file() {
+ let dir = temp_dir("latest-alias");
+ fs::create_dir_all(&dir).expect("create dir");
+
+ prepare_latest_alias(&dir).expect("prepare latest alias");
+
+ let target = fs::read_link(dir.join("latest.jsonl")).expect("read latest symlink");
+ assert_eq!(
+ target,
+ PathBuf::from(format!("{}.jsonl", Utc::now().format("%Y-%m-%d")))
+ );
+ let _ = fs::remove_dir_all(&dir);
+ }
+
+ #[test]
+ fn structured_record_shape_remains_stable() {
+ let snapshot = test_snapshot();
+ let payload = serde_json::to_value(json!({
+ "timestamp": Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true),
+ "schema_version": APP_LOG_SCHEMA_VERSION,
+ "product": APP_LOG_PRODUCT,
+ "category": "bootstrap",
+ "event": "runtime.launch",
+ "level": "info",
+ "message": "app launch",
+ "runtime_mode": "localhost-dev",
+ "run_id": snapshot.run_id,
+ "platform_runtime": "app-macos-native",
+ "core": snapshot.core,
+ "build": snapshot.build,
+ "host": snapshot.host,
+ "metadata": {
+ "home_screen": "Radroots"
+ }
+ }))
+ .expect("serialize");
+
+ assert_eq!(payload["schema_version"], "radroots.app.log.v1");
+ assert_eq!(payload["event"], "runtime.launch");
+ assert_eq!(payload["platform_runtime"], "app-macos-native");
+ assert_eq!(payload["metadata"]["home_screen"], "Radroots");
+ }
+}
diff --git a/crates/shared/core/src/runtime.rs b/crates/shared/core/src/runtime.rs
@@ -1,19 +1,43 @@
-use std::time::{SystemTime, UNIX_EPOCH};
+use std::{
+ path::PathBuf,
+ time::{SystemTime, UNIX_EPOCH},
+};
+
+use serde::{Deserialize, Serialize};
+use thiserror::Error;
pub const APP_ID: &str = "org.radroots.app";
pub const APP_NAME: &str = "Radroots";
-pub const APP_PLATFORM_RUNTIME: &str = "app-desktop-gpui";
+pub const APP_PLATFORM_RUNTIME: &str = "app-macos-native";
pub const APP_PROJECTION_SOURCE: &str = "gpui-native";
pub const APP_RUNTIME_ORIGIN: &str = "gpui://localhost";
pub const APP_HOST_PLATFORM: &str = "desktop";
+pub const APP_RUNTIME_CONFIG_ENV: &str = "RADROOTS_APP_RUNTIME_CONFIG_JSON";
+pub const APP_RUNTIME_CONFIG_SCHEMA: &str = "radroots.app.runtime-config.v1";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AppRuntimeMode {
+ LocalhostDev,
Development,
Production,
}
#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AppRuntimeConfig {
+ pub runtime_mode: AppRuntimeMode,
+ pub run_id: String,
+ pub bundle_identifier: String,
+ pub bundle_name: String,
+ pub marketing_version: String,
+ pub build_number: String,
+ pub platform_name: String,
+ pub operating_system_version: String,
+ pub host_locale: String,
+ pub runtime_origin: String,
+ pub local_log_root: PathBuf,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct AppBuildIdentity {
pub package_name: String,
pub package_version: String,
@@ -23,7 +47,7 @@ pub struct AppBuildIdentity {
pub git_commit: Option<String>,
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct AppCoreRuntimeMetadata {
pub package_name: String,
pub package_version: String,
@@ -32,7 +56,7 @@ pub struct AppCoreRuntimeMetadata {
pub rust_toolchain: String,
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct AppHostRuntimeMetadata {
pub app_identifier: String,
pub app_name: String,
@@ -61,6 +85,72 @@ pub struct AppRuntimeSnapshot {
pub host: AppHostRuntimeMetadata,
}
+#[derive(Debug, Error)]
+pub enum AppRuntimeConfigError {
+ #[error("missing required runtime config env: {APP_RUNTIME_CONFIG_ENV}")]
+ MissingEnv,
+ #[error("invalid app runtime config json")]
+ InvalidJson(#[source] serde_json::Error),
+ #[error("unsupported app runtime config schema: {0}")]
+ UnsupportedSchema(String),
+ #[error("unsupported runtime mode: {0}")]
+ UnsupportedRuntimeMode(String),
+ #[error("missing required runtime config field: {0}")]
+ MissingField(&'static str),
+}
+
+#[derive(Deserialize)]
+struct RawRuntimeConfig {
+ schema_version: String,
+ runtime_mode: String,
+ run_id: String,
+ bundle_identifier: String,
+ bundle_name: String,
+ marketing_version: String,
+ build_number: String,
+ platform_name: String,
+ operating_system_version: String,
+ host_locale: String,
+ runtime_origin: String,
+ local_log_root: String,
+}
+
+impl AppRuntimeConfig {
+ pub fn from_env() -> Result<Self, AppRuntimeConfigError> {
+ let raw =
+ std::env::var(APP_RUNTIME_CONFIG_ENV).map_err(|_| AppRuntimeConfigError::MissingEnv)?;
+ Self::from_json_str(&raw)
+ }
+
+ pub fn from_json_str(raw: &str) -> Result<Self, AppRuntimeConfigError> {
+ let config: RawRuntimeConfig =
+ serde_json::from_str(raw).map_err(AppRuntimeConfigError::InvalidJson)?;
+
+ if config.schema_version != APP_RUNTIME_CONFIG_SCHEMA {
+ return Err(AppRuntimeConfigError::UnsupportedSchema(
+ config.schema_version,
+ ));
+ }
+
+ Ok(Self {
+ runtime_mode: parse_config_runtime_mode(&config.runtime_mode)?,
+ run_id: require_value("run_id", config.run_id)?,
+ bundle_identifier: require_value("bundle_identifier", config.bundle_identifier)?,
+ bundle_name: require_value("bundle_name", config.bundle_name)?,
+ marketing_version: require_value("marketing_version", config.marketing_version)?,
+ build_number: require_value("build_number", config.build_number)?,
+ platform_name: require_value("platform_name", config.platform_name)?,
+ operating_system_version: require_value(
+ "operating_system_version",
+ config.operating_system_version,
+ )?,
+ host_locale: require_value("host_locale", config.host_locale)?,
+ runtime_origin: require_value("runtime_origin", config.runtime_origin)?,
+ local_log_root: require_path_value("local_log_root", config.local_log_root)?,
+ })
+ }
+}
+
impl AppRuntimeCapture {
pub fn current(mode: &AppRuntimeMode) -> Self {
Self {
@@ -73,10 +163,36 @@ impl AppRuntimeCapture {
impl AppRuntimeSnapshot {
pub fn capture(build: AppBuildIdentity) -> Self {
- let mode = parse_runtime_mode(&build.build_profile);
+ let mode = parse_build_runtime_mode(&build.build_profile);
Self::from_capture(build, mode, AppRuntimeCapture::current(&mode))
}
+ pub fn from_config(build: AppBuildIdentity, config: &AppRuntimeConfig) -> Self {
+ Self {
+ title: APP_NAME.to_owned(),
+ runtime_mode: config.runtime_mode,
+ run_id: config.run_id.clone(),
+ core: AppCoreRuntimeMetadata {
+ package_name: env!("CARGO_PKG_NAME").to_owned(),
+ package_version: env!("CARGO_PKG_VERSION").to_owned(),
+ package_authors: env!("CARGO_PKG_AUTHORS").to_owned(),
+ rust_edition: "2024".to_owned(),
+ rust_toolchain: env!("CARGO_PKG_RUST_VERSION").to_owned(),
+ },
+ build,
+ host: AppHostRuntimeMetadata {
+ app_identifier: config.bundle_identifier.clone(),
+ app_name: config.bundle_name.clone(),
+ app_version: config.marketing_version.clone(),
+ app_build: config.build_number.clone(),
+ platform_name: config.platform_name.clone(),
+ operating_system: config.operating_system_version.clone(),
+ host_locale: config.host_locale.clone(),
+ runtime_origin: config.runtime_origin.clone(),
+ },
+ }
+ }
+
pub fn from_capture(
build: AppBuildIdentity,
runtime_mode: AppRuntimeMode,
@@ -116,18 +232,51 @@ impl AppRuntimeSnapshot {
pub fn runtime_mode_label(mode: &AppRuntimeMode) -> &'static str {
match mode {
+ AppRuntimeMode::LocalhostDev => "localhost-dev",
AppRuntimeMode::Development => "development",
AppRuntimeMode::Production => "production",
}
}
-fn parse_runtime_mode(build_profile: &str) -> AppRuntimeMode {
+fn parse_build_runtime_mode(build_profile: &str) -> AppRuntimeMode {
match build_profile.trim() {
"release" => AppRuntimeMode::Production,
_ => AppRuntimeMode::Development,
}
}
+fn parse_config_runtime_mode(value: &str) -> Result<AppRuntimeMode, AppRuntimeConfigError> {
+ match value.trim() {
+ "localhost-dev" => Ok(AppRuntimeMode::LocalhostDev),
+ "development" => Ok(AppRuntimeMode::Development),
+ "production" => Ok(AppRuntimeMode::Production),
+ other => Err(AppRuntimeConfigError::UnsupportedRuntimeMode(
+ other.to_owned(),
+ )),
+ }
+}
+
+fn require_path_value(
+ field: &'static str,
+ value: String,
+) -> Result<PathBuf, AppRuntimeConfigError> {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ return Err(AppRuntimeConfigError::MissingField(field));
+ }
+
+ Ok(PathBuf::from(trimmed))
+}
+
+fn require_value(field: &'static str, value: String) -> Result<String, AppRuntimeConfigError> {
+ let trimmed = value.trim();
+ if trimmed.is_empty() {
+ return Err(AppRuntimeConfigError::MissingField(field));
+ }
+
+ Ok(trimmed.to_owned())
+}
+
fn detect_host_locale() -> String {
[
std::env::var("LC_ALL").ok(),
@@ -162,10 +311,12 @@ fn build_run_id(mode: &AppRuntimeMode) -> String {
#[cfg(test)]
mod tests {
+ use std::path::PathBuf;
+
use super::{
- APP_HOST_PLATFORM, APP_ID, APP_NAME, APP_PROJECTION_SOURCE, APP_RUNTIME_ORIGIN,
- AppBuildIdentity, AppRuntimeCapture, AppRuntimeMode, AppRuntimeSnapshot,
- runtime_mode_label,
+ APP_HOST_PLATFORM, APP_ID, APP_NAME, APP_PROJECTION_SOURCE, APP_RUNTIME_CONFIG_SCHEMA,
+ APP_RUNTIME_ORIGIN, AppBuildIdentity, AppRuntimeCapture, AppRuntimeConfig,
+ AppRuntimeConfigError, AppRuntimeMode, AppRuntimeSnapshot, runtime_mode_label,
};
fn test_build_identity() -> AppBuildIdentity {
@@ -179,6 +330,25 @@ mod tests {
}
}
+ fn test_runtime_config_json() -> String {
+ format!(
+ r#"{{
+ "schema_version":"{APP_RUNTIME_CONFIG_SCHEMA}",
+ "runtime_mode":"localhost-dev",
+ "run_id":"run-localhost-dev-20260417T000000Z-deadbeefcafefeed",
+ "bundle_identifier":"org.radroots.app.macos",
+ "bundle_name":"Radroots",
+ "marketing_version":"0.1.0",
+ "build_number":"dev",
+ "platform_name":"macos",
+ "operating_system_version":"macos-15.5",
+ "host_locale":"en_US.UTF-8",
+ "runtime_origin":"gpui://localhost",
+ "local_log_root":"/tmp/radroots/logs"
+ }}"#
+ )
+ }
+
#[test]
fn runtime_snapshot_surfaces_core_and_host_metadata() {
let snapshot = AppRuntimeSnapshot::from_capture(
@@ -211,6 +381,47 @@ mod tests {
}
#[test]
+ fn runtime_config_requires_supported_schema() {
+ let error = AppRuntimeConfig::from_json_str(
+ r#"{"schema_version":"unsupported","runtime_mode":"localhost-dev","run_id":"x","bundle_identifier":"y","bundle_name":"z","marketing_version":"0.1.0","build_number":"1","platform_name":"macos","operating_system_version":"macos-15.5","host_locale":"en","runtime_origin":"gpui://localhost","local_log_root":"/tmp/logs"}"#,
+ )
+ .expect_err("schema mismatch should fail");
+
+ assert!(
+ error
+ .to_string()
+ .contains("unsupported app runtime config schema")
+ );
+ }
+
+ #[test]
+ fn runtime_config_surfaces_explicit_local_log_root() {
+ let config =
+ AppRuntimeConfig::from_json_str(&test_runtime_config_json()).expect("valid config");
+
+ assert_eq!(config.runtime_mode, AppRuntimeMode::LocalhostDev);
+ assert_eq!(config.bundle_identifier, "org.radroots.app.macos");
+ assert_eq!(config.local_log_root, PathBuf::from("/tmp/radroots/logs"));
+ }
+
+ #[test]
+ fn runtime_snapshot_uses_explicit_runtime_config_host_identity() {
+ let snapshot = AppRuntimeSnapshot::from_config(
+ test_build_identity(),
+ &AppRuntimeConfig::from_json_str(&test_runtime_config_json()).expect("valid config"),
+ );
+
+ assert_eq!(
+ snapshot.run_id,
+ "run-localhost-dev-20260417T000000Z-deadbeefcafefeed"
+ );
+ assert_eq!(snapshot.host.app_identifier, "org.radroots.app.macos");
+ assert_eq!(snapshot.host.platform_name, "macos");
+ assert_eq!(snapshot.host.operating_system, "macos-15.5");
+ assert_eq!(runtime_mode_label(&snapshot.runtime_mode), "localhost-dev");
+ }
+
+ #[test]
fn runtime_snapshot_falls_back_to_build_profile_when_git_commit_is_missing() {
let mut build = test_build_identity();
build.git_commit = None;
@@ -229,4 +440,30 @@ mod tests {
assert_eq!(snapshot.host.app_build, "release");
assert_eq!(runtime_mode_label(&snapshot.runtime_mode), "production");
}
+
+ #[test]
+ fn runtime_config_rejects_empty_required_fields() {
+ let error = AppRuntimeConfig::from_json_str(&format!(
+ r#"{{
+ "schema_version":"{APP_RUNTIME_CONFIG_SCHEMA}",
+ "runtime_mode":"localhost-dev",
+ "run_id":"",
+ "bundle_identifier":"org.radroots.app.macos",
+ "bundle_name":"Radroots",
+ "marketing_version":"0.1.0",
+ "build_number":"dev",
+ "platform_name":"macos",
+ "operating_system_version":"macos-15.5",
+ "host_locale":"en_US.UTF-8",
+ "runtime_origin":"gpui://localhost",
+ "local_log_root":"/tmp/radroots/logs"
+ }}"#
+ ))
+ .expect_err("missing run id should fail");
+
+ assert!(
+ matches!(error, AppRuntimeConfigError::MissingField("run_id")),
+ "unexpected error: {error}"
+ );
+ }
}
diff --git a/crates/shared/ui/src/text.rs b/crates/shared/ui/src/text.rs
@@ -1,5 +1,5 @@
use gpui::SharedString;
-use radroots_app_core::{AppRuntimeMode, AppRuntimeSnapshot};
+use radroots_app_core::{AppRuntimeMode, AppRuntimeSnapshot, runtime_mode_label};
use radroots_app_i18n::{AppTextKey, app_text};
use crate::LabelValueRow;
@@ -151,11 +151,10 @@ fn text_row(label: AppTextKey, value: AppTextKey) -> LabelValueRow {
}
fn runtime_mode_text(mode: &AppRuntimeMode) -> String {
- let key = match mode {
- AppRuntimeMode::Development => AppTextKey::ValueRuntimeModeDevelopment,
- AppRuntimeMode::Production => AppTextKey::ValueRuntimeModeProduction,
- };
- app_text(key)
+ match mode {
+ AppRuntimeMode::Production => app_text(AppTextKey::ValueRuntimeModeProduction),
+ _ => runtime_mode_label(mode).to_owned(),
+ }
}
#[cfg(test)]
diff --git a/scripts/run.sh b/scripts/run.sh
@@ -4,8 +4,121 @@ set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
repo_root="$(git -C "${script_dir}" rev-parse --show-toplevel)"
+workspace_version() {
+ python3 - <<'PY' "${repo_root}/Cargo.toml"
+import re
+import sys
+
+path = sys.argv[1]
+with open(path, "r", encoding="utf-8") as handle:
+ cargo_toml = handle.read()
+
+match = re.search(r'^\[workspace\.package\][\s\S]*?^version\s*=\s*"([^"]+)"', cargo_toml, re.MULTILINE)
+if not match:
+ raise SystemExit("missing workspace.package.version")
+
+print(match.group(1), end="")
+PY
+}
+
+current_runtime_mode() {
+ printf '%s' "${RADROOTS_APP_RUNTIME_MODE:-localhost-dev}"
+}
+
+current_run_id() {
+ if [[ -n "${RADROOTS_APP_RUN_ID:-}" ]]; then
+ printf '%s' "${RADROOTS_APP_RUN_ID}"
+ return
+ fi
+
+ RADROOTS_APP_RUNTIME_MODE_FOR_RUN_ID="$(current_runtime_mode)" python3 - <<'PY'
+import os
+import secrets
+import time
+
+runtime_mode = os.environ["RADROOTS_APP_RUNTIME_MODE_FOR_RUN_ID"].strip().lower() or "unknown"
+timestamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
+suffix = secrets.token_hex(8)
+print(f"app-{runtime_mode}-{timestamp}-{suffix}", end="")
+PY
+}
+
+current_platform_name() {
+ case "$(uname -s)" in
+ Darwin) printf 'macos' ;;
+ Linux) printf 'linux' ;;
+ *) uname -s | tr '[:upper:]' '[:lower:]' ;;
+ esac
+}
+
+current_bundle_identifier() {
+ if [[ "$(uname -s)" == "Darwin" ]]; then
+ printf 'org.radroots.app.macos'
+ return
+ fi
+
+ printf 'org.radroots.app.desktop'
+}
+
+current_os_version() {
+ printf '%s-%s' "$(current_platform_name)" "$(uname -r)"
+}
+
+build_runtime_config_json() {
+ local runtime_mode="$1"
+ local run_id="$2"
+ local bundle_identifier="$3"
+ local platform_name="$4"
+
+ RADROOTS_APP_RUNTIME_CONFIG_SCHEMA="radroots.app.runtime-config.v1" \
+ RADROOTS_APP_RUNTIME_MODE="${runtime_mode}" \
+ RADROOTS_APP_RUN_ID="${run_id}" \
+ RADROOTS_APP_BUNDLE_IDENTIFIER="${bundle_identifier}" \
+ RADROOTS_APP_BUNDLE_NAME="Radroots" \
+ RADROOTS_APP_MARKETING_VERSION="$(workspace_version)" \
+ RADROOTS_APP_BUILD_NUMBER="${RADROOTS_APP_BUILD:-dev}" \
+ RADROOTS_APP_PLATFORM_NAME="${platform_name}" \
+ RADROOTS_APP_OS_VERSION="$(current_os_version)" \
+ RADROOTS_APP_HOST_LOCALE="${LANG:-system-default}" \
+ RADROOTS_APP_RUNTIME_ORIGIN="gpui://localhost" \
+ RADROOTS_APP_LOCAL_LOG_ROOT="${repo_root}/logs" \
+ python3 - <<'PY'
+import json
+import os
+
+print(json.dumps({
+ "schema_version": os.environ["RADROOTS_APP_RUNTIME_CONFIG_SCHEMA"],
+ "runtime_mode": os.environ["RADROOTS_APP_RUNTIME_MODE"],
+ "run_id": os.environ["RADROOTS_APP_RUN_ID"],
+ "bundle_identifier": os.environ["RADROOTS_APP_BUNDLE_IDENTIFIER"],
+ "bundle_name": os.environ["RADROOTS_APP_BUNDLE_NAME"],
+ "marketing_version": os.environ["RADROOTS_APP_MARKETING_VERSION"],
+ "build_number": os.environ["RADROOTS_APP_BUILD_NUMBER"],
+ "platform_name": os.environ["RADROOTS_APP_PLATFORM_NAME"],
+ "operating_system_version": os.environ["RADROOTS_APP_OS_VERSION"],
+ "host_locale": os.environ["RADROOTS_APP_HOST_LOCALE"],
+ "runtime_origin": os.environ["RADROOTS_APP_RUNTIME_ORIGIN"],
+ "local_log_root": os.environ["RADROOTS_APP_LOCAL_LOG_ROOT"],
+}, sort_keys=True, separators=(",", ":")), end="")
+PY
+}
+
cd "${repo_root}"
+runtime_mode="$(current_runtime_mode)"
+run_id="$(current_run_id)"
+platform_name="$(current_platform_name)"
+bundle_identifier="$(current_bundle_identifier)"
+
+export RADROOTS_APP_RUN_ID="${run_id}"
+export RADROOTS_APP_RUNTIME_CONFIG_JSON="$(
+ build_runtime_config_json \
+ "${runtime_mode}" \
+ "${run_id}" \
+ "${bundle_identifier}" \
+ "${platform_name}"
+)"
+
if [[ "$(uname -s)" == "Darwin" ]]; then
exec "${repo_root}/platforms/macos/Scripts/run-macos-host.sh" "$@"
fi