use std::process::ExitCode;
use std::time::Duration;
use std::time::Instant;
use clap::Parser;
use tracing::Level;
use tracing::enabled;
use tracing::level_filters::LevelFilter;
use tracing::trace;
use crate::commands::CliArguments;
use crate::commands::MagoCommand;
use crate::config::Configuration;
use crate::consts::MAXIMUM_PHP_VERSION;
use crate::consts::MINIMUM_PHP_VERSION;
use crate::consts::VERSION;
use crate::error::Error;
use crate::utils::configure_colors;
use crate::utils::logger::initialize_logger;
use crate::version_check::VersionCheck;
use crate::version_check::VersionPin;
mod baseline;
mod commands;
mod config;
mod consts;
mod error;
mod macros;
mod service;
mod updater;
mod utils;
mod version_check;
#[cfg(all(
not(feature = "dhat-heap"),
any(target_os = "macos", target_os = "windows", target_env = "musl", target_env = "gnu")
))]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
const EXIT_CODE_ERROR: u8 = 2;
pub fn main() -> ExitCode {
let main_start = Instant::now();
#[cfg(feature = "dhat-heap")]
let _profiler = dhat::Profiler::new_heap();
let code = run(main_start).unwrap_or_else(|error| {
tracing::error!("{}", error);
tracing::trace!("Exiting with error code due to: {:#?}", error);
ExitCode::from(EXIT_CODE_ERROR)
});
if enabled!(Level::TRACE) {
trace!("total process time: {:?}", main_start.elapsed());
}
code
}
#[inline(always)]
pub fn run(main_start: Instant) -> Result<ExitCode, Error> {
let clap_start = Instant::now();
let arguments = CliArguments::parse();
let clap_duration = clap_start.elapsed();
configure_colors(arguments.colors);
let logger_start = Instant::now();
initialize_logger(
if cfg!(debug_assertions) { LevelFilter::DEBUG } else { LevelFilter::INFO },
"MAGO_LOG",
arguments.colors,
);
let logger_duration = logger_start.elapsed();
let trace_enabled = enabled!(Level::TRACE);
let pre_logger_elapsed =
if trace_enabled { main_start.elapsed() - clap_duration - logger_duration } else { Duration::ZERO };
let php_version = arguments.get_php_version()?;
let CliArguments { workspace, config, threads, allow_unsupported_php_version, no_version_check, command, .. } =
arguments;
let config_load_start = trace_enabled.then(Instant::now);
let configuration = Configuration::load(
workspace,
config.as_deref(),
php_version,
threads,
allow_unsupported_php_version,
no_version_check,
)?;
let config_load_duration = config_load_start.map(|s| s.elapsed()).unwrap_or_default();
if let MagoCommand::SelfUpdate(cmd) = command {
return commands::self_update::execute(cmd, configuration.version);
}
check_project_version(&configuration)?;
if !configuration.allow_unsupported_php_version {
if configuration.php_version < MINIMUM_PHP_VERSION {
return Err(Error::PHPVersionIsTooOld(MINIMUM_PHP_VERSION, configuration.php_version));
}
if configuration.php_version > MAXIMUM_PHP_VERSION {
return Err(Error::PHPVersionIsTooNew(MAXIMUM_PHP_VERSION, configuration.php_version));
}
}
let rayon_init_start = trace_enabled.then(Instant::now);
rayon::ThreadPoolBuilder::new()
.num_threads(configuration.threads)
.stack_size(configuration.stack_size)
.build_global()?;
let rayon_init_duration = rayon_init_start.map(|s| s.elapsed()).unwrap_or_default();
if trace_enabled {
trace!("Process startup (dyld and runtime init) took {:?}.", pre_logger_elapsed);
trace!("CLI arguments parsed in {:?}.", clap_duration);
trace!("Logger initialized in {:?}.", logger_duration);
trace!("Configuration loaded in {:?}.", config_load_duration);
trace!("Rayon thread pool built in {:?}.", rayon_init_duration);
trace!("Ready to dispatch command after {:?}.", main_start.elapsed());
}
let command_start = trace_enabled.then(Instant::now);
let result = match command {
MagoCommand::Init(cmd) => cmd.execute(configuration, None),
MagoCommand::Config(cmd) => cmd.execute(configuration),
MagoCommand::ListFiles(cmd) => cmd.execute(configuration, arguments.colors),
MagoCommand::Lint(cmd) => cmd.execute(configuration, arguments.colors),
MagoCommand::Format(cmd) => cmd.execute(configuration, arguments.colors),
MagoCommand::Ast(cmd) => cmd.execute(configuration, arguments.colors),
MagoCommand::Analyze(cmd) => cmd.execute(configuration, arguments.colors),
MagoCommand::Guard(cmd) => cmd.execute(configuration, arguments.colors),
MagoCommand::GenerateCompletions(cmd) => cmd.execute(),
MagoCommand::SelfUpdate(_) => {
unreachable!("The self-update command should have been handled before this point.")
}
};
if let Some(start) = command_start {
trace!("Command finished in {:?}.", start.elapsed());
trace!("Total time spent inside main so far: {:?}.", main_start.elapsed());
}
result
}
fn check_project_version(configuration: &Configuration) -> Result<(), Error> {
let Some(pin_string) = configuration.version.as_deref() else {
return Ok(());
};
let pin = VersionPin::parse(pin_string)?;
let result = pin.check(VERSION)?;
match result {
VersionCheck::Match => Ok(()),
VersionCheck::MajorDrift => {
let installed_major = VERSION.split('.').next().unwrap_or(VERSION);
tracing::error!("Major versions may have incompatible config schemas; refusing to run.");
tracing::error!("Run `mago self-update --to-project-version` to sync to the pinned major.");
tracing::error!(
"Or reinstall the matching binary, or bump `version` in mago.toml to `{installed_major}` once you have reviewed the changelog."
);
Err(Error::ProjectMajorVersionMismatch(pin.to_string(), VERSION.to_string()))
}
VersionCheck::MinorDrift | VersionCheck::PatchDrift => {
if !configuration.no_version_check {
tracing::warn!("mago.toml is pinned to `{pin}` but the installed mago binary is `{VERSION}`.");
tracing::warn!("Run `mago self-update --to-project-version` to sync.");
tracing::warn!("Pass `--no-version-check` or set `MAGO_NO_VERSION_CHECK=1` to silence this warning.");
}
Ok(())
}
}
}