unity_hub/unity/
installation.rs1use std::cmp::Ordering;
2use std::path::{Path, PathBuf};
3use std::convert::TryFrom;
4
5#[derive(Deserialize, Serialize)]
6#[serde(rename_all = "PascalCase")]
7pub struct AppInfo {
8 pub c_f_bundle_version: String,
9 pub unity_build_number: String,
10}
11
12pub trait Installation: Eq + Ord {
13 fn path(&self) -> &PathBuf;
14
15 fn version(&self) -> &Version;
16
17 #[cfg(target_os = "windows")]
18 fn location(&self) -> PathBuf {
19 self.path().join("Editor\\Unity.exe")
20 }
21
22 #[cfg(target_os = "macos")]
23 fn location(&self) -> PathBuf {
24 self.path().join("Unity.app")
25 }
26
27 #[cfg(target_os = "linux")]
28 fn location(&self) -> PathBuf {
29 self.path().join("Editor/Unity")
30 }
31
32 #[cfg(any(target_os = "windows", target_os = "linux"))]
33 fn exec_path(&self) -> PathBuf {
34 self.location()
35 }
36
37 #[cfg(target_os = "macos")]
38 fn exec_path(&self) -> PathBuf {
39 self.path().join("Unity.app/Contents/MacOS/Unity")
40 }
41
42 fn installed_modules(&self) -> Result<impl IntoIterator<Item = Module>, UnityError> {
43 let modules = self.get_modules()?;
44 let installed_modules = modules.into_iter().filter(|m| m.is_installed);
45 Ok(installed_modules)
46 }
47
48 fn get_modules(&self) -> Result<Vec<Module>, UnityError> {
49 let modules_json_path = self.path().join("modules.json");
50 let file_content = fs::read_to_string(&modules_json_path)?;
51 let modules: Vec<Module> = serde_json::from_str(&file_content)?;
52 Ok(modules)
53 }
54}
55
56#[derive(PartialEq, Eq, Debug, Clone)]
57pub struct UnityInstallation {
58 version: Version,
59 path: PathBuf,
60}
61
62impl Installation for UnityInstallation {
63 fn path(&self) -> &PathBuf {
64 &self.path
65 }
66
67 fn version(&self) -> &Version {
68 &self.version
69 }
70}
71
72impl Ord for UnityInstallation {
73 fn cmp(&self, other: &UnityInstallation) -> Ordering {
74 self.version.cmp(&other.version)
75 }
76}
77
78impl PartialOrd for UnityInstallation {
79 fn partial_cmp(&self, other: &UnityInstallation) -> Option<Ordering> {
80 Some(self.cmp(other))
81 }
82}
83
84#[cfg(target_os = "macos")]
85fn adjust_path(path:&Path) -> Option<&Path> {
86 if path.is_file() {
88 if let Some(name) = path.file_name() {
89 if name == "Unity" {
90 path.parent()
91 .and_then(|path| path.parent())
92 .and_then(|path| path.parent())
93 .and_then(|path| path.parent())
94 } else {
95 None
96 }
97 } else {
98 None
99 }
100 } else {
101 None
102 }
103}
104
105#[cfg(target_os = "windows")]
106fn adjust_path(path:&Path) -> Option<&Path> {
107 if path.is_file() {
108 if let Some(name) = path.file_name() {
109 if name == "Unity.exe" {
110 path.parent().and_then(|path| path.parent())
111 } else {
112 None
113 }
114 } else {
115 None
116 }
117 } else {
118 None
119 }
120}
121
122#[cfg(target_os = "linux")]
123fn adjust_path(path:&Path) -> Option<&Path> {
124 if path.is_file() {
125 if let Some(name) = path.file_name() {
126 if name == "Unity" {
127 path.parent().and_then(|path| path.parent())
128 } else {
129 None
130 }
131 } else {
132 None
133 }
134 } else {
135 None
136 }
137}
138
139#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
140fn adjust_path(path:&Path) -> Option<&Path> {
141 None
142}
143
144impl UnityInstallation {
145 pub fn new<P: AsRef<Path>>(path: P) -> Result<UnityInstallation, UnityHubError> {
146 let path = path.as_ref();
147 let path = if let Some(p) = adjust_path(path) {
148 p
149 } else {
150 path
151 };
152
153 let version = Version::try_from(path)?;
154 Ok(UnityInstallation {
155 version,
156 path: path.to_path_buf(),
157 })
158 }
159
160 pub fn version(&self) -> &Version {
166 &self.version
167 }
168
169 pub fn into_version(self) -> Version {
170 self.version
171 }
172
173 pub fn version_owned(&self) -> Version {
174 self.version.to_owned()
175 }
176
177 pub fn path(&self) -> &PathBuf {
178 &self.path
179 }
180
181 #[cfg(target_os = "windows")]
182 pub fn location(&self) -> PathBuf {
183 self.path().join("Editor\\Unity.exe")
184 }
185
186 #[cfg(target_os = "macos")]
187 pub fn location(&self) -> PathBuf {
188 self.path().join("Unity.app")
189 }
190
191 #[cfg(target_os = "linux")]
192 pub fn location(&self) -> PathBuf {
193 self.path().join("Editor/Unity")
194 }
195
196 #[cfg(any(target_os = "windows", target_os = "linux"))]
197 pub fn exec_path(&self) -> PathBuf {
198 self.location()
199 }
200
201 #[cfg(target_os = "macos")]
202 pub fn exec_path(&self) -> PathBuf {
203 self.path().join("Unity.app/Contents/MacOS/Unity")
204 }
205}
206
207use std::{fmt, fs};
208use serde::{Deserialize, Serialize};
209use unity_version::Version;
210use crate::error::UnityHubError;
211use crate::unity::error::UnityError;
212use crate::unity::hub::module::Module;
213
214impl fmt::Display for UnityInstallation {
215 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216 write!(f, "{}: {}", self.version, self.path.display())
217 }
218}
219
220pub trait FromInstallation<T:Sized> {
221 fn from_installation(value: T) -> Self;
222}
223
224impl<I> FromInstallation<I> for UnityInstallation
225where
226 I: Installation,
227 I: Sized, {
229 fn from_installation(value: I) -> UnityInstallation {
230 UnityInstallation {
231 path: value.location().to_path_buf(),
232 version: value.version().clone(),
233 }
234 }
235}
236
237#[cfg(all(test, target_os = "macos"))]
248mod tests {
249 use super::*;
250 use plist::to_writer_xml;
251 use std::fs;
252 use std::fs::File;
253 use std::path::Path;
254 use std::str::FromStr;
255 use proptest::proptest;
256 use tempfile::Builder;
257
258 fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
259 let path = base_dir.join("Unity");
260 let mut dir_builder = fs::DirBuilder::new();
261 dir_builder.recursive(true);
262 dir_builder.create(&path).unwrap();
263
264 let info_plist_path = path.join("Unity.app/Contents/Info.plist");
265 let exec_path = path.join("Unity.app/Contents/MacOS/Unity");
266 dir_builder
267 .create(info_plist_path.parent().unwrap())
268 .unwrap();
269
270 dir_builder
271 .create(exec_path.parent().unwrap())
272 .unwrap();
273
274 let info = AppInfo {
275 c_f_bundle_version: String::from_str(version).unwrap(),
276 unity_build_number: String::from_str("ssdsdsdd").unwrap(),
277 };
278
279 let file = File::create(info_plist_path).unwrap();
280 File::create(exec_path).unwrap();
281
282 to_writer_xml(&file, &info).unwrap();
283 path
284 }
285
286 macro_rules! prepare_unity_installation {
287 ($version:expr) => {{
288 let test_dir = Builder::new()
289 .prefix("installation")
290 .rand_bytes(5)
291 .tempdir()
292 .unwrap();
293 let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
294 (test_dir, unity_path)
295 }};
296 }
297
298 #[test]
299 fn create_installtion_from_path() {
300 let (_t, path) = prepare_unity_installation!("2017.1.2f5");
301 let subject = UnityInstallation::new(path).unwrap();
302
303 assert_eq!(subject.version.to_string(), "2017.1.2f5");
304 }
305
306 #[test]
307 fn create_installation_from_executable_path() {
308 let(_t, path) = prepare_unity_installation!("2017.1.2f5");
309 let installation = UnityInstallation::new(path).unwrap();
310 let subject = UnityInstallation::new(installation.exec_path()).unwrap();
311
312 assert_eq!(subject.version.to_string(), "2017.1.2f5");
313 }
314
315 proptest! {
316 #[test]
317 fn doesnt_crash(ref s in "\\PC*") {
318 let _ = UnityInstallation::new(Path::new(s).to_path_buf()).is_ok();
319 }
320
321 #[test]
322 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}") {
323 let (_t, path) = prepare_unity_installation!(s);
324 UnityInstallation::new(path).unwrap();
325 }
326 }
327}
328
329#[cfg(all(test, target_os = "linux"))]
330mod linux_tests {
331 use std::fs;
332 use std::fs::{create_dir_all, File};
333 use std::path::PathBuf;
334 use crate::unity::{Installation, UnityInstallation};
335 use crate::unity::hub::module::Module;
336
337 macro_rules! prepare_unity_installation {
338 ($version:expr) => {{
339 let test_dir = tempfile::Builder::new()
340 .prefix("installation")
341 .rand_bytes(5)
342 .tempdir()
343 .unwrap();
344 let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
345 (test_dir, unity_path)
346 }};
347 }
348
349 fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
350 let path = base_dir.join(version);
351 let mut dir_builder = fs::DirBuilder::new();
352 dir_builder.recursive(true);
353 dir_builder.create(&path).unwrap();
354
355 let exec_path = path.join("Editor/Unity");
356 dir_builder
357 .create(exec_path.parent().unwrap())
358 .unwrap();
359 File::create(exec_path).unwrap();
360 path
361 }
362
363 }