use std::cmp::Ordering;
use std::path::{Path, PathBuf};
use std::convert::TryFrom;
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct AppInfo {
pub c_f_bundle_version: String,
pub unity_build_number: String,
}
pub trait Installation: Eq + Ord {
fn path(&self) -> &PathBuf;
fn version(&self) -> &Version;
#[cfg(target_os = "windows")]
fn location(&self) -> PathBuf {
self.path().join("Editor\\Unity.exe")
}
#[cfg(target_os = "macos")]
fn location(&self) -> PathBuf {
self.path().join("Unity.app")
}
#[cfg(target_os = "linux")]
fn location(&self) -> PathBuf {
self.path().join("Editor/Unity")
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
fn exec_path(&self) -> PathBuf {
self.location()
}
#[cfg(target_os = "macos")]
fn exec_path(&self) -> PathBuf {
self.path().join("Unity.app/Contents/MacOS/Unity")
}
fn installed_modules(&self) -> Result<impl IntoIterator<Item = Module>, UnityError> {
let modules = self.get_modules()?;
let installed_modules = modules.into_iter().filter(|m| m.is_installed);
Ok(installed_modules)
}
fn get_modules(&self) -> Result<Vec<Module>, UnityError> {
let modules_json_path = self.path().join("modules.json");
let file_content = fs::read_to_string(&modules_json_path).map_err(|e| {
error!("Failed to read modules.json file from {}: {}", &modules_json_path.display(), e);
e
})?;
let modules: Vec<Module> = serde_json::from_str(&file_content)?;
Ok(modules)
}
#[cfg(feature = "mutate")]
fn write_modules(&self, modules: Vec<Module>) -> Result<(), UnityError> {
use log::info;
let modules_json_path = self.path().join("modules.json");
info!("Writing modules.json to {}", modules_json_path.display());
let json_content = serde_json::to_string_pretty(&modules)?;
std::fs::write(&modules_json_path, json_content)?;
Ok(())
}
}
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct UnityInstallation {
version: Version,
path: PathBuf,
}
impl Installation for UnityInstallation {
fn path(&self) -> &PathBuf {
&self.path
}
fn version(&self) -> &Version {
&self.version
}
}
impl Ord for UnityInstallation {
fn cmp(&self, other: &UnityInstallation) -> Ordering {
self.version.cmp(&other.version)
}
}
impl PartialOrd for UnityInstallation {
fn partial_cmp(&self, other: &UnityInstallation) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[cfg(target_os = "macos")]
fn adjust_path(path:&Path) -> Option<&Path> {
if path.is_file() {
if let Some(name) = path.file_name() {
if name == "Unity" {
path.parent()
.and_then(|path| path.parent())
.and_then(|path| path.parent())
.and_then(|path| path.parent())
} else {
None
}
} else {
None
}
} else {
None
}
}
#[cfg(target_os = "windows")]
fn adjust_path(path:&Path) -> Option<&Path> {
if path.is_file() {
if let Some(name) = path.file_name() {
if name == "Unity.exe" {
path.parent().and_then(|path| path.parent())
} else {
None
}
} else {
None
}
} else {
None
}
}
#[cfg(target_os = "linux")]
fn adjust_path(path:&Path) -> Option<&Path> {
if path.is_file() {
if let Some(name) = path.file_name() {
if name == "Unity" {
path.parent().and_then(|path| path.parent())
} else {
None
}
} else {
None
}
} else {
None
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn adjust_path(path:&Path) -> Option<&Path> {
None
}
impl UnityInstallation {
pub fn new<P: AsRef<Path>>(path: P) -> Result<UnityInstallation, UnityHubError> {
let path = path.as_ref();
let path = if let Some(p) = adjust_path(path) {
p
} else {
path
};
trace!("Create UnityInstallation object with path: {}", path.display());
let version = Version::try_from(path)?;
Ok(UnityInstallation {
version,
path: path.to_path_buf(),
})
}
pub fn version(&self) -> &Version {
&self.version
}
pub fn into_version(self) -> Version {
self.version
}
pub fn version_owned(&self) -> Version {
self.version.to_owned()
}
pub fn path(&self) -> &PathBuf {
&self.path
}
#[cfg(target_os = "windows")]
pub fn location(&self) -> PathBuf {
self.path().join("Editor\\Unity.exe")
}
#[cfg(target_os = "macos")]
pub fn location(&self) -> PathBuf {
self.path().join("Unity.app")
}
#[cfg(target_os = "linux")]
pub fn location(&self) -> PathBuf {
self.path().join("Editor/Unity")
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
pub fn exec_path(&self) -> PathBuf {
self.location()
}
#[cfg(target_os = "macos")]
pub fn exec_path(&self) -> PathBuf {
self.path().join("Unity.app/Contents/MacOS/Unity")
}
}
use std::{fmt, fs};
use std::fmt::Debug;
use log::{error, trace};
use serde::{Deserialize, Serialize};
use unity_version::Version;
use crate::error::UnityHubError;
use crate::unity::error::UnityError;
use crate::unity::hub::module::Module;
impl fmt::Display for UnityInstallation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.version, self.path.display())
}
}
pub trait FromInstallation<T:Sized> {
fn from_installation(value: T) -> Self;
}
impl<I> FromInstallation<I> for UnityInstallation
where
I: Installation,
I: Debug,
I: Sized, {
fn from_installation(value: I) -> UnityInstallation {
trace!("Create UnityInstallation object from Installation object {:?}", value);
UnityInstallation {
path: value.path().to_path_buf(),
version: value.version().clone(),
}
}
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
use plist::to_writer_xml;
use std::fs;
use std::fs::File;
use std::path::Path;
use std::str::FromStr;
use proptest::proptest;
use tempfile::Builder;
fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
let path = base_dir.join("Unity");
let mut dir_builder = fs::DirBuilder::new();
dir_builder.recursive(true);
dir_builder.create(&path).unwrap();
let info_plist_path = path.join("Unity.app/Contents/Info.plist");
let exec_path = path.join("Unity.app/Contents/MacOS/Unity");
dir_builder
.create(info_plist_path.parent().unwrap())
.unwrap();
dir_builder
.create(exec_path.parent().unwrap())
.unwrap();
let info = AppInfo {
c_f_bundle_version: String::from_str(version).unwrap(),
unity_build_number: String::from_str("ssdsdsdd").unwrap(),
};
let file = File::create(info_plist_path).unwrap();
File::create(exec_path).unwrap();
to_writer_xml(&file, &info).unwrap();
path
}
macro_rules! prepare_unity_installation {
($version:expr) => {{
let test_dir = Builder::new()
.prefix("installation")
.rand_bytes(5)
.tempdir()
.unwrap();
let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
(test_dir, unity_path)
}};
}
#[test]
fn create_installtion_from_path() {
let (_t, path) = prepare_unity_installation!("2017.1.2f5");
let subject = UnityInstallation::new(path).unwrap();
assert_eq!(subject.version.to_string(), "2017.1.2f5");
}
#[test]
fn create_installation_from_executable_path() {
let(_t, path) = prepare_unity_installation!("2017.1.2f5");
let installation = UnityInstallation::new(path).unwrap();
let subject = UnityInstallation::new(installation.exec_path()).unwrap();
assert_eq!(subject.version.to_string(), "2017.1.2f5");
}
proptest! {
#[test]
fn doesnt_crash(ref s in "\\PC*") {
let _ = UnityInstallation::new(Path::new(s).to_path_buf()).is_ok();
}
#[test]
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}") {
let (_t, path) = prepare_unity_installation!(s);
UnityInstallation::new(path).unwrap();
}
}
}
#[cfg(all(test, target_os = "linux"))]
mod linux_tests {
use std::fs;
use std::fs::{create_dir_all, File};
use std::path::PathBuf;
use crate::unity::{Installation, UnityInstallation};
use crate::unity::hub::module::Module;
macro_rules! prepare_unity_installation {
($version:expr) => {{
let test_dir = tempfile::Builder::new()
.prefix("installation")
.rand_bytes(5)
.tempdir()
.unwrap();
let unity_path = create_unity_installation(&test_dir.path().to_path_buf(), $version);
(test_dir, unity_path)
}};
}
fn create_unity_installation(base_dir: &PathBuf, version: &str) -> PathBuf {
let path = base_dir.join(version);
let mut dir_builder = fs::DirBuilder::new();
dir_builder.recursive(true);
dir_builder.create(&path).unwrap();
let exec_path = path.join("Editor/Unity");
dir_builder
.create(exec_path.parent().unwrap())
.unwrap();
File::create(exec_path).unwrap();
path
}
}