app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

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:
MCargo.lock | 162+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 4++++
Mcrates/launchers/desktop/Cargo.toml | 1+
Mcrates/launchers/desktop/src/app.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Mcrates/launchers/desktop/src/lib.rs | 6++++--
Mcrates/launchers/desktop/src/main.rs | 4++--
Mcrates/launchers/desktop/src/menus.rs | 4++--
Mcrates/shared/core/Cargo.toml | 9+++++++++
Mcrates/shared/core/src/lib.rs | 12+++++++++---
Acrates/shared/core/src/logging.rs | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/shared/core/src/runtime.rs | 255++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/shared/ui/src/text.rs | 11+++++------
Mscripts/run.sh | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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