use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use crate::infra::error::{CriewError, ErrorCode, Result};
#[derive(Debug, Clone, Copy)]
struct Asset {
relative_path: &'static str,
contents: &'static [u8],
executable: bool,
}
include!(concat!(env!("OUT_DIR"), "/b4_vendor_assets.rs"));
pub fn ensure_installed(data_dir: &Path) -> Result<Option<PathBuf>> {
if ASSETS.is_empty() {
return Ok(None);
}
let root = installation_root(data_dir);
for asset in ASSETS {
write_asset(&root, *asset)?;
}
Ok(Some(root.join("b4.sh")))
}
pub fn script_path(data_dir: &Path) -> PathBuf {
installation_root(data_dir).join("b4.sh")
}
fn installation_root(data_dir: &Path) -> PathBuf {
data_dir.join("vendor").join("b4")
}
fn write_asset(root: &Path, asset: Asset) -> Result<()> {
let path = root.join(asset.relative_path);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
CriewError::with_source(
ErrorCode::B4,
format!(
"failed to create embedded b4 directory {}",
parent.display()
),
error,
)
})?;
}
let needs_write = match fs::read(&path) {
Ok(existing) => existing != asset.contents,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => true,
Err(error) => {
return Err(CriewError::with_source(
ErrorCode::B4,
format!("failed to read embedded b4 asset {}", path.display()),
error,
));
}
};
if needs_write {
fs::write(&path, asset.contents).map_err(|error| {
CriewError::with_source(
ErrorCode::B4,
format!("failed to write embedded b4 asset {}", path.display()),
error,
)
})?;
}
#[cfg(unix)]
if asset.executable {
let permissions = fs::Permissions::from_mode(0o755);
fs::set_permissions(&path, permissions).map_err(|error| {
CriewError::with_source(
ErrorCode::B4,
format!(
"failed to mark embedded b4 asset executable {}",
path.display()
),
error,
)
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use super::{ensure_installed, script_path};
#[test]
fn ensure_installed_writes_runtime_vendor_tree() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_nanos();
let temp_root = std::env::temp_dir().join(format!(
"criew-b4-vendor-test-{}-{nonce}",
std::process::id()
));
let installed = ensure_installed(&temp_root).expect("embedded b4 install");
if let Some(script) = installed {
assert_eq!(script, script_path(&temp_root));
assert!(script.exists(), "embedded b4 script should exist");
assert!(
temp_root
.join("vendor/b4/patatt/patatt/__init__.py")
.exists(),
"embedded patatt runtime should exist"
);
} else {
assert!(!script_path(&temp_root).exists());
}
let _ = fs::remove_dir_all(&temp_root);
}
#[test]
fn ensure_installed_is_idempotent_for_existing_runtime_tree() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_nanos();
let temp_root = std::env::temp_dir().join(format!(
"criew-b4-vendor-repeat-test-{}-{nonce}",
std::process::id()
));
let first = ensure_installed(&temp_root).expect("first embedded b4 install");
let second = ensure_installed(&temp_root).expect("second embedded b4 install");
assert_eq!(first, second);
if let Some(script) = second {
assert_eq!(script, script_path(&temp_root));
assert!(script.exists(), "embedded b4 script should still exist");
}
let _ = fs::remove_dir_all(&temp_root);
}
#[test]
fn ensure_installed_reports_directory_conflicts() {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before unix epoch")
.as_nanos();
let temp_root = std::env::temp_dir().join(format!(
"criew-b4-vendor-conflict-test-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(&temp_root).expect("create temp root");
fs::write(temp_root.join("vendor"), "not a directory").expect("write blocking vendor file");
let error = ensure_installed(&temp_root).expect_err("conflicting vendor path should fail");
assert!(
error
.to_string()
.contains("failed to create embedded b4 directory")
);
let _ = fs::remove_dir_all(&temp_root);
}
}