oliphaunt-wasix-tools 0.1.0

WASIX pg_dump and psql assets for oliphaunt-wasix
Documentation
use std::env;
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};

use sha2::{Digest, Sha256};

const ARTIFACT_SCHEMA: &str = "oliphaunt-artifact-manifest-v1";
const ARTIFACT_PRODUCT: &str = "oliphaunt-wasix-tools";
const ARTIFACT_KIND: &str = "wasix-tools";
const ARTIFACT_TARGET: &str = "portable";

fn main() {
    println!("cargo:rerun-if-env-changed=OLIPHAUNT_WASM_GENERATED_ASSETS_DIR");
    emit_expected_asset_inputs();

    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set by Cargo"));
    let out = out_dir.join("generated_tools.rs");
    if let Some(asset_dir) = find_asset_dir() {
        emit_rerun_directives(&asset_dir);
        write_generated_tools(&out, &asset_dir);
    } else if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() {
        panic!("release packaging requires package-local WASIX tools payload");
    } else {
        write_source_only_tools(&out);
    }
}

fn emit_expected_asset_inputs() {
    if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_ASSETS_DIR") {
        emit_tool_probes(&PathBuf::from(path));
    }

    let manifest_dir =
        PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"));
    if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) {
        emit_tool_probes(&repo_root.join("target/oliphaunt-wasix/assets"));
    }
    emit_tool_probes(&manifest_dir.join("payload"));
}

fn emit_tool_probes(dir: &Path) {
    println!("cargo:rerun-if-changed={}", dir.display());
    println!(
        "cargo:rerun-if-changed={}",
        dir.join("bin/pg_dump.wasix.wasm").display()
    );
    println!(
        "cargo:rerun-if-changed={}",
        dir.join("bin/psql.wasix.wasm").display()
    );
}

fn find_asset_dir() -> Option<PathBuf> {
    let manifest_dir =
        PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"));
    let package_payload = manifest_dir.join("payload");
    if package_payload.join("bin/pg_dump.wasix.wasm").is_file()
        && package_payload.join("bin/psql.wasix.wasm").is_file()
    {
        return Some(package_payload);
    }

    if let Some(path) = env::var_os("OLIPHAUNT_WASM_GENERATED_ASSETS_DIR") {
        let path = PathBuf::from(path);
        if path.join("bin/pg_dump.wasix.wasm").is_file()
            && path.join("bin/psql.wasix.wasm").is_file()
        {
            return Some(path);
        }
    }

    if let Some(repo_root) = repo_root_from_manifest_dir(&manifest_dir) {
        let target_assets = repo_root.join("target/oliphaunt-wasix/assets");
        if target_assets.join("bin/pg_dump.wasix.wasm").is_file()
            && target_assets.join("bin/psql.wasix.wasm").is_file()
        {
            return Some(target_assets);
        }
    }
    None
}

fn repo_root_from_manifest_dir(manifest_dir: &Path) -> Option<PathBuf> {
    for ancestor in manifest_dir.ancestors() {
        if ancestor.join(".git").exists() && ancestor.join("Cargo.toml").is_file() {
            return Some(ancestor.to_path_buf());
        }
    }
    None
}

fn emit_rerun_directives(asset_dir: &Path) {
    println!("cargo:rerun-if-changed={}", asset_dir.display());
    visit_files(asset_dir, &mut |path| {
        println!("cargo:rerun-if-changed={}", path.display());
    });
}

fn visit_files(path: &Path, f: &mut impl FnMut(&Path)) {
    let Ok(entries) = fs::read_dir(path) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            visit_files(&path, f);
        } else if path.is_file() {
            f(&path);
        }
    }
}

fn write_generated_tools(out: &Path, asset_dir: &Path) {
    let pg_dump = asset_dir.join("bin/pg_dump.wasix.wasm");
    let psql = asset_dir.join("bin/psql.wasix.wasm");
    for required in [&pg_dump, &psql] {
        assert!(
            required.is_file(),
            "generated WASIX tools directory {} is missing required file {}",
            asset_dir.display(),
            required.display()
        );
    }
    let text = format!(
        "pub const HAS_EMBEDDED_TOOLS: bool = true;\n\
         pub fn pg_dump_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({pg_dump})) }}\n\
         pub fn psql_wasm() -> Option<&'static [u8]> {{ Some(include_bytes!({psql})) }}\n",
        pg_dump = rust_string_literal(&pg_dump),
        psql = rust_string_literal(&psql),
    );
    fs::write(out, text).expect("write generated WASIX tool include module");
    emit_artifact_manifest(
        out.parent().expect("generated tool output has parent"),
        asset_dir,
        &[&pg_dump, &psql],
    );
}

fn write_source_only_tools(out: &Path) {
    fs::write(
        out,
        "pub const HAS_EMBEDDED_TOOLS: bool = false;\n\
         pub fn pg_dump_wasm() -> Option<&'static [u8]> { None }\n\
         pub fn psql_wasm() -> Option<&'static [u8]> { None }\n",
    )
    .expect("write source-only WASIX tool include module");
}

fn rust_string_literal(path: &Path) -> String {
    format!("{:?}", path.to_string_lossy())
}

fn emit_artifact_manifest(out_dir: &Path, asset_dir: &Path, files: &[&Path]) {
    let version = env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION is set by Cargo");
    let manifest_path = out_dir.join("oliphaunt-artifact.toml");
    let mut text = format!(
        "schema = {ARTIFACT_SCHEMA:?}\nproduct = {ARTIFACT_PRODUCT:?}\nversion = {version:?}\nkind = {ARTIFACT_KIND:?}\ntarget = {ARTIFACT_TARGET:?}\n"
    );
    for file in files {
        let relative = file
            .strip_prefix(asset_dir)
            .ok()
            .map(|path| path.to_string_lossy().replace('\\', "/"))
            .unwrap_or_else(|| {
                file.file_name()
                    .unwrap_or_default()
                    .to_string_lossy()
                    .into_owned()
            });
        let sha256 = sha256_file(file).expect("hash WASIX tools artifact file");
        text.push_str(&format!(
            "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = false\n",
            file.display().to_string(),
            relative,
            sha256,
        ));
    }
    fs::write(&manifest_path, text).expect("write WASIX tools Cargo artifact manifest");
    println!("cargo::metadata=manifest={}", manifest_path.display());
}

fn sha256_file(path: &Path) -> io::Result<String> {
    let mut file = fs::File::open(path)?;
    let mut hasher = Sha256::new();
    let mut buffer = [0u8; 128 * 1024];
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        hasher.update(&buffer[..read]);
    }
    Ok(format!("{:x}", hasher.finalize()))
}