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    #[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 the path points to a file it could be the executable
104    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    // //TODO remove clone()
180    // pub fn installed_modules(&self) -> InstalledComponents {
181    //     InstalledComponents::new(self.clone())
182    // }
183
184    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, // Ensures this does not apply to trait objects
250{
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// impl<I> From<I> for UnityInstallation
261// where I: Installation {
262//     fn from(value: I) -> Self {
263//         UnityInstallation {
264//             version: value.version().to_owned(),
265//             path: value.location().to_path_buf(),
266//         }
267//     }
268// }
269
270#[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    // #[test]
387    // fn installation_recognizes_installed_webgl_module() {
388    //     let(_t, path) = prepare_unity_installation!("2021.3.35f1");
389    //     //Create WegGL module directory, so that the installation thinks its installed
390    //     create_dir_all(path.join("Editor/Data/PlaybackEngines/WebGLSupport")).unwrap();
391    //     let installation = UnityInstallation::new(path).unwrap();
392    //     let mut components = installation.installed_modules().unwrap().into_iter();
393    //     let has_webgl_component = components.any(|c| c.id() == "webgl");
394    //
395    //     assert_eq!(has_webgl_component, true);
396    // }
397    //
398    // #[test]
399    // fn installation_recognizes_non_installed_webgl_module() {
400    //     let(_t, path) = prepare_unity_installation!("2021.3.35f1");
401    //     //Create WegGL module directory, so that the installation thinks its installed
402    //     let installation = UnityInstallation::new(path).unwrap();
403    //     let mut components = installation.installed_modules().unwrap().into_iter();
404    //     let has_webgl_component = components.any(|c| c.id() == "webgl");
405    //
406    //     assert_eq!(has_webgl_component, false);
407    // }
408}