libui-ffi 0.2.0

Easy to build low-level bindings to 'libui-ng'
Documentation
extern crate bindgen;
extern crate cc;
extern crate embed_resource;
extern crate pkg_config;

use bindgen::Builder as BindgenBuilder;

use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    // Determine build platform
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
    let target_triple = env::var("TARGET").unwrap();
    let msvc = target_triple.contains("msvc");
    let apple = target_triple.contains("apple");
    let unix = cfg!(target_family = "unix") && !apple;

    // Fetch the submodule if needed
    if cfg!(feature = "fetch") {
        // Init or update the submodule with libui if needed
        if !Path::new("libui/.git").exists() {
            Command::new("git")
                .args(&["version"])
                .status()
                .expect("Git does not appear to be installed. Error");
            Command::new("git")
                .args(&["submodule", "update", "--init"])
                .status()
                .expect("Unable to init libui submodule. Error");
            if target_os == "windows" {
                Command::new("git")
                    .current_dir("libui")
                    .args(&["apply", "../patches/allow_small_pbar.patch"])
                    .status()
                    .expect("Unable to apply patch. Error");
            }
        } else {
            Command::new("git")
                .args(&["submodule", "update", "--recursive"])
                .status()
                .expect("Unable to update libui submodule. Error");
        }
    }

    // Generate libui bindings on the fly
    let bindings = BindgenBuilder::default()
        .header("wrapper.h")
        .opaque_type("max_align_t") // For some reason this ends up too large
        //.rustified_enum(".*")
        .trust_clang_mangling(false) // clang sometimes wants to treat these functions as C++
        .generate()
        .expect("Unable to generate bindings");

    let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
    bindings
        .write_to_file(out_path.join("bindings.rs"))
        .expect("Couldn't write bindings");

    // Build libui if needed. Otherwise, assume it's in lib/
    if cfg!(feature = "build") {
        let mut base_config = cc::Build::new();
        let src_base = env::var("SRC_BASE").unwrap_or("libui".to_string());
        let src_path = |x| format!("{}/{}", src_base, x);

        // libui might emit lots of warning we can do nothing about here
        base_config.warnings(false);

        // Add source files that are common to all platforms
        base_config.include(src_path("/common"));

        for filename in [
            "common/attribute.c",
            "common/attrlist.c",
            "common/attrstr.c",
            "common/areaevents.c",
            "common/control.c",
            "common/debug.c",
            "common/matrix.c",
            "common/opentype.c",
            "common/shouldquit.c",
            "common/tablemodel.c",
            "common/tablevalue.c",
            "common/userbugs.c",
            "common/utf.c",
        ]
        .iter()
        {
            base_config.file(src_path(filename));
        }

        if target_os == "windows" {
            base_config.cpp(true);
            base_config.include(src_path("/windows"));

            for filename in [
                "windows/alloc.cpp",
                "windows/area.cpp",
                "windows/areadraw.cpp",
                "windows/areaevents.cpp",
                "windows/areascroll.cpp",
                "windows/areautil.cpp",
                "windows/attrstr.cpp",
                "windows/box.cpp",
                "windows/button.cpp",
                "windows/checkbox.cpp",
                "windows/colorbutton.cpp",
                "windows/colordialog.cpp",
                "windows/combobox.cpp",
                "windows/container.cpp",
                "windows/control.cpp",
                "windows/d2dscratch.cpp",
                "windows/datetimepicker.cpp",
                "windows/debug.cpp",
                "windows/draw.cpp",
                "windows/drawmatrix.cpp",
                "windows/drawpath.cpp",
                "windows/drawtext.cpp",
                "windows/dwrite.cpp",
                "windows/editablecombo.cpp",
                "windows/entry.cpp",
                "windows/events.cpp",
                "windows/fontbutton.cpp",
                "windows/fontdialog.cpp",
                "windows/fontmatch.cpp",
                "windows/form.cpp",
                "windows/graphemes.cpp",
                "windows/grid.cpp",
                "windows/group.cpp",
                "windows/image.cpp",
                "windows/init.cpp",
                "windows/label.cpp",
                "windows/main.cpp",
                "windows/menu.cpp",
                "windows/multilineentry.cpp",
                "windows/opentype.cpp",
                "windows/parent.cpp",
                "windows/progressbar.cpp",
                "windows/radiobuttons.cpp",
                "windows/separator.cpp",
                "windows/sizing.cpp",
                "windows/slider.cpp",
                "windows/spinbox.cpp",
                "windows/stddialogs.cpp",
                "windows/tab.cpp",
                "windows/table.cpp",
                "windows/tabledispinfo.cpp",
                "windows/tabledraw.cpp",
                "windows/tableediting.cpp",
                "windows/tablemetrics.cpp",
                "windows/tabpage.cpp",
                "windows/text.cpp",
                "windows/utf16.cpp",
                "windows/utilwin.cpp",
                "windows/window.cpp",
                "windows/winpublic.cpp",
                "windows/winutil.cpp",
            ]
            .iter()
            {
                base_config.file(src_path(filename));
            }

            // See https://github.com/nabijaczleweli/rust-embed-resource/issues/11
            let target = env::var("TARGET").unwrap();
            if let Some(tool) = cc::windows_registry::find_tool(target.as_str(), "cl.exe") {
                for (key, value) in tool.env() {
                    env::set_var(key, value);
                }
            }
            embed_resource::compile(src_path("/windows/resources.rc"));

            link("user32", false);
            link("kernel32", false);
            link("gdi32", false);
            link("comctl32", false);
            link("uxtheme", false);
            link("msimg32", false);
            link("comdlg32", false);
            link("d2d1", false);
            link("dwrite", false);
            link("ole32", false);
            link("oleaut32", false);
            link("oleacc", false);
            link("uuid", false);
            link("windowscodecs", false);
        } else if unix {
            base_config.include(src_path("/unix"));

            let pkg_cfg = pkg_config::Config::new().probe("gtk+-3.0").unwrap();
            for inc in pkg_cfg.include_paths {
                base_config.include(inc);
            }

            for filename in [
                "unix/alloc.c",
                "unix/area.c",
                "unix/attrstr.c",
                "unix/box.c",
                "unix/button.c",
                "unix/cellrendererbutton.c",
                "unix/checkbox.c",
                "unix/child.c",
                "unix/colorbutton.c",
                "unix/combobox.c",
                "unix/control.c",
                "unix/datetimepicker.c",
                "unix/debug.c",
                "unix/draw.c",
                "unix/drawmatrix.c",
                "unix/drawpath.c",
                "unix/drawtext.c",
                "unix/editablecombo.c",
                "unix/entry.c",
                "unix/fontbutton.c",
                "unix/fontmatch.c",
                "unix/form.c",
                "unix/future.c",
                "unix/graphemes.c",
                "unix/grid.c",
                "unix/group.c",
                "unix/image.c",
                "unix/label.c",
                "unix/main.c",
                "unix/menu.c",
                "unix/multilineentry.c",
                "unix/opentype.c",
                "unix/progressbar.c",
                "unix/radiobuttons.c",
                "unix/separator.c",
                "unix/slider.c",
                "unix/spinbox.c",
                "unix/stddialogs.c",
                "unix/tab.c",
                "unix/table.c",
                "unix/tablemodel.c",
                "unix/text.c",
                "unix/util.c",
                "unix/window.c",
            ]
            .iter()
            {
                base_config.file(src_path(filename));
            }
        } else if apple {
            base_config.include(src_path("/darwin"));

            // https://github.com/sbmpost/AutoRaise/issues/69
            // https://youtrack.jetbrains.com/issue/KT-48807
            // "In Xcode 13 RC localizedAttributedStringForKey:value:table: method got NS_FORMAT_ARGUMENT(1) macro attribute. 
            // Previously, this attribute was applicable only to functions that return a C string, CFString or NSString. 
            // Recently, Apple added support in Clang for another type, NSAttributedString. Clang that ships with Kotlin/Native
            // does not have this patch and fails to process localizedAttributedStringForKey:value:table: declaration.
            // What this workaround does is it makes NS_FORMAT_ARGUMENT(1) a no-op."
            base_config.flag("-DNS_FORMAT_ARGUMENT(A)=");

            // libui-ng uses this flag. I found it to cause a linker error on MaCOS 11.6.
            // Undefined symbol "___isPlatformVersionAtLeast", caused by the @available attribute when mixing XCode/CLTools versions.
            // I'll leave it here in case its different for someone.
            //base_config.flag("-mmacosx-version-min=10.8");
            
            for filename in [
                "darwin/aat.m",
                "darwin/alloc.m",
                "darwin/area.m",
                "darwin/areaevents.m",
                "darwin/attrstr.m",
                "darwin/autolayout.m",
                "darwin/box.m",
                "darwin/button.m",
                "darwin/checkbox.m",
                "darwin/colorbutton.m",
                "darwin/combobox.m",
                "darwin/control.m",
                "darwin/datetimepicker.m",
                "darwin/debug.m",
                "darwin/draw.m",
                "darwin/drawtext.m",
                "darwin/editablecombo.m",
                "darwin/entry.m",
                "darwin/event.m",
                "darwin/fontbutton.m",
                "darwin/fontmatch.m",
                "darwin/fonttraits.m",
                "darwin/fontvariation.m",
                "darwin/form.m",
                "darwin/future.m",
                "darwin/graphemes.m",
                "darwin/grid.m",
                "darwin/group.m",
                "darwin/image.m",
                "darwin/label.m",
                "darwin/main.m",
                "darwin/menu.m",
                "darwin/multilineentry.m",
                "darwin/nstextfield.m",
                "darwin/opentype.m",
                "darwin/progressbar.m",
                "darwin/radiobuttons.m",
                "darwin/scrollview.m",
                "darwin/separator.m",
                "darwin/slider.m",
                "darwin/spinbox.m",
                "darwin/stddialogs.m",
                "darwin/tab.m",
                "darwin/table.m",
                "darwin/tablecolumn.m",
                "darwin/text.m",
                "darwin/undocumented.m",
                "darwin/util.m",
                "darwin/window.m",
                "darwin/winmoveresize.m",
            ]
            .iter()
            {
                base_config.file(src_path(filename));
            }
            println!("cargo:rustc-link-lib=framework=AppKit");
        } else {
            panic!("unrecognized platform! cannot build libui from source");
        }

        // Link everything together into `libui.a`.  This will get linked
        // together because of the `links="ui"` flag in the `Cargo.toml` file,
        // and because the `.compile()` function emits
        // `cargo:rustc-link-lib=static=ui`.
        base_config.compile("libui.a");
    } else {
        // If we're not building the library, then assume it's pre-built and
        // exists in `lib/`
        let mut dst = env::current_dir().expect("Unable to retrieve current directory location.");
        dst.push("lib");

        let libname = if msvc { "libui" } else { "ui" };

        println!("cargo:rustc-link-search=native={}", dst.display());
        println!("cargo:rustc-link-lib={}", libname);
    }
}

/// Tell cargo to link the given library, and optionally to bundle it in.
pub fn link(name: &str, bundled: bool) {
    let target = env::var("TARGET").unwrap();
    let target: Vec<_> = target.split('-').collect();
    if target.get(2) == Some(&"windows") {
        println!("cargo:rustc-link-lib=dylib={}", name);
        if bundled && target.get(3) == Some(&"gnu") {
            let dir = env::var("CARGO_MANIFEST_DIR").unwrap();
            println!("cargo:rustc-link-search=native={}/{}", dir, target[0]);
        }
    } else {
        println!("cargo:rustc-link-lib=dylib={}", name);
    }
}

/// Add the given framework to the linker path
pub fn link_framework(name: &str) {
    println!("cargo:rustc-link-lib=framework={}", name);
}