uvm-install2 0.13.0

Install specified unity version.
Documentation
use crate::install::error::InstallerErrorInner::{InstallationFailed, InstallerCreateFailed};
use crate::install::error::{InstallerError, InstallerResult};
use crate::install::installer::{BaseInstaller, Installer, InstallerWithDestination, Pkg};
use crate::install::{InstallHandler, UnityEditor, UnityModule};
use log::{debug, info, warn};
use std::fs::DirBuilder;
use std::path::Path;
use std::process::{Command, Stdio};
use std::{fs, io};
use thiserror_context::Context;

pub type EditorPkgInstaller = Installer<UnityEditor, Pkg, InstallerWithDestination>;
pub type ModulePkgNativeInstaller = Installer<UnityModule, Pkg, BaseInstaller>;
pub type ModulePkgInstaller = Installer<UnityModule, Pkg, InstallerWithDestination>;

impl<V, I> Installer<V, Pkg, I> {
    fn move_files<P: AsRef<Path>, D: AsRef<Path>>(
        &self,
        source: P,
        destination: D,
    ) -> InstallerResult<()> {
        let source = source.as_ref();
        let destination = destination.as_ref();
        debug!(
            "move all files from {} into {}",
            source.display(),
            destination.display()
        );
        for entry in fs::read_dir(&source)?.filter_map(io::Result::ok) {
            let new_location = destination.join(entry.file_name());
            debug!(
                "move {} to {}",
                entry.path().display(),
                new_location.display()
            );
            if new_location.exists() && new_location.is_dir() {
                warn!(
                    "target directory already exists. {}",
                    new_location.display()
                );
                warn!("delete directory: {}", new_location.display());
                fs::remove_dir_all(&new_location)?;
            }

            fs::rename(entry.path(), &new_location)?;
        }
        Ok(())
    }

    fn xar<P: AsRef<Path>, D: AsRef<Path>>(
        &self,
        installer: P,
        destination: D,
    ) -> InstallerResult<()> {
        let installer = installer.as_ref();
        let destination = destination.as_ref();

        debug!(
            "unpack installer {} to temp destination {}",
            installer.display(),
            destination.display()
        );
        let child = Command::new("xar")
            .arg("-x")
            .arg("-f")
            .arg(installer)
            .arg("-C")
            .arg(destination)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let output = child.wait_with_output()?;
        if !output.status.success() {
            return Err(InstallerCreateFailed(format!(
                "failed to extract installer from pkg package:/n{}",
                String::from_utf8_lossy(&output.stderr)
            ))
            .into());
        }
        Ok(())
    }

    fn untar<P: AsRef<Path>, D: AsRef<Path>>(
        &self,
        base_payload_path: P,
        destination: D,
    ) -> Result<(), InstallerError> {
        let base_payload_path = base_payload_path.as_ref();
        let payload = self.find_payload(&base_payload_path).context("unable to find payload in package")?;
        debug!("untar payload at {}", payload.display());
        self.tar(&payload, destination)
    }

    fn tar<P: AsRef<Path>, D: AsRef<Path>>(
        &self,
        source: P,
        destination: D,
    ) -> InstallerResult<()> {
        let source = source.as_ref();
        let destination = destination.as_ref();

        let tar_child = Command::new("tar")
            .arg("-C")
            .arg(destination)
            .arg("-zmxf")
            .arg(source)
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let tar_output = tar_child.wait_with_output()?;
        if !tar_output.status.success() {
            return Err(InstallerCreateFailed(format!(
                "failed to untar payload:/n{}",
                String::from_utf8_lossy(&tar_output.stderr)
            ))
            .into());
        }

        Ok(())
    }
}

impl EditorPkgInstaller {
    fn cleanup_editor<D: AsRef<Path>>(&self, destination: D) -> InstallerResult<()> {
        use std::fs;
        let destination = destination.as_ref();
        let tmp_unity_directory = destination.join("Unity");
        if !tmp_unity_directory.exists() {
            return Err(InstallerCreateFailed(
                "Failed to create temp unity install directory".to_string(),
            )
            .into());
        }

        self.move_files(&tmp_unity_directory, &destination)?;
        fs::remove_dir_all(&tmp_unity_directory)?;
        Ok(())
    }
}

impl ModulePkgInstaller {
    fn cleanup_ios_support<D: AsRef<Path>>(&self, destination: D) -> InstallerResult<()> {
        use std::fs;
        let destination = destination.as_ref();
        debug!("cleanup ios support package at {}", destination.display());

        let tmp_ios_support_directory = destination.join("iOSSupport");
        if tmp_ios_support_directory.exists() {
            debug!(
                "move ios files from {} to {}",
                tmp_ios_support_directory.display(),
                destination.display()
            );
            self.move_files(&tmp_ios_support_directory, &destination)?;
            fs::remove_dir_all(&tmp_ios_support_directory)?;
        }
        Ok(())
    }
}

impl InstallHandler for EditorPkgInstaller {
    fn install_handler(&self) -> InstallerResult<()> {
        let destination = self.destination();
        let installer = self.installer();

        debug!(
            "install editor from pkg {} to {}",
            installer.display(),
            destination.display()
        );

        let tmp_destination = destination.join("tmp");
        DirBuilder::new().recursive(true).create(&tmp_destination)?;
        self.xar(installer, &tmp_destination)?;
        self.untar(&tmp_destination, destination)?;
        self.cleanup_editor(destination)?;
        self.cleanup(&tmp_destination)?;

        Ok(())
    }

    fn error_handler(&self) {
        self.cleanup_directory_failable(&self.destination());
    }

    fn installer(&self) -> &Path {
        self.installer()
    }

    fn before_install(&self) -> InstallerResult<()> {
        if self.destination().exists() {
            if self.destination().is_dir() {
                info!("Destination directory {} already exists, removing it", self.destination().display());
                fs::remove_dir_all(self.destination()).context("failed to remove the existing destination directory")?;
            } else {
                info!("Destination file {} already exists, removing it", self.destination().display());
                fs::remove_file(self.destination()).context("failed to remove the existing destination file")?;
            }
        }
        Ok(())
    }
}

impl InstallHandler for ModulePkgInstaller {
    fn install_handler(&self) -> InstallerResult<()> {
        let destination = self.destination();
        let installer = self.installer();

        debug!(
            "install module from pkg {} to {}",
            installer.display(),
            destination.display()
        );

        let tmp_destination = destination.join("tmp");
        DirBuilder::new().recursive(true).create(&tmp_destination).context("failed to create temp install directory")?;
        self.xar(installer, &tmp_destination)?;
        self.untar(&tmp_destination, destination).context("failed to unpack the payload")?;
        self.cleanup_ios_support(destination).context("failed to cleanup ios support destination")?;
        self.cleanup(&tmp_destination).context("failed to cleanup temp files")?;
        Ok(())
    }

    fn after_install(&self) -> InstallerResult<()> {
        if let Some((from, to)) = &self.rename() {
            uvm_move_dir::move_dir(from, to)?;
        }
        Ok(())
    }

    fn error_handler(&self) {
        self.cleanup_directory_failable(&self.destination());
    }

    fn installer(&self) -> &Path {
        self.installer()
    }

    fn before_install(&self) -> InstallerResult<()> {
        if self.destination().exists() {
            if self.destination().is_dir() {
                info!("Destination directory {} already exists, removing it", self.destination().display());
                fs::remove_dir_all(self.destination()).context("failed to remove the existing destination directory")?;
            } else {
                info!("Destination file {} already exists, removing it", self.destination().display());
                fs::remove_file(self.destination()).context("failed to remove the existing destination file")?;
            }
        }
        Ok(())
    }
}

impl InstallHandler for ModulePkgNativeInstaller {
    fn install_handler(&self) -> InstallerResult<()> {
        let installer = self.installer();
        debug!("install from pkg {}", installer.display());

        let child = Command::new("sudo")
            .arg("installer")
            .arg("-package")
            .arg(installer)
            .arg("-target")
            .arg("/")
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let output = child.wait_with_output()?;
        if !output.status.success() {
            return Err(InstallationFailed(
                installer.display().to_string(),
                String::from_utf8_lossy(&output.stderr).to_string(),
            )
            .into());
        }
        Ok(())
    }

    fn installer(&self) -> &Path {
        self.installer()
    }

    fn after_install(&self) -> InstallerResult<()> {
        if let Some((from, to)) = &self.rename() {
            uvm_move_dir::move_dir(from, to)?
            //.context("failed to rename installed module")?;
        }
        Ok(())
    }
}