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