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).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 the path points to a file it could be the executable
90    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    // //TODO remove clone()
166    // pub fn installed_modules(&self) -> InstalledComponents {
167    //     InstalledComponents::new(self.clone())
168    // }
169
170    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, // Ensures this does not apply to trait objects
236{
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// impl<I> From<I> for UnityInstallation
247// where I: Installation {
248//     fn from(value: I) -> Self {
249//         UnityInstallation {
250//             version: value.version().to_owned(),
251//             path: value.location().to_path_buf(),
252//         }
253//     }
254// }
255
256#[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    // #[test]
373    // fn installation_recognizes_installed_webgl_module() {
374    //     let(_t, path) = prepare_unity_installation!("2021.3.35f1");
375    //     //Create WegGL module directory, so that the installation thinks its installed
376    //     create_dir_all(path.join("Editor/Data/PlaybackEngines/WebGLSupport")).unwrap();
377    //     let installation = UnityInstallation::new(path).unwrap();
378    //     let mut components = installation.installed_modules().unwrap().into_iter();
379    //     let has_webgl_component = components.any(|c| c.id() == "webgl");
380    //
381    //     assert_eq!(has_webgl_component, true);
382    // }
383    //
384    // #[test]
385    // fn installation_recognizes_non_installed_webgl_module() {
386    //     let(_t, path) = prepare_unity_installation!("2021.3.35f1");
387    //     //Create WegGL module directory, so that the installation thinks its installed
388    //     let installation = UnityInstallation::new(path).unwrap();
389    //     let mut components = installation.installed_modules().unwrap().into_iter();
390    //     let has_webgl_component = components.any(|c| c.id() == "webgl");
391    //
392    //     assert_eq!(has_webgl_component, false);
393    // }
394}