rosie-sys 1.3.1

A crate to build or link to librosie to access the Rosie Pattern Language
Documentation

//Depending on the Cargo features, some parts of this file are not run
#![allow(dead_code)]
#![allow(unreachable_code)]

use std::fs; // Used to copy the C headers
use std::path::{PathBuf, Path};

extern crate cc;

#[cfg(feature = "link_shared_librosie")]
extern crate pkg_config;

//TODO: Test on Windows.  I suspect it won't work, and I may need to follow the example from Teseract,
//  using the vcpkg crate, here:
//https://github.com/ccouzens/tesseract-sys/blob/master/build.rs

fn main() -> Result<(), i32> {

    //If both "build-mode" features are specified then it's an error
    #[cfg(all(feature = "link_shared_librosie", feature = "build_static_librosie"))]
    {
        // println!("cargo:warning=Error: both link_shared_librosie and build_static_librosie are specified");
        println!("Error: both link_shared_librosie and build_static_librosie are specified");
        return Err(-1);
    }

    //See if we're linking a shared librosie
    #[cfg(feature = "link_shared_librosie")]
    {
        //First, see if we canc locate the library using pkg_config
        let librosie = pkg_config::Config::new()
            .cargo_metadata(true)
            .print_system_libs(true)
            .atleast_version("1.3.0")
            .probe("rosie");
        if librosie.is_ok() {
            //pkg_config should output the necessary output for cargo
            return Ok(());
        }

        //If pkg_config didn't find librosie, try one more time to see if it's installed by trying to build something that links it
        if librosie_installed() {
            println!("cargo:rustc-link-lib=rosie");
            return Ok(());
        }

        //We got here because we failed to find an existing librosie
        println!("Error: link_shared_librosie specified, but librosie 1.3.0 or higher couldn't be found");
        return Err(-1);
    }

    //If we're not linking the shared lib, then we're building a private copy
    #[cfg(feature = "build_static_librosie")]
    {
        //Build librosie from source
        if librosie_src_build() {
            println!("cargo:rustc-link-lib=rosie");
            return Ok(());
        }

        //We got here because we failed to find or build librosie.
        println!("Error: librosie build failure");
        return Err(-1);
    }

    //If neither env vars are set then it's an error
    #[cfg(not(any(feature = "link_shared_librosie", feature = "build_static_librosie")))]
    {
        println!("Error: either the link_shared_librosie or build_static_librosie feature must be specified");
        return Err(-1);
    }

    //If we didn't take any of the above paths then exit, having built nothing,
    //NOTE: This will never be hit, given the logic above
    // println!("cargo:warning=ROSIE NOT BUILT.  Either the link_shared_librosie or build_static_librosie feature must be specified to build librosie");
    // return Ok(());
}

//If we haven't found it using one of the pkg trackers, try to compile the smoke.c file to "smoke it out"
//Thanks to the zlib crate for this idea:  https://github.com/rust-lang/libz-sys/blob/main/build.rs
fn librosie_installed() -> bool {

    let smoke_file : PathBuf = ["src", "smoke.c"].iter().collect();
    let mut cfg = cc::Build::new();
    cfg.cargo_metadata(false);
    cfg.file(smoke_file);
    if cfg.try_compile("smoke").is_ok() {
        return true;
    }

    false
}

//Build the librosie library from source
//
// It looks like the "make-cmd" crate is in really sad shape.  No updates for the last 6 years, and it's just
//  a very thin wrapper around std::process::Command anyway.
//
// I'm at a crossroads.  Should I (1.) use the Rust cc crate directly or should I (2.) call the host system's
// `make` using the std::process::Command?
//
// 1. Rust cc is better supported by Cargo and will likely be more robust against platform iregularities, but
//  I lose out on getting to leverage the maintainence done by the librosie (and rpeg) Makefiles.
// 2. Running the Makefile using std::process::Command preserves the rosie Makefile work, but then I have to
//  do the "autoconfigure" work myself, which sounds like a nightmare.
//
// I think I'll choose 1, because it's the devil I know.  In other words, I don't know what I don't know about
//  possible configs this might end up needing to run on.  But in the case of 1., I can verify that everything
//  is building as expected.
fn librosie_src_build() -> bool {

    //Incoming from Cargo
    let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
    let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());

    //The build products
    let rosie_lib_dir = out_dir.clone(); //cc's default is to put the build lib in the out_dir, and that's fine with us
    let rosie_include_dir = out_dir.join("include");
    let rosie_home_dir = out_dir.join("rosie_home");

    //The C source file locations
    let lua_cjson_dir : PathBuf = ["src", "lua-cjson"].iter().collect();
    let rpeg_src_dir : PathBuf = ["src", "rpeg"].iter().collect();
    let rpeg_compiler_dir = rpeg_src_dir.join("compiler");
    let rpeg_runtime_dir = rpeg_src_dir.join("runtime");
    let librosie_src_dir : PathBuf = ["src", "librosie"].iter().collect();

    //The extra artifacts, included in the manifest
    let rosie_include_src_dir = manifest_dir.join(&librosie_src_dir);
    let rosie_home_src_dir = manifest_dir.join("src").join("rosie_home");

    //Locate the LUA headers and library, which should have been built by the mlua crate dependency
    let lua_include_dir = PathBuf::from(std::env::var_os("DEP_LUA_INCLUDE").unwrap());
    let lua_lib_dir = PathBuf::from(std::env::var_os("DEP_LUA_LIB").unwrap());
    // let lua_a : PathBuf = lua_lib_dir.join("liblua5.3.a");
    
    //This is needed so the librosie we're about to build will succeed in linking with the mlua output lib
    println!("cargo:rustc-link-search={}", lua_lib_dir.display());
    println!("cargo:rustc-link-lib=static=lua5.3");

    //The C src files needed to build librosie
    let compile_src_files : Vec<PathBuf> = vec![
        // lua-cjson
        lua_cjson_dir.join("fpconv.c"),
        lua_cjson_dir.join("strbuf.c"),
        lua_cjson_dir.join("lua_cjson.c"),

        // rpeg compiler
        rpeg_compiler_dir.join("rbuf.c"),
        rpeg_compiler_dir.join("lpcap.c"),
        rpeg_compiler_dir.join("lptree.c"),
        rpeg_compiler_dir.join("lpcode.c"),
        rpeg_compiler_dir.join("lpprint.c"),

        // rpeg runtime
        rpeg_runtime_dir.join("buf.c"),
        rpeg_runtime_dir.join("capture.c"),
        rpeg_runtime_dir.join("file.c"),
        rpeg_runtime_dir.join("json.c"),
        rpeg_runtime_dir.join("ktable.c"),
        rpeg_runtime_dir.join("rplx.c"),
        rpeg_runtime_dir.join("vm.c"),

        // librosie
        librosie_src_dir.join("librosie.c"),
    ];

    //Invoke the C compiler to perform the librosie build
    let mut cfg = cc::Build::new();
    cfg.static_flag(true);
    cfg.include(lua_include_dir);
    cfg.include(rpeg_src_dir.join("include"));
    cfg.define("LPEG_DEBUG", None); // Needed by the rpeg compiler build
    cfg.define("NDEBUG", None); // Needed by the rpeg compiler and librosie build
    cfg.define("LUA_COMPAT_5_2", None); // Needed by the librosie build
    cfg.define("_GNU_SOURCE", None); // Required to access libdl on Linux and harmless on Mac
    cfg.files(compile_src_files.iter());
    cfg.compile("rosie");
    println!("cargo:rerun-if-changed=src");
    
    //rosie_home files, to copy into the build output.  Currently these are just copies of the
    //files built by the main Rosie project's Makefile.  This is ok because they all have
    //platform-independent formats.
    let rosie_home_files : Vec<PathBuf> = vec![
        PathBuf::from("CHANGELOG"),
        PathBuf::from("CONTRIBUTORS"),
        PathBuf::from("LICENSE"),
        PathBuf::from("README"),
        PathBuf::from("VERSION"),
        
        //QUESTION: It's highly debatable whether we should include the "extra" files in this crate.
        //CONCLUSION: NO.  They're almsot 1MB, and they aren't used automatically by anything else in
        // rosie-sys or rosie-rs.  If people want them, it's easy enough to get them from the upstream
        // rosie repository.

        //"extra" directory within rosie_home.  Kept for completeness.
        //
        // PathBuf::from("extra").join("WSL").join("rosie_install.sh"),

        // PathBuf::from("extra").join("docker").join("README.md"),
        // PathBuf::from("extra").join("docker").join("arch"),
        // PathBuf::from("extra").join("docker").join("centos"),
        // PathBuf::from("extra").join("docker").join("elementary"),
        // PathBuf::from("extra").join("docker").join("fedora"),
        // PathBuf::from("extra").join("docker").join("nixos"),
        // PathBuf::from("extra").join("docker").join("run"),
        // PathBuf::from("extra").join("docker").join("syslog-2018-09-05.rpl"),
        // PathBuf::from("extra").join("docker").join("ubuntu"),
        // PathBuf::from("extra").join("docker").join("valgrind-fedora"),

        // PathBuf::from("extra").join("emacs").join("rpl-mode.el"),

        // PathBuf::from("extra").join("examples").join("README.md"),
        // PathBuf::from("extra").join("examples").join("anbmcndm.rpl"),
        // PathBuf::from("extra").join("examples").join("anbn.rpl"),
        // PathBuf::from("extra").join("examples").join("anbncn.rpl"),
        // PathBuf::from("extra").join("examples").join("anbncndn.rpl"),
        // PathBuf::from("extra").join("examples").join("backref.rpl"),
        // PathBuf::from("extra").join("examples").join("distinct.rpl"),
        // PathBuf::from("extra").join("examples").join("dyck.rpl"),
        // PathBuf::from("extra").join("examples").join("html.rpl"),
        // PathBuf::from("extra").join("examples").join("ipv4.py"),
        // PathBuf::from("extra").join("examples").join("reverse.rpl"),
        // PathBuf::from("extra").join("examples").join("sloc.py"),
        // PathBuf::from("extra").join("examples").join("sloc_C.sh"),
        // PathBuf::from("extra").join("examples").join("sloc_lua.sh"),

        // PathBuf::from("extra").join("examples").join("images").join("p1.gif"),
        // PathBuf::from("extra").join("examples").join("images").join("readme-fig1.png"),
        // PathBuf::from("extra").join("examples").join("images").join("readme-fig2.png"),
        // PathBuf::from("extra").join("examples").join("images").join("readme-fig3.png"),
        // PathBuf::from("extra").join("examples").join("images").join("readme-fig4.png"),
        // PathBuf::from("extra").join("examples").join("images").join("readme-fig5.png"),

        // PathBuf::from("extra").join("vim").join("ftdetect").join("rosie.vim"),

        // PathBuf::from("extra").join("vim").join("syntax").join("rosie.vim"),

        PathBuf::from("lib").join("argparse.luac"),
        PathBuf::from("lib").join("ast.luac"),
        PathBuf::from("lib").join("boot.luac"),
        PathBuf::from("lib").join("builtins.luac"),
        PathBuf::from("lib").join("cli-common.luac"),
        PathBuf::from("lib").join("cli-match.luac"),
        PathBuf::from("lib").join("cli-parser.luac"),
        PathBuf::from("lib").join("cli.luac"),
        PathBuf::from("lib").join("color.luac"),
        PathBuf::from("lib").join("common.luac"),
        PathBuf::from("lib").join("compile.luac"),
        PathBuf::from("lib").join("engine_module.luac"),
        PathBuf::from("lib").join("environment.luac"),
        PathBuf::from("lib").join("expand.luac"),
        PathBuf::from("lib").join("expr.luac"),
        PathBuf::from("lib").join("infix.luac"),
        PathBuf::from("lib").join("init.luac"),
        PathBuf::from("lib").join("list.luac"),
        PathBuf::from("lib").join("loadpkg.luac"),
        PathBuf::from("lib").join("parse.luac"),
        PathBuf::from("lib").join("parse_core.luac"),
        PathBuf::from("lib").join("rcfile.luac"),
        PathBuf::from("lib").join("recordtype.luac"),
        PathBuf::from("lib").join("repl.luac"),
        PathBuf::from("lib").join("strict.luac"),
        PathBuf::from("lib").join("submodule.luac"),
        PathBuf::from("lib").join("thread.luac"),
        PathBuf::from("lib").join("trace.luac"),
        PathBuf::from("lib").join("ui.luac"),
        PathBuf::from("lib").join("unittest.luac"),
        PathBuf::from("lib").join("ustring.luac"),
        PathBuf::from("lib").join("util.luac"),
        PathBuf::from("lib").join("violation.luac"),
        PathBuf::from("lib").join("writer.luac"),

        PathBuf::from("rpl").join("all.rpl"),
        PathBuf::from("rpl").join("char.rpl"),
        PathBuf::from("rpl").join("csv.rpl"),
        PathBuf::from("rpl").join("date.rpl"),
        PathBuf::from("rpl").join("id.rpl"),
        PathBuf::from("rpl").join("json.rpl"),
        PathBuf::from("rpl").join("net.rpl"),
        PathBuf::from("rpl").join("num.rpl"),
        PathBuf::from("rpl").join("os.rpl"),
        PathBuf::from("rpl").join("re.rpl"),
        PathBuf::from("rpl").join("time.rpl"),
        PathBuf::from("rpl").join("ts.rpl"),
        PathBuf::from("rpl").join("ver.rpl"),
        PathBuf::from("rpl").join("word.rpl"),

        PathBuf::from("rpl").join("Unicode").join("Ascii.rpl"),
        PathBuf::from("rpl").join("Unicode").join("Block.rpl"),
        PathBuf::from("rpl").join("Unicode").join("Category.rpl"),
        PathBuf::from("rpl").join("Unicode").join("GraphemeBreak.rpl"),
        PathBuf::from("rpl").join("Unicode").join("LineBreak.rpl"),
        PathBuf::from("rpl").join("Unicode").join("NumericType.rpl"),
        PathBuf::from("rpl").join("Unicode").join("Property.rpl"),
        PathBuf::from("rpl").join("Unicode").join("Script.rpl"),
        PathBuf::from("rpl").join("Unicode").join("SentenceBreak.rpl"),
        PathBuf::from("rpl").join("Unicode").join("WordBreak.rpl"),

        PathBuf::from("rpl").join("builtin").join("prelude.rpl"),

        PathBuf::from("rpl").join("rosie").join("rcfile.rpl"),
        PathBuf::from("rpl").join("rosie").join("rpl_1_1.rpl"),
        PathBuf::from("rpl").join("rosie").join("rpl_1_2.rpl"),
        PathBuf::from("rpl").join("rosie").join("rpl_1_3.rpl"),
    ];

    //Copy the rosie_home files to the build artifacts output dir
    create_empty_dir(&rosie_home_dir).unwrap();
    for file in &rosie_home_files {
        let dest_path = rosie_home_dir.join(file);
        create_parent_dir(&dest_path).unwrap();
        fs::copy(rosie_home_src_dir.join(file), dest_path).unwrap();
    }

    //Set the env variable that we'll use to communicate the location of the rosie_home dir, to embed in the return value
    //for the rosie_home_default call.  This is inelegant, but should work for now.
    //TODO: In the future, we should embed the CONTENTS of the rosie_home into the binary, not just the path
    println!("cargo:rustc-env=ROSIE_HOME={}", rosie_home_dir.display());
    
    //Copy the rosie header file(s) so anyone who needs to use this crate for a C dependency build against it
    create_empty_dir(&rosie_include_dir).unwrap();
    for include_file in &["librosie.h"] {
        fs::copy(rosie_include_src_dir.join(include_file), rosie_include_dir.join(include_file)).unwrap();
    }
    //Tell cargo how to set `DEP_ROSIE_INCLUDE`
    println!("cargo:include={}", rosie_include_dir.display());

    //Tell cargo so anyone who depends on this crate can find our lib.  I.e. so cargo will set `DEP_ROSIE_LIB` 
    println!("cargo:lib={}", rosie_lib_dir.display());

    true
}

fn create_empty_dir<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
    if path.as_ref().exists() {
        fs::remove_dir_all(&path)?;
    }
    fs::create_dir_all(&path)
}

fn create_parent_dir<P: AsRef<Path>>(path: P) -> Result<(), std::io::Error> {
    let parent = path.as_ref().parent().ok_or(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("Can't get parent of path: {}", path.as_ref().display())))?;
    fs::create_dir_all(&parent)
}