node-app-build 5.23.2

Mini app developer CLI: scaffold, validate, package node-app-* Debian packages
//! `node-app package --target <arch>`
//!
//! Wraps the .deb build pipeline:
//!   1. Run `bun build` (or `cargo build --release --target=<rust-target>`).
//!   2. Invoke `infra/scripts/create_app_deb_package.sh --module <name> --target <arch>`.
//!
//! The script lives at the project root; we locate it by walking upwards from
//! the project path until we find a `infra/scripts/create_app_deb_package.sh`.

use crate::commands::validate;
use crate::manifest::AppType;
use anyhow::{bail, Context, Result};
use std::path::{Path, PathBuf};
use std::process::Command;

pub fn run(
    project_path: &Path,
    target: &str,
    version: Option<&str>,
    out: Option<&Path>,
) -> Result<()> {
    let manifest = validate::run(project_path)?;
    match (target, manifest.app_type) {
        ("all", AppType::Native) => {
            bail!("native (cdylib) apps cannot ship as Architecture: all; use --target arm64 or amd64")
        }
        ("amd64" | "arm64", AppType::Bun) => {
            bail!("bun apps must ship as Architecture: all; use --target all")
        }
        ("amd64" | "arm64" | "all", _) => {}
        (other, _) => bail!("unsupported --target '{}': use amd64, arm64, or all", other),
    }

    let project_path = project_path.canonicalize()?;
    let script = locate_deb_script(&project_path)?;

    // 1. Build the payload.
    match manifest.app_type {
        AppType::Bun => {
            run_step(
                "bun install",
                Command::new("bun")
                    .arg("install")
                    .current_dir(&project_path),
            )?;
            // `bun run build` is convention; if no `build` script is declared it's a no-op.
            run_step(
                "bun run build",
                Command::new("bun")
                    .args(["run", "build"])
                    .current_dir(&project_path),
            )
            .ok();
            if manifest.has_ui {
                let ui_dir = project_path.join(
                    manifest
                        .ui_path
                        .as_deref()
                        .unwrap_or("ui/dist")
                        .split('/')
                        .next()
                        .unwrap_or("ui"),
                );
                if ui_dir.is_dir() {
                    run_step(
                        "bun install (ui)",
                        Command::new("bun").arg("install").current_dir(&ui_dir),
                    )
                    .ok();
                    run_step(
                        "bun run build (ui)",
                        Command::new("bun")
                            .args(["run", "build"])
                            .current_dir(&ui_dir),
                    )
                    .ok();
                }
            }
        }
        AppType::Native => {
            let rust_target = match target {
                "amd64" => "x86_64-unknown-linux-gnu",
                "arm64" => "aarch64-unknown-linux-gnu",
                _ => unreachable!("validated above"),
            };
            run_step(
                "cargo build --release",
                Command::new("cargo")
                    .args(["build", "--release", "--target", rust_target])
                    .current_dir(&project_path),
            )?;
            // Stage the .so where create_app_deb_package.sh expects it.
            let crate_underscored = manifest.name.replace('-', "_");
            let so_src = project_path
                .join("target")
                .join(rust_target)
                .join("release")
                .join(format!("lib{}.so", crate_underscored));
            if !so_src.exists() {
                bail!(
                    "expected cdylib output not found: {} — does Cargo.toml set crate-type = [\"cdylib\"]?",
                    so_src.display()
                );
            }
            let stage_dir = project_path
                .join("build")
                .join(target);
            std::fs::create_dir_all(&stage_dir)?;
            std::fs::copy(&so_src, stage_dir.join("app.so"))?;
        }
        AppType::Standalone => {
            let rust_target = match target {
                "amd64" => "x86_64-unknown-linux-gnu",
                "arm64" => "aarch64-unknown-linux-gnu",
                _ => unreachable!("validated above"),
            };
            run_step(
                "cargo build --release",
                Command::new("cargo")
                    .args(["build", "--release", "--target", rust_target])
                    .current_dir(&project_path),
            )?;
        }
    }

    // 2. Build the .deb. The script looks for modules/<name>/ relative to its
    //    own project root, so we splice the project-path's basename into the
    //    invocation. For projects that aren't yet under modules/ the user must
    //    symlink first; the CLI prints a friendly message in that case.
    let module_name = &manifest.name;
    let mut cmd = Command::new(&script);
    cmd.arg("--module").arg(module_name);
    cmd.arg("--target").arg(target);
    if let Some(v) = version {
        cmd.arg("--version").arg(v);
    }
    if let Some(out) = out {
        cmd.arg("--out").arg(out);
    }
    cmd.current_dir(script.parent().unwrap().parent().unwrap().parent().unwrap());

    let status = cmd.status().with_context(|| "spawning deb build script")?;
    if !status.success() {
        bail!("create_app_deb_package.sh exited with status {}", status);
    }
    Ok(())
}

fn locate_deb_script(start: &Path) -> Result<PathBuf> {
    let mut cur = Some(start);
    while let Some(dir) = cur {
        let candidate = dir.join("infra/scripts/create_app_deb_package.sh");
        if candidate.is_file() {
            return Ok(candidate);
        }
        cur = dir.parent();
    }
    bail!(
        "could not find infra/scripts/create_app_deb_package.sh by walking upwards from {} — \
         is the project under a Node Lightning OS workspace?",
        start.display()
    )
}

fn run_step(label: &str, cmd: &mut Command) -> Result<()> {
    println!("» {}", label);
    let status = cmd.status().with_context(|| format!("spawning {}", label))?;
    if !status.success() {
        bail!("{} failed (exit {})", label, status);
    }
    Ok(())
}