use std::fs::{self, File, Metadata};
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
const ROOT_EXPORT_PREFIX: &str = "//! @root_export ";
const SOURCE_HEADER_LINES: usize = 20;
pub(crate) struct ModuleLeaf {
absolute_path: PathBuf,
exports: Vec<String>,
hash: u64,
}
pub(crate) fn discover_module_leaves(src_root: &Path) -> Result<Vec<ModuleLeaf>, String> {
let metadata = super::checked_metadata(src_root)?;
if !metadata.is_dir() {
return Err(format!(
"Fix: make source root a real directory at {}",
src_root.display()
));
}
let mut leaves = Vec::new();
walk_source_tree(src_root, src_root, &mut leaves)?;
leaves.sort_by(|left, right| left.absolute_path.cmp(&right.absolute_path));
Ok(leaves)
}
fn walk_source_tree(
src_root: &Path,
dir: &Path,
leaves: &mut Vec<ModuleLeaf>,
) -> Result<(), String> {
println!("cargo:rerun-if-changed={}", dir.display());
let mut entries = fs::read_dir(dir)
.map_err(|error| {
format!(
"Fix: make source directory readable at {}: {error}",
dir.display()
)
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| {
format!(
"Fix: make every source entry under {} readable: {error}",
dir.display()
)
})?;
entries.sort_by_key(|entry| entry.path());
for entry in entries {
let path = entry.path();
let metadata = super::checked_metadata(&path)?;
if metadata.is_dir() {
walk_source_tree(src_root, &path, leaves)?;
} else if is_module_leaf(&path, &metadata) {
let relative = path.strip_prefix(src_root).map_err(|error| {
format!(
"Fix: keep source file {} inside source root {}: {error}",
path.display(),
src_root.display()
)
})?;
leaves.push(ModuleLeaf {
absolute_path: path.canonicalize().map_err(|error| {
format!(
"Fix: canonicalize Rust source path at {}: {error}",
path.display()
)
})?,
exports: read_root_exports(&path)?,
hash: stable_path_hash(relative),
});
}
}
Ok(())
}
fn is_module_leaf(path: &Path, metadata: &Metadata) -> bool {
metadata.is_file()
&& path.extension().is_some_and(|extension| extension == "rs")
&& !path
.file_name()
.is_some_and(|name| name == "lib.rs" || name == "mod.rs")
}
fn read_root_exports(path: &Path) -> Result<Vec<String>, String> {
let file = File::open(path).map_err(|error| {
format!(
"Fix: make Rust source readable at {}: {error}",
path.display()
)
})?;
let mut exports = Vec::new();
for (index, line) in BufReader::new(file)
.lines()
.take(SOURCE_HEADER_LINES)
.enumerate()
{
let line = line.map_err(|error| {
format!(
"Fix: make line {} readable in {}: {error}",
index + 1,
path.display()
)
})?;
if let Some(export) = parse_root_export(&line, path, index + 1)? {
exports.push(export);
}
}
Ok(exports)
}
fn parse_root_export(
line: &str,
path: &Path,
line_number: usize,
) -> Result<Option<String>, String> {
let Some(raw_name) = line.strip_prefix(ROOT_EXPORT_PREFIX) else {
return Ok(None);
};
let name = raw_name.trim();
if is_rust_identifier(name) {
Ok(Some(name.to_owned()))
} else {
Err(format!(
"Fix: change @root_export directive at {}:{line_number} to one Rust identifier",
path.display()
))
}
}
fn is_rust_identifier(name: &str) -> bool {
let mut bytes = name.bytes();
let Some(first) = bytes.next() else {
return false;
};
matches!(first, b'A'..=b'Z' | b'a'..=b'z' | b'_')
&& bytes.all(|byte| matches!(byte, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_'))
}
fn stable_path_hash(path: &Path) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in path.to_string_lossy().replace('\\', "/").bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
pub(crate) fn render_module_tree(leaves: &[ModuleLeaf]) -> String {
let mut output = String::from(
"// Generated source module enumeration for zero-conflict migration.\n\
// Not included by lib.rs until the migration commit activates it.\n",
);
for leaf in leaves {
output.push_str(&format!(
"#[path = {path:?}] mod __vyre_auto_{hash:016x};\n",
path = leaf.absolute_path.display().to_string(),
hash = leaf.hash
));
}
for leaf in leaves {
for export in &leaf.exports {
output.push_str(&format!(
"pub use __vyre_auto_{hash:016x}::{export} as {export};\n",
hash = leaf.hash
));
}
}
output
}