feat(java): Termux 上自动安装系统 openjdk(内置镜像 + 隔离 apt 状态)
- 新增 game::java::termux:内置 SAU/TUNA/USTC/官方四个 termux-main 镜像, 按顺序 apt-get update+install;lists/cache 隔离在 $PREFIX/tmp/ull-java-termux, 不改用户 apt 配置;装完从 $PREFIX/lib/jvm 定位,幂等(已装则不走网络) - JdkVendor 增加 termux(17/21/25),--vendor termux 只走系统包; Termux 上缺省安装先试系统包,失败再走归档链并带出每一跳原因 - standard_roots 已能扫 $PREFIX/lib/jvm(上一条提交),装完即被 java list 发现 - CLI/presenter 的 vendor 展示与帮助文案同步;设置白名单与契约测试同步 - 文档补 Termux 上的 Java 行为
This commit is contained in:
1 parent
16c61ca8b7
commit
c9efdf0fd1
11 files changed
+379
-11
No files matched your search
@@ -23,7 +23,7 @@ use super::error::{io_error, JavaError, JavaErrorKind};
|
||||
use super::extract;
|
||||
use super::location::{JavaInstallRoot, LocationMode};
|
||||
use super::probe::{self, JavaOrigin, JavaRuntime};
|
||||
use super::release::JdkRelease;
|
||||
use super::release::{JdkArchive, JdkRelease};
|
||||
use super::source::{self, ChainLog};
|
||||
use super::target::AdoptiumTarget;
|
||||
use super::vendor::{self, JdkVendor};
|
||||
@@ -184,7 +184,6 @@ pub async fn install(
|
||||
location: &JavaInstallRoot,
|
||||
request: InstallRequest,
|
||||
) -> Result<InstalledJdk, JavaError> {
|
||||
let target = AdoptiumTarget::host()?;
|
||||
let domestic = engine.region().is_domestic();
|
||||
let priority = request.order(domestic);
|
||||
let mut chain = ChainLog::new();
|
||||
@@ -192,6 +191,71 @@ pub async fn install(
|
||||
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
||||
let _ = super::location::cleanup_orphans(&location.root);
|
||||
|
||||
// Termux:JDK 由系统包管理器装进 `$PREFIX`,不是可下载的归档。
|
||||
// 默认(没钉发行版)时优先走它;`--vendor termux` 时只走它。
|
||||
let want_termux = match request.vendor {
|
||||
Some(JdkVendor::Termux) => true,
|
||||
Some(_) => false,
|
||||
None => super::termux::available(),
|
||||
};
|
||||
if want_termux {
|
||||
if !super::termux::available() {
|
||||
return Err(
|
||||
JavaError::new(JavaErrorKind::Unsupported, "termux 源只在 Termux 里可用")
|
||||
.with_hint("普通 Linux/Windows 用 --vendor temurin|oracle|microsoft|zulu"),
|
||||
);
|
||||
}
|
||||
match super::termux::install(request.major).await {
|
||||
Ok(home) => {
|
||||
let mut runtime = probe::probe(&extract::java_binary(&home), JavaOrigin::Managed);
|
||||
runtime.vendor = Some(JdkVendor::Termux.id().to_string());
|
||||
let version = runtime
|
||||
.version
|
||||
.clone()
|
||||
.unwrap_or_else(|| request.major.to_string());
|
||||
let release = JdkRelease {
|
||||
vendor: JdkVendor::Termux,
|
||||
major: request.major,
|
||||
version,
|
||||
semver: None,
|
||||
release_name: format!("Termux openjdk-{}", request.major),
|
||||
archive: JdkArchive {
|
||||
name: format!("openjdk-{}.deb", request.major),
|
||||
url: String::new(),
|
||||
sha256: String::new(),
|
||||
size: 0,
|
||||
},
|
||||
};
|
||||
chain.hit(JdkVendor::Termux, format!("命中 {}", release.summary()));
|
||||
return Ok(InstalledJdk {
|
||||
major: release.major,
|
||||
vendor: JdkVendor::Termux,
|
||||
release,
|
||||
root: home,
|
||||
mode: location.mode,
|
||||
archive: PathBuf::new(),
|
||||
runtime,
|
||||
downloaded: Some("termux".to_string()),
|
||||
bytes: 0,
|
||||
files: 0,
|
||||
extracted: false,
|
||||
chain,
|
||||
});
|
||||
}
|
||||
Err(error) => {
|
||||
if request.vendor == Some(JdkVendor::Termux) {
|
||||
return Err(error);
|
||||
}
|
||||
chain.failed(JdkVendor::Termux, error.message().to_string());
|
||||
reasons.push(format!("termux:{}", error.message()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 归档发行版需要 Adoptium 的平台参数;Termux/Android 上这里会 Unsupported,
|
||||
// 不提前 `?` 退出——让每一家都记一条降级原因,错误里能看见全貌。
|
||||
let target = AdoptiumTarget::host();
|
||||
|
||||
for candidate in &priority {
|
||||
let vendor = *candidate;
|
||||
if !vendor.supports(request.major) {
|
||||
@@ -205,6 +269,15 @@ pub async fn install(
|
||||
reasons.push(format!("{}:{reason}", vendor.id()));
|
||||
continue;
|
||||
}
|
||||
let target = match &target {
|
||||
Ok(target) => *target,
|
||||
Err(error) => {
|
||||
let message = error.message().to_string();
|
||||
chain.failed(vendor, message.clone());
|
||||
reasons.push(format!("{}:{message}", vendor.id()));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let release = match source::fetch(engine, vendor, request.major, target).await {
|
||||
Ok(release) => release,
|
||||
Err(error) => {
|
||||
|
||||
@@ -54,6 +54,7 @@ pub mod release;
|
||||
pub mod select;
|
||||
pub mod source;
|
||||
pub mod target;
|
||||
pub mod termux;
|
||||
pub mod vendor;
|
||||
pub mod zulu;
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ pub async fn fetch(
|
||||
})?;
|
||||
zulu::fetch_latest(client, table, major, target).await
|
||||
}
|
||||
JdkVendor::Termux => Err(JavaError::new(
|
||||
super::error::JavaErrorKind::Unsupported,
|
||||
"Termux 的 JDK 由系统包管理器安装,不走归档下载链",
|
||||
)
|
||||
.with_hint("在 Termux 里用 `ull java install <major>`(默认走 termux 源)")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
//! Termux 系统 JDK(`openjdk-<major>` 包)的安装与定位。
|
||||
//!
|
||||
//! Termux 的 openjdk 是 `.deb`(bionic),由 apt/dpkg 装进 `$PREFIX`,与 Temurin 的
|
||||
//! zip/tar.gz 归档不是同一形态,所以**不走** `source::fetch` 的归档链。这里委托包管理器:
|
||||
//!
|
||||
//! 1. 用内置镜像生成一份**隔离**的 sources.list;
|
||||
//! 2. `apt-get update && install` 的 lists/cache 都指向临时目录,不污染用户的 apt 状态;
|
||||
//! 3. 装完在 `$PREFIX/lib/jvm` 下找 `java-<major>-openjdk`,用 `bin/java` 是否存在做判据。
|
||||
//!
|
||||
//! 镜像顺序是实测可用的国内源;全部失败才报错,错误里带每一跳的原因。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::platform::paths::ProcessEnv;
|
||||
use crate::platform::termux as host;
|
||||
|
||||
use super::error::{JavaError, JavaErrorKind};
|
||||
|
||||
/// 内置镜像(termux-main 仓库根)。顺序即尝试顺序。
|
||||
pub const MIRRORS: &[&str] = &[
|
||||
"https://mirrors.sau.edu.cn/termux/apt/termux-main",
|
||||
"https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main",
|
||||
"https://mirrors.ustc.edu.cn/termux/apt/termux-main",
|
||||
"https://packages.termux.dev/apt/termux-main",
|
||||
];
|
||||
|
||||
/// 当前宿主是不是 Termux。
|
||||
#[must_use]
|
||||
pub fn available() -> bool {
|
||||
host::is_termux(&ProcessEnv)
|
||||
}
|
||||
|
||||
/// 大版本 → Termux 包名(主源只有 17 / 21 / 25)。
|
||||
#[must_use]
|
||||
pub fn package_for(major: u32) -> Option<String> {
|
||||
matches!(major, 17 | 21 | 25).then(|| format!("openjdk-{major}"))
|
||||
}
|
||||
|
||||
/// `$PREFIX/lib/jvm` 下与 `major` 对应的 JDK 根(`bin/java` 必须存在)。
|
||||
#[must_use]
|
||||
pub fn installed_home(prefix: &Path, major: u32) -> Option<PathBuf> {
|
||||
let root = prefix.join("lib").join("jvm");
|
||||
let entries = std::fs::read_dir(root).ok()?;
|
||||
let dashed = format!("java-{major}-openjdk");
|
||||
let plain = format!("openjdk-{major}");
|
||||
let mut found: Vec<PathBuf> = entries
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir() && path.join("bin").join("java").is_file())
|
||||
.filter(|path| {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name == dashed || name == plain)
|
||||
})
|
||||
.collect();
|
||||
found.sort();
|
||||
found.into_iter().next()
|
||||
}
|
||||
|
||||
/// 一条 `deb` 行(测试与安装共用同一份拼法)。
|
||||
#[must_use]
|
||||
pub fn sources_line(mirror: &str) -> String {
|
||||
format!("deb {} stable main\n", mirror.trim_end_matches('/'))
|
||||
}
|
||||
|
||||
/// 装一个 Termux JDK,返回它的根目录。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 当前不在 Termux、主源没有该大版本、所有镜像都失败、装完仍找不到 `bin/java` 时返回错误。
|
||||
pub async fn install(major: u32) -> Result<PathBuf, JavaError> {
|
||||
let Some(package) = package_for(major) else {
|
||||
return Err(JavaError::new(
|
||||
JavaErrorKind::Unsupported,
|
||||
format!("Termux 主源没有 Java {major}(只有 17 / 21 / 25)"),
|
||||
)
|
||||
.with_hint("`pkg search openjdk` 看可用包;其它大版本请用归档发行版"));
|
||||
};
|
||||
let Some(prefix) = host::prefix(&ProcessEnv) else {
|
||||
return Err(
|
||||
JavaError::new(JavaErrorKind::Unsupported, "termux 源只在 Termux 里可用")
|
||||
.with_hint("普通 Linux/Windows 用 Temurin / Oracle / MS / Zulu"),
|
||||
);
|
||||
};
|
||||
|
||||
// 幂等:已经装了同一大版本就不碰网络。
|
||||
if let Some(home) = installed_home(&prefix, major) {
|
||||
return Ok(home);
|
||||
}
|
||||
|
||||
let workspace = workspace_dir(&prefix, major);
|
||||
std::fs::create_dir_all(&workspace).map_err(|error| {
|
||||
JavaError::new(
|
||||
JavaErrorKind::Io,
|
||||
format!(
|
||||
"建 Termux 安装临时目录失败({}):{error}",
|
||||
workspace.display()
|
||||
),
|
||||
)
|
||||
})?;
|
||||
|
||||
let package_for_task = package.clone();
|
||||
tokio::task::spawn_blocking(move || run_install(&prefix, &workspace, &package_for_task, major))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
JavaError::new(
|
||||
JavaErrorKind::Io,
|
||||
format!("Termux 安装任务没能完成:{error}"),
|
||||
)
|
||||
})?
|
||||
}
|
||||
|
||||
/// 逐镜像尝试 `apt-get`(阻塞,跑在 `spawn_blocking` 里)。
|
||||
fn run_install(
|
||||
prefix: &Path,
|
||||
workspace: &Path,
|
||||
package: &str,
|
||||
major: u32,
|
||||
) -> Result<PathBuf, JavaError> {
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
for mirror in MIRRORS {
|
||||
match apt_install(prefix, workspace, mirror, package) {
|
||||
Ok(()) => {
|
||||
if let Some(home) = installed_home(prefix, major) {
|
||||
return Ok(home);
|
||||
}
|
||||
failures.push(format!("{mirror}:装完找不到 bin/java"));
|
||||
}
|
||||
Err(error) => failures.push(format!("{mirror}:{error}")),
|
||||
}
|
||||
}
|
||||
Err(JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
format!(
|
||||
"Termux 包安装失败(试过 {} 个镜像):{}",
|
||||
MIRRORS.len(),
|
||||
failures.join(";")
|
||||
),
|
||||
)
|
||||
.with_hint("确认 apt 可用、磁盘空间足够,或手动 `pkg install openjdk-<major>`"))
|
||||
}
|
||||
|
||||
/// 对单个镜像跑 `apt-get update` + `install`(lists/cache 隔离在 workspace 内)。
|
||||
fn apt_install(prefix: &Path, workspace: &Path, mirror: &str, package: &str) -> Result<(), String> {
|
||||
let sources = workspace.join("sources.list");
|
||||
std::fs::write(&sources, sources_line(mirror))
|
||||
.map_err(|error| format!("写 sources.list 失败:{error}"))?;
|
||||
|
||||
let lists = workspace.join("lists");
|
||||
let cache = workspace.join("cache");
|
||||
let archives = workspace.join("archives");
|
||||
for dir in [&lists, &cache, &archives] {
|
||||
std::fs::create_dir_all(dir).map_err(|error| format!("建临时目录失败:{error}"))?;
|
||||
}
|
||||
|
||||
let options = [
|
||||
format!("Dir::Etc::sourcelist={}", sources.display()),
|
||||
"Dir::Etc::sourceparts=-".to_string(),
|
||||
format!("Dir::State::lists={}", lists.display()),
|
||||
format!("Dir::Cache={}", cache.display()),
|
||||
format!("Dir::Cache::archives={}", archives.display()),
|
||||
"Acquire::ForceIPv4=true".to_string(),
|
||||
"Acquire::Retries=2".to_string(),
|
||||
"Acquire::http::Timeout=30".to_string(),
|
||||
"Acquire::https::Timeout=30".to_string(),
|
||||
];
|
||||
|
||||
run_apt(prefix, &options, &["update"])?;
|
||||
run_apt(
|
||||
prefix,
|
||||
&options,
|
||||
&["install", "-y", "--no-install-suggests", package],
|
||||
)
|
||||
}
|
||||
|
||||
/// 执行一次 `apt-get`(`-o` 选项在前,子命令在后)。
|
||||
fn run_apt(prefix: &Path, options: &[String], args: &[&str]) -> Result<(), String> {
|
||||
let apt = prefix.join("bin").join("apt-get");
|
||||
let mut command = Command::new(&apt);
|
||||
command.env("DEBIAN_FRONTEND", "noninteractive");
|
||||
for option in options {
|
||||
command.arg("-o").arg(option);
|
||||
}
|
||||
command.args(args);
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|error| format!("执行 {} 失败:{error}", apt.display()))?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let last = stderr
|
||||
.lines()
|
||||
.rev()
|
||||
.find(|line| !line.trim().is_empty())
|
||||
.unwrap_or("(无 stderr 输出)");
|
||||
Err(format!(
|
||||
"apt-get {} 退出 {}:{last}",
|
||||
args.first().copied().unwrap_or("?"),
|
||||
output.status.code().unwrap_or(-1)
|
||||
))
|
||||
}
|
||||
|
||||
/// 临时工作区(按大版本区分,避免两次安装互相踩 lists)。
|
||||
fn workspace_dir(prefix: &Path, major: u32) -> PathBuf {
|
||||
prefix
|
||||
.join("tmp")
|
||||
.join("ull-java-termux")
|
||||
.join(format!("jdk-{major}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{installed_home, package_for, sources_line, MIRRORS};
|
||||
|
||||
#[test]
|
||||
fn only_the_majors_that_termux_ships_are_installable() {
|
||||
assert_eq!(package_for(17).as_deref(), Some("openjdk-17"));
|
||||
assert_eq!(package_for(21).as_deref(), Some("openjdk-21"));
|
||||
assert_eq!(package_for(25).as_deref(), Some("openjdk-25"));
|
||||
assert_eq!(package_for(8), None);
|
||||
assert_eq!(package_for(11), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sources_line_has_one_deb_and_keeps_the_mirror_root() {
|
||||
let line = sources_line("https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main/");
|
||||
assert_eq!(
|
||||
line,
|
||||
"deb https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main stable main\n"
|
||||
);
|
||||
assert!(MIRRORS.iter().all(|mirror| mirror.starts_with("https://")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_home_requires_bin_java_and_matching_major() {
|
||||
let base = std::env::temp_dir().join(format!("ull-termux-home-{}", std::process::id()));
|
||||
let good = base.join("lib").join("jvm").join("java-21-openjdk");
|
||||
let empty = base.join("lib").join("jvm").join("java-17-openjdk");
|
||||
std::fs::create_dir_all(good.join("bin")).unwrap();
|
||||
std::fs::write(good.join("bin").join("java"), b"").unwrap();
|
||||
std::fs::create_dir_all(&empty).unwrap();
|
||||
|
||||
assert_eq!(installed_home(&base, 21), Some(good));
|
||||
assert_eq!(installed_home(&base, 17), None, "没有 bin/java 的目录不算");
|
||||
assert_eq!(installed_home(&base, 25), None);
|
||||
|
||||
std::fs::remove_dir_all(&base).ok();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,10 @@
|
||||
//! `zulu` / `api`(Temurin)四个模块里。
|
||||
|
||||
/// 设置键 `java.vendorPriority` 的允许值(顺序即默认序的覆盖来源)。
|
||||
pub const VENDOR_IDS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
|
||||
///
|
||||
/// `termux` 只在 Termux 宿主上可用(系统包管理器安装),默认序里不出现;
|
||||
/// 显式写进优先级或 `--vendor termux` 才走它。
|
||||
pub const VENDOR_IDS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||
|
||||
/// 一家 JDK 发行版。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -28,11 +31,19 @@ pub enum JdkVendor {
|
||||
Microsoft,
|
||||
/// Azul Zulu(第四备选,API 元数据 + CDN 直链)。
|
||||
Zulu,
|
||||
/// Termux 的系统 openjdk(`pkg install openjdk-<major>`;只在 Termux 里可用)。
|
||||
Termux,
|
||||
}
|
||||
|
||||
impl JdkVendor {
|
||||
/// 全部发行版(枚举顺序,不是优先级顺序)。
|
||||
pub const ALL: [Self; 4] = [Self::Temurin, Self::Oracle, Self::Microsoft, Self::Zulu];
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::Temurin,
|
||||
Self::Oracle,
|
||||
Self::Microsoft,
|
||||
Self::Zulu,
|
||||
Self::Termux,
|
||||
];
|
||||
|
||||
/// 稳定 id(目录名段、`--vendor` 取值、设置值、JSON 字段)。
|
||||
#[must_use]
|
||||
@@ -42,6 +53,7 @@ impl JdkVendor {
|
||||
Self::Oracle => "oracle",
|
||||
Self::Microsoft => "microsoft",
|
||||
Self::Zulu => "zulu",
|
||||
Self::Termux => "termux",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +65,7 @@ impl JdkVendor {
|
||||
Self::Oracle => "Oracle OpenJDK",
|
||||
Self::Microsoft => "Microsoft OpenJDK",
|
||||
Self::Zulu => "Azul Zulu",
|
||||
Self::Termux => "Termux OpenJDK",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +79,7 @@ impl JdkVendor {
|
||||
Some(Self::Microsoft)
|
||||
}
|
||||
"zulu" | "azul" => Some(Self::Zulu),
|
||||
"termux" | "termux-openjdk" | "pkg" => Some(Self::Termux),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -80,6 +94,7 @@ impl JdkVendor {
|
||||
Self::Temurin | Self::Zulu => major >= 8,
|
||||
Self::Oracle => major >= 9,
|
||||
Self::Microsoft => matches!(major, 11 | 17 | 21 | 25),
|
||||
Self::Termux => matches!(major, 17 | 21 | 25),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +106,7 @@ impl JdkVendor {
|
||||
Self::Oracle => &[17, 21, 25],
|
||||
Self::Microsoft => &[11, 17, 21, 25],
|
||||
Self::Zulu => &[8, 11, 17, 21, 25],
|
||||
Self::Termux => &[17, 21, 25],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ pub const JAVA_INSTALL_ROOT_UNSET: &str = "";
|
||||
///
|
||||
/// 与 `crate::game::java::vendor::VENDOR_IDS` 必须是同一份集合——setting 层不依赖 game 层
|
||||
/// (分层纪律),一致性由 `tests/setting/java_install_keys.rs` 的契约测试固定。
|
||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
|
||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||
|
||||
/// launcher.json 当前 schema 版本。
|
||||
///
|
||||
|
||||
@@ -175,7 +175,10 @@ fn sha256_texts_are_parsed_the_same_way_for_every_upstream() {
|
||||
|
||||
#[test]
|
||||
fn vendor_ids_are_the_setting_whitelist() {
|
||||
assert_eq!(VENDOR_IDS, &["temurin", "oracle", "microsoft", "zulu"]);
|
||||
assert_eq!(
|
||||
VENDOR_IDS,
|
||||
&["temurin", "oracle", "microsoft", "zulu", "termux"]
|
||||
);
|
||||
for id in VENDOR_IDS {
|
||||
assert_eq!(JdkVendor::parse(id).map(|vendor| vendor.id()), Some(*id));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user