use crate::compile_db::CompileCommand;
use crate::diagnostics::{parse_all, print_all};
use crate::error::Result;
use crate::project::Project;
use std::path::PathBuf;
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, verbose: bool, dry_run: bool) -> Result<()> {
let project_section = project.config.project.as_ref().ok_or_else(|| {
crate::error::BuildError::Dependency {
name: project.root.display().to_string(),
reason: "cannot build: Smidr.toml has no [project] section (this is a workspace root)".to_string(),
}
})?;
let build_section = project.config.build.as_ref().ok_or_else(|| {
crate::error::BuildError::Dependency {
name: project.root.display().to_string(),
reason: "cannot build: Smidr.toml has no [build] section".to_string(),
}
})?;
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(&build_section.compiler, &project_section.language)?;
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: {
let mut incs = vec![project.root.join(
project.config.paths.include.as_deref().unwrap_or("include")
)];
incs.extend(project.config.paths.custom.values().map(|p| project.root.join(p)));
incs
},
cflags: build_section.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.args(&opts.cflags);
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");
}
let std_flag = match project_section.language {
crate::config::Language::C => format!(
"-std={}",
project_section.c_standard.clone().unwrap_or_default()
),
crate::config::Language::Cpp => format!(
"-std={}",
project_section.cpp_standard.as_ref()
.map(|s| s.to_string())
.unwrap_or_else(|| crate::config::CppStandard::default().to_string())
),
};
cmd.arg(std_flag);
let command_str = format!("{:?}", cmd);
if verbose || dry_run {
println!(" $ {}", command_str);
}
if dry_run {
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);
continue;
}
let output = cmd.output()?;
let stderr = String::from_utf8_lossy(&output.stderr);
let diagnostics = parse_all(&stderr);
if !diagnostics.is_empty() {
print_all(&diagnostics);
}
if !output.status.success() {
let error_detail = if diagnostics.is_empty() {
stderr.to_string()
} else {
"See error details above...".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 output_name = project_section.output_name();
std::fs::create_dir_all(build_dir.join("bin"))?;
match project_section.project_type {
crate::config::ProjectType::Binary => {
let binary_name = if cfg!(windows) {
format!("{}.exe", output_name)
} else {
output_name.to_string()
};
let binary_path = build_dir.join("bin").join(binary_name);
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(build_section.libs.iter().map(|l| format!("-l{}", l)));
link_cmd.args(&build_section.linker_flags);
if profile.lto { link_cmd.arg("-flto"); }
if profile.strip { link_cmd.arg("-s"); }
if verbose || dry_run {
println!(" $ {:?}", link_cmd);
}
if dry_run {
return Ok(());
}
let output = link_cmd.output()?;
if !output.status.success() {
return Err(crate::error::BuildError::Link(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
}
crate::config::ProjectType::StaticLibrary => {
let lib_ext = if cfg!(windows) { "lib" } else { "a" };
let lib_path = build_dir.join("bin").join(format!("lib{}.{}", output_name, lib_ext));
let mut ar_cmd = std::process::Command::new("ar");
ar_cmd.arg("rcs").arg(&lib_path).args(&object_files);
let output = ar_cmd.output()?;
if !output.status.success() {
return Err(crate::error::BuildError::Link(
String::from_utf8_lossy(&output.stderr).to_string(),
));
}
}
crate::config::ProjectType::SharedLibrary => {
let lib_ext = if cfg!(windows) { "dll" } else { "so" };
let lib_path = build_dir.join("bin").join(format!("lib{}.{}", output_name, lib_ext));
let mut link_cmd = std::process::Command::new(&opts.compiler);
link_cmd.arg("-shared").args(&object_files);
link_cmd.arg("-o").arg(&lib_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(build_section.libs.iter().map(|l| format!("-l{}", l)));
link_cmd.args(&build_section.linker_flags);
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, verbose: bool, dry_run: bool) -> Result<()> {
build_project(project, release, verbose, dry_run)?;
if dry_run {
println!("Dry run: skipping execution.");
return Ok(());
}
let project_section = project.config.project.as_ref().ok_or_else(|| {
crate::error::BuildError::Dependency {
name: project.root.display().to_string(),
reason: "cannot run: Smidr.toml has no [project] section".to_string(),
}
})?;
let profile_dir = if release { "release" } else { "debug" };
let output_name = project_section.output_name.as_ref().unwrap_or(&project_section.name);
let binary_name = if cfg!(windows) {
format!("{}.exe", output_name)
} else {
output_name.clone()
};
let binary_path = project.build_dir.join(profile_dir).join("bin").join(binary_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, verbose: bool, dry_run: bool) -> Result<()> {
clean_project(project)?;
build_project(project, release, verbose, dry_run)
}
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, language: &crate::config::Language) -> Result<&'static str> {
use crate::config::{CompilerKind, Language};
let candidates: &[&str] = match (kind, language) {
(CompilerKind::Gcc, Language::C) => &["gcc"],
(CompilerKind::Gcc, Language::Cpp) => &["g++"],
(CompilerKind::Clang, Language::C) => &["clang"],
(CompilerKind::Clang, Language::Cpp) => &["clang++"],
(CompilerKind::Tcc, _) => &["tcc"],
(CompilerKind::Auto, Language::C) => &["clang", "tcc", "cc", "gcc"],
(CompilerKind::Auto, Language::Cpp) => &["clang++", "g++"],
};
for candidate in candidates {
if command_exists(candidate) {
return Ok(candidate);
}
}
Err(crate::error::BuildError::CompilerNotFound(candidates.join(", ")))
}
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)
}
pub fn lint_project(project: &Project) -> Result<()> {
let project_section = project.config.project.as_ref().ok_or_else(|| {
crate::error::BuildError::Dependency {
name: project.root.display().to_string(),
reason: "cannot lint: Smidr.toml has no [project] section".to_string(),
}
})?;
let files = project.source_files()?;
let clang_binary = match project_section.language {
crate::config::Language::C => "clang",
crate::config::Language::Cpp => "clang++",
};
let mut cmd = std::process::Command::new(clang_binary);
cmd.arg("-fsyntax-only");
cmd.args(&files);
let status = cmd.status()?;
if !status.success() {
return Err(crate::error::BuildError::CommandFailed {
cmd: clang_binary.to_string(),
code: status.code(),
});
}
println!("Linted {} file(s).", files.len());
Ok(())
}
pub fn update_project() -> Result<()> {
let status = std::process::Command::new("cargo")
.args(["install", "smidr", "--force"])
.status()?;
if !status.success() {
return Err(crate::error::BuildError::CompilerNotFound(
"cargo".to_string(),
));
}
println!("Smidr updated successfully!");
Ok(())
}