use clap::{Args, Parser, Subcommand, ValueEnum}; use paris::{error, info, success}; use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt; use std::process::exit; use tokio::io::AsyncWriteExt; use tokio::net::TcpStream; use url::Url; use uuid::Uuid; #[derive(Debug, Parser)] #[command(name = "forge-server")] #[command(about = "A simple remote build system", long_about = None)] struct Cli { #[arg(value_name = "IP")] host: String, #[arg(value_name = "PORT", default_value_t = 9134)] port: u16, #[command(subcommand)] command: Commands, } #[derive(Debug, Subcommand)] enum Commands { /// Send a new build request #[command(arg_required_else_help = true)] Send { /// Password for authentication #[arg(long, value_name = "PASSWD")] auth: Option, /// Whether to profile the build #[arg(long, default_value_t = false)] profile: bool, /// Command(s) to run before building #[arg(long, value_name = "CMD")] prexec: Option, /// Remote git repository to clone from #[arg(long, value_name = "URL")] repo: String, /// Build system to use #[arg(long,require_equals = true, num_args = 0..=1, default_value_t = BuildSystem::Cargo,default_missing_value = "cargo")] build: BuildSystem, /// Cargo/Make subcommand to use #[arg(long, require_equals = true, num_args = 0..=1, default_value_t = BuildSubcommand::Build, default_missing_value = "build")] subcommand: BuildSubcommand, /// Comma-separated cargo features #[arg(long, value_name = "FEATURES")] features: Option>, /// Optional cargo flag #[arg(long, require_equals = true, num_args = 0..=1, default_value_t = Tag::Debug, default_missing_value = "debug")] tag: Tag, /// Basename of the repository #[arg(long, value_name = "NAME")] basename: String, }, } #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] enum BuildSystem { Cargo, Make, } impl std::fmt::Display for BuildSystem { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_possible_value() .expect("no values are skipped") .get_name() .fmt(f) } } #[derive(Serialize, Deserialize, ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] enum BuildSubcommand { Run, Build, Install, } impl std::fmt::Display for BuildSubcommand { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_possible_value() .expect("no values are skipped") .get_name() .fmt(f) } } #[derive(Serialize, Deserialize, ValueEnum, Copy, Clone, Debug, PartialEq, Eq)] enum Tag { Release, Debug, } impl std::fmt::Display for Tag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.to_possible_value() .expect("no values are skipped") .get_name() .fmt(f) } } #[derive(Serialize, Deserialize)] struct Command { build_system: BuildSystem, subcommand: BuildSubcommand, features: Option>, tag: Tag, } #[derive(Serialize, Deserialize)] struct Message { authentication: Option, uuid: Uuid, pre_exec: Option, profile: bool, command: Command, repository: Url, basename: String, } #[tokio::main] async fn main() -> Result<(), Box> { let args = Cli::parse(); match args.command { Commands::Send { auth, repo, profile, prexec, build, subcommand, features, tag, basename, } => { // Connect to a peer let remote = format!("{}:{}", args.host, args.port); let mut stream = match TcpStream::connect(&remote).await { Ok(s) => s, Err(e) => { error!("Could not connect to remote host: {}", e); exit(1); } }; success!("Connected to: {}", &remote); let test_command = Command { build_system: build, subcommand, features, tag, }; let test_message = Message { authentication: auth, uuid: Uuid::new_v4(), pre_exec: prexec, profile, command: test_command, repository: Url::parse(&repo).unwrap(), basename, }; info!("Message UUID: {}", &test_message.uuid); let j = match serde_json::to_string(&test_message) { Ok(s) => s, Err(e) => { error!("Could not serialize JSON message: {}", e); exit(1); } }; match stream.write_all(&j.as_bytes()).await { Ok(_) => { info!( "Sent JSON:\n{}", serde_json::to_string_pretty(&test_message)? ); } Err(e) => error!("Could not write to stream: {}", e), } } } Ok(()) }