#![doc = include_str!("../README.md")]
extern crate alloc;
use alloc::sync::Arc;
use clap::{Parser, ValueEnum};
use core::error::Error;
use core::time::Duration;
use futures::future::try_join_all;
#[cfg(unix)]
use rlimit::Resource;
use rlimit::increase_nofile_limit;
use std::{
collections::HashMap,
env::set_current_dir,
path::{Path, PathBuf},
process::exit,
};
use tokio::{sync::Mutex, time::sleep};
use turtle_build::{
BuildError, Console, Context, FileSystem, FjallDatabase, Module, ModuleDependencyMap,
OsCommandRunner, OsConsole, OsFileSystem, RunOptions, Statement, clean_dead, compile, parse,
run, validate_modules,
};
const DEFAULT_BUILD_FILE: &str = "build.ninja";
const DATABASE_DIRECTORY: &str = ".turtle";
const DEFAULT_FILE_COUNT_PER_PROCESS: usize = 3;
#[derive(Parser)]
#[clap(about = "The Ninja build system clone written in Rust", version)]
struct Arguments {
#[clap(help = "Specify outputs")]
outputs: Vec<String>,
#[clap(short, help = "Set a root build file")]
file: Option<String>,
#[clap(short = 'C', help = "Set a working directory")]
directory: Option<String>,
#[clap(short, help = "Set a job limit")]
job_limit: Option<usize>,
#[clap(long, help = "Set a log prefix")]
log_prefix: Option<String>,
#[clap(long, help = "Show no message on failure of build jobs")]
quiet: bool,
#[clap(long, help = "Show debug logs", env = "TURTLE_DEBUG")]
debug: bool,
#[clap(long, help = "Show profile timings", env = "TURTLE_PROFILE")]
profile: bool,
#[clap(short, help = "Use a complementary tool")]
tool: Option<Tool>,
}
#[derive(Clone, ValueEnum)]
#[clap(rename_all = "lower")]
enum Tool {
CleanDead,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let arguments = Arguments::parse();
let console = Arc::new(Mutex::new(OsConsole::new()));
if let Err(error) = execute(&arguments, &console).await {
if !arguments.quiet || !matches!(error, BuildError::Build) {
console
.lock()
.await
.write_stderr(
format!(
"{}{}\n",
arguments.log_prefix.as_deref().unwrap_or_default(),
error
)
.as_bytes(),
)
.await?;
}
sleep(Duration::from_millis(1)).await;
exit(1)
}
Ok(())
}
async fn execute(arguments: &Arguments, console: &Arc<Mutex<OsConsole>>) -> Result<(), BuildError> {
if let Some(directory) = &arguments.directory {
set_current_dir(directory)?;
}
increase_nofile_limit(u64::MAX)?;
let job_limit = arguments.job_limit.unwrap_or_else(num_cpus::get);
let file_system = OsFileSystem::new(
cfg_select! {
unix => usize::try_from(Resource::NOFILE.get_soft()?).unwrap_or(usize::MAX),
_ => usize::MAX,
}
.saturating_sub(DEFAULT_FILE_COUNT_PER_PROCESS * (job_limit + 1))
.max(1),
);
let root_module_path = file_system
.canonicalize_path(
arguments
.file
.as_deref()
.unwrap_or(DEFAULT_BUILD_FILE)
.as_ref(),
)
.await?;
let (modules, dependencies) = parse_modules(&file_system, &root_module_path).await?;
validate_modules(&dependencies)?;
let config = Arc::new(compile(&modules, &dependencies, &root_module_path)?);
let context = Arc::new(Context::new(
OsCommandRunner::new(job_limit),
console.clone(),
FjallDatabase::new(
&config
.build_directory()
.map(|string| string.as_ref().as_ref())
.unwrap_or_else(|| root_module_path.parent().unwrap())
.join(DATABASE_DIRECTORY)
.join(env!("CARGO_PKG_VERSION").replace('.', "_")),
)?,
file_system,
));
if let Some(tool) = &arguments.tool {
match tool {
Tool::CleanDead => clean_dead(&context, &config).await?,
}
} else {
run(
&context,
config.clone(),
&arguments.outputs,
RunOptions {
debug: arguments.debug,
profile: arguments.profile,
},
)
.await?;
}
Ok(())
}
async fn parse_modules(
file_system: &OsFileSystem,
path: &Path,
) -> Result<(HashMap<PathBuf, Module>, ModuleDependencyMap), BuildError> {
let mut paths = vec![file_system.canonicalize_path(path).await?];
let mut modules = HashMap::new();
let mut dependencies = HashMap::new();
while let Some(path) = paths.pop() {
let module = parse(&file_system.read_file_to_string(&path).await?)?;
let submodule_paths = try_join_all(
module
.statements()
.iter()
.filter_map(|statement| match statement {
Statement::Include(include) => Some(include.path()),
Statement::Submodule(submodule) => Some(submodule.path()),
_ => None,
})
.map(|path| resolve_submodule_path(file_system, path))
.collect::<Vec<_>>(),
)
.await?
.into_iter()
.collect::<HashMap<_, _>>();
paths.extend(submodule_paths.values().cloned());
modules.insert(path.clone(), module);
dependencies.insert(path, submodule_paths);
}
Ok((modules, dependencies))
}
async fn resolve_submodule_path(
file_system: &OsFileSystem,
path: &str,
) -> Result<(String, PathBuf), BuildError> {
Ok((
path.into(),
file_system.canonicalize_path(path.as_ref()).await?,
))
}