node-app-build 7.1.10

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! Locate the `node-app-burger` binary that `node-app test` executes
//! (Contract C8) on a developer machine or in CI.
//!
//! Order: `NODE_BURGER_BIN` (exclusive when set) → the local Burger build
//! `node-app dev` would use (Plan 02 / Contract C1: `NODE_APP_BURGER_DIR`, the
//! main checkout's sibling `node-app-burger`, `<project>/modules/node-app-burger`,
//! each honouring `CARGO_TARGET_DIR`) → `node-app-burger` on `PATH`.

use anyhow::{bail, Result};
use std::ffi::OsString;
use std::path::{Path, PathBuf};

use crate::commands::dev::host::monorepo::{
    burger_dev_checkouts, dev_burger_binary, git_common_dir, BURGER_DIR_ENV,
};

pub const BURGER_BIN_ENV: &str = "NODE_BURGER_BIN";
const BINARY: &str = "node-app-burger";

/// Search inputs, captured once so tests never mutate process env.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct BurgerBinSearch {
    /// `NODE_BURGER_BIN`, when set and non-empty.
    pub explicit: Option<PathBuf>,
    /// Burger checkouts in Contract C1 dev order.
    pub checkouts: Vec<PathBuf>,
    /// `PATH`, when set.
    pub path_var: Option<OsString>,
}

impl BurgerBinSearch {
    /// Inputs for an app project, read from the process environment and git.
    pub fn for_project(project: &Path) -> Self {
        Self {
            explicit: std::env::var_os(BURGER_BIN_ENV)
                .filter(|value| !value.is_empty())
                .map(PathBuf::from),
            checkouts: burger_dev_checkouts(
                project,
                std::env::var_os(BURGER_DIR_ENV),
                git_common_dir(project).as_deref(),
            ),
            path_var: std::env::var_os("PATH"),
        }
    }

    pub fn resolve(&self) -> Result<PathBuf> {
        if let Some(explicit) = &self.explicit {
            if explicit.is_file() {
                return Ok(explicit.clone());
            }
            bail!("{BURGER_BIN_ENV}={} does not exist", explicit.display());
        }
        if let Some(built) = dev_burger_binary(&self.checkouts) {
            return Ok(built);
        }
        let on_path: Vec<PathBuf> = self
            .path_var
            .iter()
            .flat_map(std::env::split_paths)
            .map(|dir| dir.join(BINARY))
            .collect();
        if let Some(found) = on_path.iter().find(|candidate| candidate.is_file()) {
            return Ok(found.clone());
        }
        let checkouts: Vec<String> = self
            .checkouts
            .iter()
            .map(|checkout| format!("  {} (debug build)", checkout.display()))
            .collect();
        let path_entries: Vec<String> = on_path
            .iter()
            .map(|candidate| format!("  {}", candidate.display()))
            .collect();
        bail!(
            "node-app-burger not found. Build a node-app-burger checkout (`cargo build`), install \
             the node-app-burger apt package from https://apt.economy1.cloud (stable main), or \
             set {BURGER_BIN_ENV}. Looked in:\n{}\n{}",
            checkouts.join("\n"),
            path_entries.join("\n")
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn touch(path: &Path) {
        std::fs::create_dir_all(path.parent().unwrap()).unwrap();
        std::fs::write(path, b"").unwrap();
    }

    fn built_checkout(dir: &Path) -> PathBuf {
        touch(&dir.join("Cargo.toml"));
        let binary = dir.join("target/debug/node-app-burger");
        touch(&binary);
        binary
    }

    #[test]
    fn explicit_env_wins_and_must_exist() {
        let tmp = tempfile::tempdir().unwrap();
        let checkout = tmp.path().join("node-app-burger");
        built_checkout(&checkout);
        let explicit = tmp.path().join("custom/node-app-burger");
        let search = BurgerBinSearch {
            explicit: Some(explicit.clone()),
            checkouts: vec![checkout],
            path_var: None,
        };
        let err = search.resolve().unwrap_err().to_string();
        assert!(err.contains("NODE_BURGER_BIN="), "{err}");
        touch(&explicit);
        assert_eq!(search.resolve().unwrap(), explicit);
    }

    #[test]
    fn a_built_checkout_wins_over_path() {
        let tmp = tempfile::tempdir().unwrap();
        let binary = built_checkout(&tmp.path().join("node-app-burger"));
        let bin_dir = tmp.path().join("bin");
        touch(&bin_dir.join("node-app-burger"));
        let search = BurgerBinSearch {
            explicit: None,
            checkouts: vec![tmp.path().join("node-app-burger")],
            path_var: Some(bin_dir.into_os_string()),
        };
        assert_eq!(search.resolve().unwrap(), binary);
    }

    #[test]
    fn path_is_the_fallback() {
        let tmp = tempfile::tempdir().unwrap();
        let bin_dir = tmp.path().join("bin");
        touch(&bin_dir.join("node-app-burger"));
        let search = BurgerBinSearch {
            explicit: None,
            checkouts: vec![tmp.path().join("no-checkout-here")],
            path_var: Some(bin_dir.clone().into_os_string()),
        };
        assert_eq!(search.resolve().unwrap(), bin_dir.join("node-app-burger"));
    }

    #[test]
    fn not_found_names_every_place_it_looked() {
        let tmp = tempfile::tempdir().unwrap();
        let search = BurgerBinSearch {
            explicit: None,
            checkouts: vec![tmp.path().join("node-app-burger")],
            path_var: Some(tmp.path().join("bin").into_os_string()),
        };
        let err = search.resolve().unwrap_err().to_string();
        assert!(err.contains("node-app-burger not found"), "{err}");
        assert!(
            err.contains(&tmp.path().join("node-app-burger").display().to_string()),
            "{err}"
        );
        assert!(
            err.contains(&tmp.path().join("bin/node-app-burger").display().to_string()),
            "{err}"
        );
    }

    #[test]
    fn for_project_uses_the_c1_dev_checkout_order() {
        let tmp = tempfile::tempdir().unwrap();
        let search = BurgerBinSearch::for_project(tmp.path());
        assert_eq!(
            search.checkouts.last(),
            Some(&tmp.path().join("modules").join("node-app-burger"))
        );
    }
}