dev_prune/adapters/
unreal.rs1use super::{BloatDir, EnforcePolicy, PackageManager, dir_size};
27use anyhow::{Result, anyhow};
28use std::fs;
29use std::path::{Path, PathBuf};
30
31const CACHE_DIRS: &[&str] = &["DerivedDataCache", "Intermediate"];
33
34fn uproject_in(path: &Path) -> Option<PathBuf> {
37 fs::read_dir(path)
38 .ok()?
39 .flatten()
40 .map(|entry| entry.path())
41 .find(|p| {
42 p.is_file()
43 && p.extension()
44 .is_some_and(|ext| ext.eq_ignore_ascii_case("uproject"))
45 })
46}
47
48pub struct Unreal;
50
51impl PackageManager for Unreal {
52 fn name(&self) -> &'static str {
53 "unreal"
54 }
55
56 fn detect(&self, path: &Path) -> bool {
57 uproject_in(path).is_some()
58 }
59
60 fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
61 CACHE_DIRS
62 .iter()
63 .map(|name| path.join(name))
64 .filter(|dir| dir.is_dir())
65 .map(|dir| BloatDir {
66 name: dir
67 .file_name()
68 .unwrap_or_default()
69 .to_string_lossy()
70 .into_owned(),
71 size_bytes: dir_size(&dir),
72 path: dir,
73 shared_bytes: 0,
74 })
75 .collect()
76 }
77
78 fn enforce_lockfile(&self, path: &Path, _policy: EnforcePolicy) -> Result<()> {
84 let manifest = uproject_in(path).ok_or_else(|| {
85 anyhow!("no `.uproject` file here: nothing to rebuild the derived data from.")
86 })?;
87 let content = fs::read_to_string(&manifest).map_err(|e| {
88 anyhow!(
89 "`{}` could not be read ({e}): nothing to rebuild the derived data from.",
90 manifest.display()
91 )
92 })?;
93 if !content.contains("\"FileVersion\"") {
94 return Err(anyhow!(
95 "`{}` has no `\"FileVersion\"` key: refusing to treat the derived data \
96 as regenerable from it.",
97 manifest.display()
98 ));
99 }
100 Ok(())
101 }
102
103 fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
104 println!(
105 "Unreal rebuilds DerivedDataCache/ and Intermediate/ on the next editor \
106 open and build; for a C++ project, regenerate project files first \
107 (right-click the .uproject, Generate Project Files) because the generated \
108 solution lived in Intermediate/"
109 );
110 Ok(())
111 }
112
113 fn lockfiles(&self) -> &'static [&'static str] {
116 &[]
117 }
118
119 fn opt_in(&self) -> bool {
120 true
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use std::path::PathBuf;
128 use tempfile::tempdir;
129
130 fn project(dir: &Path) -> PathBuf {
132 fs::write(
133 dir.join("Demo.uproject"),
134 "{\n\t\"FileVersion\": 3,\n\t\"EngineAssociation\": \"5.3\",\n\t\"Category\": \"\"\n}\n",
135 )
136 .unwrap();
137 dir.to_path_buf()
138 }
139
140 fn claimed(project: &Path) -> Vec<String> {
141 Unreal
142 .bloat_dirs(project)
143 .into_iter()
144 .map(|b| b.name)
145 .collect()
146 }
147
148 #[test]
149 fn detects_on_any_uproject_file() {
150 let dir = tempdir().unwrap();
151 assert!(!Unreal.detect(dir.path()));
152 project(dir.path());
153 assert!(Unreal.detect(dir.path()));
154 }
155
156 #[test]
157 fn a_uproject_directory_is_not_a_manifest() {
158 let dir = tempdir().unwrap();
161 fs::create_dir(dir.path().join("Demo.uproject")).unwrap();
162 assert!(!Unreal.detect(dir.path()));
163 }
164
165 #[test]
166 fn only_the_derived_directories_are_claimed() {
167 let dir = tempdir().unwrap();
171 let root = project(dir.path());
172 fs::create_dir(root.join("DerivedDataCache")).unwrap();
173 fs::write(root.join("DerivedDataCache").join("DDC.udd"), "cache").unwrap();
174 fs::create_dir(root.join("Intermediate")).unwrap();
175 fs::create_dir(root.join("Saved")).unwrap();
176 fs::create_dir(root.join("Binaries")).unwrap();
177 fs::create_dir(root.join("Content")).unwrap();
178
179 assert_eq!(claimed(&root), vec!["DerivedDataCache", "Intermediate"]);
180 }
181
182 #[test]
183 fn a_missing_or_bogus_manifest_is_refused() {
184 let dir = tempdir().unwrap();
185 let policy = EnforcePolicy::default();
186 assert!(Unreal.enforce_lockfile(dir.path(), policy).is_err());
187 fs::write(dir.path().join("Demo.uproject"), "hello there").unwrap();
188 assert!(Unreal.enforce_lockfile(dir.path(), policy).is_err());
189 project(dir.path());
190 assert!(Unreal.enforce_lockfile(dir.path(), policy).is_ok());
191 }
192
193 #[test]
194 fn unreal_is_opt_in() {
195 assert!(Unreal.opt_in());
196 }
197}