clib 0.2.0

Generates bindings for C libraries
Documentation
use bindgen::Builder;
use pkg_config::{Config, Library};
use serde_derive::Deserialize;
use std::{
    collections::{HashMap, HashSet},
    env,
    fmt::Debug,
    fs::{self, File},
    io::{self, Read, Write},
    ops::RangeBounds,
    path::{Path, PathBuf},
    process::Command,
    sync::Once,
};
use toml::{
    Value,
    value::Table,
};

#[derive( Debug, Deserialize )]
struct Spec {
    headers             : Vec<String>,
    dependencies        : Option<Vec<String>>,
    #[serde( rename = "header-dependencies" )]
    header_dependencies : Option<Vec<String>>,
}

fn probe<'n,'v>( name: &'n str, version: impl RangeBounds<&'v str> ) -> Result<Library, pkg_config::Error> {
    let mut cfg = Config::new();
    cfg.cargo_metadata( true ).range_version( version );
    Ok( cfg.probe( name )? )
}

fn read_file( path: impl AsRef<Path> ) -> Result<String, io::Error> {
    let mut file = File::open( path )?;
    let mut contents = String::new();
    file.read_to_string( &mut contents )?;
    Ok( contents )
}

fn load_toml( dir: impl AsRef<Path>, name: &str ) -> Option<Value> {
    let path = dir.as_ref().join( format!( "{}.toml", name ));
    read_file( path ).ok().map( |contents| toml::from_str( &contents ).unwrap() )
}

fn save_toml( dir: impl AsRef<Path> + Copy, name: &str, toml: &Value ) {
    fs::create_dir_all( dir ).ok();
    let path = dir.as_ref().join( format!( "{}.toml", name ));
    let mut file = File::create( path ).unwrap();
    file.write( toml.to_string().as_bytes() );
}

const UTF8_PATH: &'static str = "path should be valid UTF-8 string.";

#[derive( Debug, Default )]
pub struct LibInfo {
    link_paths    : Vec<String>,
    include_paths : Vec<String>,
    headers       : Vec<String>,

}

pub fn probe_library<'n,'v>( name: &'n str, version: impl RangeBounds<&'v str> ) -> Result<LibInfo,pkg_config::Error> {
    #[cfg( target_os = "freebsd" )]
    env::set_var( "PKG_CONFIG_ALLOW_CROSS", "1" );

    probe( name, version ).map( |lib| {
        let mut headers = Vec::new();
        let mut include_paths = Vec::new();
        probe_headers( name, &mut headers, &mut include_paths );

        lib.include_paths.iter().for_each( |path| {
            include_paths.push( path.to_str().expect( UTF8_PATH ).to_owned() );
        });

        LibInfo {
            link_paths    : lib.link_paths.iter().map( |path| path.to_str().expect( UTF8_PATH ).to_owned() ).collect(),
            include_paths ,
            headers       ,
        }
    })
}

fn get_includedir( pkg_name: &str ) -> String {
    let exe = env::var( "PKG_CONFIG" ).unwrap_or_else( |_| "pkg-config".to_owned() );
    let mut cmd = Command::new( exe );
    cmd.args( &[ pkg_name, "--variable", "includedir" ]);

    let output = cmd.output().expect( &format!( "`pkg-config {} --variable includedir` should run successfully.", pkg_name ));
    std::str::from_utf8( output.stdout.as_slice() ).expect( "pkg-config should generate utf8-compatible output." )
        .trim_end().to_owned()
}

fn probe_headers( pkg_name: &str, headers: &mut Vec<String>, include_dirs: &mut Vec<String> ) {
    let spec = load_spec( pkg_name );
    let include_dir = get_includedir( &pkg_name );
    let spec: Spec = toml::from_str( &spec ).expect( &format!( "{} should be valid toml file.", pkg_name ));

    spec.headers.iter().for_each( |file|
        headers.push( Path::new( &include_dir ).join( file ).to_str().expect( UTF8_PATH ).to_string() ));

    spec.dependencies.map( |import| import.iter().for_each( |pkg| probe_headers( pkg, headers, include_dirs )));
    spec.header_dependencies.map( |import_dir| import_dir.iter().for_each( |pkg| {
        include_dirs.push( get_includedir( pkg ))
    }));
}

pub fn fold_lib_info<'n,'v,'l>( name: &'n str, version: impl RangeBounds<&'v str>, lib_info_all: &'l mut LibInfo )
    -> Result<(), pkg_config::Error>
{
    probe_library( name, version )
    .map( |lib_info| {
        lib_info.link_paths.into_iter().for_each( |path| { lib_info_all.link_paths.push( path ); });
        lib_info.include_paths.into_iter().for_each( |path| { lib_info_all.include_paths.push( path ); });
        lib_info.headers.into_iter().for_each( |path| { lib_info_all.headers.push( path ); });
    })
}

fn generate_nothing() {
    let out_path = PathBuf::from( env::var("OUT_DIR").expect( "$OUT_DIR should exist." ));
    File::create( out_path.join( "bindings.rs" )).expect( "an empty bindings.rs generated." );
}

fn get_clib_out_dir() -> PathBuf {
    Path::new( &env::var("OUT_DIR").unwrap() )
        .join( "clib-0.2.0" )
}

fn save_spec( pkgs: &Value ) {
    if let Value::Table( table ) = pkgs {
        table.into_iter().for_each( |(name, value)| {
            let spec_dir = get_clib_out_dir();
            let saved_spec: Option<Value> = load_toml( &spec_dir, &name );
            match saved_spec {
                Some( saved_spec ) => if &saved_spec != value {
                    panic!( "[clib] got two different specs of {}.\nOne is \"{}\",\nthe other is \"{}\".",
                        name, saved_spec.to_string(), value.to_string()
                    );
                },
                None => save_toml( &spec_dir, &name, value ),
            }
        });
    } else {
        panic!( "{} is not a toml table", pkgs );
    }
}

fn load_spec( name: &str ) -> String {
    let spec_path = get_clib_out_dir().join( &format!( "{}.toml", name ));
    read_file( &spec_path ).expect( &format!( "{:?} should exist", spec_path ))
}

pub fn metabuild() {
    let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
    let cargo_manifest_dir = Path::new( &cargo_manifest_dir );
    let toml: Value = load_toml( &cargo_manifest_dir, "Cargo" ).unwrap();
    if let Some( package ) = toml.get("package") {
        if let Some( metadata ) = package.get("metadata") {
            if let Some( clib ) = metadata.get("clib") {
                if let Some( pkgs ) = clib.get("pkgs") {
                    println!("save_spec");
                    save_spec( pkgs );
                }
                if let Some( build_pkgs ) = clib.get("build-pkgs") {
                    build( build_pkgs );
                }
            }
        }
    }
}

fn build( pkgs: &Value ) {
    let pkgs = match pkgs {
        Value::Array( pkgs ) => pkgs,
        _ => panic!("`build-pkgs` should be list of strings."),
    };

    let mut lib_info_all = LibInfo::default();
    let out_dir = env::var("OUT_DIR").expect("$OUT_DIR should exist.");

    pkgs.iter().for_each( |pkg|
        if let Value::String( pkg ) = pkg {
            if !pkg.is_empty() {
                fold_lib_info( &pkg, .., &mut lib_info_all ).ok();
            }
        } else {
            panic!("`build-pkgs` should be list of strings.")
        }
    );

    if lib_info_all.headers.is_empty() {
        return;
    }

    let mut builder = Builder::default()
        .generate_comments( false )
    ;

    for header in lib_info_all.headers.iter() {
        builder = builder.header( header );
    }
    for path in lib_info_all.include_paths.iter() {
        let opt = format!( "-I{}", path );
        builder = builder.clang_arg( &opt );
    }

    let bindings = builder.generate().expect( "bindgen builder constructed." );
    let out_path = PathBuf::from( out_dir );
    bindings.write_to_file( out_path.join( "bindings.rs" )).expect( "bindings.rs generated." );
}