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, 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)?;
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()?;
101    let i2 = hub::list_installations()?;
102    let iter = i1.chain(i2);
103    let unique = iter.unique_by(|installation| installation.version().to_owned());
104    Ok(Installations(Box::new(unique)))
105}
106
107pub fn list_installations() -> Result<Installations, UnityHubError> {
108    #[cfg(any(target_os = "windows", target_os = "macos"))]
109    let application_path = dirs_2::application_dir();
110
111    #[cfg(target_os = "linux")]
112    let application_path = dirs_2::executable_dir();
113
114    application_path
115        .ok_or_else(|| {
116            io::Error::new(io::ErrorKind::NotFound, "unable to locate application_dir").into()
117        })
118        .and_then(|application_dir| {
119            list_installations_in_dir(&application_dir).or_else(|err| {
120                match err {
121                    UnityHubError::IoError(ref io_error) => {
122                        io_error.raw_os_error().and_then(|os_error| match os_error {
123                            2 => {
124                                warn!("{}", io_error);
125                                Some(Installations::empty())
126                            }
127                            _ => None,
128                        })
129                    }
130                    _ => None,
131                }
132                .ok_or_else(|| err)
133            })
134        })
135}
136
137pub fn list_hub_installations() -> Result<Installations, UnityHubError> {
138    hub::list_installations()
139}
140
141pub fn list_installations_in_dir(install_location: &Path) -> Result<Installations, UnityHubError> {
142    Installations::new(install_location)
143}
144
145pub fn find_installation(version: &Version) -> Result<UnityInstallation, UnityHubError> {
146    list_all_installations().and_then(|mut installations| {
147        installations
148            .find(|installation| installation.version() == version)
149            .ok_or_else(|| {
150                io::Error::new(
151                    io::ErrorKind::NotFound,
152                    format!("unable to locate installation with version {}", version),
153                )
154                .into()
155            })
156    })
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::unity::installation::AppInfo;
163    use plist::to_writer_xml;
164    use std::fs;
165    use std::fs::File;
166    use std::path::Path;
167    use std::path::PathBuf;
168    use std::str::FromStr;
169    use proptest::proptest;
170    use tempfile::Builder;
171
172    fn create_test_path(base_dir: &PathBuf, version: &str) -> PathBuf {
173        let path = base_dir.join(format!("Unity-{version}", version = version));
174        let mut dir_builder = fs::DirBuilder::new();
175        dir_builder.recursive(true);
176        dir_builder.create(&path).unwrap();
177
178        let info_plist_path = path.join(Path::new("Unity.app/Contents/Info.plist"));
179        dir_builder
180            .create(info_plist_path.parent().unwrap())
181            .unwrap();
182
183        let info = AppInfo {
184            c_f_bundle_version: String::from_str(version).unwrap(),
185            unity_build_number: String::from_str("ssdsdsdd").unwrap(),
186        };
187        let file = File::create(info_plist_path).unwrap();
188        to_writer_xml(&file, &info).unwrap();
189        path
190    }
191
192    macro_rules! prepare_unity_installations {
193        ($($input:expr),*) => {
194            {
195                let test_dir = Builder::new()
196                                .prefix("list_installations")
197                                .rand_bytes(5)
198                                .suffix("_in_directory")
199                                .tempdir()
200                                .unwrap();
201                {
202                    $(
203                        create_test_path(&test_dir.path().to_path_buf(), $input);
204                    )*
205                }
206                test_dir
207            }
208        };
209    }
210
211    #[test]
212    fn list_installations_in_directory_filters_unity_installations() {
213        let test_dir = prepare_unity_installations!["2017.1.2f3", "2017.2.3f4"];
214
215        let mut builder = Builder::new();
216        builder.prefix("some-dir");
217        builder.rand_bytes(5);
218
219        let _temp_dir1 = builder.tempdir_in(&test_dir).unwrap();
220        let _temp_dir2 = builder.tempdir_in(&test_dir).unwrap();
221        let _temp_dir3 = builder.tempdir_in(&test_dir).unwrap();
222
223        let subject = Installations::new(test_dir.path()).unwrap();
224
225        let r1 = Version::from_str("2017.1.2f3").unwrap();
226        let r2 = Version::from_str("2017.2.3f4").unwrap();
227
228        assert_eq!(fs::read_dir(&test_dir).unwrap().count(), 5);
229
230        for installation in subject {
231            let version = installation.version_owned();
232            assert!(version == r1 || version == r2);
233        }
234    }
235
236    #[test]
237    fn list_installations_in_empty_directory_returns_no_error() {
238        let test_dir = prepare_unity_installations![];
239        assert!(Installations::new(test_dir.path()).is_ok());
240    }
241
242    // #[test]
243    // fn list_installations_returns_empty_iterator_when_dir_does_not_exist() {
244    //     assert_eq!(list_installations().unwrap().count(), 0);
245    // }
246
247    #[test]
248    fn installations_can_be_converted_to_versions() {
249        let test_dir = prepare_unity_installations!["2017.1.2f3", "2017.2.3f4"];
250
251        let installations = Installations::new(test_dir.path()).unwrap();
252        let subject = installations.versions();
253
254        let r1 = Version::from_str("2017.1.2f3").unwrap();
255        let r2 = Version::from_str("2017.2.3f4").unwrap();
256        for version in subject {
257            assert!(version == r1 || version == r2);
258        }
259    }
260
261    #[test]
262    fn versions_can_be_created_from_installations() {
263        let test_dir = prepare_unity_installations!["2017.1.2f3", "2017.2.3f4"];
264
265        let installations = Installations::new(test_dir.path()).unwrap();
266        let subject = Versions::from(installations);
267
268        let r1 = Version::from_str("2017.1.2f3").unwrap();
269        let r2 = Version::from_str("2017.2.3f4").unwrap();
270        for version in subject {
271            assert!(version == r1 || version == r2);
272        }
273    }
274
275    proptest! {
276        #[test]
277        fn doesnt_crash(ref s in "\\PC*") {
278            let _ = Installations::new(Path::new(s));
279        }
280
281        #[test]
282        #[cfg(targed_os="macos")]
283        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}") {
284            let test_dir = prepare_unity_installations![
285                s
286            ];
287            let mut subject = Installations::new(test_dir.path()).unwrap();
288            assert_eq!(subject.count(), 1);
289        }
290    }
291}