mbx 1.9.0

A build cache for Rust projects
Documentation
use super::cargo::{absolute, account_session, inherited_environment, run_cargo};
use crate::config::{CliSettings, Config};
use crate::session::{self, CacheSession};
use crate::util::is_checkout_root;
use eyre::Result;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::ExitCode;

#[derive(usage::Args)]
pub(super) struct ExecArgs {
    /// Directory that identifies the project across worktrees.
    #[usage(long, value_name = "DIR")]
    project_root: Option<String>,
    /// Build command and its arguments.
    // `automatic` is the wrapper spelling: once the command is named, the rest
    // of the line belongs to it, so an option exec knows is not taken from the
    // command that meant it. A plain comment rather than a doc comment,
    // because the generated CLI reference renders those and this is a note to
    // the next reader here.
    #[usage(value_name = "COMMAND", required = true, double_dash = "automatic")]
    pub(super) command: Vec<String>,
}

pub(super) fn run(config: &Config, settings: &CliSettings, args: &ExecArgs) -> Result<ExitCode> {
    let Some((program, arguments)) = args.command.split_first() else {
        eyre::bail!("exec needs a command to run");
    };
    let program: std::ffi::OsString = program.into();
    let working_dir = std::env::current_dir()?;
    let project_root = match &args.project_root {
        Some(root) => absolute(&working_dir, root),
        None => discover_project_root(&working_dir),
    };
    let mut config = config.clone();
    config.apply_workspace_policy(&project_root)?;
    let config = &config;
    if !config.cc {
        log::warn!("the C and C++ cache is disabled, so this command is not cached");
        return run_cargo(&program, arguments, BTreeMap::new());
    }

    let session_dir = tempfile::Builder::new().prefix("mbx-session-").tempdir()?;
    let runtime = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()?;
    let session_outcome = runtime.block_on(async {
        // Outside the session directory, and outside the store the collector
        // sweeps: a configure step records these paths and expects to find
        // them on the next build.
        let Some(shims) = session::install_path_shims(&config.cache_dir.join("shims"))? else {
            log::warn!("no C or C++ compiler was found on PATH, so this command is not cached");
            return Ok((run_cargo(&program, arguments, BTreeMap::new()), None));
        };
        let session = CacheSession::start(session_dir.path(), config).await?;
        let mut environment = inherited_environment(|name| std::env::var(name).ok(), &working_dir);
        let run = session
            .begin_exec(&project_root, &args.command, &shims, &mut environment)
            .await;

        let status = run_cargo(&program, arguments, environment);

        // As in a cargo build: a compilation that was restored or published
        // before a later one failed is still worth remembering.
        if let Some(run) = run
            && let Err(error) = run.commit().await
        {
            log::warn!("the completed build was not fully recorded: {error}");
        }
        let stats = match session.finish().await {
            Ok(stats) => {
                crate::session::display_stats(&stats, config, settings.summary);
                Some(stats)
            }
            Err(error) => {
                log::warn!("the cache session did not shut down cleanly: {error}");
                None
            }
        };
        Ok((status, stats))
    });
    account_session(config, settings, session_outcome, None)
}

/// The root `mbx exec` keys paths against: the enclosing VCS checkout, so every
/// subdirectory of one project agrees on it, or the working directory outside
/// one.
pub(super) fn discover_project_root(working_dir: &Path) -> PathBuf {
    let mut directory = working_dir;
    loop {
        if is_checkout_root(directory) {
            return directory.to_path_buf();
        }
        match directory.parent() {
            Some(parent) => directory = parent,
            None => return working_dir.to_path_buf(),
        }
    }
}