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