unity_hub/unity/
mod.rs

1pub mod hub;
2mod installation;
3pub mod error;
4
5use crate::error::{UnityError, UnityHubError};
6pub use installation::FromInstallation;
7pub use installation::Installation;
8pub use installation::UnityInstallation;
9use itertools::Itertools;
10use log::warn;
11use log::{debug, trace};
12use std::path::Path;
13use std::{fs, io};
14use unity_version::Version;
15use crate::unity::hub::module::Module;
16
17pub struct Installations(Box<dyn Iterator<Item = UnityInstallation>>);
18pub struct Versions(Box<dyn Iterator<Item = Version>>);
19pub struct InstalledModules(Box<dyn Iterator<Item = Module>>);
20
21impl Installations {
22    fn new(install_location: &Path) -> Result<Installations, UnityHubError> {
23        debug!(
24            "fetch unity installations from {}",
25            install_location.display()
26        );
27        
28        // Check if directory exists first - if not, return empty list
29        if !install_location.exists() {
30            warn!("Installation directory doesn't exist, returning empty list: {}", install_location.display());
31            return Ok(Installations::empty());
32        }
33        
34        let read_dir = fs::read_dir(install_location).map_err(|err| UnityHubError::FailedToListInstallations {path: install_location.to_path_buf(), source: err})?;
35
36        let iter = read_dir
37            .filter_map(|dir_entry| dir_entry.ok())
38            .map(|entry| entry.path())
39            .map(UnityInstallation::new)
40            .filter_map(Result::ok);
41        Ok(Installations(Box::new(iter)))
42    }
43
44    fn empty() -> Installations {
45        Installations(Box::new(::std::iter::empty()))
46    }
47
48    pub fn versions(self) -> Versions {
49        self.into()
50    }
51}
52
53impl From<hub::editors::Editors> for Installations {
54    fn from(editors: hub::editors::Editors) -> Self {
55        let iter = editors
56            .into_iter()
57            .map(UnityInstallation::from_installation);
58        Installations(Box::new(iter))
59    }
60}
61
62impl TryFrom<UnityInstallation> for InstalledModules {
63    type Error = UnityError;
64
65    fn try_from(value: UnityInstallation) -> Result<Self, Self::Error> {
66        let modules = value.get_modules()?;
67        let installed_modules_iter = modules.into_iter().filter(|m| m.is_installed);
68        Ok(InstalledModules(Box::new(installed_modules_iter)))
69    }
70}
71
72impl Iterator for InstalledModules {
73    type Item = Module;
74    fn next(&mut self) -> Option<Self::Item> { self.0.next() }
75}
76
77impl Iterator for Installations {
78    type Item = UnityInstallation;
79
80    fn next(&mut self) -> Option<Self::Item> {
81        self.0.next()
82    }
83}
84
85impl Iterator for Versions {
86    type Item = Version;
87
88    fn next(&mut self) -> Option<Self::Item> {
89        self.0.next()
90    }
91}
92
93impl From<Installations> for Versions {
94    fn from(installations: Installations) -> Self {
95        let iter = installations.map(|i| i.version_owned());
96        Versions(Box::new(iter))
97    }
98}
99
100impl FromIterator<UnityInstallation> for Installations {
101    fn from_iter<I: IntoIterator<Item = UnityInstallation>>(iter: I) -> Self {
102        let c: Vec<UnityInstallation> = iter.into_iter().collect();
103        Installations(Box::new(c.into_iter()))
104    }
105}
106
107pub fn list_all_installations() -> Result<Installations, UnityHubError> {
108    let i1 = list_installations()?.map(|i| {
109        trace!("[list_all_installations] found local installation: {:?}", i);
110        i   
111    });
112    let i2 = hub::list_installations()?.map(|i| {
113        trace!("[list_all_installations] found hub installation: {:?}", i);
114        i
115    });
116    let iter = i1.chain(i2);
117    let unique = iter.unique_by(|installation| installation.version().to_owned()).map(|i| {
118        trace!("[list_all_installations] found installation: {:?}", i);
119        i
120    });
121    Ok(Installations(Box::new(unique)))
122}
123
124pub fn list_installations() -> Result<Installations, UnityHubError> {
125    #[cfg(any(target_os = "windows", target_os = "macos"))]
126    let application_path = dirs_2::application_dir();
127
128    #[cfg(target_os = "linux")]
129    let application_path = dirs_2::executable_dir();
130
131    application_path
132        .ok_or_else(|| {
133            let io_err = io::Error::new(io::ErrorKind::NotFound, "unable to locate application_dir");
134            UnityHubError::ApplicationDirectoryNotFound { source: io_err }
135        })
136        .and_then(|application_dir| {
137            list_installations_in_dir(&application_dir)
138        })
139}
140
141pub fn list_hub_installations() -> Result<Installations, UnityHubError> {
142    hub::list_installations()
143}
144
145pub fn list_installations_in_dir(install_location: &Path) -> Result<Installations, UnityHubError> {
146    Installations::new(install_location)
147}
148
149pub fn find_installation(version: &Version) -> Result<UnityInstallation, UnityHubError> {
150    list_all_installations().and_then(|mut installations| {
151        installations
152            .find(|installation| installation.version() == version)
153            .ok_or_else(|| {
154                let io_err = io::Error::new(
155                    io::ErrorKind::NotFound,
156                    format!("unable to locate installation with version {}", version),
157                );
158                UnityHubError::InstallationNotFound { 
159                    version: version.to_string(),
160                    source: io_err 
161                }
162            })
163    })
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::unity::installation::AppInfo;
170    use plist::to_writer_xml;
171    use std::fs;
172    use std::fs::File;
173    use std::path::Path;
174    use std::path::PathBuf;
175    use std::str::FromStr;
176    use proptest::proptest;
177    use tempfile::Builder;
178
179    fn create_test_path(base_dir: &PathBuf, version: &str) -> PathBuf {
180        let path = base_dir.join(format!("Unity-{version}", version = version));
181        let mut dir_builder = fs::DirBuilder::new();
182        dir_builder.recursive(true);
183        dir_builder.create(&path).unwrap();
184
185        let info_plist_path = path.join(Path::new("Unity.app/Contents/Info.plist"));
186        dir_builder
187            .create(info_plist_path.parent().unwrap())
188            .unwrap();
189
190        let info = AppInfo {
191            c_f_bundle_version: String::from_str(version).unwrap(),
192            unity_build_number: String::from_str("ssdsdsdd").unwrap(),
193        };
194        let file = File::create(info_plist_path).unwrap();
195        to_writer_xml(&file, &info).unwrap();
196        path
197    }
198
199    macro_rules! prepare_unity_installations {
200        ($($input:expr),*) => {
201            {
202                let test_dir = Builder::new()
203                                .prefix("list_installations")
204                                .rand_bytes(5)
205                                .suffix("_in_directory")
206                                .tempdir()
207                                .unwrap();
208                {
209                    $(
210                        create_test_path(&test_dir.path().to_path_buf(), $input);
211                    )*
212                }
213                test_dir
214            }
215        };
216    }
217
218    #[test]
219    fn list_installations_in_directory_filters_unity_installations() {
220        let test_dir = prepare_unity_installations!["2017.1.2f3", "2017.2.3f4"];
221
222        let mut builder = Builder::new();
223        builder.prefix("some-dir");
224        builder.rand_bytes(5);
225
226        let _temp_dir1 = builder.tempdir_in(&test_dir).unwrap();
227        let _temp_dir2 = builder.tempdir_in(&test_dir).unwrap();
228        let _temp_dir3 = builder.tempdir_in(&test_dir).unwrap();
229
230        let subject = Installations::new(test_dir.path()).unwrap();
231
232        let r1 = Version::from_str("2017.1.2f3").unwrap();
233        let r2 = Version::from_str("2017.2.3f4").unwrap();
234
235        assert_eq!(fs::read_dir(&test_dir).unwrap().count(), 5);
236
237        for installation in subject {
238            let version = installation.version_owned();
239            assert!(version == r1 || version == r2);
240        }
241    }
242
243    #[test]
244    fn list_installations_in_empty_directory_returns_no_error() {
245        let test_dir = prepare_unity_installations![];
246        assert!(Installations::new(test_dir.path()).is_ok());
247    }
248
249    #[test]
250    fn list_installations_returns_empty_iterator_when_dir_does_not_exist() {
251        assert_eq!(list_installations().unwrap().count(), 0);
252    }
253
254    #[test]
255    fn installations_can_be_converted_to_versions() {
256        let test_dir = prepare_unity_installations!["2017.1.2f3", "2017.2.3f4"];
257
258        let installations = Installations::new(test_dir.path()).unwrap();
259        let subject = installations.versions();
260
261        let r1 = Version::from_str("2017.1.2f3").unwrap();
262        let r2 = Version::from_str("2017.2.3f4").unwrap();
263        for version in subject {
264            assert!(version == r1 || version == r2);
265        }
266    }
267
268    #[test]
269    fn versions_can_be_created_from_installations() {
270        let test_dir = prepare_unity_installations!["2017.1.2f3", "2017.2.3f4"];
271
272        let installations = Installations::new(test_dir.path()).unwrap();
273        let subject = Versions::from(installations);
274
275        let r1 = Version::from_str("2017.1.2f3").unwrap();
276        let r2 = Version::from_str("2017.2.3f4").unwrap();
277        for version in subject {
278            assert!(version == r1 || version == r2);
279        }
280    }
281
282    proptest! {
283        #[test]
284        fn doesnt_crash(ref s in "\\PC*") {
285            let _ = Installations::new(Path::new(s));
286        }
287
288        #[test]
289        #[cfg(target_os="macos")]
290        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}") {
291            let test_dir = prepare_unity_installations![
292                s
293            ];
294            let subject = Installations::new(test_dir.path()).unwrap();
295            assert_eq!(subject.count(), 1);
296        }
297    }
298}