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 = "aarch64-unknown-linux-gnu";
const PART_ROOTS: &[&str] = &[
oliphaunt_tools_linux_arm64_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", "91b2b6e82c385300222db349575f0126fff7c8281fa29f19b766c4bfb606c3db"),
("runtime/bin/pg_dump", "b6b9ed385dd69fd2c0ede8c80380f50484bcf7917a59e06c7b0ddefb7274a238"),
("runtime/bin/psql", "433c332f8fa35860601074b556a213e666cf75a07586a2dfd34d415b82a30fb6"),
];
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/")
}