turtle-build 0.5.15

Ninja-compatible build system for high-level programming languages
Documentation
#![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; // stdin, stdout, and stderr

#[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?;

    // Skip dropping the configuration and context, which is slow on large graphs.
    exit(0)
}