bamts-cli 0.1.0

BamTS compiler command-line driver
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    let out_dir = required_path("OUT_DIR");
    let profile_dir = out_dir
        .ancestors()
        .nth(3)
        .expect("OUT_DIR does not have Cargo's profile/build/package/out shape");
    let dependencies = profile_dir.join("deps");
    let target = required("TARGET");
    let host = required("HOST");
    let archive_name = if target.contains("windows-msvc") {
        "bamts_node.lib"
    } else {
        "libbamts_node.a"
    };
    let archive = out_dir.join(archive_name);
    let wrapper = out_dir.join("embedded_node.rs");
    fs::write(&wrapper, wrapper_source()).expect("could not write bamts-node staticlib wrapper");

    let rustc = env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc"));
    match select_node_rlib(&rustc, &target, &dependencies, &wrapper, &out_dir) {
        Some(node_rlib) => {
            assemble_staticlib(
                &rustc,
                &target,
                &dependencies,
                &node_rlib,
                &archive,
                &wrapper,
            );
        }
        None => write_check_placeholder(&dependencies, &archive),
    }

    cargo_line("rerun-if-changed=../bamts-node/src");
    cargo_line("rerun-if-changed=../bamts-node/Cargo.toml");
    cargo_line(&format!(
        "rustc-env=BAMTS_NODE_STATICLIB={}",
        archive.display()
    ));
    cargo_line(&format!("rustc-env=BAMTS_HOST_TARGET={host}"));
    cargo_line(&format!("rustc-env=BAMTS_BUILD_TARGET={target}"));
}

fn wrapper_source() -> &'static str {
    "extern crate bamts_node;\n#[used]\nstatic KEEP_NODE: fn() -> bamts_node::NodeHost = bamts_node::NodeHost::new;\n#[used]\nstatic KEEP_AOT_MAIN: extern \"C\" fn() -> i32 = bamts_node::main;\n"
}

fn select_node_rlib(
    rustc: &OsStr,
    target: &str,
    dependencies: &Path,
    wrapper: &Path,
    out_dir: &Path,
) -> Option<PathBuf> {
    let candidates = artifacts(dependencies, "libbamts_node-", "rlib");
    if candidates.is_empty() {
        return None;
    }
    let mut compatible = candidates
        .iter()
        .filter(|candidate| {
            metadata_supports_aot_main(rustc, target, dependencies, candidate, wrapper, out_dir)
        })
        .cloned()
        .collect::<Vec<_>>();
    compatible.sort_by(|left, right| {
        let modified = |path: &PathBuf| fs::metadata(path).and_then(|value| value.modified()).ok();
        modified(right)
            .cmp(&modified(left))
            .then_with(|| left.cmp(right))
    });
    Some(compatible.first().cloned().unwrap_or_else(|| {
        panic!(
            "no bamts-node rlib in `{}` has the metadata required for the aot-main entrypoint; candidates: {}",
            dependencies.display(),
            display_paths(&candidates)
        )
    }))
}

fn metadata_supports_aot_main(
    rustc: &OsStr,
    target: &str,
    dependencies: &Path,
    candidate: &Path,
    wrapper: &Path,
    out_dir: &Path,
) -> bool {
    let probe = out_dir.join(format!(
        "bamts-node-probe-{}.rmeta",
        candidate
            .file_stem()
            .and_then(OsStr::to_str)
            .unwrap_or("unknown")
    ));
    let output = Command::new(rustc)
        .arg("--crate-name")
        .arg("bamts_node_probe")
        .arg("--crate-type")
        .arg("lib")
        .arg("--emit=metadata")
        .arg("--edition=2024")
        .arg("--target")
        .arg(target)
        .arg("--extern")
        .arg(format!("bamts_node={}", candidate.display()))
        .arg("-L")
        .arg(format!("dependency={}", dependencies.display()))
        .arg("-o")
        .arg(&probe)
        .arg(wrapper)
        .output()
        .unwrap_or_else(|error| {
            panic!("could not start `{}`: {error}", Path::new(rustc).display())
        });
    let _ = fs::remove_file(probe);
    output.status.success()
}

fn assemble_staticlib(
    rustc: &OsStr,
    target: &str,
    dependencies: &Path,
    node_rlib: &Path,
    archive: &Path,
    wrapper: &Path,
) {
    let mut command = Command::new(rustc);
    command
        .arg("--crate-name")
        .arg("bamts_node_embedded")
        .arg("--crate-type")
        .arg("staticlib")
        .arg("--edition=2024")
        .arg("--target")
        .arg(target)
        .arg("--extern")
        .arg(format!("bamts_node={}", node_rlib.display()))
        .arg("-L")
        .arg(format!("dependency={}", dependencies.display()))
        .arg("-C")
        .arg(if required("PROFILE") == "release" {
            "opt-level=3"
        } else {
            "opt-level=0"
        })
        .arg("-o")
        .arg(archive)
        .arg(wrapper);
    let output = command.output().unwrap_or_else(|error| {
        panic!("could not start `{}`: {error}", Path::new(rustc).display())
    });
    if !output.status.success() {
        panic!(
            "bamts-node staticlib assembly failed with {}\nstdout:\n{}\nstderr:\n{}",
            output.status,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }
}

fn write_check_placeholder(dependencies: &Path, archive: &Path) {
    if artifacts(dependencies, "libbamts_node-", "rmeta").len() != 1 {
        panic!(
            "bamts-node build dependency did not produce exactly one metadata artifact in `{}`",
            dependencies.display()
        );
    }
    fs::write(archive, b"!<arch>\n").unwrap_or_else(|error| {
        panic!(
            "could not create check-only runtime archive `{}`: {error}",
            archive.display()
        )
    });
}

fn artifacts(directory: &Path, prefix: &str, extension: &str) -> Vec<PathBuf> {
    let suffix = format!(".{extension}");
    let mut artifacts = fs::read_dir(directory)
        .unwrap_or_else(|error| panic!("could not inspect `{}`: {error}", directory.display()))
        .filter_map(Result::ok)
        .filter_map(|entry| {
            let name = entry.file_name();
            let name = name.to_string_lossy();
            (name.starts_with(prefix) && name.ends_with(&suffix)).then(|| entry.path())
        })
        .collect::<Vec<_>>();
    artifacts.sort();
    artifacts
}

fn display_paths(paths: &[PathBuf]) -> String {
    paths
        .iter()
        .map(|path| path.display().to_string())
        .collect::<Vec<_>>()
        .join(", ")
}

fn required(name: &str) -> String {
    env::var(name).unwrap_or_else(|_| panic!("Cargo did not provide required {name}"))
}

fn required_path(name: &str) -> PathBuf {
    PathBuf::from(required(name))
}

fn cargo_line(directive: &str) {
    writeln!(io::stdout().lock(), "cargo:{directive}").expect("could not emit Cargo directive");
}