winit_surface_window 0.2.0

A helper library to use existing Android Surfaces (like Presentations) as windows within the Rust ecosystem, compatible with raw-window-handle.
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    println!("cargo:rerun-if-changed=java");
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();

    // Android targets only
    if target_os != "android" {
        return;
    }

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let java_source_dir = manifest_dir.join("java");

    // Skip if Java source directory is missing
    if !java_source_dir.exists() {
        println!("cargo:warning=No java directory found, skipping Java compilation");
        return;
    }

    // Locate Android SDK path
    let android_sdk_root = match env::var("ANDROID_SDK_ROOT").or_else(|_| env::var("ANDROID_HOME"))
    {
        Ok(path) => path,
        Err(_) => {
            println!("cargo:warning=ANDROID_SDK_ROOT or ANDROID_HOME environment variable not set, skipping Java compilation");
            return;
        }
    };

    let sdk_root_path = Path::new(&android_sdk_root);
    if !sdk_root_path.exists() {
        println!(
            "cargo:warning=Android SDK path does not exist: {:?}, skipping Java compilation",
            sdk_root_path
        );
        return;
    }

    // Locate latest build tools version
    let build_tools_dir = sdk_root_path.join("build-tools");
    let latest_build_tools = match find_latest_sdk_dir(&build_tools_dir) {
        Some(path) => path,
        None => {
            println!(
                "cargo:warning=No build-tools version found in SDK, skipping Java compilation"
            );
            return;
        }
    };

    // Build tools version info available in latest_build_tools

    // Locate d8 tool executable
    let d8_executable = if cfg!(target_os = "windows") {
        "d8.bat"
    } else {
        "d8"
    };
    let d8_path = latest_build_tools.join(d8_executable);

    if !d8_path.exists() {
        println!(
            "cargo:warning=d8 not found at {:?}, skipping Java compilation",
            d8_path
        );
        return;
    }

    // Locate latest Android platform
    let platforms_dir = sdk_root_path.join("platforms");
    let latest_platform = match find_latest_sdk_dir(&platforms_dir) {
        Some(path) => path,
        None => {
            println!("cargo:warning=No platform version found in SDK, skipping Java compilation");
            return;
        }
    };

    // Platform version info available in latest_platform

    let android_jar = latest_platform.join("android.jar");
    if !android_jar.exists() {
        println!(
            "cargo:warning=android.jar not found in {:?}, skipping Java compilation",
            latest_platform
        );
        return;
    }

    // Setup output directory structure
    let java_classes_dir = out_dir.join("java_classes");
    if let Err(e) = fs::create_dir_all(&java_classes_dir) {
        println!(
            "cargo:warning=Failed to create java_classes directory: {}",
            e
        );
        return;
    }

    // Collect all Java source files
    let java_files = find_java_files(&java_source_dir);
    if java_files.is_empty() {
        println!("cargo:warning=No Java files found in java directory");
        return;
    }

    // Locate Java compiler
    let javac_path = match find_javac() {
        Some(path) => path,
        None => {
            println!("cargo:warning=Unable to find javac compiler. Please ensure it is installed and either JAVA_HOME is set or javac is in PATH");
            return;
        }
    };

    // Compile Java sources to bytecode
    let javac_status = Command::new(&javac_path)
        .args(["-d", java_classes_dir.to_str().unwrap()])
        .args(["-cp", android_jar.to_str().unwrap()])
        .args(["-encoding", "UTF-8"])
        .args(["-sourcepath", java_source_dir.to_str().unwrap()])
        .args(["-source", "8"])
        .args(["-target", "8"])
        .args(&java_files)
        .status();

    match javac_status {
        Ok(status) => {
            if !status.success() {
                println!("cargo:warning=Java compilation failed");
                return;
            }
        }
        Err(e) => {
            println!(
                "cargo:warning=Failed to execute javac at {:?}: {}",
                javac_path, e
            );
            return;
        }
    }

    // Collect compiled class files
    let class_files = find_class_files(&java_classes_dir);
    if class_files.is_empty() {
        println!("cargo:warning=No class files found for DEX conversion");
        return;
    }

    // Class file count available in class_files.len()

    // Convert bytecode to Android DEX format
    let final_dex_path = out_dir.join("presentation_helper.dex");
    let temp_dex_dir = out_dir.join("temp_dex");
    if let Err(e) = fs::create_dir_all(&temp_dex_dir) {
        println!("cargo:warning=Failed to create temp_dex directory: {}", e);
        return;
    }

    // Try dx first, fallback to d8
    let dx_executable = if cfg!(target_os = "windows") {
        "dx.bat"
    } else {
        "dx"
    };
    let dx_path = latest_build_tools.join(dx_executable);

    let dex_command_result = if dx_path.exists() {
        // Prefer dx for better compatibility
        Command::new(&dx_path)
            .arg("--dex")
            .arg("--output")
            .arg(&final_dex_path)
            .args(&class_files)
            .status()
    } else {
        // Fall back to d8 if dx is unavailable
        Command::new(&d8_path)
            .arg("--output")
            .arg(&temp_dex_dir)
            .arg("--classpath")
            .arg(&android_jar)
            .arg("--min-api")
            .arg("21")
            .arg("--no-desugaring")
            .args(&class_files)
            .status()
    };

    let d8_status = dex_command_result;

    match d8_status {
        Ok(status) => {
            if !status.success() {
                println!("cargo:warning=DEX conversion failed");
                return;
            }
        }
        Err(e) => {
            println!("cargo:warning=Failed to execute DEX tool: {}", e);
            return;
        }
    }

    // Move DEX file if needed
    let dex_exists = if final_dex_path.exists() {
        // dx outputs directly to target
        true
    } else {
        // d8 outputs to temp directory, move to target
        let generated_dex = temp_dex_dir.join("classes.dex");
        if generated_dex.exists() {
            if let Err(e) = fs::rename(&generated_dex, &final_dex_path) {
                println!(
                    "cargo:warning=Unable to move classes.dex to final location: {}",
                    e
                );
                return;
            }
            true
        } else {
            false
        }
    };

    if dex_exists {
        // Validate DEX file format
        match fs::read(&final_dex_path) {
            Ok(dex_data) => {
                if dex_data.len() < 8 {
                    println!(
                        "cargo:warning=Generated DEX file is too small: {} bytes",
                        dex_data.len()
                    );
                    return;
                }

                // Validate DEX magic bytes
                let dex_magic = &dex_data[0..4];
                if dex_magic != b"dex\n" {
                    println!("cargo:warning=Generated file is not a valid DEX file (incorrect magic number)");
                    return;
                }

                // DEX file format validation successful
            }
            Err(e) => {
                println!("cargo:warning=Failed to validate DEX file: {}", e);
                return;
            }
        }

        println!(
            "cargo:rustc-env=PRESENTATION_DEX_PATH={}",
            final_dex_path.display()
        );
        // Build completed successfully
    } else {
        println!(
            "cargo:warning=DEX tool executed successfully, but expected DEX file was not found"
        );
    }
}

/// Find the latest SDK directory (build-tools or platforms)
fn find_latest_sdk_dir(parent_dir: &Path) -> Option<PathBuf> {
    if !parent_dir.exists() {
        return None;
    }

    let mut latest_version: Option<(Vec<u32>, PathBuf)> = None;

    for entry in fs::read_dir(parent_dir).ok()? {
        let entry = entry.ok()?;
        let path = entry.path();
        if path.is_dir() {
            let version_str = entry.file_name().to_string_lossy().to_string();
            let version_parts: Vec<u32> = version_str
                .split('.')
                .map(|s| s.replace("android-", "").parse().unwrap_or(0))
                .collect();

            if let Some((ref latest_parts, _)) = latest_version {
                if version_parts > *latest_parts {
                    latest_version = Some((version_parts, path));
                }
            } else {
                latest_version = Some((version_parts, path));
            }
        }
    }
    latest_version.map(|(_, path)| path)
}

/// Find javac compiler
fn find_javac() -> Option<PathBuf> {
    // First try JAVA_HOME
    if let Ok(java_home) = env::var("JAVA_HOME") {
        let java_home_path = Path::new(&java_home);
        let javac_paths = [
            java_home_path.join("bin").join("javac.exe"),
            java_home_path.join("bin").join("javac"),
        ];

        for javac_path in &javac_paths {
            if javac_path.exists() {
                return Some(javac_path.clone());
            }
        }
    }

    // Fall back to PATH
    Some(PathBuf::from("javac"))
}

/// Recursively find all Java files in a directory
fn find_java_files(dir: &Path) -> Vec<PathBuf> {
    let mut java_files = Vec::new();

    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                java_files.extend(find_java_files(&path));
            } else if path.extension().and_then(|s| s.to_str()) == Some("java") {
                java_files.push(path);
            }
        }
    }

    java_files
}

/// Recursively find all class files in a directory
fn find_class_files(dir: &Path) -> Vec<PathBuf> {
    let mut class_files = Vec::new();

    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                class_files.extend(find_class_files(&path));
            } else if path.extension().and_then(|s| s.to_str()) == Some("class") {
                class_files.push(path);
            }
        }
    }

    class_files
}