From e090c67297490d3740b1aae528097a670a2ab140 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 22:31:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(platform):=20=E8=AF=86=E5=88=AB=20Termux?= =?UTF-8?q?=20=E5=AE=BF=E4=B8=BB=E5=B9=B6=E6=8A=8A=20$PREFIX/lib/jvm=20?= =?UTF-8?q?=E7=BA=B3=E5=85=A5=20JDK=20=E6=89=AB=E6=8F=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 platform::termux:用 TERMUX_VERSION / 默认 PREFIX 识别 Termux, 推导 $PREFIX 与 $PREFIX/lib/jvm,不把路径写死进调用方 - java::discover 的 standard_roots 在 Unix 分支追加 Termux 的 JVM 根, 环境可注入(standard_roots_in),补上「Termux 装了 openjdk 但没设 JAVA_HOME/PATH 就发现不了」的盲点 - 单测覆盖识别、负例与扫描结果 --- crates/core/src/game/java/discover.rs | 54 +++++++++++++++++- crates/core/src/platform/mod.rs | 1 + crates/core/src/platform/termux.rs | 82 +++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 crates/core/src/platform/termux.rs diff --git a/crates/core/src/game/java/discover.rs b/crates/core/src/game/java/discover.rs index b44a3e5..7cc2700 100644 --- a/crates/core/src/game/java/discover.rs +++ b/crates/core/src/game/java/discover.rs @@ -8,6 +8,7 @@ //! | --- | --- | //! | Windows | `%ProgramFiles%\{Java, Eclipse Adoptium, Microsoft, Zulu, Amazon Corretto, BellSoft, Semeru}\*`(含 `ProgramFiles(x86)` / `ProgramW6432` 变体,WOW64 才看得到 32 位安装)、`%APPDATA%\.minecraft\runtime\*`(**官方启动器装的运行时**,跨产品兼容)、`HKLM\SOFTWARE\JavaSoft\JDK`(老 Oracle 写的,新版发行版通常没有→容错跳过) | //! | Linux | `/usr/lib/jvm/*`、`/usr/java/*` | +//! | Termux | `$PREFIX/lib/jvm/*`(`java-17-openjdk` 这类;`$PREFIX` 由 [`crate::platform::termux`] 识别) | //! | macOS | `/Library/Java/JavaVirtualMachines/*.jdk/Contents/Home` | //! //! 三个纪律: @@ -22,6 +23,9 @@ use std::path::{Path, PathBuf}; use std::process::Command; +use crate::platform::paths::{EnvSource, ProcessEnv}; +use crate::platform::termux; + use super::extract::java_binary_name; use super::probe::{self, discover_managed_roots, system_candidates, JavaOrigin, JavaRuntime}; @@ -73,6 +77,12 @@ pub fn scan_java_binaries() -> Vec { /// 候选 JDK 根目录(`bin/java` 的父目录的父目录)。 #[must_use] pub fn standard_roots() -> Vec { + standard_roots_in(&ProcessEnv) +} + +/// 同 [`standard_roots`],但环境变量可注入(测试 / 便携化封装用)。 +#[must_use] +pub fn standard_roots_in(env: &dyn EnvSource) -> Vec { let mut roots = Vec::new(); if cfg!(windows) { for variable in ["ProgramW6432", "ProgramFiles", "ProgramFiles(x86)"] { @@ -99,6 +109,10 @@ pub fn standard_roots() -> Vec { for base in ["/usr/lib/jvm", "/usr/java"] { roots.extend(child_dirs(Path::new(base))); } + // Termux 的 JDK 落在 `$PREFIX/lib/jvm`(如 `java-17-openjdk`),不在 `/usr` 下。 + if let Some(root) = termux::jvm_root(env) { + roots.extend(child_dirs(&root)); + } } roots.sort(); roots.dedup(); @@ -211,8 +225,9 @@ pub fn path_key(path: &Path) -> String { #[cfg(test)] mod tests { - use super::{dedupe, parse_java_home, path_key, standard_roots}; + use super::{dedupe, parse_java_home, path_key, standard_roots, standard_roots_in}; use crate::game::java::probe::{JavaOrigin, JavaRuntime}; + use crate::platform::paths::MapEnv; fn runtime(path: &str, origin: JavaOrigin) -> JavaRuntime { JavaRuntime { @@ -323,4 +338,41 @@ mod tests { let roots = standard_roots(); assert!(roots.iter().all(|root| !root.as_os_str().is_empty())); } + + #[cfg(unix)] + #[test] + fn termux_jvm_root_is_scanned() { + let base = std::env::temp_dir().join(format!("ull-termux-jvm-{}", std::process::id())); + let jdk = base.join("lib").join("jvm").join("java-17-openjdk"); + std::fs::create_dir_all(jdk.join("bin")).unwrap(); + std::fs::write(jdk.join("bin").join("java"), b"").unwrap(); + + let env = MapEnv::new() + .with("TERMUX_VERSION", "0.118.3") + .with("PREFIX", base.to_str().unwrap()); + let roots = standard_roots_in(&env); + assert!( + roots.contains(&jdk), + "Termux 的 $PREFIX/lib/jvm 必须进扫描表:{roots:?}" + ); + + std::fs::remove_dir_all(&base).ok(); + } + + #[cfg(unix)] + #[test] + fn non_termux_env_does_not_scan_prefix() { + let base = std::env::temp_dir().join(format!("ull-not-termux-{}", std::process::id())); + let jdk = base.join("lib").join("jvm").join("java-17-openjdk"); + std::fs::create_dir_all(&jdk).unwrap(); + + let env = MapEnv::new().with("PREFIX", base.to_str().unwrap()); + let roots = standard_roots_in(&env); + assert!( + !roots.contains(&jdk), + "非 Termux 不该扫 $PREFIX/lib/jvm:{roots:?}" + ); + + std::fs::remove_dir_all(&base).ok(); + } } diff --git a/crates/core/src/platform/mod.rs b/crates/core/src/platform/mod.rs index 5691acb..964d0a4 100644 --- a/crates/core/src/platform/mod.rs +++ b/crates/core/src/platform/mod.rs @@ -40,6 +40,7 @@ pub mod os_info; pub mod paths; pub mod receipt; pub mod redirect; +pub mod termux; pub mod theme_probe; pub mod vk_probe; pub mod wallpaper; diff --git a/crates/core/src/platform/termux.rs b/crates/core/src/platform/termux.rs new file mode 100644 index 0000000..99ea5ef --- /dev/null +++ b/crates/core/src/platform/termux.rs @@ -0,0 +1,82 @@ +//! Termux(Android 上的用户空间)宿主识别。 +//! +//! Termux 的软件前缀不在 `/usr`,而是 `$PREFIX`(典型值 +//! `/data/data/com.termux/files/usr`);JDK 由 `pkg install openjdk-17` 落到 +//! `$PREFIX/lib/jvm/java-17-openjdk`。这里只做**识别与路径推导**,不碰包管理, +//! 也不把 `$PREFIX` 当成编译期常量写死。 + +use std::path::PathBuf; + +use super::paths::EnvSource; + +/// Termux 的默认前缀(`PREFIX` 缺失时的兜底)。 +pub const DEFAULT_PREFIX: &str = "/data/data/com.termux/files/usr"; + +/// 当前环境是否运行在 Termux 里。 +/// +/// 判据取 Termux 自己导出的两个变量:`TERMUX_VERSION`(版本)或 `PREFIX` +/// 指向默认前缀。两者都没有时按普通 Linux 处理——不猜。 +#[must_use] +pub fn is_termux(env: &dyn EnvSource) -> bool { + env.var("TERMUX_VERSION").is_some() + || env + .var("PREFIX") + .is_some_and(|prefix| prefix.trim_end_matches('/') == DEFAULT_PREFIX) +} + +/// Termux 的软件前缀(仅当识别为 Termux 时)。 +#[must_use] +pub fn prefix(env: &dyn EnvSource) -> Option { + if !is_termux(env) { + return None; + } + Some(env.var("PREFIX").map_or_else( + || PathBuf::from(DEFAULT_PREFIX), + PathBuf::from, + )) +} + +/// Termux 的 JDK 安装根(`$PREFIX/lib/jvm`)。 +#[must_use] +pub fn jvm_root(env: &dyn EnvSource) -> Option { + prefix(env).map(|prefix| prefix.join("lib").join("jvm")) +} + +#[cfg(test)] +mod tests { + use super::{is_termux, jvm_root, prefix, DEFAULT_PREFIX}; + use crate::platform::paths::MapEnv; + + #[test] + fn plain_linux_is_not_termux() { + let env = MapEnv::new().with("HOME", "/home/u"); + assert!(!is_termux(&env)); + assert_eq!(prefix(&env), None); + assert_eq!(jvm_root(&env), None); + } + + #[test] + fn default_prefix_is_recognised() { + let env = MapEnv::new().with("PREFIX", DEFAULT_PREFIX); + assert!(is_termux(&env)); + assert_eq!( + jvm_root(&env).unwrap(), + std::path::PathBuf::from(DEFAULT_PREFIX).join("lib/jvm") + ); + } + + #[test] + fn version_variable_alone_is_enough() { + let env = MapEnv::new() + .with("TERMUX_VERSION", "0.118.3") + .with("PREFIX", "/data/data/com.termux/files/usr"); + assert!(is_termux(&env)); + } + + #[test] + fn other_prefix_is_not_termux() { + // `PREFIX` 被搬到别的目录不在识别范围内——只有版本变量或默认前缀算数。 + let env = MapEnv::new().with("PREFIX", "/opt/termux"); + assert!(!is_termux(&env)); + } +}