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:
AskaEth committed 2026-11-06 22:54:53 +08:00
1 parent 16c61ca8b7
commit c9efdf0fd1
11 files changed
+379 -11

No files matched your search

+6 -4
View File
@@ -34,8 +34,8 @@ pub const fn general() -> &'static str {
config set <key> <value> 写配置(走 setting schema 校验) config set <key> <value> 写配置(走 setting schema 校验)
java list 列出可用 Java(含每个 JDK 的实际落点) java list 列出可用 Java(含每个 JDK 的实际落点)
java install <major> [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版; java install <major> [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版;
--vendor temurin|oracle|microsoft|zulu 钉死一家) --vendor temurin|oracle|microsoft|zulu|termux 钉死一家)
java vendors 列出四家发行版的镜像/支持版本/当前优先级 java vendors 列出各发行版的镜像/支持版本/当前优先级
agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP; agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP;
[--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读) [--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读)
agent config [--port N] 打印 Claude Code / Cursor 的配置样例 agent config [--port N] 打印 Claude Code / Cursor 的配置样例
@@ -157,7 +157,7 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java %prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
并打印每个 JDK 的实际落点(managed 项带目录) 并打印每个 JDK 的实际落点(managed 项带目录)
%prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证 %prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序 %prog% java vendors 列出各发行版的镜像/支持版本/当前优先级顺序
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走): 落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/ portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
@@ -168,7 +168,9 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL
发行版回退链(D23):国内 temurin(TUNA) → oracle(华为云→南大) → microsoft(aka.ms) 发行版回退链(D23):国内 temurin(TUNA) → oracle(华为云→南大) → microsoft(aka.ms)
→ zulu;国外 temurin → microsoft → oracle → zulu。设置 java.vendorPriority 可改顺序 → zulu;国外 temurin → microsoft → oracle → zulu。设置 java.vendorPriority 可改顺序
(只写几家也行,其余按默认序补齐);--vendor 钉死一家、不自动降级。 (只写几家也行,其余按默认序补齐);--vendor 钉死一家、不自动降级。
每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进 --json 的 chain 块与人读的「降级:」行。", Termux 上缺省先用系统包(pkg install openjdk-<major>,支持 17/21/25),失败再走归档链;
--vendor termux 只走系统包。每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进
--json 的 chain 块与人读的「降级:」行。",
), ),
"agent" => Some( "agent" => Some(
"\ "\
+5 -1
View File
@@ -8,7 +8,7 @@
use serde_json::{json, Value}; use serde_json::{json, Value};
use unlucky_core::download::MirrorTable; use unlucky_core::download::MirrorTable;
use unlucky_core::game::java::{vendor, JdkVendor}; use unlucky_core::game::java::{termux, vendor, JdkVendor};
use super::java::{region_profile, JavaSettings}; use super::java::{region_profile, JavaSettings};
use crate::context::Ctx; use crate::context::Ctx;
@@ -114,6 +114,10 @@ fn vendor_json(table: &MirrorTable, item: JdkVendor, index: usize) -> Value {
.zulu() .zulu()
.map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)]) .map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)])
.unwrap_or_default(), .unwrap_or_default(),
JdkVendor::Termux => termux::MIRRORS
.iter()
.map(|mirror| format!("{mirror}(系统包:pkg install openjdk-<major>)"))
.collect(),
}; };
json!({ json!({
"rank": index + 1, "rank": index + 1,
+75 -2
View File
@@ -23,7 +23,7 @@ use super::error::{io_error, JavaError, JavaErrorKind};
use super::extract; use super::extract;
use super::location::{JavaInstallRoot, LocationMode}; use super::location::{JavaInstallRoot, LocationMode};
use super::probe::{self, JavaOrigin, JavaRuntime}; use super::probe::{self, JavaOrigin, JavaRuntime};
use super::release::JdkRelease; use super::release::{JdkArchive, JdkRelease};
use super::source::{self, ChainLog}; use super::source::{self, ChainLog};
use super::target::AdoptiumTarget; use super::target::AdoptiumTarget;
use super::vendor::{self, JdkVendor}; use super::vendor::{self, JdkVendor};
@@ -184,7 +184,6 @@ pub async fn install(
location: &JavaInstallRoot, location: &JavaInstallRoot,
request: InstallRequest, request: InstallRequest,
) -> Result<InstalledJdk, JavaError> { ) -> Result<InstalledJdk, JavaError> {
let target = AdoptiumTarget::host()?;
let domestic = engine.region().is_domestic(); let domestic = engine.region().is_domestic();
let priority = request.order(domestic); let priority = request.order(domestic);
let mut chain = ChainLog::new(); let mut chain = ChainLog::new();
@@ -192,6 +191,71 @@ pub async fn install(
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。 // 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
let _ = super::location::cleanup_orphans(&location.root); 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 { for candidate in &priority {
let vendor = *candidate; let vendor = *candidate;
if !vendor.supports(request.major) { if !vendor.supports(request.major) {
@@ -205,6 +269,15 @@ pub async fn install(
reasons.push(format!("{}:{reason}", vendor.id())); reasons.push(format!("{}:{reason}", vendor.id()));
continue; 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 { let release = match source::fetch(engine, vendor, request.major, target).await {
Ok(release) => release, Ok(release) => release,
Err(error) => { Err(error) => {
+1
View File
@@ -54,6 +54,7 @@ pub mod release;
pub mod select; pub mod select;
pub mod source; pub mod source;
pub mod target; pub mod target;
pub mod termux;
pub mod vendor; pub mod vendor;
pub mod zulu; pub mod zulu;
+5
View File
@@ -56,6 +56,11 @@ pub async fn fetch(
})?; })?;
zulu::fetch_latest(client, table, major, target).await 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 源)")),
} }
} }
+251
View File
@@ -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();
}
}
+18 -2
View File
@@ -15,7 +15,10 @@
//! `zulu` / `api`(Temurin)四个模块里。 //! `zulu` / `api`(Temurin)四个模块里。
/// 设置键 `java.vendorPriority` 的允许值(顺序即默认序的覆盖来源)。 /// 设置键 `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 发行版。 /// 一家 JDK 发行版。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
@@ -28,11 +31,19 @@ pub enum JdkVendor {
Microsoft, Microsoft,
/// Azul Zulu(第四备选,API 元数据 + CDN 直链)。 /// Azul Zulu(第四备选,API 元数据 + CDN 直链)。
Zulu, Zulu,
/// Termux 的系统 openjdk(`pkg install openjdk-<major>`;只在 Termux 里可用)。
Termux,
} }
impl JdkVendor { 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 字段)。 /// 稳定 id(目录名段、`--vendor` 取值、设置值、JSON 字段)。
#[must_use] #[must_use]
@@ -42,6 +53,7 @@ impl JdkVendor {
Self::Oracle => "oracle", Self::Oracle => "oracle",
Self::Microsoft => "microsoft", Self::Microsoft => "microsoft",
Self::Zulu => "zulu", Self::Zulu => "zulu",
Self::Termux => "termux",
} }
} }
@@ -53,6 +65,7 @@ impl JdkVendor {
Self::Oracle => "Oracle OpenJDK", Self::Oracle => "Oracle OpenJDK",
Self::Microsoft => "Microsoft OpenJDK", Self::Microsoft => "Microsoft OpenJDK",
Self::Zulu => "Azul Zulu", Self::Zulu => "Azul Zulu",
Self::Termux => "Termux OpenJDK",
} }
} }
@@ -66,6 +79,7 @@ impl JdkVendor {
Some(Self::Microsoft) Some(Self::Microsoft)
} }
"zulu" | "azul" => Some(Self::Zulu), "zulu" | "azul" => Some(Self::Zulu),
"termux" | "termux-openjdk" | "pkg" => Some(Self::Termux),
_ => None, _ => None,
} }
} }
@@ -80,6 +94,7 @@ impl JdkVendor {
Self::Temurin | Self::Zulu => major >= 8, Self::Temurin | Self::Zulu => major >= 8,
Self::Oracle => major >= 9, Self::Oracle => major >= 9,
Self::Microsoft => matches!(major, 11 | 17 | 21 | 25), 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::Oracle => &[17, 21, 25],
Self::Microsoft => &[11, 17, 21, 25], Self::Microsoft => &[11, 17, 21, 25],
Self::Zulu => &[8, 11, 17, 21, 25], Self::Zulu => &[8, 11, 17, 21, 25],
Self::Termux => &[17, 21, 25],
} }
} }
+1 -1
View File
@@ -39,7 +39,7 @@ pub const JAVA_INSTALL_ROOT_UNSET: &str = "";
/// ///
/// 与 `crate::game::java::vendor::VENDOR_IDS` 必须是同一份集合——setting 层不依赖 game 层 /// 与 `crate::game::java::vendor::VENDOR_IDS` 必须是同一份集合——setting 层不依赖 game 层
/// (分层纪律),一致性由 `tests/setting/java_install_keys.rs` 的契约测试固定。 /// (分层纪律),一致性由 `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 版本。 /// launcher.json 当前 schema 版本。
/// ///
+4 -1
View File
@@ -175,7 +175,10 @@ fn sha256_texts_are_parsed_the_same_way_for_every_upstream() {
#[test] #[test]
fn vendor_ids_are_the_setting_whitelist() { 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 { for id in VENDOR_IDS {
assert_eq!(JdkVendor::parse(id).map(|vendor| vendor.id()), Some(*id)); assert_eq!(JdkVendor::parse(id).map(|vendor| vendor.id()), Some(*id));
} }
+4
View File
@@ -80,6 +80,10 @@ fn sources_of(table: &unlucky_core::download::MirrorTable, vendor: JdkVendor) ->
.zulu() .zulu()
.map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)]) .map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)])
.unwrap_or_default(), .unwrap_or_default(),
JdkVendor::Termux => unlucky_core::game::java::termux::MIRRORS
.iter()
.map(|mirror| format!("{mirror}(系统包:pkg install openjdk-<major>)"))
.collect(),
} }
} }
+9
View File
@@ -135,6 +135,15 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \
(fontdb 不扫 Android 的字体目录),无需额外环境变量。 (fontdb 不扫 Android 的字体目录),无需额外环境变量。
- 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。 - 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。
### 4.1 Termux 上的 Java
`ull java install <major>` 在 Termux 上**默认先用系统包**(`openjdk-17/21/25`):
- 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序尝试;
- `apt-get` 的 lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不改用户的 apt 配置;
- 装完从 `$PREFIX/lib/jvm/java-<major>-openjdk` 发现,`ull java list` 直接可见;
- `--vendor termux` 只走系统包;`temurin|oracle|microsoft|zulu` 仍走归档下载链。
## 5. GPU(未在 glibc 侧验证) ## 5. GPU(未在 glibc 侧验证)
termux-x11 的 X 客户端**可以**走硬件:本机 bionic 栈实测 termux-x11 的 X 客户端**可以**走硬件:本机 bionic 栈实测