use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use include_dir::{include_dir, Dir, DirEntry};
use thiserror::Error;
pub static DEFAULTS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/extensions");
#[derive(Debug, Default, Clone, Copy)]
pub struct MaterializeStats {
pub written: usize,
pub skipped: usize,
}
#[derive(Debug, Error)]
pub enum MaterializeError {
#[error("could not create directory {path:?}: {source}")]
CreateDir {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("could not write {path:?}: {source}")]
Write {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("could not chmod {path:?}: {source}")]
Chmod {
path: PathBuf,
#[source]
source: io::Error,
},
}
pub fn materialize_to(
target_root: &Path,
force: bool,
) -> Result<MaterializeStats, MaterializeError> {
let mut stats = MaterializeStats::default();
for entry in DEFAULTS.entries() {
if let DirEntry::Dir(sub) = entry {
let sub_target = target_root.join(sub.path());
fs::create_dir_all(&sub_target).map_err(|source| MaterializeError::CreateDir {
path: sub_target.clone(),
source,
})?;
materialize_dir(sub, target_root, force, &mut stats)?;
}
}
Ok(stats)
}
fn materialize_dir(
dir: &Dir<'_>,
target_root: &Path,
force: bool,
stats: &mut MaterializeStats,
) -> Result<(), MaterializeError> {
for entry in dir.entries() {
match entry {
DirEntry::Dir(sub) => {
let sub_target = target_root.join(sub.path());
fs::create_dir_all(&sub_target).map_err(|source| MaterializeError::CreateDir {
path: sub_target.clone(),
source,
})?;
materialize_dir(sub, target_root, force, stats)?;
}
DirEntry::File(file) => {
let dest = target_root.join(file.path());
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent).map_err(|source| MaterializeError::CreateDir {
path: parent.to_path_buf(),
source,
})?;
}
if dest.exists() && !force {
stats.skipped += 1;
continue;
}
fs::write(&dest, file.contents()).map_err(|source| MaterializeError::Write {
path: dest.clone(),
source,
})?;
let is_manifest = dest
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|name| name == "_manifest.toml");
if !is_manifest {
set_executable(&dest)?;
}
stats.written += 1;
}
}
}
Ok(())
}
#[cfg(unix)]
fn set_executable(path: &Path) -> Result<(), MaterializeError> {
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o755);
fs::set_permissions(path, perms).map_err(|source| MaterializeError::Chmod {
path: path.to_path_buf(),
source,
})
}
#[cfg(not(unix))]
fn set_executable(_path: &Path) -> Result<(), MaterializeError> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_contains_expected_groups() {
let dirs: Vec<_> = DEFAULTS
.entries()
.iter()
.filter_map(|e| match e {
DirEntry::Dir(d) => d.path().file_name().and_then(|s| s.to_str()),
DirEntry::File(_) => None,
})
.collect();
for expected in &["dev", "prod", "org"] {
assert!(
dirs.contains(expected),
"expected `{expected}` group in DEFAULTS, got {dirs:?}",
);
}
}
#[test]
fn materialize_writes_manifests_and_scripts() {
let tmp = tempfile::tempdir().unwrap();
let stats = materialize_to(tmp.path(), false).unwrap();
assert!(
stats.written >= 6,
"expected ≥6 files, got {}",
stats.written
);
assert_eq!(stats.skipped, 0);
for group in &["dev", "prod", "org"] {
let manifest = tmp.path().join(group).join("_manifest.toml");
assert!(
manifest.exists(),
"manifest missing for {group}: {}",
manifest.display(),
);
let script = tmp.path().join(group).join("hello");
assert!(
script.exists(),
"script missing for {group}: {}",
script.display(),
);
}
}
#[test]
#[cfg(unix)]
fn materialize_sets_exec_bit_on_scripts_only() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().unwrap();
materialize_to(tmp.path(), false).unwrap();
let script_mode = fs::metadata(tmp.path().join("dev/hello"))
.unwrap()
.permissions()
.mode();
assert_eq!(
script_mode & 0o777,
0o755,
"hello script should be 0o755, got {script_mode:o}",
);
let manifest_mode = fs::metadata(tmp.path().join("dev/_manifest.toml"))
.unwrap()
.permissions()
.mode();
assert_eq!(
manifest_mode & 0o111,
0,
"manifest should not be executable, got {manifest_mode:o}",
);
}
#[test]
fn materialize_is_idempotent_without_force() {
let tmp = tempfile::tempdir().unwrap();
let first = materialize_to(tmp.path(), false).unwrap();
let second = materialize_to(tmp.path(), false).unwrap();
assert!(first.written >= 6);
assert_eq!(second.written, 0, "second run should write nothing");
assert_eq!(second.skipped, first.written);
}
#[test]
fn materialize_skips_top_level_files() {
let tmp = tempfile::tempdir().unwrap();
materialize_to(tmp.path(), false).unwrap();
assert!(
!tmp.path().join("README.md").exists(),
"top-level README.md must not be materialized",
);
}
#[test]
fn materialize_force_overwrites_existing_files() {
let tmp = tempfile::tempdir().unwrap();
materialize_to(tmp.path(), false).unwrap();
let target = tmp.path().join("dev/hello");
fs::write(&target, "edited by user\n").unwrap();
let stats = materialize_to(tmp.path(), true).unwrap();
assert_eq!(stats.skipped, 0);
assert!(stats.written >= 6);
let body = fs::read_to_string(&target).unwrap();
assert!(
!body.contains("edited by user"),
"force should overwrite, got: {body}",
);
}
}