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 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}