diff --git a/crates/cli/src/cli/help.rs b/crates/cli/src/cli/help.rs index 9a1ce60..eaadf7f 100644 --- a/crates/cli/src/cli/help.rs +++ b/crates/cli/src/cli/help.rs @@ -34,8 +34,8 @@ pub const fn general() -> &'static str { config set 写配置(走 setting schema 校验) java list 列出可用 Java(含每个 JDK 的实际落点) java install [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版; - --vendor temurin|oracle|microsoft|zulu 钉死一家) - java vendors 列出四家发行版的镜像/支持版本/当前优先级 + --vendor temurin|oracle|microsoft|zulu|termux 钉死一家) + java vendors 列出各发行版的镜像/支持版本/当前优先级 agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP; [--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读) agent config [--port N] 打印 Claude Code / Cursor 的配置样例 @@ -157,7 +157,7 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL %prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java 并打印每个 JDK 的实际落点(managed 项带目录) %prog% java install [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证 -%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序 +%prog% java vendors 列出各发行版的镜像/支持版本/当前优先级顺序 落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走): portable(默认) <数据根>/.ULL/data/java-managed/jdk--/ @@ -168,7 +168,9 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL 发行版回退链(D23):国内 temurin(TUNA) → oracle(华为云→南大) → microsoft(aka.ms) → zulu;国外 temurin → microsoft → oracle → zulu。设置 java.vendorPriority 可改顺序 (只写几家也行,其余按默认序补齐);--vendor 钉死一家、不自动降级。 -每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进 --json 的 chain 块与人读的「降级:」行。", +Termux 上缺省先用系统包(pkg install openjdk-,支持 17/21/25),失败再走归档链; +--vendor termux 只走系统包。每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进 +--json 的 chain 块与人读的「降级:」行。", ), "agent" => Some( "\ diff --git a/crates/cli/src/commands/java_vendors.rs b/crates/cli/src/commands/java_vendors.rs index 96cfb22..e3972eb 100644 --- a/crates/cli/src/commands/java_vendors.rs +++ b/crates/cli/src/commands/java_vendors.rs @@ -8,7 +8,7 @@ use serde_json::{json, Value}; 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 crate::context::Ctx; @@ -114,6 +114,10 @@ fn vendor_json(table: &MirrorTable, item: JdkVendor, index: usize) -> Value { .zulu() .map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)]) .unwrap_or_default(), + JdkVendor::Termux => termux::MIRRORS + .iter() + .map(|mirror| format!("{mirror}(系统包:pkg install openjdk-)")) + .collect(), }; json!({ "rank": index + 1, diff --git a/crates/core/src/game/java/install.rs b/crates/core/src/game/java/install.rs index 654dbc6..fb490d2 100644 --- a/crates/core/src/game/java/install.rs +++ b/crates/core/src/game/java/install.rs @@ -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 { - 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) => { diff --git a/crates/core/src/game/java/mod.rs b/crates/core/src/game/java/mod.rs index 45e0110..dd2aad7 100644 --- a/crates/core/src/game/java/mod.rs +++ b/crates/core/src/game/java/mod.rs @@ -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; diff --git a/crates/core/src/game/java/source.rs b/crates/core/src/game/java/source.rs index 112691f..a463a84 100644 --- a/crates/core/src/game/java/source.rs +++ b/crates/core/src/game/java/source.rs @@ -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 `(默认走 termux 源)")), } } diff --git a/crates/core/src/game/java/termux.rs b/crates/core/src/game/java/termux.rs new file mode 100644 index 0000000..b808e00 --- /dev/null +++ b/crates/core/src/game/java/termux.rs @@ -0,0 +1,251 @@ +//! Termux 系统 JDK(`openjdk-` 包)的安装与定位。 +//! +//! 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--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 { + 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 { + 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 = 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 { + 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 { + let mut failures: Vec = 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-`")) +} + +/// 对单个镜像跑 `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(); + } +} diff --git a/crates/core/src/game/java/vendor.rs b/crates/core/src/game/java/vendor.rs index 76dd270..31c9a4f 100644 --- a/crates/core/src/game/java/vendor.rs +++ b/crates/core/src/game/java/vendor.rs @@ -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-`;只在 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], } } diff --git a/crates/core/src/setting/keys.rs b/crates/core/src/setting/keys.rs index 5e31902..d04063d 100644 --- a/crates/core/src/setting/keys.rs +++ b/crates/core/src/setting/keys.rs @@ -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 版本。 /// diff --git a/crates/core/tests/game/java_vendor_chain.rs b/crates/core/tests/game/java_vendor_chain.rs index 0647e22..db97db5 100644 --- a/crates/core/tests/game/java_vendor_chain.rs +++ b/crates/core/tests/game/java_vendor_chain.rs @@ -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)); } diff --git a/crates/presenter/src/host/java.rs b/crates/presenter/src/host/java.rs index 71229bb..691bd81 100644 --- a/crates/presenter/src/host/java.rs +++ b/crates/presenter/src/host/java.rs @@ -80,6 +80,10 @@ fn sources_of(table: &unlucky_core::download::MirrorTable, vendor: JdkVendor) -> .zulu() .map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)]) .unwrap_or_default(), + JdkVendor::Termux => unlucky_core::game::java::termux::MIRRORS + .iter() + .map(|mirror| format!("{mirror}(系统包:pkg install openjdk-)")) + .collect(), } } diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index ab16508..0bb732b 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -135,6 +135,15 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ (fontdb 不扫 Android 的字体目录),无需额外环境变量。 - 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。 +### 4.1 Termux 上的 Java + +`ull java install ` 在 Termux 上**默认先用系统包**(`openjdk-17/21/25`): + +- 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序尝试; +- `apt-get` 的 lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不改用户的 apt 配置; +- 装完从 `$PREFIX/lib/jvm/java--openjdk` 发现,`ull java list` 直接可见; +- `--vendor termux` 只走系统包;`temurin|oracle|microsoft|zulu` 仍走归档下载链。 + ## 5. GPU(未在 glibc 侧验证) termux-x11 的 X 客户端**可以**走硬件:本机 bionic 栈实测