acpica-bindings 0.1.2

Incomplete rust bindings to Intel's ACPICA kernel subsystem
Documentation
use std::{
    env,
    ffi::OsString,
    fs::{self, File},
    path::{Path, PathBuf},
};

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    let out_dir_path = PathBuf::from(out_dir.clone());

    let acpica_dir = find_source_dir(out_dir_path.clone());

    compile(&acpica_dir);
}

use flate2::read::GzDecoder;
use tar::Archive;

fn find_source_dir(out_dir: PathBuf) -> PathBuf {
    // Find source directory already extracted by a previous compilation
    let dirs =
        fs::read_dir(out_dir.clone()).expect("Should have been able to read files in OUT_DIR");

    for dir in dirs {
        let entry = dir.expect("Should have been able to get info about dir entry");

        // Don't use a file as the source directory
        if !entry
            .file_type()
            .expect("Should have been able to get file type")
            .is_dir()
        {
            continue;
        }

        if entry
            .file_name()
            .into_string()
            .unwrap_or(String::new())
            .starts_with("acpica-")
        {
            return entry.path();
        }
    }

    // If the source is not already extracted, extract it

    // Find the tarball
    let mut dirs = fs::read_dir(".").expect("Should have been able to read files in crate root");

    let source_tarball_path = dirs.find_map(|dir| {
        let dir = dir.expect("Should have been able to get info about dir entry");
        let name = dir.file_name().into_string().expect("Should have been able to get file name of dir entry");
        let is_dir = dir.file_type().expect("Should have been able to get file type").is_dir();

        if !is_dir && name.starts_with("acpica-") && name.ends_with(".tar.gz") {
            Some(name)
        } else {
            None
        }
    }).expect("No source tarball: There should be a file in the crate root starting with 'acpica-' and ending with '.tar.gz'");

    // Extract the tarball
    let tarball = File::open(source_tarball_path.clone())
        .expect("Should have been able to open tarball directory");
    let tar = GzDecoder::new(tarball);
    let mut archive = Archive::new(tar);

    for e in archive.entries().expect("Could not parse archive") {
        let mut e = e.expect("Should have been able to load entry from archive");

        let mut path = out_dir.clone();
        path.push(
            e.path()
                .expect("Should have been able to get path of entry in archive"),
        );

        e.unpack(path)
            .expect("Should have been able to unpack entry");
    }

    let mut dir_path = out_dir.clone();
    dir_path.push(source_tarball_path.strip_suffix(".tar.gz").unwrap());

    // Change the source files to fit the specific use case
    update_source(dir_path.clone());

    dir_path
}

/// The ACPICA sources link to the system libc by default.
/// This is an issue for rust OS projects, which may not link to libc at all.
/// Thankfully, by removing definitions of the macros `ACPI_USE_SYSTEM_CLIBRARY` and `ACPI_USE_STANDARD_HEADERS`,
/// ACPICA will link to its own libc functions instead.
///
/// This function comments out all such definitions.
fn update_source(acpica_dir: PathBuf) {
    // Copy acrust.h to include/platfom
    let mut acrust_path = acpica_dir.clone();
    acrust_path.push("source/include/platform/acrust.h");

    #[allow(unused_mut)] // This needs to be mutable for the non-builtin-cache code to work, but would raise a warning otherwise
    let mut acrust_text = fs::read_to_string("./acrust.h")
        .expect("Should have been able to read 'acrust.h'. There should be a file in the crate root with the name 'acrust.h'");

    #[cfg(not(feature = "builtin_cache"))]
    {
        acrust_text = acrust_text.replace("#define ACPI_CACHE_T", "// #define ACPI_CACHE_T");
        acrust_text = acrust_text.replace(
            "#define ACPI_USE_LOCAL_CACHE",
            "// #define ACPI_USE_LOCAL_CACHE",
        );
    }

    fs::write(acrust_path, acrust_text).expect("Should have been able to write to 'acrust.h': There should be a folder inside the source directory called source/include/platform");

    // Replace
    let mut acenv_path = acpica_dir.clone();
    acenv_path.push("source/include/platform/acenv.h");

    let env_text =
        fs::read_to_string(acenv_path.clone()).expect("Should have been able to read 'acenv.h'");

    let (pre_platform_includes, after_platform_includes) = {
        let (pre_platform_includes, rest) = env_text
            .split_once("#if defined(_LINUX)")
            .expect("acenv.h should have contained section of platform-specific includes. This is currently detected using the string '#if defined(_LINUX)'.");
        let (_, post_platform_includes) = rest
            .split_once("#endif")
            .expect("The platform-specific headers should have ended in '#endif'");

        (pre_platform_includes, post_platform_includes)
    };

    let new_env_text =
        pre_platform_includes.to_string() + r#"#include "acrust.h""# + after_platform_includes;

    std::fs::write(acenv_path, new_env_text.as_bytes())
        .expect("Should have been abl to write 'acenv.h'");
}

/// Compiles the code using the [`cc`] crate
fn compile(acpica_dir: &Path) {
    let mut compilation = cc::Build::new();

    // ACPICA miscompiles if compiled with -O2, so limit the optimisation level to -O1
    // TODO: debug these miscompilations? 
    let opt_level = env::var("OPT_LEVEL")
        .unwrap()
        .parse::<u32>()
        .unwrap()
        .min(1);

    compilation
        .warnings(false) // Otherwise lots of annoying warnings are printed
        .include(acpica_dir.to_str().unwrap().to_string() + "/source/include") // Add `source/include` as an include directory
        .define("ACPI_DEBUG_OUTPUT", None) // Set the ACPI_DEBUG_OUTPUT symbol
        .flag("-fno-stack-protector")
        .opt_level(opt_level);

    let mut components_path = acpica_dir.to_path_buf();
    components_path.push("source/components");

    // Only the 'components' directory is needed for the kernel subsystem - the other directories are for cli utils
    let components = fs::read_dir(components_path)
        .expect("Source directory should contain sub-directory '/source/components'");

    for component_dir in components {
        let component_dir =
            component_dir.expect("Should have been able to get info about dir entry");

        // TODO: These might be nice to have
        // Exclude the debugger and disassembler dirs because they give 'undefined type' errors
        if [OsString::from("debugger"), OsString::from("disassembler")]
            .contains(&component_dir.file_name())
        {
            continue;
        }

        for c_file in fs::read_dir(component_dir.path())
            .expect("Should have been able to read component directory")
        {
            let c_file = c_file.expect("Should have been able to get info about dir entry");

            compilation.file(c_file.path());
        }
    }

    compilation.compile("acpica")
}