#![doc = include_str!("../README.md")]
extern crate alloc;
use alloc::sync::Arc;
use clap::{Parser, ValueEnum};
use core::{convert::Infallible, error::Error};
#[cfg(unix)]
use rlimit::Resource;
use rlimit::increase_nofile_limit;
use std::{env::set_current_dir, path::Path, process::exit};
use tokio::sync::Mutex;
use turtle_build::{
BuildError, Console, Context, LogDatabase, OsCommandRunner, OsConsole, OsFileSystem, PathPool,
RunOptions, clean_dead, compile, job_limit, load, run,
};
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(
short = 'n',
help = "Run builds without running commands",
conflicts_with = "tool"
)]
dry_run: bool,
#[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()));
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?;
}
console.lock().await.flush().await?;
exit(1)
}
async fn execute(
arguments: &Arguments,
console: &Arc<Mutex<OsConsole>>,
) -> Result<Infallible, 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(|| job_limit(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 = Path::new(arguments.file.as_deref().unwrap_or(DEFAULT_BUILD_FILE));
let modules = load(&file_system, root_module_path).await?;
let path_pool = Arc::new(PathPool::new());
let config = Arc::new(compile(&modules, root_module_path, &path_pool)?);
let context = Arc::new(Context::new(
OsCommandRunner::new(job_limit),
console.clone(),
LogDatabase::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('.', "_")),
&path_pool,
)
.await?,
file_system,
path_pool,
));
if let Some(tool) = &arguments.tool {
match tool {
Tool::CleanDead => clean_dead(&context, &config).await?,
}
} else {
run(
&context,
config,
&arguments.outputs,
RunOptions {
debug: arguments.debug,
dry_run: arguments.dry_run,
profile: arguments.profile,
},
)
.await?;
}
console.lock().await.flush().await?;
exit(0)
}