use std::path::Path;
fn main() {
let store_features = [
cfg!(feature = "store-fjall"),
cfg!(feature = "store-redis"),
cfg!(feature = "store-dynamodb"),
cfg!(feature = "store-firestore"),
cfg!(feature = "store-cosmosdb"),
];
let enabled_count = store_features.iter().filter(|&&f| f).count();
if enabled_count == 0 {
println!(
"cargo:warning=No storage backend feature enabled! Enable one of: store-fjall, store-redis, store-dynamodb, store-firestore, store-cosmosdb"
);
}
if enabled_count > 1 {
println!(
"cargo:warning=Multiple storage backend features enabled — only one will be used at runtime."
);
}
let secret_features = [
cfg!(feature = "keyring"),
cfg!(feature = "aws-secrets"),
cfg!(feature = "gcp-secrets"),
];
let secret_count = secret_features.iter().filter(|&&f| f).count();
if secret_count > 1 {
println!(
"cargo:warning=Multiple secret store features enabled — only one will be used at runtime."
);
}
#[cfg(feature = "ui")]
build_ui();
}
#[cfg(feature = "ui")]
const MIN_NODE_MAJOR: u32 = 20;
#[cfg(feature = "ui")]
fn mtime(path: &Path) -> Option<std::time::SystemTime> {
std::fs::metadata(path).ok()?.modified().ok()
}
#[cfg(feature = "ui")]
fn build_ui() {
let ui_dir = Path::new("../did-hosting-ui");
let dist_dir = Path::new("ui-dist");
if !ui_dir.is_dir() {
if !dist_dir.join("index.html").exists() {
std::fs::create_dir_all(dist_dir)
.unwrap_or_else(|e| panic!("failed to create {}: {e}", dist_dir.display()));
println!(
"cargo:warning=No management UI bundle found in did-hosting-control/ui-dist and \
the ../did-hosting-ui workspace is absent — UI routes will return 404. Build \
from the workspace checkout, or disable the `ui` feature."
);
}
return;
}
for dir in &["app", "components", "lib"] {
let path = ui_dir.join(dir);
if path.is_dir() {
track_dir_recursive(&path);
}
}
for file in &[
"package.json",
"package-lock.json",
"tsconfig.json",
"app.json",
"App.tsx",
"index.ts",
] {
let path = ui_dir.join(file);
if path.exists() {
println!("cargo:rerun-if-changed={}", path.display());
}
}
let needs_build = if let Ok(dist_meta) = std::fs::metadata(dist_dir.join("index.html")) {
let dist_mtime = dist_meta
.modified()
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
any_source_newer(ui_dir, dist_mtime)
} else {
true };
if !needs_build {
return;
}
check_node_version();
let node_modules = ui_dir.join("node_modules");
let needs_install = !node_modules.exists()
|| match (
mtime(&ui_dir.join("package-lock.json")),
mtime(&node_modules.join(".package-lock.json")),
) {
(Some(lock), Some(installed)) => lock > installed,
_ => false,
};
if needs_install {
run_npm(ui_dir, &["install", "--prefer-offline"]);
}
let _ = std::fs::remove_dir_all(dist_dir);
run_npm(ui_dir, &["run", "build:web"]);
}
#[cfg(feature = "ui")]
fn check_node_version() {
let output = match std::process::Command::new("node").arg("--version").output() {
Ok(o) => o,
Err(e) => panic!(
"failed to invoke `node --version`: {e}. The `ui` feature requires \
Node.js {MIN_NODE_MAJOR}+ to build the management UI. Install it \
(e.g. via nvm) or disable the `ui` feature."
),
};
if !output.status.success() {
panic!(
"`node --version` exited non-zero. Install Node.js {MIN_NODE_MAJOR}+ \
or disable the `ui` feature."
);
}
let raw = String::from_utf8_lossy(&output.stdout);
let version = raw.trim().trim_start_matches('v');
let major: u32 = version
.split('.')
.next()
.and_then(|s| s.parse().ok())
.unwrap_or_else(|| {
panic!("could not parse Node.js version from `node --version` output: {raw:?}")
});
if major < MIN_NODE_MAJOR {
panic!(
"Node.js {major} detected; the `ui` feature requires Node.js \
{MIN_NODE_MAJOR}+ (Metro/Expo depend on Array.prototype.toReversed, \
added in Node 20). Upgrade Node — e.g. `nvm install 22 && nvm use \
22` — or disable the `ui` feature."
);
}
}
#[cfg(feature = "ui")]
fn run_npm(cwd: &Path, args: &[&str]) {
let status = std::process::Command::new("npm")
.current_dir(cwd)
.args(args)
.status()
.unwrap_or_else(|e| panic!("failed to run npm {}: {e}", args.join(" ")));
if !status.success() {
panic!("npm {} failed with {status}", args.join(" "));
}
}
#[cfg(feature = "ui")]
fn any_source_newer(ui_dir: &Path, threshold: std::time::SystemTime) -> bool {
for dir_name in &["app", "components", "lib"] {
let path = ui_dir.join(dir_name);
if path.is_dir() && dir_has_newer(&path, threshold) {
return true;
}
}
for file in &["package.json", "tsconfig.json", "app.json"] {
let path = ui_dir.join(file);
if let Ok(meta) = std::fs::metadata(&path)
&& let Ok(mtime) = meta.modified()
&& mtime > threshold
{
return true;
}
}
false
}
#[cfg(feature = "ui")]
fn dir_has_newer(dir: &Path, threshold: std::time::SystemTime) -> bool {
let Ok(entries) = std::fs::read_dir(dir) else {
return false;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with('.') || name == "node_modules" {
continue;
}
if dir_has_newer(&path, threshold) {
return true;
}
} else if let Ok(meta) = std::fs::metadata(&path)
&& let Ok(mtime) = meta.modified()
&& mtime > threshold
{
return true;
}
}
false
}
#[cfg(feature = "ui")]
fn track_dir_recursive(dir: &Path) {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let name = path.file_name().unwrap_or_default().to_string_lossy();
if name.starts_with('.') || name == "node_modules" {
continue;
}
track_dir_recursive(&path);
} else {
println!("cargo:rerun-if-changed={}", path.display());
}
}
}
}