mirror of
https://git.ely.my.id/Tenshi/Hi3-Patcher
synced 2026-11-04 21:46:48 +01:00
340 lines
11 KiB
Rust
340 lines
11 KiB
Rust
//! Discovery and optional download of the Android SDK Build-Tools and a Java
|
|
//! 17+ runtime used to zip-align and sign patched APKs.
|
|
use anyhow::{Context, Result, ensure};
|
|
use sha2::{Digest, Sha256};
|
|
use std::{
|
|
env, fs,
|
|
io::{Read, Write},
|
|
path::{Path, PathBuf},
|
|
process::{Command, Stdio},
|
|
time::{Duration, Instant},
|
|
};
|
|
use zip::ZipArchive;
|
|
|
|
const ALIGN: &str = if cfg!(windows) {
|
|
"zipalign.exe"
|
|
} else {
|
|
"zipalign"
|
|
};
|
|
|
|
/// Locate a Build-Tools directory containing `zipalign` and `apksigner.jar`.
|
|
pub fn build_tools() -> Result<PathBuf> {
|
|
let mut candidates = vec![local_build_tools()?];
|
|
if let Some(p) = env::var_os("HI3_ANDROID_TOOLS") {
|
|
candidates.push(PathBuf::from(p));
|
|
}
|
|
for key in ["ANDROID_HOME", "ANDROID_SDK_ROOT"] {
|
|
if let Some(root) = env::var_os(key) {
|
|
let path = PathBuf::from(root).join("build-tools");
|
|
if path.is_dir() {
|
|
let mut entries = fs::read_dir(path)?
|
|
.filter_map(|e| e.ok().map(|v| v.path()))
|
|
.collect::<Vec<_>>();
|
|
entries.sort();
|
|
entries.reverse();
|
|
candidates.extend(entries);
|
|
}
|
|
}
|
|
}
|
|
if let Some(paths) = env::var_os("PATH") {
|
|
candidates.extend(env::split_paths(&paths));
|
|
}
|
|
let exe = env::current_exe()?;
|
|
candidates.push(exe.parent().unwrap().join("tools/android-build-tools"));
|
|
candidates
|
|
.into_iter()
|
|
.find(|p| p.join(ALIGN).is_file() && p.join("lib/apksigner.jar").is_file())
|
|
.context("Android Build-Tools are not installed. Run the Android setup to download them.")
|
|
}
|
|
|
|
pub fn local_build_tools() -> Result<PathBuf> {
|
|
Ok(env::current_exe()?
|
|
.parent()
|
|
.context("patcher directory missing")?
|
|
.join("android-build-tools"))
|
|
}
|
|
|
|
pub fn install_build_tools(destination: &Path) -> Result<()> {
|
|
let (platform, hash) = if cfg!(windows) {
|
|
(
|
|
"windows",
|
|
"5753c679a1b90bcf6fbc9945a2ce39dfb9e74f1df0831a1e1866ae5b594326f0",
|
|
)
|
|
} else if cfg!(target_os = "linux") {
|
|
(
|
|
"linux",
|
|
"bd3a4966912eb8b30ed0d00b0cda6b6543b949d5ffe00bea54c04c81e1561d88",
|
|
)
|
|
} else {
|
|
anyhow::bail!("Automatic Android tools setup supports Windows and Linux only.");
|
|
};
|
|
ensure!(
|
|
cfg!(target_arch = "x86_64"),
|
|
"Automatic Android tools setup supports x86-64 Windows and Linux only."
|
|
);
|
|
ensure!(
|
|
!destination.exists(),
|
|
"Tools folder already exists but is incomplete: {}. Move it aside, then retry.",
|
|
destination.display()
|
|
);
|
|
let work = tempfile::tempdir_in(destination.parent().context("tools parent missing")?)
|
|
.context("cannot write beside the patcher. Move it to a writable folder and retry.")?;
|
|
let archive = work.path().join("tools.zip");
|
|
let total = if cfg!(windows) {
|
|
59_878_107
|
|
} else {
|
|
61_958_799
|
|
};
|
|
download(
|
|
&format!("https://dl.google.com/android/repository/build-tools_r35_{platform}.zip"),
|
|
&archive,
|
|
total,
|
|
)?;
|
|
verify_download(&archive, hash)?;
|
|
let unpacked = work.path().join("unpacked");
|
|
ZipArchive::new(fs::File::open(&archive)?)?.extract(&unpacked)?;
|
|
let folder = fs::read_dir(&unpacked)?
|
|
.next()
|
|
.context("empty Build-Tools archive")??
|
|
.path();
|
|
let align = folder.join(ALIGN);
|
|
ensure!(
|
|
align.is_file() && folder.join("lib/apksigner.jar").is_file(),
|
|
"incomplete Build-Tools archive"
|
|
);
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
fs::set_permissions(&align, fs::Permissions::from_mode(0o755))?;
|
|
}
|
|
fs::rename(folder, destination).context("cannot install Android Build-Tools")?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn local_java() -> Result<PathBuf> {
|
|
Ok(env::current_exe()?
|
|
.parent()
|
|
.context("patcher directory missing")?
|
|
.join("java"))
|
|
}
|
|
|
|
fn java_name(name: &str) -> String {
|
|
if cfg!(windows) {
|
|
format!("{name}.exe")
|
|
} else {
|
|
name.to_owned()
|
|
}
|
|
}
|
|
|
|
/// Path to a `java` launcher backed by a supported (17+) runtime with keytool.
|
|
pub fn check_java() -> Result<PathBuf> {
|
|
let mut candidates = Vec::new();
|
|
if let Some(root) = env::var_os("JAVA_HOME") {
|
|
candidates.push(PathBuf::from(root).join("bin"));
|
|
}
|
|
candidates.push(local_java()?.join("bin"));
|
|
if let Some(paths) = env::var_os("PATH") {
|
|
candidates.extend(env::split_paths(&paths));
|
|
}
|
|
candidates
|
|
.into_iter()
|
|
.find(|bin| supported_java(bin))
|
|
.context("No supported Java found. Android signing requires Java 17+ with keytool.")
|
|
}
|
|
|
|
/// The `java`/`keytool` executable names under a `bin` directory.
|
|
pub fn java_tool(bin: &Path, name: &str) -> PathBuf {
|
|
bin.join(java_name(name))
|
|
}
|
|
|
|
fn java_major(output: &str) -> Option<u32> {
|
|
output.lines().find_map(|line| {
|
|
let version = line.trim().strip_prefix("java.specification.version = ")?;
|
|
version.strip_prefix("1.").unwrap_or(version).parse().ok()
|
|
})
|
|
}
|
|
|
|
fn supported_java(bin: &Path) -> bool {
|
|
if !bin.join(java_name("java")).is_file() || !bin.join(java_name("keytool")).is_file() {
|
|
return false;
|
|
}
|
|
let Ok(output) = Command::new(bin.join(java_name("java")))
|
|
.args(["-XshowSettings:properties", "-version"])
|
|
.output()
|
|
else {
|
|
return false;
|
|
};
|
|
output.status.success()
|
|
&& java_major(&String::from_utf8_lossy(&output.stderr)).is_some_and(|v| v >= 17)
|
|
}
|
|
|
|
pub fn install_java(destination: &Path) -> Result<()> {
|
|
ensure!(
|
|
cfg!(target_arch = "x86_64") && (cfg!(windows) || cfg!(target_os = "linux")),
|
|
"Automatic Java setup supports x86-64 Windows and Linux only."
|
|
);
|
|
ensure!(
|
|
!destination.exists(),
|
|
"Java folder already exists but is unusable: {}. Move it aside, then retry.",
|
|
destination.display()
|
|
);
|
|
let (archive_name, total, hash) = if cfg!(windows) {
|
|
(
|
|
"OpenJDK17U-jdk_x64_windows_hotspot_17.0.20.1_1.zip",
|
|
190_817_615,
|
|
"e53a79c3c3d86865bd7e787903884331068e71321714ffd44f145785affc7cb0",
|
|
)
|
|
} else {
|
|
(
|
|
"OpenJDK17U-jdk_x64_linux_hotspot_17.0.20.1_1.tar.gz",
|
|
193_252_603,
|
|
"3808d1d15e3ec6bd5b84057fb5d84c33d8a1536a258146bcea2e603fc726e08e",
|
|
)
|
|
};
|
|
let work = tempfile::tempdir_in(destination.parent().context("Java parent missing")?)
|
|
.context("cannot write beside the patcher. Move it to a writable folder and retry.")?;
|
|
let archive = work.path().join(archive_name);
|
|
download(
|
|
&format!(
|
|
"https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.20.1%2B1/{archive_name}"
|
|
),
|
|
&archive,
|
|
total,
|
|
)?;
|
|
verify_download(&archive, hash)?;
|
|
let unpacked = work.path().join("unpacked");
|
|
fs::create_dir(&unpacked)?;
|
|
if cfg!(windows) {
|
|
ZipArchive::new(fs::File::open(&archive)?)?.extract(&unpacked)?;
|
|
} else {
|
|
let output = Command::new("tar")
|
|
.arg("-xzf")
|
|
.arg(&archive)
|
|
.arg("-C")
|
|
.arg(&unpacked)
|
|
.output()
|
|
.context("cannot extract Java. Install tar, then retry.")?;
|
|
ensure!(
|
|
output.status.success(),
|
|
"Java extraction failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
let folder = fs::read_dir(&unpacked)?
|
|
.next()
|
|
.context("empty Java archive")??
|
|
.path();
|
|
ensure!(
|
|
supported_java(&folder.join("bin")),
|
|
"downloaded Java cannot run on this system."
|
|
);
|
|
fs::rename(folder, destination).context("cannot install portable Java")?;
|
|
Ok(())
|
|
}
|
|
|
|
fn download(url: &str, archive: &Path, total: u64) -> Result<()> {
|
|
let mut downloaded = 0;
|
|
let started = Instant::now();
|
|
let mut updated = started;
|
|
let mut file = fs::File::create(archive)?;
|
|
let errors = tempfile::tempfile()?;
|
|
let mut child = Command::new(if cfg!(windows) { "curl.exe" } else { "curl" })
|
|
.args([
|
|
"--fail",
|
|
"--location",
|
|
"--silent",
|
|
"--show-error",
|
|
"--proto",
|
|
"=https",
|
|
"--proto-redir",
|
|
"=https",
|
|
"--connect-timeout",
|
|
"30",
|
|
"--max-time",
|
|
"600",
|
|
])
|
|
.arg(url)
|
|
.stdout(Stdio::piped())
|
|
.stderr(errors.try_clone()?)
|
|
.spawn()
|
|
.context("cannot start curl. Install curl, then retry the download.")?;
|
|
let transfer = (|| -> Result<()> {
|
|
draw_download(downloaded, total, started.elapsed())?;
|
|
let mut stream = child.stdout.take().context("missing download stream")?;
|
|
let mut buffer = [0; 64 * 1024];
|
|
loop {
|
|
let count = stream.read(&mut buffer)?;
|
|
if count == 0 {
|
|
break;
|
|
}
|
|
file.write_all(&buffer[..count])?;
|
|
downloaded += count as u64;
|
|
if updated.elapsed() >= Duration::from_millis(250) {
|
|
draw_download(downloaded, total, started.elapsed())?;
|
|
updated = Instant::now();
|
|
}
|
|
}
|
|
Ok(())
|
|
})();
|
|
if transfer.is_err() {
|
|
let _ = child.kill();
|
|
}
|
|
let status = child.wait()?;
|
|
if transfer.is_ok() {
|
|
draw_download(downloaded, total, started.elapsed())?;
|
|
}
|
|
println!();
|
|
transfer?;
|
|
let mut errors = errors;
|
|
use std::io::{Seek, SeekFrom};
|
|
errors.seek(SeekFrom::Start(0))?;
|
|
let mut message = String::new();
|
|
errors.read_to_string(&mut message)?;
|
|
ensure!(status.success(), "download failed: {}", message.trim());
|
|
ensure!(downloaded == total, "incomplete download");
|
|
drop(file);
|
|
Ok(())
|
|
}
|
|
|
|
fn download_progress(downloaded: u64, total: u64, elapsed: Duration) -> String {
|
|
let fraction = (downloaded as f64 / total as f64).min(1.0);
|
|
let filled = (fraction * 20.0) as usize;
|
|
let speed = downloaded as f64 / elapsed.as_secs_f64().max(0.001);
|
|
let remaining = if downloaded >= total {
|
|
"00:00".to_owned()
|
|
} else if downloaded == 0 {
|
|
"--:--".to_owned()
|
|
} else {
|
|
let seconds = ((total - downloaded) as f64 / speed).ceil() as u64;
|
|
format!("{:02}:{:02}", seconds / 60, seconds % 60)
|
|
};
|
|
format!(
|
|
"Downloading ... [{}{}] {:3.0}% {:.2} MB/s ETA {remaining}",
|
|
"X".repeat(filled),
|
|
" ".repeat(20 - filled),
|
|
fraction * 100.0,
|
|
speed / 1_000_000.0
|
|
)
|
|
}
|
|
|
|
fn draw_download(downloaded: u64, total: u64, elapsed: Duration) -> Result<()> {
|
|
crossterm::execute!(
|
|
std::io::stdout(),
|
|
crossterm::cursor::MoveToColumn(0),
|
|
crossterm::terminal::Clear(crossterm::terminal::ClearType::CurrentLine),
|
|
crossterm::style::Print(download_progress(downloaded, total, elapsed))
|
|
)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn verify_download(archive: &Path, hash: &str) -> Result<()> {
|
|
let mut file = fs::File::open(archive)?;
|
|
let mut digest = Sha256::new();
|
|
std::io::copy(&mut file, &mut digest)?;
|
|
ensure!(
|
|
format!("{:x}", digest.finalize()) == hash,
|
|
"download checksum mismatch; nothing installed."
|
|
);
|
|
Ok(())
|
|
}
|