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
59#[derive(PartialEq, Eq, Debug, Clone)]
60pub struct UnityInstallation {
61 version: Version,
62 path: PathBuf,
63}
64
65impl Installation for UnityInstallation {
66 fn path(&self) -> &PathBuf {
67 &self.path
68 }
69
70 fn version(&self) -> &Version {
71 &self.version
72 }
73}
74
75impl Ord for UnityInstallation {
76 fn cmp(&self, other: &UnityInstallation) -> Ordering {
77 self.version.cmp(&other.version)
78 }
79}
80
81impl PartialOrd for UnityInstallation {
82 fn partial_cmp(&self, other: &UnityInstallation) -> Option<Ordering> {
83 Some(self.cmp(other))
84 }
85}
86
87#[cfg(target_os = "macos")]
88fn adjust_path(path:&Path) -> Option<&Path> {
89 if path.is_file() {
91 if let Some(name) = path.file_name() {
92 if name == "Unity" {
93 path.parent()
94 .and_then(|path| path.parent())
95 .and_then(|path| path.parent())
96 .and_then(|path| path.parent())
97 } else {
98 None
99 }
100 } else {
101 None
102 }
103 } else {
104 None
105 }
106}
107
108#[cfg(target_os = "windows")]
109fn adjust_path(path:&Path) -> Option<&Path> {
110 if path.is_file() {
111 if let Some(name) = path.file_name() {
112 if name == "Unity.exe" {
113 path.parent().and_then(|path| path.parent())
114 } else {
115 None
116 }
117 } else {
118 None
119 }
120 } else {
121 None
122 }
123}
124
125#[cfg(target_os = "linux")]
126fn adjust_path(path:&Path) -> Option<&Path> {
127 if path.is_file() {
128 if let Some(name) = path.file_name() {
129 if name == "Unity" {
130 path.parent().and_then(|path| path.parent())
131 } else {
132 None
133 }
134 } else {
135 None
136 }
137 } else {
138 None
139 }
140}
141
142#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
143fn adjust_path(path:&Path) -> Option<&Path> {
144 None
145}
146
147impl UnityInstallation {
148 pub fn new<P: AsRef<Path>>(path: P) -> Result<UnityInstallation, UnityHubError> {
149 let path = path.as_ref();
150 let path = if let Some(p) = adjust_path(path) {
151 p
152 } else {
153 path
154 };
155
156 trace!("Create UnityInstallation object with path: {}", path.display());
157
158 let version = Version::try_from(path)?;
159 Ok(UnityInstallation {
160 version,
161 path: path.to_path_buf(),
162 })
163 }
164
165 pub fn version(&self) -> &Version {
171 &self.version
172 }
173
174 pub fn into_version(self) -> Version {
175 self.version
176 }
177
178 pub fn version_owned(&self) -> Version {
179 self.version.to_owned()
180 }
181
182 pub fn path(&self) -> &PathBuf {
183 &self.path
184 }
185
186 #[cfg(target_os = "windows")]
187 pub fn location(&self) -> PathBuf {
188 self.path().join("Editor\\Unity.exe")
189 }
190
191 #[cfg(target_os = "macos")]
192 pub fn location(&self) -> PathBuf {
193 self.path().join("Unity.app")
194 }
195
196 #[cfg(target_os = "linux")]
197 pub fn location(&self) -> PathBuf {
198 self.path().join("Editor/Unity")
199 }
200
201 #[cfg(any(target_os = "windows", target_os = "linux"))]
202 pub fn exec_path(&self) -> PathBuf {
203 self.location()
204 }
205
206 #[cfg(target_os = "macos")]
207 pub fn exec_path(&self) -> PathBuf {
208 self.path().join("Unity.app/Contents/MacOS/Unity")
209 }
210}
211
212use std::{fmt, fs};
213use std::fmt::Debug;
214use log::{debug, error, trace};
215use serde::{Deserialize, Serialize};
216use unity_version::Version;
217use crate::error::UnityHubError;
218use crate::unity::error::UnityError;
219use crate::unity::hub::module::Module;
220
221impl fmt::Display for UnityInstallation {
222 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223 write!(f, "{}: {}", self.version, self.path.display())
224 }
225}
226
227pub trait FromInstallation<T:Sized> {
228 fn from_installation(value: T) -> Self;
229}
230
231impl<I> FromInstallation<I> for UnityInstallation
232where
233 I: Installation,
234 I: Debug,
235 I: Sized, {
237 fn from_installation(value: I) -> UnityInstallation {
238 trace!("Create UnityInstallation object from Installation object {:?}", value);
239 UnityInstallation {
240 path: value.path().to_path_buf(),
241 version: value.version().clone(),
242 }
243 }
244}
245
246#[cfg(all(test, target_os = "macos"))]
257mod tests {
258 use super::*;
259 use plist::to_writer_xml;
260 use std::fs;
261 use std::fs::File;
262 use std::path::Path;
263 use std::str::FromStr;
264 use proptest::proptest;
265 use tempfile::Builder;
266
267 fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
268 let path = base_dir.join("Unity");
269 let mut dir_builder = fs::DirBuilder::new();
270 dir_builder.recursive(true);
271 dir_builder.create(&path).unwrap();
272
273 let info_plist_path = path.join("Unity.app/Contents/Info.plist");
274 let exec_path = path.join("Unity.app/Contents/MacOS/Unity");
275 dir_builder
276 .create(info_plist_path.parent().unwrap())
277 .unwrap();
278
279 dir_builder
280 .create(exec_path.parent().unwrap())
281 .unwrap();
282
283 let info = AppInfo {
284 c_f_bundle_version: String::from_str(version).unwrap(),
285 unity_build_number: String::from_str("ssdsdsdd").unwrap(),
286 };
287
288 let file = File::create(info_plist_path).unwrap();
289 File::create(exec_path).unwrap();
290
291 to_writer_xml(&file, &info).unwrap();
292 path
293 }
294
295 macro_rules! prepare_unity_installation {
296 ($version:expr) => {{
297 let test_dir = Builder::new()
298 .prefix("installation")
299 .rand_bytes(5)
300 .tempdir()
301 .unwrap();
302 let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
303 (test_dir, unity_path)
304 }};
305 }
306
307 #[test]
308 fn create_installtion_from_path() {
309 let (_t, path) = prepare_unity_installation!("2017.1.2f5");
310 let subject = UnityInstallation::new(path).unwrap();
311
312 assert_eq!(subject.version.to_string(), "2017.1.2f5");
313 }
314
315 #[test]
316 fn create_installation_from_executable_path() {
317 let(_t, path) = prepare_unity_installation!("2017.1.2f5");
318 let installation = UnityInstallation::new(path).unwrap();
319 let subject = UnityInstallation::new(installation.exec_path()).unwrap();
320
321 assert_eq!(subject.version.to_string(), "2017.1.2f5");
322 }
323
324 proptest! {
325 #[test]
326 fn doesnt_crash(ref s in "\\PC*") {
327 let _ = UnityInstallation::new(Path::new(s).to_path_buf()).is_ok();
328 }
329
330 #[test]
331 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}") {
332 let (_t, path) = prepare_unity_installation!(s);
333 UnityInstallation::new(path).unwrap();
334 }
335 }
336}
337
338#[cfg(all(test, target_os = "linux"))]
339mod linux_tests {
340 use std::fs;
341 use std::fs::{create_dir_all, File};
342 use std::path::PathBuf;
343 use crate::unity::{Installation, UnityInstallation};
344 use crate::unity::hub::module::Module;
345
346 macro_rules! prepare_unity_installation {
347 ($version:expr) => {{
348 let test_dir = tempfile::Builder::new()
349 .prefix("installation")
350 .rand_bytes(5)
351 .tempdir()
352 .unwrap();
353 let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
354 (test_dir, unity_path)
355 }};
356 }
357
358 fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
359 let path = base_dir.join(version);
360 let mut dir_builder = fs::DirBuilder::new();
361 dir_builder.recursive(true);
362 dir_builder.create(&path).unwrap();
363
364 let exec_path = path.join("Editor/Unity");
365 dir_builder
366 .create(exec_path.parent().unwrap())
367 .unwrap();
368 File::create(exec_path).unwrap();
369 path
370 }
371
372 }