1mod error;
2mod install;
3mod sys;
4use crate::error::InstallError::{InstallFailed, InstallerCreatedFailed, LoadingInstallerFailed};
5pub use error::*;
6use install::utils;
7use install::{InstallManifest, Loader};
8use lazy_static::lazy_static;
9use log::{debug, info, trace};
10use ssri::Integrity;
11use std::collections::HashSet;
12use std::env::VarError;
13use std::fs::File;
14use std::ops::{Deref, DerefMut};
15use std::path::{Path, PathBuf};
16use std::{fs, io};
17use sys::create_installer;
18pub use unity_hub::error::UnityError;
19pub use unity_hub::error::UnityHubError;
20pub use unity_hub::unity;
21use unity_hub::unity::hub;
22use unity_hub::unity::hub::editors::EditorInstallation;
23use unity_hub::unity::hub::module::Module;
24use unity_hub::unity::hub::paths;
25use unity_hub::unity::hub::paths::locks_dir;
26use unity_hub::unity::{Installation, UnityInstallation};
27pub use unity_version::error::VersionError;
28pub use unity_version::Version;
29use uvm_install_graph::{InstallGraph, InstallStatus, UnityComponent, Walker};
30pub use uvm_live_platform::error::LivePlatformError;
31pub use uvm_live_platform::fetch_release;
32use uvm_live_platform::Release;
33
34lazy_static! {
35 static ref UNITY_BASE_PATTERN: &'static Path = Path::new("{UNITY_PATH}");
36}
37
38impl AsRef<Path> for UNITY_BASE_PATTERN {
39 fn as_ref(&self) -> &Path {
40 self.deref()
41 }
42}
43
44fn print_graph<'a>(graph: &'a InstallGraph<'a>) {
45 use console::Style;
46
47 for node in graph.topo().iter(graph.context()) {
48 let component = graph.component(node).unwrap();
49 let install_status = graph.install_status(node).unwrap();
50 let prefix: String = [' '].iter().cycle().take(graph.depth(node) * 2).collect();
51
52 let style = match install_status {
53 InstallStatus::Unknown => Style::default().dim(),
54 InstallStatus::Missing => Style::default().yellow().blink(),
55 InstallStatus::Installed => Style::default().green(),
56 };
57
58 info!(
59 "{}- {} ({})",
60 prefix,
61 component,
62 style.apply_to(install_status)
63 );
64 }
65}
66#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
67pub fn ensure_installation_architecture_is_correct<I: Installation>(
68 installation: &I,
69) -> io::Result<bool> {
70 match std::env::var("UVM_ARCHITECTURE_CHECK_ENABLED") {
71 Ok(value)
72 if value == "1"
73 || value == "true"
74 || value == "True"
75 || value == "TRUE"
76 || value == "yes"
77 || value == "Yes"
78 || value == "YES" =>
79 {
80 sys::ensure_installation_architecture_is_correct(installation)
81 }
82 _ => Ok(true),
83 }
84}
85
86#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
87pub fn ensure_installation_architecture_is_correct<I: Installation>(
88 installation: &I,
89) -> io::Result<bool> {
90 Ok(true)
91}
92
93pub fn install<V, P, I>(
94 version: V,
95 mut requested_modules: Option<I>,
96 install_sync: bool,
97 destination: Option<P>,
98) -> Result<UnityInstallation>
99where
100 V: AsRef<Version>,
101 P: AsRef<Path>,
102 I: IntoIterator,
103 I::Item: Into<String>,
104{
105 let version = version.as_ref();
106 let version_string = version.to_string();
107
108 let locks_dir = locks_dir().ok_or_else(|| {
109 InstallError::LockProcessFailure(io::Error::new(
110 io::ErrorKind::NotFound,
111 "Unable to locate locks directory.",
112 ))
113 })?;
114
115 fs::DirBuilder::new().recursive(true).create(&locks_dir)?;
116 lock_process!(locks_dir.join(format!("{}.lock", version_string)));
117
118 let unity_release = fetch_release(version.to_owned())?;
119 eprintln!("{:#?}", unity_release);
120 let mut graph = InstallGraph::from(&unity_release);
121
122 let mut editor_installation: Option<EditorInstallation> = None;
125 let base_dir = if let Some(destination) = destination {
126 let destination = destination.as_ref();
127 if destination.exists() && !destination.is_dir() {
128 return Err(io::Error::new(
129 io::ErrorKind::InvalidInput,
130 "Requested destination is not a directory.",
131 )
132 .into());
133 }
134
135 editor_installation = Some(EditorInstallation::new(
136 version.to_owned(),
137 destination.to_path_buf(),
138 ));
139 destination.to_path_buf()
140 } else {
141 hub::paths::install_path()
142 .map(|path| path.join(format!("{}", version)))
143 .or_else(|| {
144 {
145 #[cfg(any(target_os = "windows", target_os = "macos"))]
146 let application_path = dirs_2::application_dir();
147 #[cfg(target_os = "linux")]
148 let application_path = dirs_2::executable_dir();
149 application_path
150 }
151 .map(|path| path.join(format!("Unity-{}", version)))
152 })
153 .expect("default installation directory")
154 };
155 let mut additional_modules = vec![];
156 let installation = UnityInstallation::new(&base_dir);
157 if let Ok(ref installation) = installation {
158 info!("Installation found at {}", installation.path().display());
159 if ensure_installation_architecture_is_correct(installation)? {
160 let modules = installation.installed_modules()?;
161 let mut module_ids: HashSet<String> =
162 modules.into_iter().map(|m| m.id().to_string()).collect();
163 module_ids.insert("Unity".to_string());
164 graph.mark_installed(&module_ids);
165 } else {
166 info!("Architecture mismatch, reinstalling");
167 info!("Fetch installed modules:");
168 additional_modules = installation
169 .installed_modules()?
170 .into_iter()
171 .map(|m| m.id().to_string())
172 .collect();
173 fs::remove_dir_all(installation.path())?;
175 let version_string =
176 format!("{}-{}", unity_release.version, unity_release.short_revision);
177 let installer_dir = paths::cache_dir()
178 .map(|c| c.join(&format!("installer/{}", version_string)))
179 .ok_or_else(|| {
180 io::Error::new(
181 io::ErrorKind::Other,
182 "Unable to fetch cache installer directory",
183 )
184 })?;
185 if installer_dir.exists() {
186 info!("Delete installer cache: {}", installer_dir.display());
187 fs::remove_dir_all(installer_dir)?;
188 }
189 info!("Cleanup done");
190 graph.mark_all_missing();
191 }
192 } else {
193 info!("\nFresh install");
194 graph.mark_all_missing();
195 }
196
197 let additional_modules_iterator = additional_modules.into_iter();
200 let base_iterator = ["Unity".to_string()].into_iter();
201 let all_components: HashSet<String> = match requested_modules {
202 Some(modules) => modules
203 .into_iter()
204 .flat_map(|module| {
205 let module = module.into();
206 let node = graph.get_node_id(&module).ok_or_else(|| {
207 debug!(
208 "Unsupported module '{}' for selected api version {}",
209 module, version
210 );
211 InstallError::UnsupportedModule(module.to_string(), version.to_string())
212 });
213
214 match node {
215 Ok(node) => {
216 let mut out = vec![Ok(module.to_string())];
217 out.append(
218 &mut graph
219 .get_dependend_modules(node)
220 .iter()
221 .map({
222 |((c, _), _)| match c {
223 UnityComponent::Editor(_) => Ok("Unity".to_string()),
224 UnityComponent::Module(m) => Ok(m.id().to_string()),
225 }
226 })
227 .collect(),
228 );
229 if install_sync {
230 out.append(
231 &mut graph
232 .get_sub_modules(node)
233 .iter()
234 .map({
235 |((c, _), _)| match c {
236 UnityComponent::Editor(_) => Ok("Unity".to_string()),
237 UnityComponent::Module(m) => Ok(m.id().to_string()),
238 }
239 })
240 .collect(),
241 );
242 }
243 out
244 }
245 Err(err) => vec![Err(err.into())],
246 }
247 })
248 .chain(base_iterator.map(|c| Ok(c)))
249 .chain(additional_modules_iterator.map(|c| Ok(c)))
250 .collect::<Result<HashSet<_>>>(),
251 None => base_iterator.map(|c| Ok(c)).collect::<Result<HashSet<_>>>(),
252 }?;
253
254 debug!("\nAll requested components");
255 for c in all_components.iter() {
256 debug!("- {}", c);
257 }
258
259 graph.keep(&all_components);
260
261 info!("\nInstall Graph");
262 print_graph(&graph);
263
264 install_module_and_dependencies(&graph, &base_dir)?;
265 let installation = installation.or_else(|_| UnityInstallation::new(&base_dir))?;
266 let mut modules = match installation.get_modules() {
267 Err(_) => unity_release
268 .downloads
269 .first()
270 .cloned()
271 .map(|d| {
272 let mut modules = vec![];
273 for module in &d.modules {
274 fetch_modules_from_release(&mut modules, module);
275 }
276 modules
277 })
278 .unwrap(),
279 Ok(m) => m,
280 };
281
282 for module in modules.iter_mut() {
283 if module.is_installed == false {
284 module.is_installed = all_components.contains(module.id());
285 trace!("module {} is installed", module.id());
286 }
287 }
288
289 write_modules_json(&installation, modules)?;
290
291 if let Some(installation) = editor_installation {
293 let mut _editors = unity_hub::Editors::load().and_then(|mut editors| {
294 editors.add(&installation);
295 editors.flush()?;
296 Ok(())
297 });
298 }
299
300 Ok(installation)
301}
302
303fn fetch_modules_from_release(modules: &mut Vec<Module>, module: &uvm_live_platform::Module) {
304 modules.push(module.clone().into());
305 for sub_module in module.sub_modules() {
306 fetch_modules_from_release(modules, sub_module);
307 }
308}
309
310fn write_modules_json(
311 installation: &UnityInstallation,
312 modules: Vec<unity_hub::unity::hub::module::Module>,
313) -> io::Result<()> {
314 use console::style;
315 use std::fs::OpenOptions;
316 use std::io::Write;
317
318 let output_path = installation
319 .location()
320 .parent()
321 .unwrap()
322 .join("modules.json");
323 info!(
324 "{}",
325 style(format!("write {}", output_path.display())).green()
326 );
327 let mut f = OpenOptions::new()
328 .write(true)
329 .truncate(true)
330 .create(true)
331 .open(output_path)?;
332
333 let j = serde_json::to_string_pretty(&modules)?;
334 write!(f, "{}", j)?;
335 trace!("{}", j);
336 Ok(())
337}
338
339struct UnityComponent2<'a>(UnityComponent<'a>);
340
341impl<'a> Deref for UnityComponent2<'a> {
342 type Target = UnityComponent<'a>;
343
344 fn deref(&self) -> &Self::Target {
345 &self.0
346 }
347}
348
349impl<'a> DerefMut for UnityComponent2<'a> {
350 fn deref_mut(&mut self) -> &mut Self::Target {
351 &mut self.0
352 }
353}
354
355impl<'a> InstallManifest for UnityComponent2<'a> {
356 fn is_editor(&self) -> bool {
357 match self.0 {
358 UnityComponent::Editor(_) => true,
359 _ => false,
360 }
361 }
362 fn id(&self) -> &str {
363 match self.0 {
364 UnityComponent::Editor(_) => "Unity",
365 UnityComponent::Module(m) => m.id(),
366 }
367 }
368 fn install_size(&self) -> u64 {
369 let download_size = match self.0 {
370 UnityComponent::Editor(e) => e.download_size,
371 UnityComponent::Module(m) => m.download_size,
372 };
373 download_size.to_bytes() as u64
374 }
375
376 fn download_url(&self) -> &str {
377 match self.0 {
378 UnityComponent::Editor(e) => &e.release_file.url,
379 UnityComponent::Module(m) => &m.release_file().url,
380 }
381 }
382
383 fn integrity(&self) -> Option<Integrity> {
385 match self.0 {
386 UnityComponent::Editor(e) => e.release_file.integrity.clone(),
387 UnityComponent::Module(m) => m.release_file().integrity.clone(),
388 }
389 }
390
391 fn install_rename_from_to<P: AsRef<Path>>(&self, base_path: P) -> Option<(PathBuf, PathBuf)> {
392 match self.0 {
393 UnityComponent::Editor(_) => None,
394 UnityComponent::Module(m) => {
395 if let Some(extracted_path_rename) = &m.extracted_path_rename() {
396 Some((
397 strip_unity_base_url(&extracted_path_rename.from, &base_path),
398 strip_unity_base_url(&extracted_path_rename.to, &base_path),
399 ))
400 } else {
401 None
402 }
403 }
404 }
405 }
406
407 fn install_destination<P: AsRef<Path>>(&self, base_path: P) -> Option<PathBuf> {
408 match self.0 {
409 UnityComponent::Editor(_) => Some(base_path.as_ref().to_path_buf()),
410 UnityComponent::Module(m) => {
411 if let Some(destination) = &m.destination() {
412 Some(strip_unity_base_url(destination, &base_path))
413 } else {
414 None
415 }
416 }
417 }
418 }
419}
420
421fn strip_unity_base_url<P: AsRef<Path>, Q: AsRef<Path>>(path: P, base_dir: Q) -> PathBuf {
422 let path = path.as_ref();
423 base_dir
424 .as_ref()
425 .join(&path.strip_prefix(&UNITY_BASE_PATTERN).unwrap_or(path))
426}
427
428fn install_module_and_dependencies<'a, P: AsRef<Path>>(
429 graph: &'a InstallGraph<'a>,
430 base_dir: P,
431) -> Result<()> {
432 let base_dir = base_dir.as_ref();
433 for node in graph.topo().iter(graph.context()) {
434 if let Some(InstallStatus::Missing) = graph.install_status(node) {
435 let component = graph.component(node).unwrap();
436 let module = UnityComponent2(component);
437 let version = &graph.release().version;
438 let hash = &graph.release().short_revision;
439
440 info!("install {}", module.id());
441 info!("download installer for {}", module.id());
442
443 let loader = Loader::new(version, hash, &module);
444 let installer = loader
445 .download()
446 .map_err(|installer_err| LoadingInstallerFailed(installer_err))?;
447
448 info!("create installer for {}", component);
449 let installer = create_installer(base_dir, installer, &module)
450 .map_err(|installer_err| InstallerCreatedFailed(installer_err))?;
451
452 info!("install {}", component);
453 installer
454 .install()
455 .map_err(|installer_err| InstallFailed(module.id().to_string(), installer_err))?;
456 }
457 }
458
459 Ok(())
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465 use rstest::rstest;
466 use std::cmp::Ordering;
467 use std::env;
468 use std::fmt::{Display, Formatter};
469 use test_binary::build_test_binary;
470 use unity_version::ReleaseType;
471
472 #[derive(PartialEq, Eq, Debug, Clone)]
473 pub struct MockInstallation {
474 version: Version,
475 path: PathBuf,
476 }
477
478 impl MockInstallation {
479 pub fn new<V: Into<Version>, P: AsRef<Path>>(version: V, path: P) -> Self {
480 Self {
481 version: version.into(),
482 path: path.as_ref().to_path_buf(),
483 }
484 }
485 }
486
487 impl Default for MockInstallation {
488 fn default() -> Self {
489 Self {
490 version: Version::new(6000, 0, 0, ReleaseType::Final, 1),
491 path: PathBuf::from("/Applications/Unity/6000.0.0f1"),
492 }
493 }
494 }
495
496 impl Installation for MockInstallation {
497 fn path(&self) -> &PathBuf {
498 &self.path
499 }
500
501 fn version(&self) -> &Version {
502 &self.version
503 }
504 }
505
506 impl Ord for MockInstallation {
507 fn cmp(&self, other: &MockInstallation) -> Ordering {
508 self.version.cmp(&other.version)
509 }
510 }
511
512 impl PartialOrd for MockInstallation {
513 fn partial_cmp(&self, other: &MockInstallation) -> Option<Ordering> {
514 Some(self.cmp(other))
515 }
516 }
517
518 enum TestArch {
519 Arch64,
520 X86,
521 }
522
523 impl Display for TestArch {
524 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
525 match self {
526 Self::Arch64 => write!(f, "{}", "aarch64"),
527 Self::X86 => write!(f, "{}", "x86_64"),
528 }
529 }
530 }
531
532 lazy_static! {
533 static ref TEST_UNITY_VERSION_ARM_SUPPORT: Version =
534 Version::new(6000, 0, 0, ReleaseType::Final, 1);
535 static ref TEST_UNITY_VERSION_NO_ARM_SUPPORT: Version =
536 Version::new(2020, 0, 0, ReleaseType::Final, 1);
537 }
538
539 #[rstest(
540 env_val, test_arch, test_version, expected,
541 case::test_arch_check_enabled_with_arm_binary("true", TestArch::Arch64, TEST_UNITY_VERSION_ARM_SUPPORT.clone(), true),
542 case::test_arch_check_disabled_with_arm_binary("false", TestArch::Arch64, TEST_UNITY_VERSION_ARM_SUPPORT.clone(), true),
543 case::test_arch_check_disabled_with_x86_binary_and_arm_compatible_version_available("false", TestArch::X86, TEST_UNITY_VERSION_ARM_SUPPORT.clone(), true),
544 case::test_arch_check_enabled_with_x86_binary_and_arm_compatible_version_not_available("true", TestArch::X86, TEST_UNITY_VERSION_NO_ARM_SUPPORT.clone(), true),
545 case::test_arch_check_disabled_with_x86_binary_and_arm_compatible_version_not_available("false", TestArch::X86, TEST_UNITY_VERSION_NO_ARM_SUPPORT.clone(), true),
546 case::test_arch_check_enabled_with_x86_binary_and_arm_compatible_version_available("true", TestArch::X86, TEST_UNITY_VERSION_ARM_SUPPORT.clone(), false),
547 )]
548 #[serial_test::serial]
549 fn test_architecture_check(
550 env_val: &str,
551 test_arch: TestArch,
552 test_version: Version,
553 expected: bool,
554 ) {
555 std::env::set_var("UVM_ARCHITECTURE_CHECK_ENABLED", env_val);
556 let expected = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
557 expected
558 } else {
559 true
560 };
561 }
562
563 fn run_arch_test(binary_arch: TestArch, unity_version: Version, expected_result: bool) {
564 #[cfg(target_os = "macos")]
565 const OS_SUFFIX: &str = "apple-darwin";
566 #[cfg(target_os = "linux")]
567 const OS_SUFFIX: &str = "unknown-linux-gnu";
568 #[cfg(target_os = "windows")]
569 const OS_SUFFIX: &str = "pc-windows-msvc";
570
571 let test_bin_path =
572 build_test_binary("fake-bin", "test-bins").expect("error building test binary");
573 let test_bin_path_str = test_bin_path.to_str().unwrap();
574
575 let aarch_bin_path = test_bin_path_str.replace(
577 "target/debug",
578 format!("target/{}-{}/debug", binary_arch, OS_SUFFIX).as_str(),
579 );
580
581 println!("{}", aarch_bin_path);
582 let temp_unity_installation =
583 tempfile::tempdir().expect("error creating temporary directory");
584 let unity_exec_path = temp_unity_installation
585 .path()
586 .join("Unity.app/Contents/MacOS/Unity");
587 if let Some(parent) = unity_exec_path.parent() {
588 fs::create_dir_all(parent).expect("failed to create parent directories");
589 }
590 fs::copy(aarch_bin_path, &unity_exec_path).expect("failed to copy file");
591 println!("{}", unity_exec_path.display());
592
593 let installation = MockInstallation::new(unity_version, temp_unity_installation.path());
594 assert_eq!(
595 ensure_installation_architecture_is_correct(&installation).unwrap(),
596 expected_result
597 );
598 }
599}