use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use anyhow::{Context, Result};
use ignore::WalkBuilder;
pub fn resolve_project_root(arg: Option<&Path>) -> Result<PathBuf> {
if let Some(p) = arg {
let p = p
.canonicalize()
.with_context(|| format!("canonicalize project: {}", p.display()))?;
ensure_project(&p)?;
return Ok(p);
}
let cwd = std::env::current_dir().context("get cwd")?;
let mut cur: &Path = &cwd;
loop {
if is_project(cur) {
return Ok(cur.to_path_buf());
}
match cur.parent() {
Some(p) => cur = p,
None => anyhow::bail!(
"no Unity project root found above {}: needs `Assets/` + `ProjectSettings/`",
cwd.display()
),
}
}
}
fn is_project(p: &Path) -> bool {
p.join("Assets").is_dir() && p.join("ProjectSettings").is_dir()
}
fn ensure_project(p: &Path) -> Result<()> {
if !is_project(p) {
anyhow::bail!(
"not a Unity project: {} (missing Assets/ or ProjectSettings/)",
p.display()
);
}
Ok(())
}
pub fn walk_meta_files<F, V>(project_root: &Path, factory: F) -> Result<()>
where
F: Fn() -> V + Sync,
V: FnMut(&Path) + Send + 'static,
{
let assets = project_root.join("Assets");
if !assets.is_dir() {
anyhow::bail!("Assets/ not found at {}", assets.display());
}
let packages = project_root.join("Packages");
let mut builder = WalkBuilder::new(&assets);
if packages.is_dir() {
builder.add(&packages);
}
let walker = builder
.standard_filters(true) .follow_links(false)
.filter_entry(|e| !is_unity_hidden(e.file_name()))
.build_parallel();
let err: Arc<Mutex<Option<anyhow::Error>>> = Arc::new(Mutex::new(None));
walker.run(|| {
let mut visit = factory();
let err = Arc::clone(&err);
Box::new(move |res| {
use ignore::WalkState;
let entry = match res {
Ok(e) => e,
Err(e) => {
*err.lock().unwrap() = Some(anyhow::anyhow!("walk error: {e}"));
return WalkState::Quit;
}
};
if entry.file_type().is_some_and(|t| t.is_file()) {
let path = entry.path();
if path.extension().is_some_and(|e| e == "meta") {
visit(path);
}
}
WalkState::Continue
})
});
if let Some(e) = err.lock().unwrap().take() {
return Err(e);
}
Ok(())
}
fn is_unity_hidden(name: &std::ffi::OsStr) -> bool {
let bytes = name.as_encoded_bytes();
bytes.first() == Some(&b'.') || bytes.last() == Some(&b'~')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_non_project() {
let tmp = std::env::temp_dir().join(format!("unity-assetdb-walk-test-{}", std::process::id()));
std::fs::create_dir_all(&tmp).unwrap();
let result = resolve_project_root(Some(&tmp));
assert!(result.is_err());
std::fs::remove_dir_all(&tmp).ok();
}
}