use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
use anyhow::{Result, anyhow};
use std::fs;
use std::path::{Path, PathBuf};
const CACHE_DIRS: &[&str] = &["DerivedDataCache", "Intermediate"];
fn uproject_in(path: &Path) -> Option<PathBuf> {
fs::read_dir(path)
.ok()?
.flatten()
.map(|entry| entry.path())
.find(|p| {
p.is_file()
&& p.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("uproject"))
})
}
pub struct Unreal;
impl PackageManager for Unreal {
fn name(&self) -> &'static str {
"unreal"
}
fn detect(&self, path: &Path) -> bool {
uproject_in(path).is_some()
}
fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
CACHE_DIRS
.iter()
.map(|name| path.join(name))
.filter(|dir| dir.is_dir())
.map(|dir| BloatDir {
name: dir
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned(),
size_bytes: dir_size(&dir),
path: dir,
shared_bytes: 0,
})
.collect()
}
fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
let manifest = uproject_in(path).ok_or_else(|| {
anyhow!("no `.uproject` file here: nothing to rebuild the derived data from.")
})?;
let content = fs::read_to_string(&manifest).map_err(|e| {
anyhow!(
"`{}` could not be read ({e}): nothing to rebuild the derived data from.",
manifest.display()
)
})?;
if !content.contains("\"FileVersion\"") {
return Err(anyhow!(
"`{}` has no `\"FileVersion\"` key: refusing to treat the derived data \
as regenerable from it.",
manifest.display()
));
}
Ok(())
}
fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
println!(
"Unreal rebuilds DerivedDataCache/ and Intermediate/ on the next editor \
open and build; for a C++ project, regenerate project files first \
(right-click the .uproject, Generate Project Files) because the generated \
solution lived in Intermediate/"
);
Ok(())
}
fn lockfiles(&self) -> &'static [&'static str] {
&[]
}
fn opt_in(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
fn project(dir: &Path) -> PathBuf {
fs::write(
dir.join("Demo.uproject"),
"{\n\t\"FileVersion\": 3,\n\t\"EngineAssociation\": \"5.3\",\n\t\"Category\": \"\"\n}\n",
)
.unwrap();
dir.to_path_buf()
}
fn claimed(project: &Path) -> Vec<String> {
Unreal
.bloat_dirs(project)
.into_iter()
.map(|b| b.name)
.collect()
}
#[test]
fn detects_on_any_uproject_file() {
let dir = tempdir().unwrap();
assert!(!Unreal.detect(dir.path()));
project(dir.path());
assert!(Unreal.detect(dir.path()));
}
#[test]
fn a_uproject_directory_is_not_a_manifest() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("Demo.uproject")).unwrap();
assert!(!Unreal.detect(dir.path()));
}
#[test]
fn only_the_derived_directories_are_claimed() {
let dir = tempdir().unwrap();
let root = project(dir.path());
fs::create_dir(root.join("DerivedDataCache")).unwrap();
fs::write(root.join("DerivedDataCache").join("DDC.udd"), "cache").unwrap();
fs::create_dir(root.join("Intermediate")).unwrap();
fs::create_dir(root.join("Saved")).unwrap();
fs::create_dir(root.join("Binaries")).unwrap();
fs::create_dir(root.join("Content")).unwrap();
assert_eq!(claimed(&root), vec!["DerivedDataCache", "Intermediate"]);
}
#[test]
fn a_missing_or_bogus_manifest_is_refused() {
let dir = tempdir().unwrap();
let policy = EnforcePolicy::default();
assert!(Unreal.enforce_lockfile(dir.path(), policy).is_err());
fs::write(dir.path().join("Demo.uproject"), "hello there").unwrap();
assert!(Unreal.enforce_lockfile(dir.path(), policy).is_err());
project(dir.path());
assert!(Unreal.enforce_lockfile(dir.path(), policy).is_ok());
}
#[test]
fn unreal_is_opt_in() {
assert!(Unreal.opt_in());
}
}