mirror of
https://git.ely.my.id/Tenshi/Hi3-Patcher
synced 2026-11-04 21:46:48 +01:00
Initial Commit
This commit is contained in:
commit
737780597b
51 files changed
+8675
No files matched your search
@@ -0,0 +1,339 @@
|
||||
//! 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(())
|
||||
}
|
||||
Reference in new issue
Block a user