use clap::ArgMatches;
use clap::error::ErrorKind;
use miette::Report;
use netsuke::theme::ThemeContext;
use netsuke::{
cli, cli_localization, diagnostic_json, locale_resolution, localization, manifest, output_mode,
output_prefs, runner,
};
use ortho_config::Localizer;
use std::ffi::OsString;
use std::io::{self, IsTerminal, Write};
use std::process::ExitCode;
use std::sync::{Arc, OnceLock};
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{Registry, fmt, reload};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DiagMode {
Human,
Json,
}
impl DiagMode {
const fn from_json_enabled(enabled: bool) -> Self {
if enabled { Self::Json } else { Self::Human }
}
const fn is_json(self) -> bool {
matches!(self, Self::Json)
}
}
mod config_resolution;
mod observability;
#[path = "startup_tracing.rs"]
mod startup_tracing;
use config_resolution::{merge_cli_or_exit, resolve_json_mode_or_exit};
use startup_tracing::StartupWriter;
fn settle_startup_diagnostics(writer: &StartupWriter, mode: DiagMode) {
if mode.is_json() {
writer.discard();
} else if let Err(err) = writer.release_to_stderr() {
drop(writeln!(
io::stderr(),
"failed to flush startup diagnostics: {err}"
));
}
}
fn main() -> ExitCode {
let args: Vec<OsString> = std::env::args_os().collect();
let env = locale_resolution::SystemEnv;
let system_locale = locale_resolution::SysLocale;
run_with_args(args, &env, &system_locale)
}
fn run_with_args(
args: Vec<OsString>,
env: &impl locale_resolution::LocaleEnvProvider,
system_locale: &impl locale_resolution::SystemLocale,
) -> ExitCode {
let json_hint = locale_resolution::resolve_startup_json(&args, env);
let startup_writer = StartupWriter::buffering();
init_tracing(LevelFilter::WARN, startup_writer.clone());
observability::init_metrics();
let localizer = startup_localizer(&args, env, system_locale);
let startup_mode = DiagMode::from_json_enabled(json_hint);
let (parsed_cli, matches) =
match parse_cli_or_exit(args, &localizer, startup_mode, &startup_writer) {
Ok(parsed) => parsed,
Err(code) => return code,
};
let verbose = parsed_cli.verbose;
if is_informational_help(&parsed_cli) {
settle_startup_diagnostics(&startup_writer, startup_mode);
return finish_run(run_cli(&parsed_cli, system_locale, startup_mode), verbose);
}
let (mode, discovered_layers) =
match resolve_json_mode_or_exit(&parsed_cli, &matches, startup_mode) {
Ok(resolved) => resolved,
Err(code) => {
settle_startup_diagnostics(&startup_writer, startup_mode);
return finish_run(code, verbose);
}
};
settle_startup_diagnostics(&startup_writer, mode);
let merged_cli = match merge_cli_or_exit(&parsed_cli, &matches, mode, discovered_layers) {
Ok(merged) => merged,
Err(code) => return finish_run(code, verbose),
};
let merged_verbose = merged_cli.verbose;
let runtime_mode = DiagMode::from_json_enabled(merged_cli.json);
finish_run(
run_cli(&merged_cli, system_locale, runtime_mode),
merged_verbose,
)
}
fn finish_run(exit_code: ExitCode, verbose: bool) -> ExitCode {
if verbose {
observability::emit_metrics_snapshot();
}
exit_code
}
const fn is_informational_help(cli: &cli::Cli) -> bool {
matches!(
&cli.command,
Some(cli::Commands::Help(args))
if !matches!(args.topic.as_ref(), Some(cli::HelpTopic::Targets))
)
}
fn run_cli(
cli: &cli::Cli,
system_locale: &impl locale_resolution::SystemLocale,
runtime_mode: DiagMode,
) -> ExitCode {
configure_runtime(cli, system_locale, runtime_mode);
let output_mode = output_mode::resolve(cli.accessibility_override(), Some(cli.color));
let prefs = output_prefs::resolve_from_theme(
cli.theme_preference(),
ThemeContext::new(None, Some(cli.color), output_mode),
);
match runner::run(cli, prefs) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => handle_runner_error(err, prefs, runtime_mode),
}
}
static TRACING_FILTER: OnceLock<reload::Handle<LevelFilter, Registry>> = OnceLock::new();
const fn startup_filter(mode: DiagMode, verbose: bool) -> LevelFilter {
if mode.is_json() {
LevelFilter::OFF
} else if verbose {
LevelFilter::TRACE
} else {
LevelFilter::WARN
}
}
fn init_tracing(initial: LevelFilter, writer: StartupWriter) {
let (filter, handle) = reload::Layer::new(initial);
if Registry::default()
.with(filter)
.with(
fmt::layer()
.with_writer(writer)
.with_ansi(io::stderr().is_terminal()),
)
.try_init()
.is_ok()
{
TRACING_FILTER.set(handle).ok();
}
}
fn set_tracing_filter(level: LevelFilter) {
if let Some(handle) = TRACING_FILTER.get() {
handle.modify(|filter| *filter = level).ok();
}
}
fn startup_localizer(
args: &[OsString],
env: &impl locale_resolution::LocaleEnvProvider,
system_locale: &impl locale_resolution::SystemLocale,
) -> Arc<dyn Localizer> {
let startup_locale = locale_resolution::resolve_startup_locale(args, env, system_locale);
let localizer = Arc::from(cli_localization::build_localizer(startup_locale.as_deref()));
localization::set_localizer(Arc::clone(&localizer));
localizer
}
fn parse_cli_or_exit(
args: Vec<OsString>,
localizer: &Arc<dyn Localizer>,
mode: DiagMode,
startup_writer: &StartupWriter,
) -> Result<(cli::Cli, ArgMatches), ExitCode> {
match cli::parse_with_localizer_from(args, localizer) {
Ok(parsed) => Ok(parsed),
Err(err) => {
settle_startup_diagnostics(startup_writer, mode);
if matches!(
err.kind(),
ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
) {
err.exit();
}
if mode.is_json() {
Err(diagnostic_json::emit_or_fallback(
diagnostic_json::render_error_json(&err),
))
} else {
err.exit();
}
}
}
}
fn configure_runtime(
merged_cli: &cli::Cli,
system_locale: &impl locale_resolution::SystemLocale,
mode: DiagMode,
) {
set_tracing_filter(startup_filter(mode, merged_cli.verbose));
let runtime_locale = locale_resolution::resolve_runtime_locale(merged_cli, system_locale);
let runtime_localizer = Arc::from(cli_localization::build_localizer(runtime_locale.as_deref()));
localization::set_localizer(Arc::clone(&runtime_localizer));
}
fn handle_runner_error(
err: anyhow::Error,
prefs: output_prefs::OutputPrefs,
mode: DiagMode,
) -> ExitCode {
if mode.is_json() {
return diagnostic_json::emit_or_fallback(render_runtime_error_json(&err));
}
let prefix = prefs.error_prefix();
match err.downcast::<runner::RunnerError>() {
Ok(runner_err) => {
let report = Report::new(runner_err);
drop(writeln!(io::stderr(), "{prefix} {report:?}"));
}
Err(other_err) => {
tracing::error!(error = %other_err, "runner failed");
drop(writeln!(io::stderr(), "{prefix} {other_err}"));
}
}
ExitCode::FAILURE
}
fn render_runtime_error_json(err: &anyhow::Error) -> serde_json::Result<String> {
if let Some(runner_err) = err.downcast_ref::<runner::RunnerError>() {
return diagnostic_json::render_diagnostic_json(runner_err);
}
if let Some(manifest_err) = err
.chain()
.find_map(|cause| cause.downcast_ref::<manifest::ManifestError>())
{
return diagnostic_json::render_diagnostic_json(manifest_err);
}
if let Some(report) = err.downcast_ref::<Report>() {
return diagnostic_json::render_report_json(report);
}
diagnostic_json::render_error_json(err.as_ref())
}
#[cfg(test)]
#[path = "main_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "main_config_tests.rs"]
mod config_tests;