use std::{
cell::RefCell,
ffi::OsStr,
fs::{self, File},
hash::{DefaultHasher, Hash, Hasher},
io::Write,
path::{Path, PathBuf},
process::Command,
time::Instant,
sync::Mutex,
};
use build_file::EmbargoBuildFile;
use colored::Colorize;
use cxx_file::{CxxFile, CxxFileType};
use log::{debug, error};
use topological_sort::TopologicalSort;
use walkdir::{DirEntry, WalkDir};
use crate::{
cli::{BuildArgs, BuildProfile},
embargo_toml::*,
error::{EmbargoError, EmbargoResult}
};
use rayon::prelude::*;
mod cxx_file;
mod build_file;
mod serde_helpers;
pub fn build_project(args: &BuildArgs, embargo_toml: &EmbargoFile, embargo_toml_path: &Path) -> EmbargoResult {
let now = Instant::now();
let cwd = embargo_toml_path.to_path_buf();
println!("{} {} v{} ({})", "Compiling".green().bold(), embargo_toml.package.name, embargo_toml.package.version, cwd.display());
let mut src_dir = cwd.clone();
src_dir.push(embargo_toml.source_path());
let mut buildfile_path = cwd.clone();
buildfile_path.push(embargo_toml.build_path());
match args.profile {
BuildProfile::Debug => {
buildfile_path.push(embargo_toml.target_path_debug());
},
BuildProfile::Release => {
buildfile_path.push(embargo_toml.target_path_release());
},
}
let _ = fs::create_dir_all(&buildfile_path);
buildfile_path.push("Embargo.build");
let embargo_build = match fs::read_to_string(&buildfile_path) {
Ok(file) => {
let toml: EmbargoBuildFile = toml::from_str(&file)?;
Some(toml)
},
Err(_) => None,
};
let mut new_embargo_build = EmbargoBuildFile::new();
new_embargo_build.embargo_toml_modified = hash_helper(embargo_toml);
let embargo_toml_modified = if let Some(ref embargo_build) = embargo_build {
new_embargo_build.embargo_toml_modified != embargo_build.embargo_toml_modified
} else {
false
};
for entry in WalkDir::new(src_dir.clone())
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| is_valid_file_ext(e.path())) {
let mod_time = entry.metadata()?.modified()?;
let path = entry.path();
let filename = path.file_name().unwrap_or_default();
debug!("Reading {}", filename.to_str().unwrap_or_default());
let ext = path.extension().unwrap_or_default();
let hash = hash_helper(mod_time);
let file_type = if is_source(ext) {
CxxFileType::Source
} else if is_header(ext) {
CxxFileType::Header
} else {
continue;
};
let new_cxx_file = CxxFile::new(file_type, hash, path)?;
new_embargo_build.source_files.insert(path.to_path_buf(), RefCell::new(new_cxx_file));
}
let (fresh_build, files_changed) = if let Some(old_build) = embargo_build {
let mut files_changed = false;
for (path, file) in new_embargo_build.source_files.iter() {
if let Some(last_file) = old_build.source_files.get(path) {
if file.borrow().modified() != last_file.borrow().modified() {
if !files_changed {
files_changed = true;
}
file.borrow_mut().set_changed();
}
}
}
if files_changed {
let mut ts = TopologicalSort::<PathBuf>::new();
for (path, file) in new_embargo_build.source_files.iter() {
for dep in file.borrow().dependencies() {
ts.add_dependency(dep, path);
}
}
while let Some(path) = ts.pop() {
if let Some(file) = new_embargo_build.source_files.get(&path) {
if file.borrow().changed() {
continue;
}
let deps = file.borrow().dependencies().to_vec();
for dep_path in &deps {
if let Some(dep_file) = new_embargo_build.source_files.get(dep_path) {
if dep_file.borrow().changed() {
file.borrow_mut().set_changed();
}
}
}
}
}
}
(false, files_changed)
} else {
(true, true)
};
if cfg!(debug_assertions) {
for (path, file) in &new_embargo_build.source_files {
if file.borrow().changed() {
debug!("File changed: {}", path.display());
}
}
}
let mut bin_path = buildfile_path.clone();
bin_path.pop();
let object_path = {
let mut object_path = bin_path.clone();
object_path.push(embargo_toml.object_path());
object_path
};
fs::create_dir_all(&object_path)?;
bin_path.push(embargo_toml.bin_path());
let _ = fs::create_dir_all(&bin_path);
bin_path.push(&embargo_toml.package.name);
let fs_guard = Mutex::new(());
new_embargo_build.source_files
.par_iter_mut()
.filter(|(p, f)| (
f.borrow().changed() ||
fresh_build ||
embargo_toml_modified
) && is_source(p.extension().unwrap_or_default()))
.map(|(p, _)| p)
.for_each(|path|
{
let mut object_path = object_path.clone();
let mut sub = subtract_path(path, &src_dir).unwrap_or_default();
sub.pop();
object_path.push(sub);
{
let _unused = fs_guard.lock().unwrap();
let exists = fs::exists(&object_path).unwrap_or(false);
if !exists {
let _ = fs::create_dir_all(&object_path);
debug!("Creating object directory: {}", &object_path.display());
}
}
let mut command = Command::new(embargo_toml.compiler());
let mut args = Vec::new();
args.push("-c");
args.push(path.as_os_str().to_str().unwrap_or_default());
args.push("-o");
let filename = path.file_name().unwrap_or_default();
let filename = filename.to_str().unwrap_or_default();
let filename_o = filename.replace("cpp", "o");
object_path.push(filename_o);
args.push(object_path.to_str().unwrap_or_default());
debug!("{} {}", embargo_toml.compiler(), args.iter().fold(String::new(), |s, a| s + " " + a));
match command.args(args).output() {
Ok(output) => {
if output.status.success() {
debug!("Compiling {}...", filename);
} else {
eprintln!("{}", String::from_utf8_lossy(&output.stderr));
}
},
Err(_) => {
eprintln!("Compilation failed");
}
}
}
);
if !files_changed && !embargo_toml_modified {
return Ok(Some("No changed files detected.".to_owned()))
}
debug!("Linking object files...");
let objects = WalkDir::new(object_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(is_obj_file)
.map(|e| e.path().as_os_str().to_str().unwrap_or_default().to_owned())
.collect::<Vec<_>>();
let mut args = Vec::new();
args.push("-o".to_owned());
args.push(bin_path.as_os_str().to_str().unwrap_or_default().to_owned());
for o in objects {
args.push(o);
}
match Command::new(embargo_toml.linker()).args(args).output() {
Ok(output) => {
if !output.status.success() {
return Err(EmbargoError::new(&String::from_utf8_lossy(&output.stderr)));
}
},
Err(e) => {
error!("{}", e);
return Err(EmbargoError::new("error linking executable"))
}
}
let new_buildfile = File::create(buildfile_path).ok();
let new_str = toml::to_string_pretty(&new_embargo_build)?;
if let Some(mut file) = new_buildfile {
file.write_all(new_str.as_bytes())?;
}
let build_time = (now.elapsed().as_millis() as f32) / 1000.;
println!("{} compiling project in {build_time:.2}s", "Finished".green().bold());
Ok(None)
}
fn hash_helper<T: Hash>(t: T) -> u64 {
let mut hasher = DefaultHasher::new();
t.hash(&mut hasher);
hasher.finish()
}
fn is_source(entry: &OsStr) -> bool {
entry == "cpp" || entry == "c"
}
fn is_header(ext: &OsStr) -> bool {
ext == "h" ||
ext == "hpp"
}
fn is_obj_file(entry: &DirEntry) -> bool {
let p = entry.path();
let ext = p.extension().unwrap_or_default();
ext == "o"
}
fn is_valid_file_ext(path: &Path) -> bool {
let ext = path.extension().unwrap_or_default();
is_header(ext) || is_source(ext)
}
fn subtract_path(lhs: &Path, rhs: &Path) -> Option<PathBuf> {
let mut out = lhs.components();
for r in rhs.components() {
let l = out.next();
if let (Some(_), true) = (l, l != Some(r)) {
return None
}
}
Some(PathBuf::from_iter(out))
}
#[cfg(test)]
mod tests {
use std::{path::PathBuf, str::FromStr};
use super::*;
#[test]
fn test_subtract_path() {
let p1 = PathBuf::from_str("/tmp/test/this/path/test.txt").unwrap();
let p2 = PathBuf::from_str("/tmp/test/this/").unwrap();
let result1 = subtract_path(&p1, &p2).unwrap();
let testval1 = PathBuf::from_str("path/test.txt").unwrap();
assert_eq!(&result1, &testval1);
let testval2 = PathBuf::from_str("path.test.jpg").unwrap();
assert_ne!(&result1, &testval2);
let p3 = PathBuf::from_str("/usr/test/this/").unwrap();
let result2 = subtract_path(&p1, &p3);
assert!(result2.is_none());
}
}