use clap::{Parser, Subcommand};
use indoc::indoc;
use colored::Colorize;
use crate::compiler::CompilerFlags;
mod compiler;
mod docgen;
mod format;
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(arg_required_else_help = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
#[command(about = "Creates a template project at a given directory.")]
New {
name: String
},
#[command(about = "Initializes a template project at the cwd.\n\x1b[31m")]
Init,
#[command(about = "Builds the project to the target directory using gcc or clang, if available.\x1b[31m")]
Build,
#[command(about = "Runs the project's main file, or a standalone c file.\x1b[31m")]
Run {
path: Option<String>
},
#[command(about = "Runs the project's test suite.\n\x1b[33m")]
Test,
#[command(about = "Removes compiled programs from the project.\x1b[33m")]
Clean,
#[command(about = "Generates documentation for the project using doxygen, if available.\x1b[33m")]
Doc {
#[arg(short, long)]
open: bool
},
#[command(about = "Formats the project's code using clang-format, if available.\n\x1b[36m")]
Format,
#[command(about = "Creates a REPL with gcc or clang, if available.\x1b[36m")]
Repl,
#[command(about = "Updates to the latest version of cpkg.\n\x1b[35m")]
Upgrade
}
fn init_project(proj: &std::path::Path) -> std::io::Result<()> {
let src = proj.join("src");
std::fs::create_dir(&src)?;
let main = src.join("main.c");
std::fs::write(&main, indoc! {r#"
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
"#})?;
let tests = proj.join("tests");
std::fs::create_dir(&tests)?;
let main_test = tests.join("main.test.c");
std::fs::write(main_test, indoc!{r#"
#include <assert.h>
int main() {
assert( (1 + 2 == 3) && "C is broken" );
}
"#})?;
let config = proj.join("cpkg.toml");
let name = proj.file_name().unwrap().to_string_lossy();
std::fs::write(config, indoc::formatdoc! {r#"
[package]
name = "{name}"
[dependencies]
"#})?;
if which::which("git").is_ok() {
let ignore = proj.join(".gitignore");
std::fs::write(ignore, indoc!{r#"
/target
"#})?;
}
Ok(())
}
fn main() -> anyhow::Result<()> {
let args = Cli::parse();
match &args.command {
Commands::New { name } => {
let p = std::path::Path::new(name);
if p.exists() {
anyhow::bail!("Cannot create new project at already existing '{name}'");
} else {
std::fs::create_dir(&p)?;
init_project(&p)?;
}
},
Commands::Init => {
let p = std::env::current_dir()?;
if p.read_dir()?.next().is_none() {
init_project(&p)?;
} else {
anyhow::bail!("Cannot initialize project at non-empty directory");
}
},
Commands::Test => {
let config = std::path::Path::new("cpkg.toml");
if !config.exists() {
anyhow::bail!("No cpkg.toml detected, this doesn't seem to be a valid project.");
}
let target = std::path::Path::new("target");
if !target.exists() {
std::fs::create_dir(&target)?;
}
let out = target.join("test");
if !out.exists() {
std::fs::create_dir(&out)?;
}
let backend = compiler::try_locate()?;
let now = std::time::Instant::now();
let src = std::path::Path::new("src");
let tests = std::path::Path::new("tests");
let tests = walkdir::WalkDir::new(tests)
.into_iter()
.chain(walkdir::WalkDir::new(src).into_iter())
.flat_map(std::convert::identity) .filter(|e| e.file_type().is_file()) .filter(|e| e.path().to_string_lossy().ends_with(".test.c")) .map(|e| e.path().to_owned());
let mut compiled_tests = vec![];
for test_path in tests { use std::hash::{Hash, Hasher};
let mut hasher = std::hash::DefaultHasher::new();
test_path.hash(&mut hasher);
let hash = hasher.finish().to_string();
let out = out.join(hash);
backend.compile(&test_path, &[], &out, &Default::default())?;
compiled_tests.push((test_path, out));
}
for (src, compiled) in &compiled_tests {
let out = std::process::Command::new(compiled)
.output()?;
if out.status.success() {
println!("{} {}", " PASSED ".on_bright_green().white(), src.display());
} else {
eprintln!("{} {}: {}", " FAILED ".on_bright_red().white(), src.display(), String::from_utf8_lossy(&out.stderr).trim_end());
}
}
println!("Successfully ran {} tests in {}s.", compiled_tests.len(), now.elapsed().as_secs_f32());
},
Commands::Build => {
let config = std::path::Path::new("cpkg.toml");
if !config.exists() {
anyhow::bail!("No cpkg.toml detected, this doesn't seem to be a valid project.");
}
let main = std::path::Path::new("src/main.c");
if !main.exists() {
anyhow::bail!("No entrypoint found (create src/main.c)");
}
let target = std::path::Path::new("target");
if !target.exists() {
std::fs::create_dir(target)?;
}
let now = std::time::Instant::now();
let out = target.join("out");
let backend = compiler::try_locate()?;
backend.compile(main, &[], &out, &Default::default())?;
println!("Successfully built program in {}s", now.elapsed().as_secs_f32());
},
Commands::Run { path } => {
if let Some(path) = path {
let path = std::path::Path::new(path);
if !path.exists() {
anyhow::bail!("File does not exist.");
}
let temp = std::env::temp_dir();
let temp_bin = temp.join("cpkg_run");
let b = compiler::try_locate()?;
b.compile(&path, &[], &temp_bin, &Default::default())?;
std::process::Command::new(&temp_bin)
.spawn()?;
return Ok(());
}
let config = std::path::Path::new("cpkg.toml");
if !config.exists() {
anyhow::bail!("No cpkg.toml detected, this doesn't seem to be a valid project.");
}
let main = std::path::Path::new("src/main.c");
if !main.exists() {
anyhow::bail!("No entrypoint found (create src/main.c)");
}
let target = std::path::Path::new("target");
if !target.exists() {
std::fs::create_dir(target)?;
}
let out = target.join("out");
let b = compiler::try_locate()?;
b.compile(main, &[], &out, &Default::default())?;
std::process::Command::new(out)
.spawn()?;
},
Commands::Clean => {
let config = std::path::Path::new("cpkg.toml");
if !config.exists() {
anyhow::bail!("No cpkg.toml detected, this doesn't seem to be a valid project.");
}
let target = std::path::Path::new("target");
if !target.exists() {
anyhow::bail!("Failed to clean target directory. Doesn't seem to exist.");
}
std::fs::remove_dir_all(target)?;
println!("Removed target directory.");
},
Commands::Doc { open } => {
let config = std::path::Path::new("cpkg.toml");
if !config.exists() {
anyhow::bail!("No cpkg.toml detected, this doesn't seem to be a valid project.");
}
let backend = docgen::try_locate()?;
let target = std::path::Path::new("target");
if !target.exists() {
std::fs::create_dir(target)?;
}
let doc = target.join("doc");
if !doc.exists() {
std::fs::create_dir(&doc)?;
}
let now = std::time::Instant::now();
let proj = std::path::Path::new("src");
backend.generate(proj, &doc)?;
println!("Generated documentation in {}s", now.elapsed().as_secs_f32());
if *open {
backend.open(&doc)?;
}
},
Commands::Format => {
let config = std::path::Path::new("cpkg.toml");
if !config.exists() {
anyhow::bail!("No cpkg.toml detected, this doesn't seem to be a valid project.");
}
let backend = format::try_locate()?;
let now = std::time::Instant::now();
backend.format(std::path::Path::new("src"))?;
backend.format(std::path::Path::new("tests"))?;
println!("Formatted code in {}s", now.elapsed().as_secs_f32());
},
Commands::Repl => {
use std::io::Write;
println!("{}", "Please note that the repl is very basic and experimental.\nYour code will run entirely each line.".yellow());
let backend = compiler::try_locate()?;
let temp = std::env::temp_dir();
let temp_repl = temp.join("cpkg_repl.c");
let temp_bin = temp.join("cpkg_repl");
let mut stdout = std::io::stdout().lock();
let mut buffer = String::new();
let mut editor = rustyline::DefaultEditor::new()?;
loop {
let temp = editor.readline("> ")?;
editor.add_history_entry(&temp)?;
let total = [buffer.clone(), temp].join("");
std::fs::write(&temp_repl, indoc::formatdoc!(r#"
int main() {{
{total}
return 0;
}}
"#))?;
match backend.compile(&temp_repl, &[], &temp_bin, &CompilerFlags::REPL) {
Ok(_) => {
let mut out = std::process::Command::new(&temp_bin)
.output()?;
if out.status.success() {
buffer = total;
if out.stdout.ends_with(b"\n") {
stdout.write(&out.stdout)?;
} else { out.stdout.push(b'\n');
stdout.write(&out.stdout)?;
}
} else {
stdout.write(b"Failed to run: ")?;
stdout.write(&out.stderr)?;
stdout.write(b"\n")?;
}
stdout.flush()?;
},
Err(_) => ()
}
}
},
Commands::Upgrade => {
self_update::backends::github::Update::configure()
.repo_owner("DvvCz")
.repo_name("cpkg")
.bin_name("cpkg")
.show_download_progress(true)
.current_version(self_update::cargo_crate_version!())
.build()?
.update()?;
}
}
Ok(())
}