#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "a build script's panic is a compile-time refusal, not a runtime abort; \
see the note above"
)]
use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use std::{env, fs};
fn main() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let catalog = locate(&manifest);
println!("cargo:rerun-if-changed={}", catalog.display());
println!("cargo:rerun-if-changed=build.rs");
let mut files = BTreeMap::new();
let mut roots: Vec<PathBuf> = fs::read_dir(&catalog)
.unwrap_or_else(|error| panic!("cannot list {}: {error}", catalog.display()))
.map(|entry| entry.expect("cannot read a catalog entry").path())
.filter(|path| path.join("setup.json").is_file())
.collect();
roots.sort();
for setup in &roots {
collect(&catalog, setup, &mut files);
}
assert!(
!files.is_empty(),
"{} holds no setup: every directory in it lacks a setup.json, so this \
binary would ship unable to answer `list`",
catalog.display()
);
let mut source = String::from("&[\n");
for (relative, absolute) in &files {
writeln!(
source,
" ({:?}, include_bytes!({:?})),",
relative,
absolute.display().to_string()
)
.expect("writing to a String cannot fail");
}
source.push(']');
let out = PathBuf::from(env::var_os("OUT_DIR").expect("cargo always sets OUT_DIR"));
fs::write(out.join("embedded_setups.rs"), source).expect("cannot write the embedded catalog");
}
fn locate(manifest: &Path) -> PathBuf {
let tool = env!("CARGO_PKG_NAME")
.strip_suffix("-setup-system")
.expect("every crate using this script is named <tool>-setup-system");
let root = manifest.join("setups");
let scoped = root.join(tool);
let chosen = if scoped.is_dir() { scoped } else { root };
assert!(
chosen.is_dir(),
"no setup catalog at {} — this binary would ship unable to answer `list`",
chosen.display()
);
chosen
}
fn collect(root: &Path, current: &Path, out: &mut BTreeMap<String, PathBuf>) {
let mut entries: Vec<_> = fs::read_dir(current)
.unwrap_or_else(|error| panic!("cannot list {}: {error}", current.display()))
.map(|entry| entry.expect("cannot read a catalog entry").path())
.collect();
entries.sort();
for path in entries {
let metadata = fs::symlink_metadata(&path)
.unwrap_or_else(|e| panic!("cannot stat {}: {e}", path.display()));
assert!(
!metadata.is_symlink(),
"{} is a symbolic link; a setup's digest records where a link points, \
and an embedded copy cannot carry that. Replace it with its content.",
path.display()
);
if metadata.is_dir() {
collect(root, &path, out);
continue;
}
assert!(
!is_executable(&metadata),
"{} is executable; a setup's digest records the executable bit on Unix, \
and an embedded copy cannot carry it. Remove the bit, or teach this \
script and the materializer to round-trip it together.",
path.display()
);
let relative = path
.strip_prefix(root)
.expect("collect only ever descends into root")
.components()
.map(|component| component.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/");
out.insert(relative, path);
}
}
#[cfg(unix)]
fn is_executable(metadata: &fs::Metadata) -> bool {
use std::os::unix::fs::PermissionsExt;
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
fn is_executable(_metadata: &fs::Metadata) -> bool {
false
}