use std::{ env::set_current_dir, path::{Path, PathBuf}, process::Command, }; use clap::CommandFactory; use colored::Colorize; use glob::glob; const MAIN_C: &str = include_str!("templates/main.c"); const GITIGNORE: &str = "target/\ncompile_commands.json\n"; #[derive(clap::Parser)] #[clap(version, about)] pub struct App { #[clap(subcommand)] command: Subcommand, } #[derive(clap::Subcommand)] enum Subcommand { /// Create a new C project New { /// The name of the new project name: String, }, /// Initialize a new C project Init, /// Run the local project Run { /// Force recompilation of the project #[arg(long, short)] force_recompile: bool, /// The build mode to use mode: Option, /// Arguments to pass to the project binary #[arg(long, short)] args: Option>, }, /// Build the local project Build { /// Force recompilation of the project #[arg(long, short)] force_recompile: bool, /// The build mode to use mode: Option, }, /// Clean all in progress files Clean, /// Utility functions Utils { #[clap(subcommand)] command: UtilSubcommand, }, /// List available build modes List, /// Add a new package to the project (throguh pkg-config) Add { /// The package to be added package: String, }, } #[derive(clap::Subcommand)] enum UtilSubcommand { /// Generate shell completions Completions { /// The shell to generate completions for #[arg(value_enum, long, short)] shell: ShellCompletions, }, /// Generate compile_commands.json for IDE support GenCompileCommands { /// The build mode to generate for mode: Option, }, } #[derive(Clone, clap::ValueEnum)] enum ShellCompletions { Bash, Fish, PowerShell, Zsh, } impl App { pub fn run(self) { match self.command { Subcommand::New { name } => { if let Err(e) = create_project(&name) { eprintln!("Error creating project '{name}': {e}"); std::process::exit(1); } } Subcommand::Init => { if let Err(e) = create_project(".") { eprintln!("Error initializing project: {e}"); std::process::exit(1); } } Subcommand::Run { mode, args, force_recompile, } => { if let Err(e) = build(&mode, force_recompile) { eprintln!("Error building project: {e}"); std::process::exit(1); }; if let Err(e) = run(&mode, args) { eprintln!("Error running project: {e}"); std::process::exit(1); } } Subcommand::Build { mode, force_recompile, } => { if let Err(e) = build(&mode, force_recompile) { eprintln!("Error building project: {e}"); std::process::exit(1); } } Subcommand::Clean => { if let Err(e) = clean() { eprintln!("Error cleaning project: {e}"); std::process::exit(1); } } Subcommand::Utils { command } => match command { UtilSubcommand::Completions { shell } => { let name = env!("CARGO_PKG_NAME"); let mut command = App::command(); match shell { ShellCompletions::Bash => clap_complete::generate( clap_complete::shells::Bash, &mut command, name, &mut std::io::stdout(), ), ShellCompletions::Fish => clap_complete::generate( clap_complete::shells::Fish, &mut command, name, &mut std::io::stdout(), ), ShellCompletions::PowerShell => clap_complete::generate( clap_complete::shells::PowerShell, &mut command, name, &mut std::io::stdout(), ), ShellCompletions::Zsh => clap_complete::generate( clap_complete::shells::Zsh, &mut command, name, &mut std::io::stdout(), ), } } UtilSubcommand::GenCompileCommands { mode } => { if let Err(e) = gen_compile_commands(&mode) { eprintln!("Error generating compile commands: {e}"); std::process::exit(1); } } }, Subcommand::List => { if let Err(e) = list() { eprintln!("Error listing build profiles: {e}"); std::process::exit(1); } } Subcommand::Add { package } => { if let Err(e) = add_package(&package) { eprintln!("Error adding package {package} to project: {e}"); std::process::exit(1); } } } } } fn add_package(package: &str) -> std::io::Result<()> { let status = Command::new("pkg-config") .arg("--exists") .arg(&package) .status() .map_err(|_| { std::io::Error::new( std::io::ErrorKind::NotFound, "pkg-config not found - please install it first", ) })?; if !status.success() { eprintln!( " {} pkg-config could not find package '{package}'", "Error".red().bold() ); eprintln!( " {} try installing the development package for '{package}', e.g.:", "Hint".yellow().bold() ); eprintln!(" Arch: sudo pacman -S {package}"); eprintln!(" Debian: sudo apt install lib{package}-dev"); std::process::exit(1); } let mut conf = get_config().ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "no Pallet.toml found in local directory", ))?; let deps = conf.dependencies.get_or_insert_with(Vec::new); if deps.contains(&package.to_owned()) { println!( " {} '{package}' is already a dependency", "Warning".yellow().bold() ); return Ok(()); } deps.push(package.to_owned()); std::fs::write( "Pallet.toml", toml::to_string_pretty(&conf).expect("valid TOML"), )?; println!(" {} {package}", "Added".green().bold()); Ok(()) } fn gen_compile_commands(mode: &Option) -> std::io::Result<()> { let conf = get_config().ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "no Pallet.toml found in current directory", ))?; let build_config = conf.get_or_default(mode).ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "build layout not found", ))?; let compiler = conf.compiler.as_deref().unwrap_or("gcc"); let cwd = std::env::current_dir()?; let obj_dir = format!("target/{}/obj", build_config.name); let source_files: Vec = glob("src/*.c") .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{e}")))? .filter_map(|e| e.ok()) .collect(); let entries: Vec = source_files .iter() .map(|src| { let stem = src.file_stem().unwrap().to_string_lossy(); let obj = format!("{}/{}.o", obj_dir, stem); let command = std::iter::once(compiler.to_string()) .chain(build_config.args.iter().cloned()) .chain(["-c".to_string(), src.to_string_lossy().to_string()]) .chain(["-o".to_string(), obj]) .collect::>() .join(" "); serde_json::json!({ "directory": cwd.to_string_lossy(), "file": cwd.join(src).to_string_lossy(), "command": command }) }) .collect(); let json = serde_json::to_string_pretty(&entries) .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{e}")))?; std::fs::write("compile_commands.json", json)?; println!(" {} compile_commands.json", "Generated".green().bold()); Ok(()) } fn list() -> std::io::Result<()> { let conf = get_config().ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "no Pallet.toml found in local directory", ))?; for build in conf.build { println!(" - {}: {:?}", build.name.green().bold(), build.args); } Ok(()) } fn create_project>(directory: P) -> std::io::Result<()> { let name = if directory.as_ref().to_string_lossy() == "." { String::new() } else { format!(" '{}'", directory.as_ref().to_string_lossy()) }; println!( " {} binary (application){}", "Creating".green().bold(), name ); let pathdir = directory.as_ref(); if pathdir.exists() && pathdir.to_string_lossy() != "." { return Err(std::io::Error::new( std::io::ErrorKind::AlreadyExists, "specified directory already exists", )); } if !pathdir.exists() && pathdir.to_string_lossy() != "." { std::fs::create_dir(pathdir)?; } set_current_dir(pathdir)?; std::fs::create_dir("src")?; std::fs::write("src/main.c", MAIN_C)?; std::fs::write(".gitignore", GITIGNORE)?; let config = crate::config::Config::new(&pathdir.to_string_lossy()); let serial = toml::to_string_pretty(&config).expect("a valid TOML structure"); std::fs::write("Pallet.toml", &serial)?; Ok(()) } fn get_config() -> Option { let p = PathBuf::from("Pallet.toml"); if !p.exists() { return None; } let raw = std::fs::read_to_string(&p).expect("A valid config file"); toml::from_str(&raw).ok() } fn build(mode: &Option, force_recompile: bool) -> std::io::Result<()> { let conf = match get_config() { Some(conf) => conf, None => { eprintln!("Error opening config file. No Pallet.toml in current directory."); std::process::exit(1); } }; let build_config = conf.get_or_default(mode).ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "build layout not found", ))?; println!( " {} '{}' profile for project '{}'", "Building".green().bold(), build_config.name, conf.name ); let start = std::time::Instant::now(); let obj_dir = format!("target/{}/obj", build_config.name); std::fs::create_dir_all(&obj_dir)?; let compiler = conf.compiler.as_deref().unwrap_or("gcc"); let mut extra_compile_flags: Vec = Vec::new(); let mut extra_link_flags: Vec = Vec::new(); if let Some(deps) = &conf.dependencies { if !deps.is_empty() { let cflags = Command::new("pkg-config") .arg("--cflags") .args(deps) .output() .map_err(|_| { std::io::Error::new(std::io::ErrorKind::NotFound, "pkg-config not found") })?; if !cflags.status.success() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "pkg-config --cflags failed - is the package installed?", )); } let libs = Command::new("pkg-config") .arg("--libs") .args(deps) .output() .map_err(|_| { std::io::Error::new(std::io::ErrorKind::NotFound, "pkg-config not found") })?; if !libs.status.success() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "pkg-config --libs failed - is the pacakge installed?", )); } extra_compile_flags = String::from_utf8_lossy(&cflags.stdout) .split_whitespace() .map(str::to_owned) .collect(); extra_link_flags = String::from_utf8_lossy(&libs.stdout) .split_whitespace() .map(str::to_owned) .collect(); } } let source_files: Vec = glob("src/*.c") .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{e}")))? .filter_map(|e| e.ok()) .collect(); let mut any_changed = false; for src in &source_files { let stem = src.file_stem().unwrap().to_string_lossy(); let obj_path = format!("{}/{}.o", obj_dir, stem); let hash_path = format!("{}/{}.c.hash", obj_dir, stem); let hash = hash_file(src)?; let needs_compile = force_recompile || match std::fs::read_to_string(&hash_path) { Ok(old) => old.trim() != hash.trim(), Err(_) => true, }; if needs_compile { println!(" {} {}", "Compiling".green().bold(), src.display()); let status = Command::new(compiler) .args(&build_config.args) .args(&extra_compile_flags) .arg("-c") .arg(src) .arg("-o") .arg(&obj_path) .status()?; if !status.success() { return Err(std::io::Error::new( std::io::ErrorKind::Other, format!("failed to compile {}", src.display()), )); } std::fs::write(&hash_path, &hash)?; any_changed = true; } } let binary = format!("target/{}/{}", build_config.name, conf.name); let binary_exists = PathBuf::from(&binary).exists(); if !any_changed && binary_exists { println!( " {} (code not changed, recompile not made)", "Finished".green().bold() ); return Ok(()); } let obj_files: Vec = source_files .iter() .map(|src| { let stem = src.file_stem().unwrap().to_string_lossy(); format!("{}/{}.o", obj_dir, stem) }) .collect(); let status = Command::new(compiler) .args(&obj_files) .args(&extra_link_flags) .arg("-o") .arg(&binary) .status()?; if !status.success() { return Err(std::io::Error::new( std::io::ErrorKind::Other, "linking failed", )); } let stop = start.elapsed(); gen_compile_commands(&mode)?; println!( " {} '{}' profile for project '{}' in {:.2}s", "Finished".green().bold(), build_config.name, conf.name, stop.as_secs_f64() ); Ok(()) } fn hash_file(path: &Path) -> std::io::Result { let contents = std::fs::read_to_string(path)?; Ok(sha256::digest(contents)) } fn run(mode: &Option, args: Option>) -> std::io::Result<()> { let conf = match get_config() { Some(conf) => conf, None => { eprintln!("Error opening config file. No Pallet.toml in current directory."); std::process::exit(1); } }; let build_config = conf.get_or_default(mode).ok_or(std::io::Error::new( std::io::ErrorKind::NotFound, "build layout not found", ))?; println!( " {} '{}' profile for project '{}'", "Running".green().bold(), build_config.name, conf.name ); let mut command = Command::new(format!("target/{}/{}", build_config.name, conf.name)); if let Some(args) = args { for arg in args { command.arg(arg); } } let mut child = command.spawn()?; child.wait()?; Ok(()) } fn clean() -> std::io::Result<()> { if get_config().is_none() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, "no Pallet.toml found in local dir", )); } std::fs::remove_dir_all("target/")?; println!( " {} removed files in target/ directory", "Successfully".green().bold() ); Ok(()) }