unity_hub/unity/
installation.rs

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