2026-03-24 13:40:47 +00:00

233 lines
6.7 KiB
Rust

use clap::{Parser, Subcommand};
use std::env;
use std::path::{Path, PathBuf};
use std::process::{Command, ExitStatus};
/// PROMETEU Dispatcher (CLI).
///
/// The main entry point for the user. This binary does not implement
/// compilation or execution logic itself; instead, it acts as a smart
/// front-end that locates and dispatches commands to specialized
/// components like `prometeu-host-desktop-winit` or `prometeu-build-pipeline`.
#[derive(Parser)]
#[command(name = "prometeu")]
#[command(about = "Dispatcher for the Prometeu ecosystem", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
/// Executes a cartridge using the available runtime.
Run {
/// Path to the cartridge (directory or .pmc file).
cart: String,
},
/// Executes a cartridge in Assisted Mode (Debug).
/// The runtime will wait for a DevTools connection before starting.
Debug {
/// Path to the cartridge.
cart: String,
/// TCP port for the DevTools server (default: 7777).
#[arg(long, default_value_t = 7777)]
port: u16,
},
/// Compiles a source project into a cartridge directory with `program.pbx`.
Build {
/// Project source directory.
project_dir: String,
/// Whether to explain the dependency resolution process.
#[arg(long)]
explain_deps: bool,
},
/// Packages a cartridge directory into a distributable .pmc file.
Pack {
/// Cartridge directory.
cart_dir: String,
},
/// Diagnostic commands to verify project or cartridge integrity.
Verify {
#[command(subcommand)]
target: VerifyCommands,
},
}
#[derive(Subcommand)]
enum VerifyCommands {
/// Verifies a project
C {
/// Project directory
project_dir: String,
/// Whether to explain the dependency resolution process.
#[arg(long)]
explain_deps: bool,
},
/// Verifies a cartridge or PMC file
P {
/// Path to the cartridge or PMC
cart_or_pmc: String,
},
}
fn main() {
let cli = Cli::parse();
let exe_dir = match env::current_exe() {
Ok(exe_path) => exe_path.parent().unwrap_or_else(|| Path::new("../../../..")).to_path_buf(),
Err(e) => {
eprintln!("Error obtaining executable path: {}", e);
std::process::exit(1);
}
};
match cli.command {
Some(Commands::Run { cart }) => {
dispatch(&exe_dir, "prometeu-runtime", &["--run", &cart]);
}
Some(Commands::Debug { cart, port }) => {
dispatch(
&exe_dir,
"prometeu-runtime",
&["--debug", &cart, "--port", &port.to_string()],
);
}
Some(Commands::Build { project_dir, explain_deps }) => {
let mut args = vec!["build", &project_dir];
if explain_deps {
args.push("--explain-deps");
}
dispatch(&exe_dir, "prometeuc", &args);
}
Some(Commands::Pack { .. }) => {
not_implemented("pack", "prometeup");
}
Some(Commands::Verify { target }) => match target {
VerifyCommands::C { project_dir, explain_deps } => {
let mut args = vec!["verify", &project_dir];
if explain_deps {
args.push("--explain-deps");
}
dispatch(&exe_dir, "prometeuc", &args);
}
VerifyCommands::P { .. } => not_implemented("verify p", "prometeup"),
},
None => {
use clap::CommandFactory;
Cli::command().print_help().unwrap();
println!();
}
}
}
fn get_binary_path(dir: &Path, name: &str) -> PathBuf {
let mut path = dir.join(name);
if cfg!(target_os = "windows") {
path.set_extension("exe");
}
path
}
fn dispatch(exe_dir: &Path, bin_name: &str, args: &[&str]) {
let bin_path = get_binary_path(exe_dir, bin_name);
if !bin_path.exists() {
eprintln!(
"prometeu: command '{}' is not yet available in this distribution on {}",
match bin_name {
"prometeu-runtime" => "run/debug",
"prometeuc" => "build/verify c",
"prometeup" => "pack/verify p",
_ => bin_name,
},
exe_dir.display()
);
std::process::exit(1);
}
execute_bin(&bin_path, args);
}
fn execute_bin(bin_path: &Path, args: &[&str]) {
let status = Command::new(bin_path)
.args(args)
.stdin(std::process::Stdio::inherit())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status();
match status {
Ok(status) => match child_exit_code(status) {
Ok(code) => std::process::exit(code),
Err(message) => {
eprintln!("Error: {} ({})", message, bin_path.display());
std::process::exit(1);
}
},
Err(e) => {
eprintln!("Error executing {}: {}", bin_path.display(), e);
std::process::exit(1);
}
}
}
fn not_implemented(cmd: &str, _bin_name: &str) {
eprintln!("prometeu: command '{}' is not yet available in this distribution", cmd);
std::process::exit(1);
}
fn child_exit_code(status: ExitStatus) -> Result<i32, &'static str> {
status.code().ok_or("child process terminated without a numeric exit code")
}
#[cfg(test)]
mod tests {
use super::child_exit_code;
use std::process::ExitStatus;
#[test]
fn child_exit_code_preserves_zero() {
let status = exit_status_with_code(0);
assert_eq!(child_exit_code(status), Ok(0));
}
#[test]
fn child_exit_code_preserves_non_zero_one() {
let status = exit_status_with_code(1);
assert_eq!(child_exit_code(status), Ok(1));
}
#[test]
fn child_exit_code_preserves_non_zero_other() {
let status = exit_status_with_code(42);
assert_eq!(child_exit_code(status), Ok(42));
}
#[cfg(unix)]
#[test]
fn child_exit_code_rejects_status_without_numeric_code() {
use std::os::unix::process::ExitStatusExt;
let status = ExitStatus::from_raw(9);
assert_eq!(
child_exit_code(status),
Err("child process terminated without a numeric exit code")
);
}
#[cfg(unix)]
fn exit_status_with_code(code: i32) -> ExitStatus {
use std::os::unix::process::ExitStatusExt;
ExitStatus::from_raw(code << 8)
}
#[cfg(windows)]
fn exit_status_with_code(code: i32) -> ExitStatus {
use std::os::windows::process::ExitStatusExt;
ExitStatus::from_raw(code as u32)
}
}