ghidra 0.0.3

Typed Rust bindings for an embedded Ghidra JVM
Documentation
use std::{
    env, fs, io,
    path::{Path, PathBuf},
    process::Command,
};

const BRIDGE_SOURCE_ROOT: &str = "bridge/src/main/java";

fn main() {
    println!("cargo:rerun-if-changed={BRIDGE_SOURCE_ROOT}");
    println!("cargo:rerun-if-env-changed=GHIDRA_INSTALL_DIR");

    let install_dir = match env::var_os("GHIDRA_INSTALL_DIR") {
        Some(value) => PathBuf::from(value),
        None => return,
    };
    let sources =
        java_sources(Path::new(BRIDGE_SOURCE_ROOT)).expect("discover Java bridge sources");
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set"));
    let classes = out_dir.join("classes");
    let bridge_jar = out_dir.join("ghidra-bridge.jar");
    if classes.exists() {
        fs::remove_dir_all(&classes).expect("clear Java class output directory");
    }
    fs::create_dir_all(&classes).expect("create Java class output directory");

    let classpath = env::join_paths(ghidra_jars(&install_dir).expect("discover Ghidra jars"))
        .expect("build javac classpath");

    let status = Command::new("javac")
        .arg("-cp")
        .arg(classpath)
        .arg("-proc:none")
        .arg("-d")
        .arg(&classes)
        .args(&sources)
        .status()
        .expect("run javac for Ghidra bridge");
    assert!(status.success(), "javac failed for Ghidra bridge sources");
    let status = Command::new("jar")
        .arg("--create")
        .arg("--file")
        .arg(&bridge_jar)
        .arg("-C")
        .arg(&classes)
        .arg(".")
        .status()
        .expect("package Ghidra bridge jar");
    assert!(status.success(), "jar failed for {}", bridge_jar.display());
    println!("cargo:rustc-env=GHIDRA_BRIDGE_JAR={}", bridge_jar.display());
}

fn java_sources(root: &Path) -> io::Result<Vec<PathBuf>> {
    let mut sources = Vec::new();
    visit_java(root, &mut sources)?;
    sources.sort();
    Ok(sources)
}

fn visit_java(path: &Path, sources: &mut Vec<PathBuf>) -> io::Result<()> {
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            visit_java(&path, sources)?;
        } else if path.extension().is_some_and(|ext| ext == "java") {
            sources.push(path);
        }
    }
    Ok(())
}

fn ghidra_jars(install_dir: &Path) -> io::Result<Vec<PathBuf>> {
    let root = install_dir.join("Ghidra");
    let mut jars = Vec::new();
    visit_jars(&root, &mut jars)?;
    jars.sort();
    Ok(jars)
}

fn visit_jars(path: &Path, jars: &mut Vec<PathBuf>) -> io::Result<()> {
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_dir() {
            visit_jars(&path, jars)?;
        } else if path.extension().is_some_and(|ext| ext == "jar") {
            jars.push(path);
        }
    }
    Ok(())
}