microswitch 2.0.0

μSwitch is a cross-platform application which enables musicians to play sounds using switches. It's designed for people with disabilities to help them practise playing music and to perform in a concert.
Documentation
extern crate image;
extern crate yaml_rust;
use std::{env, fs};
use std::path::PathBuf;
use yaml_rust::YamlLoader;
use path_absolutize::Absolutize;
use std::collections::HashSet;

fn package_dir() -> String {
    env::var("CARGO_MANIFEST_DIR").expect("No CARGO_MANIFEST_DIR env var")
}

fn out_dir() -> String {
    env::var("OUT_DIR").expect("No OUT_DIR env var")
}

fn rust_string(input: &str) -> String {
    const START: &str = "r####\"";
    const END: &str = "\"####";
    let mut result = String::new();
    result.push_str(START);
    assert!(!input.contains(END));
    result.push_str(input);
    result.push_str(END);
    result
}

const EMPTY_EMBEDDED_CONFIG: &str = r##"
use std::collections::HashMap;

pub fn embedded_samples() -> HashMap<&'static str, &'static [u8]> {
    HashMap::new()
}
pub fn embedded_config() ->  Option<&'static str> {
    None
}
"##;

fn build_embedded_config() {
    let out_file: PathBuf = [&out_dir(), "embedded_config.rs"].iter().collect();
    let config_path = env::var("MICROSWITCH_EMBED_CONFIG_PATH");

    if Err(env::VarError::NotPresent) == config_path {
        println!("DEBUG: writing empty embedded config to {}", out_file.to_str().unwrap());
        fs::write(&out_file, EMPTY_EMBEDDED_CONFIG).expect("failed to write embedded_config.rs");
        return;
    }

    let config_path = config_path.expect("Invalid MICROSWITCH_EMBED_CONFIG_PATH");
    let config_path = PathBuf::from(config_path);
    let config_path = config_path.absolutize().unwrap().into_owned();

    println!("DEBUG: writing embedded config from {} to {}", config_path.to_str().unwrap(), out_file.to_str().unwrap());

    let config_str = fs::read_to_string(&config_path).expect("Failed to read MICROSWITCH_EMBED_CONFIG_PATH");
    let mut resolve_path = config_path;
    resolve_path.pop();

    let mut config = YamlLoader::load_from_str(&config_str).unwrap();
    let config = config.remove(0);

    let mut seen_paths: HashSet<String> = HashSet::new();
    let mut generated_source = String::new();

    generated_source.push_str("use std::collections::HashMap;\n\n");

    generated_source.push_str("pub fn embedded_samples() -> HashMap<&'static str, &'static [u8]> {\n");
    generated_source.push_str("    let items: Vec<(&'static str, &'static [u8])> = vec![\n");

    let banks = config["banks"].as_vec().expect("expected banks to be an array");
    for bank in banks {
        let samples = bank["samples"].as_vec().expect("expected banks[x].samples to be an array");

        for sample in samples {
            let sample_file = sample["file"].as_str().expect("expected banks[x].samples[x].file to be a string");
            let mut sample_file_resolved = PathBuf::from(&resolve_path);
            sample_file_resolved.push(sample_file);

            if seen_paths.contains(sample_file) {
                continue;
            }

            seen_paths.insert(sample_file.to_string());
            let sample_file_resolved = sample_file_resolved.to_str().unwrap();

            generated_source.push_str("        (");
            generated_source.push_str(&rust_string(&sample_file));
            generated_source.push_str(", include_bytes!(");
            generated_source.push_str(&rust_string(sample_file_resolved));
            generated_source.push_str(")),\n");
        }
    }

    generated_source.push_str("    ];\n");
    generated_source.push_str("    items.into_iter().collect()\n");
    generated_source.push_str("}\n");

    generated_source.push_str("pub fn embedded_config() -> Option<&'static str> {\n");
    generated_source.push_str("Some(\n");
    generated_source.push_str(&rust_string(&config_str));
    generated_source.push_str("\n");
    generated_source.push_str(")\n");
    generated_source.push_str("}");

    fs::write(&out_file, &generated_source).expect("failed to write embedded_config.rs");
}

fn build_window_icon() {
    let out_dir = out_dir();
    let img_path: PathBuf = [package_dir().as_str(), "resources", "microswitch-icon-32.png"].iter().collect();
    let out_path: PathBuf = [out_dir.as_str(), "microswitch-icon-32-rgba"].iter().collect();

    let img = image::open(img_path).expect("Failed to read/decode microswitch-icon-32-rgba");
    let img = img.to_rgba8();
    let rgba = img.into_raw();
    println!("DEBUG: writing window icon to {}", out_path.to_str().unwrap());
    fs::write(&out_path, rgba).expect("Failed to write to microswitch-icon-32-rgba");
}

#[cfg(target_os = "windows")]
fn main() {
    println!("cargo:rerun-if-changed=build.rs");
    println!("cargo:rerun-if-env-changed=MICROSWITCH_EMBED_CONFIG_PATH");

    let package_dir = package_dir();
    let resources_dir: PathBuf = [package_dir.as_str(), "resources"].iter().collect();

    println!("cargo:rustc-link-search=native={}", resources_dir.to_str().unwrap());
    println!("cargo:rustc-link-lib=dylib={}", "resources");

    build_window_icon();
    // example:
    //   $env:MICROSWITCH_EMBED_CONFIG_PATH = 'example\config.yaml'
    //   cargo build -vv
    build_embedded_config();
}

#[cfg(not(target_os = "windows"))]
fn main() {
    build_window_icon();
    build_embedded_config();
}