unity_hub/unity/
installation.rs1use std::cmp::Ordering;
2use std::path::{Path, PathBuf};
3use std::convert::TryFrom;
4
5#[derive(Deserialize, Serialize)]
6#[serde(rename_all = "PascalCase")]
7pub struct AppInfo {
8 pub c_f_bundle_version: String,
9 pub unity_build_number: String,
10}
11
12pub trait Installation: Eq + Ord {
13 fn path(&self) -> &PathBuf;
14
15 fn version(&self) -> &Version;
16
17 #[cfg(target_os = "windows")]
18 fn location(&self) -> PathBuf {
19 self.path().join("Editor\\Unity.exe")
20 }
21
22 #[cfg(target_os = "macos")]
23 fn location(&self) -> PathBuf {
24 self.path().join("Unity.app")
25 }
26
27 #[cfg(target_os = "linux")]
28 fn location(&self) -> PathBuf {
29 self.path().join("Editor/Unity")
30 }
31
32 #[cfg(any(target_os = "windows", target_os = "linux"))]
33 fn exec_path(&self) -> PathBuf {
34 self.location()
35 }
36
37 #[cfg(target_os = "macos")]
38 fn exec_path(&self) -> PathBuf {
39 self.path().join("Unity.app/Contents/MacOS/Unity")
40 }
41
42 fn installed_modules(&self) -> Result<impl IntoIterator<Item = Module>, UnityError> {
43 let modules = self.get_modules()?;
44 let installed_modules = modules.into_iter().filter(|m| m.is_installed);
45 Ok(installed_modules)
46 }
47
48 fn get_modules(&self) -> Result<Vec<Module>, UnityError> {
49 let modules_json_path = self.path().join("modules.json");
50 let file_content = fs::read_to_string(&modules_json_path).map_err(|e| {
51 error!("Failed to read modules.json file from {}: {}", &modules_json_path.display(), e);
52 e
53 })?;
54 let modules: Vec<Module> = serde_json::from_str(&file_content)?;
55 Ok(modules)
56 }
57
58 #[cfg(feature = "mutate")]
59 fn write_modules(&self, modules: Vec<Module>) -> Result<(), UnityError> {
60 use log::info;
61
62 let modules_json_path = self.path().join("modules.json");
63 info!("Writing modules.json to {}", modules_json_path.display());
64
65 let json_content = serde_json::to_string_pretty(&modules)?;
66
67 std::fs::write(&modules_json_path, json_content)?;
68
69 Ok(())
70 }
71}
72
73#[derive(PartialEq, Eq, Debug, Clone)]
74pub struct UnityInstallation {
75 version: Version,
76 path: PathBuf,
77}
78
79impl Installation for UnityInstallation {
80 fn path(&self) -> &PathBuf {
81 &self.path
82 }
83
84 fn version(&self) -> &Version {
85 &self.version
86 }
87}
88
89impl Ord for UnityInstallation {
90 fn cmp(&self, other: &UnityInstallation) -> Ordering {
91 self.version.cmp(&other.version)
92 }
93}
94
95impl PartialOrd for UnityInstallation {
96 fn partial_cmp(&self, other: &UnityInstallation) -> Option<Ordering> {
97 Some(self.cmp(other))
98 }
99}
100
101#[cfg(target_os = "macos")]
102fn adjust_path(path:&Path) -> Option<&Path> {
103 if path.is_file() {
105 if let Some(name) = path.file_name() {
106 if name == "Unity" {
107 path.parent()
108 .and_then(|path| path.parent())
109 .and_then(|path| path.parent())
110 .and_then(|path| path.parent())
111 } else {
112 None
113 }
114 } else {
115 None
116 }
117 } else {
118 None
119 }
120}
121
122#[cfg(target_os = "windows")]
123fn adjust_path(path:&Path) -> Option<&Path> {
124 if path.is_file() {
125 if let Some(name) = path.file_name() {
126 if name == "Unity.exe" {
127 path.parent().and_then(|path| path.parent())
128 } else {
129 None
130 }
131 } else {
132 None
133 }
134 } else {
135 None
136 }
137}
138
139#[cfg(target_os = "linux")]
140fn adjust_path(path:&Path) -> Option<&Path> {
141 if path.is_file() {
142 if let Some(name) = path.file_name() {
143 if name == "Unity" {
144 path.parent().and_then(|path| path.parent())
145 } else {
146 None
147 }
148 } else {
149 None
150 }
151 } else {
152 None
153 }
154}
155
156#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
157fn adjust_path(path:&Path) -> Option<&Path> {
158 None
159}
160
161impl UnityInstallation {
162 pub fn new<P: AsRef<Path>>(path: P) -> Result<UnityInstallation, UnityHubError> {
163 let path = path.as_ref();
164 let path = if let Some(p) = adjust_path(path) {
165 p
166 } else {
167 path
168 };
169
170 trace!("Create UnityInstallation object with path: {}", path.display());
171
172 let version = Version::try_from(path)?;
173 Ok(UnityInstallation {
174 version,
175 path: path.to_path_buf(),
176 })
177 }
178
179 pub fn version(&self) -> &Version {
185 &self.version
186 }
187
188 pub fn into_version(self) -> Version {
189 self.version
190 }
191
192 pub fn version_owned(&self) -> Version {
193 self.version.to_owned()
194 }
195
196 pub fn path(&self) -> &PathBuf {
197 &self.path
198 }
199
200 #[cfg(target_os = "windows")]
201 pub fn location(&self) -> PathBuf {
202 self.path().join("Editor\\Unity.exe")
203 }
204
205 #[cfg(target_os = "macos")]
206 pub fn location(&self) -> PathBuf {
207 self.path().join("Unity.app")
208 }
209
210 #[cfg(target_os = "linux")]
211 pub fn location(&self) -> PathBuf {
212 self.path().join("Editor/Unity")
213 }
214
215 #[cfg(any(target_os = "windows", target_os = "linux"))]
216 pub fn exec_path(&self) -> PathBuf {
217 self.location()
218 }
219
220 #[cfg(target_os = "macos")]
221 pub fn exec_path(&self) -> PathBuf {
222 self.path().join("Unity.app/Contents/MacOS/Unity")
223 }
224}
225
226use std::{fmt, fs};
227use std::fmt::Debug;
228use log::{error, trace};
229use serde::{Deserialize, Serialize};
230use unity_version::Version;
231use crate::error::UnityHubError;
232use crate::unity::error::UnityError;
233use crate::unity::hub::module::Module;
234
235impl fmt::Display for UnityInstallation {
236 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237 write!(f, "{}: {}", self.version, self.path.display())
238 }
239}
240
241pub trait FromInstallation<T:Sized> {
242 fn from_installation(value: T) -> Self;
243}
244
245impl<I> FromInstallation<I> for UnityInstallation
246where
247 I: Installation,
248 I: Debug,
249 I: Sized, {
251 fn from_installation(value: I) -> UnityInstallation {
252 trace!("Create UnityInstallation object from Installation object {:?}", value);
253 UnityInstallation {
254 path: value.path().to_path_buf(),
255 version: value.version().clone(),
256 }
257 }
258}
259
260#[cfg(all(test, target_os = "macos"))]
271mod tests {
272 use super::*;
273 use plist::to_writer_xml;
274 use std::fs;
275 use std::fs::File;
276 use std::path::Path;
277 use std::str::FromStr;
278 use proptest::proptest;
279 use tempfile::Builder;
280
281 fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
282 let path = base_dir.join("Unity");
283 let mut dir_builder = fs::DirBuilder::new();
284 dir_builder.recursive(true);
285 dir_builder.create(&path).unwrap();
286
287 let info_plist_path = path.join("Unity.app/Contents/Info.plist");
288 let exec_path = path.join("Unity.app/Contents/MacOS/Unity");
289 dir_builder
290 .create(info_plist_path.parent().unwrap())
291 .unwrap();
292
293 dir_builder
294 .create(exec_path.parent().unwrap())
295 .unwrap();
296
297 let info = AppInfo {
298 c_f_bundle_version: String::from_str(version).unwrap(),
299 unity_build_number: String::from_str("ssdsdsdd").unwrap(),
300 };
301
302 let file = File::create(info_plist_path).unwrap();
303 File::create(exec_path).unwrap();
304
305 to_writer_xml(&file, &info).unwrap();
306 path
307 }
308
309 macro_rules! prepare_unity_installation {
310 ($version:expr) => {{
311 let test_dir = Builder::new()
312 .prefix("installation")
313 .rand_bytes(5)
314 .tempdir()
315 .unwrap();
316 let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
317 (test_dir, unity_path)
318 }};
319 }
320
321 #[test]
322 fn create_installtion_from_path() {
323 let (_t, path) = prepare_unity_installation!("2017.1.2f5");
324 let subject = UnityInstallation::new(path).unwrap();
325
326 assert_eq!(subject.version.to_string(), "2017.1.2f5");
327 }
328
329 #[test]
330 fn create_installation_from_executable_path() {
331 let(_t, path) = prepare_unity_installation!("2017.1.2f5");
332 let installation = UnityInstallation::new(path).unwrap();
333 let subject = UnityInstallation::new(installation.exec_path()).unwrap();
334
335 assert_eq!(subject.version.to_string(), "2017.1.2f5");
336 }
337
338 proptest! {
339 #[test]
340 fn doesnt_crash(ref s in "\\PC*") {
341 let _ = UnityInstallation::new(Path::new(s).to_path_buf()).is_ok();
342 }
343
344 #[test]
345 fn parses_all_valid_versions(ref s in r"[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,4}[fpb][0-9]{1,4}") {
346 let (_t, path) = prepare_unity_installation!(s);
347 UnityInstallation::new(path).unwrap();
348 }
349 }
350}
351
352#[cfg(all(test, target_os = "linux"))]
353mod linux_tests {
354 use std::fs;
355 use std::fs::{create_dir_all, File};
356 use std::path::PathBuf;
357 use crate::unity::{Installation, UnityInstallation};
358 use crate::unity::hub::module::Module;
359
360 macro_rules! prepare_unity_installation {
361 ($version:expr) => {{
362 let test_dir = tempfile::Builder::new()
363 .prefix("installation")
364 .rand_bytes(5)
365 .tempdir()
366 .unwrap();
367 let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
368 (test_dir, unity_path)
369 }};
370 }
371
372 fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
373 let path = base_dir.join(version);
374 let mut dir_builder = fs::DirBuilder::new();
375 dir_builder.recursive(true);
376 dir_builder.create(&path).unwrap();
377
378 let exec_path = path.join("Editor/Unity");
379 dir_builder
380 .create(exec_path.parent().unwrap())
381 .unwrap();
382 File::create(exec_path).unwrap();
383 path
384 }
385
386 }