Files
Starlight_Lancher/packages/app-lib/src/api/terracotta/binary.rs

328 lines
9.3 KiB
Rust

use eyre::{Context, bail};
use std::io::Read;
use std::path::{Path, PathBuf};
use tracing::info;
pub fn terracotta_platform_key() -> &'static str {
match (std::env::consts::OS, std::env::consts::ARCH) {
("linux", "x86_64") => "linux-x86_64",
("linux", "aarch64") => "linux-arm64",
("linux", "riscv64") => "linux-riscv64",
("linux", "loongarch64") => "linux-loongarch64",
("macos", "x86_64") => "macos-x86_64",
("macos", "aarch64") => "macos-arm64",
("windows", "x86_64") => "windows-x86_64",
("windows", "aarch64") => "windows-arm64",
("freebsd", "x86_64") => "freebsd-x86_64",
_ => "unsupported",
}
}
pub(super) fn terracotta_binary_name() -> &'static str {
if cfg!(target_os = "windows") {
"terracotta.exe"
} else {
"terracotta"
}
}
pub(super) fn versioned_terracotta_binary_name(
version: &str,
platform: &str,
) -> String {
let extension = if cfg!(target_os = "windows") {
".exe"
} else {
""
};
format!("terracotta-{version}-{platform}{extension}")
}
pub(super) fn validate_terracotta_version(version: &str) -> eyre::Result<()> {
if version.is_empty()
|| version.len() > 64
|| version.contains("..")
|| !version.chars().all(|character| {
character.is_ascii_alphanumeric()
|| matches!(character, '.' | '-' | '_')
})
{
bail!("invalid terracotta version: {version}");
}
Ok(())
}
pub(super) fn is_terracotta_executable(path: &Path) -> bool {
let Ok(metadata) = std::fs::symlink_metadata(path) else {
return false;
};
if !metadata.file_type().is_file() {
return false;
}
let mut magic = [0_u8; 4];
if std::fs::File::open(path)
.and_then(|mut file| file.read_exact(&mut magic))
.is_err()
{
return false;
}
#[cfg(target_os = "macos")]
{
matches!(
magic,
[0xce, 0xfa, 0xed, 0xfe]
| [0xcf, 0xfa, 0xed, 0xfe]
| [0xfe, 0xed, 0xfa, 0xce]
| [0xfe, 0xed, 0xfa, 0xcf]
| [0xca, 0xfe, 0xba, 0xbe]
| [0xbe, 0xba, 0xfe, 0xca]
| [0xca, 0xfe, 0xba, 0xbf]
| [0xbf, 0xba, 0xfe, 0xca]
)
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{
magic == [0x7f, b'E', b'L', b'F']
}
#[cfg(target_os = "windows")]
{
magic[0..2] == [b'M', b'Z']
}
#[cfg(not(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
target_os = "windows"
)))]
{
false
}
}
pub(super) fn find_terracotta_executable(
dir: &Path,
preferred_name: Option<&str>,
) -> Option<PathBuf> {
let mut pending = vec![dir.to_path_buf()];
let mut candidates = Vec::new();
while let Some(current) = pending.pop() {
let entries = match std::fs::read_dir(current) {
Ok(entries) => entries,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
let file_type = match entry.file_type() {
Ok(file_type) => file_type,
Err(_) => continue,
};
if file_type.is_dir() {
pending.push(path);
continue;
}
let Some(name) = path.file_name().and_then(|name| name.to_str())
else {
continue;
};
if (name == terracotta_binary_name()
|| name.starts_with("terracotta-"))
&& is_terracotta_executable(&path)
{
candidates.push(path);
}
}
}
candidates.sort();
preferred_name
.and_then(|preferred| {
candidates
.iter()
.find(|path| {
path.file_name().and_then(|name| name.to_str())
== Some(preferred)
})
.cloned()
})
.or_else(|| candidates.into_iter().next())
}
pub(super) fn terracotta_binary_path() -> PathBuf {
let base_dir = crate::state::DirectoryInfo::global_handle_if_ready()
.map(|directories| directories.config_dir.clone())
.or_else(|| {
crate::state::DirectoryInfo::initial_settings_dir_path(
crate::brand::BUNDLE_IDENTIFIER,
)
})
.unwrap_or_else(|| PathBuf::from("."));
terracotta_binary_path_in(&base_dir)
}
pub(super) fn terracotta_binary_path_in(base_dir: &Path) -> PathBuf {
base_dir.join("terracotta").join(terracotta_binary_name())
}
fn legacy_terracotta_binary_path() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(Path::to_path_buf))
.unwrap_or_else(|| PathBuf::from("."))
.join("terracotta")
.join(terracotta_binary_name())
}
pub fn terracotta_download_urls(version: &str, platform: &str) -> Vec<String> {
let artifact = format!("terracotta-{version}-{platform}-pkg.tar.gz");
vec![
format!(
"https://gitee.com/burningtnt/Terracotta/releases/download/v{version}/{artifact}"
),
format!(
"https://github.com/burningtnt/Terracotta/releases/download/v{version}/{artifact}"
),
]
}
pub(super) fn resolve_terracotta_binary_path(bin_path: &Path) -> PathBuf {
let resolved_path = if bin_path.is_absolute() {
bin_path.to_path_buf()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(bin_path)
};
let preferred_path = if resolved_path.is_dir() {
resolved_path.join(terracotta_binary_name())
} else if resolved_path.is_file() {
resolved_path
} else {
resolved_path.with_file_name(terracotta_binary_name())
};
if is_terracotta_executable(&preferred_path) {
return preferred_path;
}
preferred_path
.parent()
.and_then(|parent| find_terracotta_executable(parent, None))
.unwrap_or(preferred_path)
}
pub(super) fn resolve_installed_terracotta_binary_path() -> PathBuf {
resolve_installed_terracotta_binary_path_from(
&terracotta_binary_path(),
&legacy_terracotta_binary_path(),
)
}
pub(super) fn resolve_installed_terracotta_binary_path_from(
installed_path: &Path,
legacy_path: &Path,
) -> PathBuf {
let installed_path = resolve_terracotta_binary_path(installed_path);
if is_terracotta_executable(&installed_path) {
return installed_path;
}
let legacy_path = resolve_terracotta_binary_path(legacy_path);
if is_terracotta_executable(&legacy_path) {
legacy_path
} else {
installed_path
}
}
pub(super) async fn install_terracotta_binary(
candidate: &Path,
destination: &Path,
) -> eyre::Result<()> {
let file_name = destination
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| eyre::eyre!("invalid terracotta destination path"))?;
let backup = destination.with_file_name(format!("{file_name}.old"));
if backup.exists() {
tokio::fs::remove_file(&backup)
.await
.wrap_err("failed to remove stale terracotta backup")?;
}
if destination.exists() {
tokio::fs::rename(destination, &backup)
.await
.wrap_err("failed to back up the existing terracotta binary")?;
}
if let Err(error) = tokio::fs::rename(candidate, destination).await {
if backup.exists() {
let _ = tokio::fs::rename(&backup, destination).await;
}
return Err(error).wrap_err("failed to install terracotta binary");
}
if backup.exists() {
tokio::fs::remove_file(&backup).await.wrap_err(
"failed to remove terracotta backup after installation",
)?;
}
Ok(())
}
pub(super) async fn cleanup_legacy_versions(
new_version: &str,
) -> eyre::Result<()> {
let target_dir = terracotta_binary_path()
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("terracotta"));
if !target_dir.exists() {
return Ok(());
}
let mut entries = tokio::fs::read_dir(&target_dir).await?;
while let Some(entry) = entries.next_entry().await? {
let file_type = entry.file_type().await?;
let name = entry.file_name();
let name = name.to_string_lossy();
if file_type.is_file()
&& (should_cleanup_terracotta_file(&name)
|| name.ends_with(".tar.gz")
|| name.ends_with(".old"))
{
tokio::fs::remove_file(entry.path()).await?;
info!(
"removed extracted terracotta artifact after installing {new_version}: {name}"
);
}
if file_type.is_dir() && name.starts_with("terracotta-") {
tokio::fs::remove_dir_all(entry.path()).await?;
info!(
"removed extracted terracotta directory after installing {new_version}: {name}"
);
}
}
Ok(())
}
pub(super) fn should_cleanup_terracotta_file(name: &str) -> bool {
name.starts_with("terracotta-")
&& name != "terracotta-version.json"
&& name != "terracotta-version.json.tmp"
}