use std::{
env, fs,
path::{Path, PathBuf},
process,
};
use anyhow::Context;
use log::{debug, info};
use serde::Serialize;
use walkdir::WalkDir;
use crate::Saja;
const CLANG_PLUGIN: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/libsaja.so"));
#[derive(Serialize)]
struct CompileCommand {
directory: PathBuf,
file: PathBuf,
arguments: Vec<String>,
output: PathBuf,
}
pub fn needs_rebuild(source: &Path, object: &Path) -> bool {
let obj_t = match object.metadata().and_then(|m| m.modified()) {
Ok(t) => t,
Err(_) => return true, };
source
.metadata()
.and_then(|m| m.modified())
.map(|src_t| src_t > obj_t)
.unwrap_or(true)
}
impl Saja {
fn cache_dir() -> PathBuf {
if let Some(cache) = env::var_os("XDG_CACHE_HOME") {
return PathBuf::from(cache);
}
if let Some(home) = env::var_os("HOME") {
return PathBuf::from(home).join(".cache");
}
env::temp_dir()
}
fn plugin_cache_path(cache_dir: PathBuf) -> PathBuf {
cache_dir
.join(env!("CARGO_PKG_NAME"))
.join(env!("CARGO_PKG_VERSION"))
.join(format!("{}-{}", env::consts::ARCH, env::consts::OS))
.join("libsaja.so")
}
fn write_clang_plugin(path: PathBuf) -> anyhow::Result<PathBuf> {
if fs::read(&path).is_ok_and(|contents| contents == CLANG_PLUGIN) {
return Ok(path);
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let tmp = path.with_extension(format!("so.{}.tmp", process::id()));
fs::write(&tmp, CLANG_PLUGIN)?;
fs::rename(&tmp, &path)?;
Ok(path)
}
fn clang_plugin_path(&self) -> anyhow::Result<PathBuf> {
let preferred = Self::plugin_cache_path(Self::cache_dir());
match Self::write_clang_plugin(preferred) {
Ok(path) => Ok(path),
Err(e) => {
let fallback = Self::plugin_cache_path(env::temp_dir());
Self::write_clang_plugin(fallback)
.with_context(|| format!("could not write Clang plugin to cache: {e}"))
}
}
}
fn object_path(&self, source: &Path, build_dir: &Path) -> anyhow::Result<PathBuf> {
let out = build_dir.join("objects");
let relative = source.strip_prefix(build_dir.parent().unwrap().join("src"))?;
Ok(out.join(relative).with_extension("o"))
}
fn clang_plugin_args(&self) -> anyhow::Result<Vec<String>> {
let plugin = self.clang_plugin_path()?;
let plugin = plugin.display().to_string();
Ok(["-Xclang", "-load", "-Xclang", &plugin]
.into_iter()
.map(ToString::to_string)
.collect())
}
fn build_object(
&self,
source: &Path,
build_dir: &Path,
profile: &[String],
) -> anyhow::Result<Option<CompileCommand>> {
let object = self.object_path(source, build_dir)?;
if !needs_rebuild(source, &object) {
debug!("skipping {source:?}, up to date");
return Ok(None);
}
let include = build_dir.join("include");
if let Some(parent) = object.parent() {
std::fs::create_dir_all(parent)?;
}
let mut command = process::Command::new("clang");
let plugin_args = self.clang_plugin_args()?;
let arguments = std::iter::once("clang".to_string())
.chain(profile.iter().cloned())
.chain(plugin_args)
.chain([
"-c".to_string(),
format!("-I{}", include.display()),
"-o".to_string(),
object.display().to_string(),
source.display().to_string(),
])
.collect::<Vec<_>>();
command.args(&arguments[1..]);
debug!("{:?}", command);
let status = command.status()?;
if !status.success() {
anyhow::bail!("compilation failed");
}
Ok(Some(CompileCommand {
directory: build_dir.parent().unwrap().to_owned(),
file: source.to_owned(),
arguments,
output: object,
}))
}
fn link(&self, objects: Vec<PathBuf>, target: &Path, build_dir: &Path) -> anyhow::Result<()> {
let objects = objects.into_iter().map(|s| target.join(s));
let mut command = process::Command::new("clang");
let out = build_dir.join(&self.config.name).display().to_string();
command.args(objects).args(["-o", &out]);
debug!("{:?}", command);
let status = command.status()?;
if !status.success() {
anyhow::bail!("linking failed");
}
Ok(())
}
pub fn build(
&self,
target: &Path,
profile: &str,
no_compile_commands: &bool,
) -> anyhow::Result<()> {
let build_dir = target.join(&self.args.build);
let profile = self
.profiles
.get(profile)
.with_context(|| format!("no such profile {profile}"))?;
let sources = WalkDir::new(target.join("src"))
.into_iter()
.filter_map(Result::ok)
.map(|e| e.into_path())
.filter(|p| matches!(p.extension().and_then(|e| e.to_str()), Some("c")))
.collect::<Vec<_>>();
let exports = sources
.iter()
.map(|source| self.extract(source, target))
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
self.make_headers(exports, &build_dir.join("include"))?;
let compile_commands = sources
.iter()
.map(|s| self.build_object(s, &build_dir, profile))
.collect::<anyhow::Result<Vec<_>>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
if compile_commands.is_empty() {
info!("nothing to do");
return Ok(());
}
if !no_compile_commands {
let db = target.join("compile_commands.json");
let json = serde_json::to_string_pretty(&compile_commands)?;
fs::write(db, json)?;
}
let objects = sources
.iter()
.map(|source| self.object_path(source, &build_dir))
.collect::<anyhow::Result<Vec<_>>>()?;
self.link(objects, target, &build_dir)?;
Ok(())
}
}