uvm_install2/
lib.rs

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    //
123
124    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            // info!("{}", additional_modules.iter().join("\n"));
174            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    // info!("All available modules for Unity {}", version);
198    // print_graph(&graph);
199    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    //write new api hub editor installation
292    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    //TODO find a way without clone
384    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        // the test-bins project compiles multiple targets by default
576        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}