use crate::compile_db::CompileCommand;
use crate::diagnostics::{Diagnostic, print_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>,
dep_libs: Vec<String>,
dep_lib_dirs: Vec<PathBuf>,
}
pub fn build_project(project: &Project, release: bool) -> Result<()> {
let sources = project.source_files()?;
project.header_files()?;
let profile_dir = if release { "release" } else { "debug" };
let build_dir = project.build_dir.join(profile_dir);
std::fs::create_dir_all(&build_dir)?;
let compiler = compiler_binary(&project.config.build.compiler)?;
println!("Using compiler: {}", compiler);
let profile = if release {
project.config.get_release_profile()
} else {
project.config.get_debug_profile()
};
let mut opts = CompileOptions {
compiler: compiler.to_string(),
includes: vec![project.root.join("include")],
cflags: project.config.build.cflags.clone(),
dep_libs: Vec::new(),
dep_lib_dirs: Vec::new(),
};
for (_name, output) in &project.resolved_deps {
opts.includes.extend(output.include_dirs.clone());
opts.dep_lib_dirs.extend(output.lib_dirs.clone());
opts.dep_libs.extend(output.libs.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 = 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.arg(match profile.opt_level {
crate::config::OptLevel::None => "-O0",
crate::config::OptLevel::Speed => "-O2",
crate::config::OptLevel::Size => "-Os",
crate::config::OptLevel::Max => "-O3",
});
if profile.debug_symbols {
cmd.arg("-g");
}
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 = build_dir.join("bin").join(
project
.config
.project
.output_name
.as_ref()
.unwrap_or(&project.config.project.name),
);
std::fs::create_dir_all(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(
opts.dep_lib_dirs
.iter()
.map(|p| format!("-L{}", p.display())),
);
link_cmd.args(opts.dep_libs.iter().map(|l| format!("-l{}", l)));
link_cmd.args(project.config.build.libs.iter().map(|l| format!("-l{}", l)));
link_cmd.args(&project.config.build.linker_flags);
if profile.lto {
link_cmd.arg("-flto");
}
if profile.strip {
link_cmd.arg("-s");
}
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, release: bool) -> Result<()> {
build_project(project, release)?;
let profile_dir = if release { "release" } else { "debug" };
let binary_path = project.build_dir.join(profile_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, release: bool) -> Result<()> {
clean_project(project)?;
build_project(project, release)
}
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)
}