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