use crate::compile_db::CompileCommand;
use crate::diagnostics::{print_diagnostic, Diagnostic};
use crate::error::Result;
use crate::project::Project;
use std::path::PathBuf;
pub struct CompiledObject {
obj_path: PathBuf,
compile_command: String,
}
pub struct CompileOptions {
compiler: String,
includes: Vec<PathBuf>,
cflags: Vec<String>,
}
pub fn build_project(project: &Project) -> Result<()> {
let sources = project.source_files()?;
std::fs::create_dir_all(&project.build_dir)?;
let compiler = compiler_binary(&project.config.build.compiler)?;
println!("Using compiler: {}", compiler);
let mut opts = CompileOptions {
compiler: compiler.to_string(),
includes: vec![project.root.join("include")],
cflags: project.config.build.cflags.clone(),
};
for (name, output) in &project.resolved_deps {
let prefix = project.dep_prefix(name);
opts.includes.extend(output.include_dirs.clone());
}
let mut object_files: Vec<PathBuf> = Vec::new();
let mut compile_commands: Vec<CompileCommand> = Vec::new();
for src in sources {
let file_stem = src.file_stem().unwrap().to_str().unwrap();
let obj_path = project.build_dir.join(format!("{}.o", file_stem));
let mut cmd = std::process::Command::new(&opts.compiler);
cmd.arg("-c").arg(&src).arg("-o").arg(&obj_path);
cmd.args(
&opts
.includes
.iter()
.map(|p| format!("-I{}", p.display()))
.collect::<Vec<_>>(),
);
cmd.args(&opts.cflags);
cmd.arg(format!("-std={}", project.config.project.c_standard));
let command_str = format!("{:?}", cmd);
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
let mut printed_pretty = false;
for line in stderr.lines() {
if let Some(diag) = Diagnostic::parse_line(line) {
print_diagnostic(&diag);
printed_pretty = true;
break;
}
}
let error_detail = if printed_pretty {
"See error details above...".to_string()
} else {
stderr.to_string()
};
return Err(crate::error::BuildError::Compile(
src.display().to_string(),
error_detail,
));
}
compile_commands.push(CompileCommand {
directory: project.root.display().to_string(),
file: src.display().to_string(),
command: command_str,
output: obj_path.display().to_string(),
});
object_files.push(obj_path);
}
crate::compile_db::write(&compile_commands, &project.root.join("compile_commands.json"))?;
let binary_path = project.build_dir.join("bin").join(
project
.config
.project
.output_name
.as_ref()
.unwrap_or(&project.config.project.name),
);
std::fs::create_dir_all(project.build_dir.join("bin"))?;
let mut link_cmd = std::process::Command::new(&opts.compiler);
link_cmd.args(&object_files);
link_cmd.arg("-o").arg(&binary_path);
link_cmd.args(project.config.build.libs.iter().map(|l| format!("-l{}", l)));
let output = link_cmd.output()?;
if !output.status.success() {
return Err(crate::error::BuildError::Link(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
Ok(())
}
pub fn run_project(project: &Project) -> Result<()> {
build_project(project)?;
let binary_path = project.build_dir.join("bin").join(
project
.config
.project
.output_name
.as_ref()
.unwrap_or(&project.config.project.name),
);
println!("Running: {}", binary_path.display());
let status = std::process::Command::new(&binary_path).status()?;
if !status.success() {
return Err(crate::error::BuildError::CommandFailed {
cmd: binary_path.display().to_string(),
code: status.code(),
});
}
Ok(())
}
pub fn clean_project(project: &Project) -> Result<()> {
if project.build_dir.exists() {
std::fs::remove_dir_all(&project.build_dir)?;
println!("Cleaned: {}", project.build_dir.display());
} else {
println!("Nothing to clean.");
}
Ok(())
}
pub fn rebuild_project(project: &Project) -> Result<()> {
clean_project(project)?;
build_project(project)
}
pub fn fmt_project(project: &Project) -> Result<()> {
if !command_exists("clang-format") {
return Err(crate::error::BuildError::ToolNotFound {
tool: "clang-format".to_string(),
hint: "Install it via your package manager (e.g. `apt install clang-format`)."
.to_string(),
});
}
let files = project.formattable_files()?;
if files.is_empty() {
println!("Nothing to format.");
return Ok(());
}
let mut cmd = std::process::Command::new("clang-format");
cmd.arg("-i");
cmd.args(&files);
let status = cmd.status()?;
if !status.success() {
return Err(crate::error::BuildError::CommandFailed {
cmd: "clang-format".to_string(),
code: status.code(),
});
}
println!("Formatted {} file(s).", files.len());
Ok(())
}
fn compiler_binary(kind: &crate::config::CompilerKind) -> Result<&'static str> {
use crate::config::CompilerKind;
match kind {
CompilerKind::Gcc => {
if command_exists("gcc") {
Ok("gcc")
} else {
Err(crate::error::BuildError::CompilerNotFound(
"gcc".to_string(),
))
}
}
CompilerKind::Tcc => {
if command_exists("tcc") {
Ok("tcc")
} else {
Err(crate::error::BuildError::CompilerNotFound(
"tcc".to_string(),
))
}
}
CompilerKind::Clang => {
if command_exists("clang") {
Ok("clang")
} else {
Err(crate::error::BuildError::CompilerNotFound(
"clang".to_string(),
))
}
}
CompilerKind::Auto => {
for candidate in ["clang", "tcc", "cc", "gcc"] {
if command_exists(candidate) {
return Ok(candidate);
}
}
Err(crate::error::BuildError::CompilerNotFound(
"clang, tcc, cc, gcc".to_string(),
))
}
}
}
fn command_exists(name: &str) -> bool {
std::process::Command::new(name)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}