2026-03-22 11:16:14 -05:00
|
|
|
use std::{
|
|
|
|
|
env::set_current_dir,
|
|
|
|
|
path::{Path, PathBuf},
|
2026-03-22 18:04:40 -05:00
|
|
|
process::Command,
|
2026-03-22 11:16:14 -05:00
|
|
|
};
|
|
|
|
|
|
2026-03-22 21:04:34 -05:00
|
|
|
use clap::CommandFactory;
|
2026-03-22 18:17:51 -05:00
|
|
|
use colored::Colorize;
|
2026-03-22 18:04:40 -05:00
|
|
|
use glob::glob;
|
|
|
|
|
|
2026-03-22 11:16:14 -05:00
|
|
|
const MAIN_C: &str = include_str!("templates/main.c");
|
2026-03-22 18:44:00 -05:00
|
|
|
const GITIGNORE: &str = "target/";
|
2026-03-22 11:16:14 -05:00
|
|
|
|
|
|
|
|
#[derive(clap::Parser)]
|
2026-03-22 21:22:03 -05:00
|
|
|
#[clap(version, about)]
|
2026-03-22 11:16:14 -05:00
|
|
|
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 {
|
2026-03-23 09:55:03 -06:00
|
|
|
/// Force recompilation of the project
|
|
|
|
|
#[arg(long, short)]
|
|
|
|
|
force_recompile: bool,
|
2026-03-22 11:16:14 -05:00
|
|
|
/// The build mode to use
|
|
|
|
|
mode: Option<String>,
|
2026-03-22 18:04:40 -05:00
|
|
|
/// Arguments to pass to the project binary
|
2026-03-22 18:36:37 -05:00
|
|
|
#[arg(long, short)]
|
2026-03-22 18:04:40 -05:00
|
|
|
args: Option<Vec<String>>,
|
2026-03-22 11:16:14 -05:00
|
|
|
},
|
|
|
|
|
/// Build the local project
|
|
|
|
|
Build {
|
2026-03-23 09:55:03 -06:00
|
|
|
/// Force recompilation of the project
|
|
|
|
|
#[arg(long, short)]
|
|
|
|
|
force_recompile: bool,
|
2026-03-22 11:16:14 -05:00
|
|
|
/// The build mode to use
|
|
|
|
|
mode: Option<String>,
|
|
|
|
|
},
|
2026-03-22 18:22:27 -05:00
|
|
|
/// Clean all in progress files
|
|
|
|
|
Clean,
|
2026-03-22 21:04:34 -05:00
|
|
|
/// Utility functions
|
|
|
|
|
Utils {
|
|
|
|
|
#[clap(subcommand)]
|
|
|
|
|
command: UtilSubcommand,
|
|
|
|
|
},
|
2026-03-22 21:40:48 -05:00
|
|
|
/// List available build modes
|
|
|
|
|
List,
|
2026-03-22 21:04:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(clap::Subcommand)]
|
|
|
|
|
enum UtilSubcommand {
|
|
|
|
|
/// Generate shell completions
|
|
|
|
|
Completions {
|
|
|
|
|
/// The shell to generate completions for
|
2026-03-22 21:22:03 -05:00
|
|
|
#[arg(value_enum, long, short)]
|
2026-03-22 21:04:34 -05:00
|
|
|
shell: ShellCompletions,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, clap::ValueEnum)]
|
|
|
|
|
enum ShellCompletions {
|
|
|
|
|
Bash,
|
|
|
|
|
Fish,
|
|
|
|
|
PowerShell,
|
|
|
|
|
Zsh,
|
2026-03-22 11:16:14 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl App {
|
|
|
|
|
pub fn run(self) {
|
|
|
|
|
match self.command {
|
2026-03-23 10:27:26 -06:00
|
|
|
Subcommand::New { name } => {
|
|
|
|
|
if let Err(e) = create_project(&name) {
|
2026-03-22 11:16:14 -05:00
|
|
|
eprintln!("Error creating project '{name}': {e}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
2026-03-23 10:27:26 -06:00
|
|
|
}
|
|
|
|
|
Subcommand::Init => {
|
|
|
|
|
if let Err(e) = create_project(".") {
|
2026-03-22 11:16:14 -05:00
|
|
|
eprintln!("Error initializing project: {e}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
2026-03-23 10:27:26 -06:00
|
|
|
}
|
2026-03-23 09:55:03 -06:00
|
|
|
Subcommand::Run {
|
|
|
|
|
mode,
|
|
|
|
|
args,
|
|
|
|
|
force_recompile,
|
|
|
|
|
} => {
|
2026-03-23 10:27:26 -06:00
|
|
|
if let Err(e) = build(&mode, force_recompile) {
|
|
|
|
|
eprintln!("Error building project: {e}");
|
|
|
|
|
std::process::exit(1);
|
2026-03-22 18:04:40 -05:00
|
|
|
};
|
|
|
|
|
if let Err(e) = run(&mode, args) {
|
|
|
|
|
eprintln!("Error running project: {e}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-23 09:55:03 -06:00
|
|
|
Subcommand::Build {
|
|
|
|
|
mode,
|
|
|
|
|
force_recompile,
|
2026-03-23 10:27:26 -06:00
|
|
|
} => {
|
|
|
|
|
if let Err(e) = build(&mode, force_recompile) {
|
2026-03-22 18:04:40 -05:00
|
|
|
eprintln!("Error building project: {e}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
2026-03-23 10:27:26 -06:00
|
|
|
}
|
|
|
|
|
Subcommand::Clean => {
|
|
|
|
|
if let Err(e) = clean() {
|
2026-03-22 18:22:27 -05:00
|
|
|
eprintln!("Error cleaning project: {e}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
2026-03-23 10:27:26 -06:00
|
|
|
}
|
2026-03-22 21:04:34 -05:00
|
|
|
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(),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-03-23 10:27:26 -06:00
|
|
|
Subcommand::List => {
|
|
|
|
|
if let Err(e) = list() {
|
2026-03-22 21:40:48 -05:00
|
|
|
eprintln!("Error listing build profiles: {e}");
|
|
|
|
|
std::process::exit(1);
|
|
|
|
|
}
|
2026-03-23 10:27:26 -06:00
|
|
|
}
|
2026-03-22 11:16:14 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 21:40:48 -05:00
|
|
|
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(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 11:16:14 -05:00
|
|
|
fn create_project<P: AsRef<Path>>(directory: P) -> std::io::Result<()> {
|
2026-03-22 18:17:51 -05:00
|
|
|
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
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-22 11:16:14 -05:00
|
|
|
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)?;
|
2026-03-22 18:22:27 -05:00
|
|
|
std::fs::write(".gitignore", GITIGNORE)?;
|
2026-03-22 11:16:14 -05:00
|
|
|
|
|
|
|
|
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<crate::config::Config> {
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 09:55:03 -06:00
|
|
|
fn build(mode: &Option<String>, force_recompile: bool) -> std::io::Result<()> {
|
2026-03-22 11:16:14 -05:00
|
|
|
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",
|
|
|
|
|
))?;
|
|
|
|
|
|
2026-03-22 18:17:51 -05:00
|
|
|
println!(
|
|
|
|
|
" {} '{}' profile for project '{}'",
|
|
|
|
|
"Building".green().bold(),
|
|
|
|
|
build_config.name,
|
|
|
|
|
conf.name
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
let obj_dir = format!("target/{}/obj", build_config.name);
|
2026-03-22 18:04:40 -05:00
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
std::fs::create_dir_all(&obj_dir)?;
|
2026-03-22 21:56:40 -05:00
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
let compiler = conf.compiler.as_deref().unwrap_or("gcc");
|
2026-03-22 21:56:40 -05:00
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
let source_files: Vec<PathBuf> = glob("src/*.c")
|
|
|
|
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{e}")))?
|
|
|
|
|
.filter_map(|e| e.ok())
|
|
|
|
|
.collect();
|
2026-03-22 21:56:40 -05:00
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
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)
|
|
|
|
|
.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;
|
2026-03-22 21:56:40 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
let binary = format!("target/{}/{}", build_config.name, conf.name);
|
|
|
|
|
let binary_exists = PathBuf::from(&binary).exists();
|
2026-03-22 18:04:40 -05:00
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
if !any_changed && binary_exists {
|
|
|
|
|
println!(
|
|
|
|
|
" {} (code not changed, recompile not made)",
|
|
|
|
|
"Finished".green().bold()
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
2026-03-22 18:04:40 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
let obj_files: Vec<String> = source_files
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|src| {
|
|
|
|
|
let stem = src.file_stem().unwrap().to_string_lossy();
|
|
|
|
|
format!("{}/{}.o", obj_dir, stem)
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-03-22 18:04:40 -05:00
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
let status = Command::new(compiler)
|
|
|
|
|
.args(&obj_files)
|
2026-03-22 18:04:40 -05:00
|
|
|
.arg("-o")
|
2026-03-23 11:00:40 -06:00
|
|
|
.arg(&binary)
|
|
|
|
|
.status()?;
|
2026-03-22 18:04:40 -05:00
|
|
|
|
2026-03-23 10:27:26 -06:00
|
|
|
if !status.success() {
|
|
|
|
|
return Err(std::io::Error::new(
|
|
|
|
|
std::io::ErrorKind::Other,
|
2026-03-23 11:00:40 -06:00
|
|
|
"linking failed",
|
2026-03-23 10:27:26 -06:00
|
|
|
));
|
|
|
|
|
}
|
2026-03-22 18:04:40 -05:00
|
|
|
|
2026-03-22 18:17:51 -05:00
|
|
|
let stop = start.elapsed();
|
|
|
|
|
|
|
|
|
|
println!(
|
|
|
|
|
" {} '{}' profile for project '{}' in {:.2}s",
|
|
|
|
|
"Finished".green().bold(),
|
|
|
|
|
build_config.name,
|
|
|
|
|
conf.name,
|
|
|
|
|
stop.as_secs_f64()
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-22 18:04:40 -05:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 11:00:40 -06:00
|
|
|
fn hash_file(path: &Path) -> std::io::Result<String> {
|
|
|
|
|
let contents = std::fs::read_to_string(path)?;
|
|
|
|
|
Ok(sha256::digest(contents))
|
2026-03-22 21:56:40 -05:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 18:04:40 -05:00
|
|
|
fn run(mode: &Option<String>, args: Option<Vec<String>>) -> 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",
|
|
|
|
|
))?;
|
|
|
|
|
|
2026-03-22 18:17:51 -05:00
|
|
|
println!(
|
|
|
|
|
" {} '{}' profile for project '{}'",
|
|
|
|
|
"Running".green().bold(),
|
|
|
|
|
build_config.name,
|
|
|
|
|
conf.name
|
|
|
|
|
);
|
|
|
|
|
|
2026-03-22 18:04:40 -05:00
|
|
|
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(())
|
2026-03-22 11:16:14 -05:00
|
|
|
}
|
2026-03-22 18:22:27 -05:00
|
|
|
|
|
|
|
|
fn clean() -> std::io::Result<()> {
|
2026-03-23 10:27:26 -06:00
|
|
|
if get_config().is_none() {
|
2026-03-22 18:22:27 -05:00
|
|
|
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(())
|
|
|
|
|
}
|