oliphaunt-tools-linux-x64-gnu 0.2.0

Cargo artifact crate for the linux-x64-gnu Oliphaunt native tools.
Documentation
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

const SCHEMA: &str = "oliphaunt-artifact-manifest-v1";
const PRODUCT: &str = "oliphaunt-tools";
const VERSION: &str = "0.2.0";
const KIND: &str = "native-tools";
const TARGET: &str = "x86_64-unknown-linux-gnu";
const PART_ROOTS: &[&str] = &[
    oliphaunt_tools_linux_x64_gnu_part_001::PAYLOAD_ROOT,
];
const FILE_SHA256: &[(&str, &str)] = &[
    ("LICENSE", "6f8300e2f43c5a012d6f914fe5affa9090b97b0be39ddf3846f01b905edd8c10"),
    ("THIRD_PARTY_LICENSES/PostgreSQL-COPYRIGHT", "3d6af92ff8a4c2cdf69afb1cf44edea727922f5cd0cf8b5f72b11cdecac8fdfd"),
    ("THIRD_PARTY_NOTICES.liboliphaunt-native.md", "ca1c6626873f715f1ee4cebd4be93cb5dd6886df7e246332819aad26a8358372"),
    ("THIRD_PARTY_NOTICES.md", "81765aa852ba082089aceef23be857f36cdf98543c480bf67b764ff011b50619"),
    ("runtime/bin/pg_basebackup", "c700bd45b482f9c3615ec5063120a6acd18ef7559c3b1ea0c400f70d97c7bea3"),
    ("runtime/bin/pg_dump", "a294036eeca7fb7fb16ea3358eba1b6cce8963d099e56be2f24a7b3278dfcdf2"),
    ("runtime/bin/psql", "485a00958d059fc01416fbb66c0fbe3692933a2561fc4abe44c68d8e550af7ee"),
];

fn main() {
    emit_manifest();
}

fn emit_manifest() {
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR is set"));
    let payload = out_dir.join("payload");
    if payload.exists() {
        fs::remove_dir_all(&payload).expect("remove stale liboliphaunt native payload");
    }
    fs::create_dir_all(&payload).expect("create liboliphaunt native payload directory");

    let part_roots = part_roots();
    if part_roots.is_empty() {
        if env::var_os("OLIPHAUNT_ARTIFACT_CRATE_REQUIRE_PAYLOAD").is_some() {
            panic!("missing liboliphaunt native payload part crates");
        }
        return;
    }

    let mut chunk_files: BTreeMap<String, Vec<(usize, PathBuf)>> = BTreeMap::new();
    for root in part_roots {
        println!("cargo::rerun-if-changed={}", root.display());
        copy_complete_files(&root.join("files"), &payload).expect("copy complete payload files");
        collect_chunks(&root.join("chunks"), &root.join("chunks"), &mut chunk_files)
            .expect("collect payload chunks");
    }

    for (relative, mut chunks) in chunk_files {
        chunks.sort_by_key(|(index, _)| *index);
        for (expected, (actual, _)) in chunks.iter().enumerate() {
            if *actual != expected {
                panic!("non-contiguous liboliphaunt chunk indexes for {relative}");
            }
        }
        let output = payload.join(&relative);
        if let Some(parent) = output.parent() {
            fs::create_dir_all(parent).expect("create reconstructed file parent");
        }
        let mut writer = fs::File::create(&output).expect("create reconstructed payload file");
        for (_, path) in chunks {
            let mut reader = fs::File::open(&path).expect("open payload chunk");
            io::copy(&mut reader, &mut writer).expect("append payload chunk");
        }
    }

    let files = collect_files(&payload).expect("collect reconstructed liboliphaunt payload files");
    if files.is_empty() {
        panic!("liboliphaunt native payload part crates produced no files");
    }
    let manifest = out_dir.join("oliphaunt-artifact.toml");
    let mut text = format!(
        "schema = {SCHEMA:?}\nproduct = {PRODUCT:?}\nversion = {VERSION:?}\nkind = {KIND:?}\ntarget = {TARGET:?}\n"
    );
    if files.len() != FILE_SHA256.len() {
        panic!("reconstructed liboliphaunt payload file count does not match the frozen inventory");
    }
    for file in files {
        let relative = file.strip_prefix(&payload)
            .expect("payload file stays under payload root")
            .to_string_lossy()
            .replace('\\', "/");
        let sha256 = FILE_SHA256.iter()
            .find_map(|(candidate, digest)| (*candidate == relative).then_some(*digest))
            .unwrap_or_else(|| panic!("reconstructed liboliphaunt payload has undeclared file {relative}"));
        text.push_str(&format!(
            "\n[[files]]\nsource = {:?}\nrelative = {:?}\nsha256 = {:?}\nexecutable = {}\n",
            file.display().to_string(),
            relative,
            sha256,
            is_executable_relative(&relative),
        ));
    }
    fs::write(&manifest, text).expect("write liboliphaunt native artifact manifest");
    println!("cargo::metadata=manifest={}", manifest.display());
}

fn part_roots() -> Vec<PathBuf> {
    PART_ROOTS.iter().map(PathBuf::from).collect()
}

fn copy_complete_files(source: &Path, destination: &Path) -> io::Result<()> {
    if !source.is_dir() {
        return Ok(());
    }
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let path = entry.path();
        let output = destination.join(path.strip_prefix(source).unwrap_or(&path));
        copy_tree_entry(&path, &output)?;
    }
    Ok(())
}

fn copy_tree_entry(source: &Path, destination: &Path) -> io::Result<()> {
    let metadata = fs::metadata(source)?;
    if metadata.is_dir() {
        fs::create_dir_all(destination)?;
        for entry in fs::read_dir(source)? {
            let entry = entry?;
            copy_tree_entry(&entry.path(), &destination.join(entry.file_name()))?;
        }
    } else if metadata.is_file() {
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent)?;
        }
        fs::copy(source, destination)?;
    }
    Ok(())
}

fn collect_chunks(
    root: &Path,
    current: &Path,
    chunks: &mut BTreeMap<String, Vec<(usize, PathBuf)>>,
) -> io::Result<()> {
    if !current.is_dir() {
        return Ok(());
    }
    for entry in fs::read_dir(current)? {
        let entry = entry?;
        let path = entry.path();
        let metadata = fs::metadata(&path)?;
        if metadata.is_dir() {
            collect_chunks(root, &path, chunks)?;
            continue;
        }
        if !metadata.is_file() {
            continue;
        }
        let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/");
        let (file_relative, part_index) = split_part_relative(&relative)
            .unwrap_or_else(|| panic!("invalid liboliphaunt chunk file name {relative}"));
        chunks.entry(file_relative).or_default().push((part_index, path));
    }
    Ok(())
}

fn split_part_relative(relative: &str) -> Option<(String, usize)> {
    let (file, index) = relative.rsplit_once(".part")?;
    if file.is_empty() || index.len() != 3 || !index.bytes().all(|byte| byte.is_ascii_digit()) {
        return None;
    }
    Some((file.to_owned(), index.parse().ok()?))
}

fn collect_files(root: &Path) -> io::Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    collect_files_inner(root, &mut files)?;
    files.sort();
    Ok(files)
}

fn collect_files_inner(path: &Path, files: &mut Vec<PathBuf>) -> io::Result<()> {
    if !path.is_dir() {
        return Ok(());
    }
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let entry_path = entry.path();
        let metadata = fs::metadata(&entry_path)?;
        if metadata.is_dir() {
            collect_files_inner(&entry_path, files)?;
        } else if metadata.is_file() {
            files.push(entry_path);
        }
    }
    Ok(())
}

fn is_executable_relative(relative: &str) -> bool {
    relative.starts_with("runtime/bin/") || relative.starts_with("bin/")
}