From e090c67297490d3740b1aae528097a670a2ab140 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 22:31:03 +0800 Subject: [PATCH 01/21] =?UTF-8?q?feat(platform):=20=E8=AF=86=E5=88=AB=20Te?= =?UTF-8?q?rmux=20=E5=AE=BF=E4=B8=BB=E5=B9=B6=E6=8A=8A=20$PREFIX/lib/jvm?= =?UTF-8?q?=20=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)); + } +} From 4582a025dbecad1b05f0bf9c2c05d599b1dad752 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 22:31:11 +0800 Subject: [PATCH 02/21] =?UTF-8?q?fix(fonts):=20Termux=20=E4=B8=8A=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=20Android=20=E7=B3=BB=E7=BB=9F=20CJK=20=E5=AD=97?= =?UTF-8?q?=E4=BD=93=EF=BC=8C=E6=B6=88=E9=99=A4=E4=B8=AD=E6=96=87=E6=96=B9?= =?UTF-8?q?=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit glibc 构建跑在 Termux 里时 target_os 仍是 linux,fontdb 只扫 Linux 常规 字体目录,/system/fonts 不在其中,中文实例名与界面文案全渲染成方框。 在字体求值入口按运行时识别 Termux,把 NotoSansCJK 等候选注册进 cosmic-text 的字体库(OnceLock 只加载一次,找不到就静默放过)。 app-iced 直接依赖 unlucky-core 复用平台识别,不再本地复制环境判断。 --- Cargo.lock | 1 + crates/app-iced/Cargo.toml | 3 +++ crates/app-iced/src/fonts.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8728054..7f65aba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5003,6 +5003,7 @@ dependencies = [ "image", "log", "unlucky-cli", + "unlucky-core", "unlucky-design", "unlucky-i18n", "unlucky-presenter", diff --git a/crates/app-iced/Cargo.toml b/crates/app-iced/Cargo.toml index e3cf139..468ab48 100644 --- a/crates/app-iced/Cargo.toml +++ b/crates/app-iced/Cargo.toml @@ -24,6 +24,9 @@ path = "src/lib.rs" # 令牌唯一真源(M61):颜色/排版/形状/间距/图标几何全部只从这里取。 unlucky-design = { path = "../design", version = "0.2.2" } +# 平台识别(Termux / $PREFIX 路径)唯一真源:字体注册这类平台适配不再各写一份环境判断。 +unlucky-core = { path = "../core", version = "0.2.1" } + # M85:UI 框架无关的宿主层(DTO / 状态 reducer / host)。**只登记依赖,本 mission 不接线**—— # 页面用它的时机由后续 iced 页面 mission 决定(接线流程见 crates/presenter/src/lib.rs 的 crate 文档)。 # 与 Slint 版共用同一套 host 与状态语义,这是 D26「重写只换渲染」的落点。 diff --git a/crates/app-iced/src/fonts.rs b/crates/app-iced/src/fonts.rs index 74c5b5e..8690f05 100644 --- a/crates/app-iced/src/fonts.rs +++ b/crates/app-iced/src/fonts.rs @@ -139,8 +139,43 @@ pub fn resolve() -> Resolved { resolve_with(None) } +/// Termux 上把 Android 的系统 CJK 字体注册进 cosmic-text 的字体库。 +/// +/// 这套 glibc 构建跑在 Termux 里(`target_os = "linux"`),fontdb 只扫 Linux 的常规字体目录, +/// Android 的 `/system/fonts` 不在其中——不注册的话中文实例名与界面文案全是方框。 +/// 第一次调用加载一次;找不到任何候选字体时静默放过(由既有回退链兜底)。 +fn register_termux_fonts() { + use std::sync::OnceLock; + + use unlucky_core::platform::paths::ProcessEnv; + use unlucky_core::platform::termux; + + static REGISTERED: OnceLock<()> = OnceLock::new(); + REGISTERED.get_or_init(|| { + if !termux::is_termux(&ProcessEnv) { + return; + } + const CANDIDATES: &[&str] = &[ + "/system/fonts/NotoSansCJK-Regular.ttc", + "/system/fonts/NotoSansCJKSC-Regular.otf", + "/system/fonts/DroidSansFallback.ttf", + ]; + let Ok(mut font_system) = iced::advanced::graphics::text::font_system().write() else { + return; + }; + let db = font_system.raw().db_mut(); + for path in CANDIDATES { + if db.load_font_file(path).is_ok() { + return; + } + } + }); +} + /// 同 [`resolve`],但把配置里 `ui.language` 的显式值喂给中文策略(D28 的第一级)。 pub fn resolve_with(setting: Option<&str>) -> Resolved { + register_termux_fonts(); + let platform = Platform::current(); let faces = system_faces(); From 1e0fc44fed9f06c351c257404874ebcef53f6993 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 22:32:09 +0800 Subject: [PATCH 03/21] =?UTF-8?q?docs(packaging):=20=E8=AE=B0=E5=BD=95=20T?= =?UTF-8?q?ermux=20glibc=20=E6=9E=84=E5=BB=BA=E4=B8=8E=20termux-x11=20?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E9=85=8D=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 packaging/docs/termux-glibc-build.md:glibc 工具链(Rust gnu 组件 + patchelf + clang/lld + Debian libgcc crt)、构建环境(LD_PRELOAD/CA/USTC 镜像)、 termux-x11 运行与截图取证、GPU 现状与已知边界。 支持矩阵四处同步:Termux 行从「GUI 未验证」改为「源码自建、CPU 渲染单机验证过」, 仍标注实验级、不在发布矩阵内。 --- README.md | 5 +- README.zh-CN.md | 2 +- .../docs/compliance-support-boundaries.md | 2 +- packaging/docs/termux-glibc-build.md | 162 ++++++++++++++++++ packaging/release-notes.md | 2 +- 5 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 packaging/docs/termux-glibc-build.md diff --git a/README.md b/README.md index c447a4f..c83ceac 100644 --- a/README.md +++ b/README.md @@ -194,8 +194,9 @@ This list is the reason to believe the rest. None of it is hidden behind marketi report lists every conflict so nothing is overwritten silently. - **Install progress is stage-level**, not byte-level, on the content-library install path. - **No native file chooser.** Import takes drag-and-drop or a pasted path. -- **Android/Termux is experimental and not promised**: no released asset targets it, and the GUI is - unverified there. +- **Android/Termux is experimental and not promised**: no released asset targets it. A GUI build is + possible from source via the glibc toolchain (`packaging/docs/termux-glibc-build.md`); it was + verified once on CPU rendering only. ## Download and run diff --git a/README.zh-CN.md b/README.zh-CN.md index 179462f..b9b544c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -158,7 +158,7 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 文件是被静默覆盖的。 - **内容库的安装进度是阶段级**,不是字节级。 - **没有原生文件选择框。** 导入靠拖拽或粘贴路径。 -- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产,那一侧的 GUI 也未验证。 +- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`),目前只在 CPU 渲染下验证过一次。 ## 下载与运行 diff --git a/packaging/docs/compliance-support-boundaries.md b/packaging/docs/compliance-support-boundaries.md index 87535e3..4e94802 100644 --- a/packaging/docs/compliance-support-boundaries.md +++ b/packaging/docs/compliance-support-boundaries.md @@ -28,7 +28,7 @@ | Linux arm64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+(`ubuntu-22.04-arm` 原生构建,**与 x64 同地板**)、xkbcommon、fontconfig、libGL、xcb | | Linux arm64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client | | Windows 8.1/8/7 | — | 不支持 | 基线已上调(D21) | -| Android Termux + X11 | — | 试验级(不承诺) | 仅 CLI(源码自建),GUI 未验证,不在本发布矩阵内 | +| Android Termux + X11 | — | 试验级(不承诺) | CLI 与 GUI 均需源码自建;GUI 已在单机 CPU 渲染下验证(`packaging/docs/termux-glibc-build.md`),不在本发布矩阵内 | `packaging/release-notes.md` 的 "Support matrix" 表与上表逐行对应(英文版同样带 published / not published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。 diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md new file mode 100644 index 0000000..ab16508 --- /dev/null +++ b/packaging/docs/termux-glibc-build.md @@ -0,0 +1,162 @@ +# Termux + termux-x11 上的 glibc 构建(试验级) + +本页记录**在 Termux 里构建 glibc 版 GUI,并在 termux-x11 里跑起来**的可复现步骤。 +2026-11-06 在一台 OnePlus 15(Android 17、Termux、KirinSU/root 环境)上实测通过: +`--software-renderer --screenshot` 出 1180×760 PNG,中文正常,`msa client_id=configured source=build`。 + +**定位**:试验级、不承诺。发布矩阵里没有面向 Termux 的资产;本文只描述源码自建路径。 +GUI 只在上述设备 + CPU 渲染(tiny-skia)上验证过,GPU 路径见 §5(尚未在 glibc 侧验证)。 + +## 1. 为什么不能直接 `cargo build` + +Termux 的 `rustc` host 是 `aarch64-linux-android`(`target_os = "android"`): + +- `crates/app-iced/Cargo.toml` 的 `x11` / `wayland` feature 只挂在 `cfg(target_os = "linux")` 下; +- iced 的窗口层 winit 0.30 在 android target 上走 Android(NativeActivity)后端, + 会拉 `android-activity` 并要求 `game-activity`/`native-activity` feature,普通进程编不过也跑不了。 + +所以要在 Termux 里产出一个 **`aarch64-unknown-linux-gnu`(glibc)** 二进制,靠 Termux 的 +[glibc 运行时](https://packages.termux.dev/apt/termux-glibc/) 原生执行。 + +## 2. 工具链(一次性) + +以下路径用变量表示,实际可放在任意本地目录(示例放在仓库内的 `.gnu/`,已 gitignore): + +```sh +PREFIX="${PREFIX:-/data/data/com.termux/files/usr}" +G="$PWD/.gnu" +``` + +### 2.1 glibc 运行时 + +确认 `$PREFIX/glibc/lib/ld-linux-aarch64.so.1` 存在(`pkg install glibc-runner` 或装过 glibc 仓库 +的包即可)。**不需要 `gcc-glibc`**:链接用 Termux 自带的 clang + lld,glibc 的 CRT/头文件由 +`glibc` 包提供。 + +### 2.2 Rust gnu 工具链 + +下载 Rust 1.99.1(或与 `rust-version` 兼容的 stable)的 `rustc` / `rust-std` / `cargo` 三个 +`aarch64-unknown-linux-gnu` 组件。官方源在国内很慢,用 USTC 镜像: + +```sh +mkdir -p "$TMPDIR/ull-gnu" && cd "$TMPDIR/ull-gnu" +for c in rustc rust-std cargo; do + curl -fLO "https://mirrors.ustc.edu.cn/rust-static/dist/$c-1.99.1-aarch64-unknown-linux-gnu.tar.xz" +done +``` + +解包后按组件布局合并到 `$G/toolchain`(`rustc/bin`、`cargo/bin` → `toolchain/bin`, +两者的 `lib/` → `toolchain/lib`,`rust-std-*/lib/rustlib/aarch64-unknown-linux-gnu` → +`toolchain/lib/rustlib/`)。 + +### 2.3 改 interpreter / rpath + +工具链里的 `rustc`、`cargo`、`rustdoc` 是 glibc ELF,默认 interpreter 是 `/lib/ld-linux-*.so.1`, +在 Termux 上不存在: + +```sh +LD="$PREFIX/glibc/lib/ld-linux-aarch64.so.1" +for f in "$G/toolchain/bin/rustc" "$G/toolchain/bin/cargo" "$G/toolchain/bin/rustdoc"; do + patchelf --set-interpreter "$LD" --set-rpath "$PREFIX/glibc/lib:$G/toolchain/lib" "$f" +done +``` + +### 2.4 gcc 运行时片段 + +`clang -fuse-ld=lld` 仍然需要 `crtbeginS.o` / `crtendS.o` / `libgcc.a`。从 Debian 的 +`libgcc-14-dev`(arm64)取即可(只解包,不安装): + +```sh +curl -fLO https://deb.debian.org/debian/pool/main/g/gcc-14/libgcc-14-dev_14.2.0-19_arm64.deb +dpkg-deb -x libgcc-14-dev_14.2.0-19_arm64.deb "$G/gnu-support" +# crt/libgcc 在 $G/gnu-support/usr/lib/gcc/aarch64-linux-gnu/14/ +``` + +### 2.5 linker 驱动 + +写一个 `$G/bin/cc-glibc`(`chmod +x`): + +```sh +#!/data/data/com.termux/files/usr/bin/sh +unset LD_PRELOAD +P=$PREFIX +S="<仓库>/.gnu/gnu-support/usr/lib/gcc/aarch64-linux-gnu/14" +C="clang --target=aarch64-unknown-linux-gnu --sysroot=/ -isystem $P/glibc/include \ + -B$P/glibc/lib -B$S -L$S -L$P/glibc/lib -fuse-ld=lld" +for a in "$@"; do + case "$a" in -c|-E|-S) exec $C "$@" ;; esac +done +exec $C -Wl,--dynamic-linker=$P/glibc/lib/ld-linux-aarch64.so.1 -Wl,-rpath,$P/glibc/lib "$@" +``` + +要点:`--sysroot=/` 而不是 `--sysroot=$PREFIX/glibc`——glibc 的 `libc.so` 是含绝对路径的 +linker script,带 sysroot 会被二次拼接;`-B $P/glibc/lib` 让 clang 找到 `Scrt1.o`/`crti.o`/`crtn.o`。 + +## 3. 构建 + +```sh +unset LD_PRELOAD # 必须:Termux 的 libtermux-exec 会被 glibc loader 当成坏 ELF +G="$PWD/.gnu" +export PATH="$G/toolchain/bin:$PATH" +export CARGO_TARGET_DIR="$G/target" +export RUSTFLAGS="-C linker=$G/bin/cc-glibc" +export CC="$G/bin/cc-glibc" CXX="$G/bin/cc-glibc" AR="$PREFIX/glibc/bin/ar" +export CARGO_HTTP_CAINFO="$PREFIX/etc/tls/cert.pem" # glibc 版 cargo 不认 Android 的 CA 路径 +export ULL_MSA_CLIENT_ID= # 可选;烘微软登录 id,值不进 git + +# crates 走 USTC 稀疏索引(官方源在部分网络下只有 ~20 KB/s) +cargo --config 'source.crates-io.replace-with="ustc"' \ + --config 'source.ustc.registry="sparse+https://mirrors.ustc.edu.cn/crates.io-index/"' \ + build --release -p unlucky-app-iced --no-default-features +``` + +`--no-default-features` 关掉 `gpu`,只编 tiny-skia(CPU)。产物:`.gnu/target/release/unlucky-launcher-iced`。 + +## 4. 运行(termux-x11) + +```sh +# 一次性:glibc 版 X 运行库(cf 源慢时可把 sources.list 里的 packages-cf 换成 packages.termux.dev) +pkg install libx11-glibc libxkbcommon-glibc fontconfig-glibc \ + libxcursor-glibc libxrandr-glibc libxi-glibc libxext-glibc \ + libxcb-glibc libwayland-glibc + +termux-x11 :0 -ac & # 配合已安装的 termux-x11 App 使用 +DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced --software-renderer +``` + +无头取证(CI 同款通道): + +```sh +DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ + --software-renderer --screenshot /tmp/ull-termux.png --data-dir /tmp/ull-termux-data +``` + +- 中文:GUI 在 Termux 上会把 `/system/fonts/NotoSansCJK-Regular.ttc` 注册进 cosmic-text + (fontdb 不扫 Android 的字体目录),无需额外环境变量。 +- 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。 + +## 5. GPU(未在 glibc 侧验证) + +termux-x11 的 X 客户端**可以**走硬件:本机 bionic 栈实测 +`glxinfo -B` → `zink Vulkan 1.4 (Adreno 840 (MESA_TURNIP))`、`Accelerated: yes`, +`vulkaninfo` 中 Adreno 设备带 `VK_KHR_xcb_surface` / `VK_KHR_xlib_surface`。 + +要用到 ULL 的 glibc 构建上,还需要 glibc 侧的 Vulkan 栈 +(`vulkan-icd-loader-glibc`、`mesa-vulkan-icd-freedreno-glibc`,可加 `swrast-glibc` 兜底), +并用默认 feature(带 `gpu`)重编: + +```sh +cargo ... build --release -p unlucky-app-iced # 不加 --no-default-features +VK_ICD_FILENAMES="$PREFIX/glibc/share/vulkan/icd.d/freedreno_icd.aarch64.json" \ + DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced --renderer=gpu +``` + +这一步尚未在本仓库验证;没配好时用 `--software-renderer` 回退即可。 + +## 6. 已知边界 + +- 没有面向 Termux 的发布资产,也不在 CI 矩阵里;每次都要源码自建。 +- 只在 CPU 渲染 + 单台设备(Adreno 840 / Android 17)上验证过。 +- **装好 JDK ≠ 能跑 MC**:MC 的 LWJGL natives 是 glibc `.so`,bionic 的 Termux JVM 加载不了; + 这条与「启动器 GUI 能显示」是两件事,未在本文覆盖。 +- `ULL_MSA_CLIENT_ID` 属于仓库 Secret,构建时从环境/Secret 注入,**不写进任何 git 文件**。 diff --git a/packaging/release-notes.md b/packaging/release-notes.md index 3a3949b..e8d2781 100644 --- a/packaging/release-notes.md +++ b/packaging/release-notes.md @@ -59,7 +59,7 @@ Published = shipped as a download of **this** release, listed in `SHA256SUMS.txt | Linux arm64, X11 | not built (unverified) | first class (unverified) | glibc 2.35+, `libxkbcommon`, `fontconfig`, `libGL`, `libxcb` | | Linux arm64, Wayland | not built (unverified) | supported (unverified) | the above plus `wayland-client` | | Windows 8.1/8/7 | not built | unsupported | the baseline is Windows 10 | -| Android — Termux + X11 | not built (no CI slot) | experimental, not promised | CLI only and only when built from source; GUI unverified | +| Android — Termux + X11 | not built (no CI slot) | experimental, not promised | CLI and GUI from source only; GUI verified once on CPU rendering (see `packaging/docs/termux-glibc-build.md`) | `windows-x64` is the only published slot this release; Windows ARM64 is kept out until its build is green (see Downloads above). Both arm64 rows are formal matrix slots, not the experimental Termux path, and From 48d1ee6616b92af420418832b27d46feebe9d7b9 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 22:54:53 +0800 Subject: [PATCH 04/21] =?UTF-8?q?feat(java):=20Termux=20=E4=B8=8A=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E5=AE=89=E8=A3=85=E7=B3=BB=E7=BB=9F=20openjdk?= =?UTF-8?q?=EF=BC=88=E5=86=85=E7=BD=AE=E9=95=9C=E5=83=8F=20+=20=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=20apt=20=E7=8A=B6=E6=80=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 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 行为 --- crates/cli/src/cli/help.rs | 10 +- crates/cli/src/commands/java_vendors.rs | 6 +- crates/core/src/game/java/install.rs | 77 +++++- crates/core/src/game/java/mod.rs | 1 + crates/core/src/game/java/source.rs | 5 + crates/core/src/game/java/termux.rs | 251 ++++++++++++++++++++ crates/core/src/game/java/vendor.rs | 20 +- crates/core/src/setting/keys.rs | 2 +- crates/core/tests/game/java_vendor_chain.rs | 5 +- crates/presenter/src/host/java.rs | 4 + packaging/docs/termux-glibc-build.md | 9 + 11 files changed, 379 insertions(+), 11 deletions(-) create mode 100644 crates/core/src/game/java/termux.rs 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 栈实测 From 7b971dc75d4a4f60b809a3fc64fe97cfb3b4623e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Fri, 6 Nov 2026 23:08:01 +0800 Subject: [PATCH 05/21] =?UTF-8?q?fix(java):=20Termux=20=E5=B9=82=E7=AD=89?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=8A=A5=E3=80=8C=E6=9C=AA=E8=B5=B0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E3=80=8D=EF=BC=8Cvendors=20=E5=9C=A8=20Termux=20?= =?UTF-8?q?=E4=B8=8A=E5=B1=95=E7=A4=BA=20termux=20=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - termux::install 返回 (home, reused),复用时 downloaded=None, CLI 不再显示「0 字节 / 0 个文件」 - java vendors 在 Termux 上把 termux 插到顺序最前(仅展示,默认回退序不变) --- crates/cli/src/commands/java_vendors.rs | 6 +++++- crates/core/src/game/java/install.rs | 8 ++++++-- crates/core/src/game/java/termux.rs | 8 ++++++-- crates/core/src/platform/termux.rs | 8 ++++---- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/commands/java_vendors.rs b/crates/cli/src/commands/java_vendors.rs index e3972eb..82bbec9 100644 --- a/crates/cli/src/commands/java_vendors.rs +++ b/crates/cli/src/commands/java_vendors.rs @@ -23,7 +23,11 @@ use crate::output::Report; pub fn vendors(ctx: &Ctx) -> Result { let settings = JavaSettings::load(ctx); let domestic = region_profile(ctx).is_domestic(); - let order = vendor::order(&settings.priority, domestic); + let mut order = vendor::order(&settings.priority, domestic); + // Termux 上系统包是一条真实可用的发行版:不进默认回退序,但展示时要让人看见。 + if termux::available() && !order.contains(&JdkVendor::Termux) { + order.insert(0, JdkVendor::Termux); + } let table = MirrorTable::builtin(); let rows: Vec = order .iter() diff --git a/crates/core/src/game/java/install.rs b/crates/core/src/game/java/install.rs index fb490d2..9201d85 100644 --- a/crates/core/src/game/java/install.rs +++ b/crates/core/src/game/java/install.rs @@ -206,7 +206,7 @@ pub async fn install( ); } match super::termux::install(request.major).await { - Ok(home) => { + Ok((home, reused)) => { let mut runtime = probe::probe(&extract::java_binary(&home), JavaOrigin::Managed); runtime.vendor = Some(JdkVendor::Termux.id().to_string()); let version = runtime @@ -235,7 +235,11 @@ pub async fn install( mode: location.mode, archive: PathBuf::new(), runtime, - downloaded: Some("termux".to_string()), + downloaded: if reused { + None + } else { + Some("termux".to_string()) + }, bytes: 0, files: 0, extracted: false, diff --git a/crates/core/src/game/java/termux.rs b/crates/core/src/game/java/termux.rs index b808e00..d80fab8 100644 --- a/crates/core/src/game/java/termux.rs +++ b/crates/core/src/game/java/termux.rs @@ -69,7 +69,10 @@ pub fn sources_line(mirror: &str) -> String { /// # Errors /// /// 当前不在 Termux、主源没有该大版本、所有镜像都失败、装完仍找不到 `bin/java` 时返回错误。 -pub async fn install(major: u32) -> Result { +/// +/// 返回 `(JDK 根, 是否复用了已装)`:包管理器刚装完与「本来就有」对调用方是两种结果 +/// (一个要报安装,一个要报「未走网络」)。 +pub async fn install(major: u32) -> Result<(PathBuf, bool), JavaError> { let Some(package) = package_for(major) else { return Err(JavaError::new( JavaErrorKind::Unsupported, @@ -86,7 +89,7 @@ pub async fn install(major: u32) -> Result { // 幂等:已经装了同一大版本就不碰网络。 if let Some(home) = installed_home(&prefix, major) { - return Ok(home); + return Ok((home, true)); } let workspace = workspace_dir(&prefix, major); @@ -109,6 +112,7 @@ pub async fn install(major: u32) -> Result { format!("Termux 安装任务没能完成:{error}"), ) })? + .map(|home| (home, false)) } /// 逐镜像尝试 `apt-get`(阻塞,跑在 `spawn_blocking` 里)。 diff --git a/crates/core/src/platform/termux.rs b/crates/core/src/platform/termux.rs index 99ea5ef..71095ce 100644 --- a/crates/core/src/platform/termux.rs +++ b/crates/core/src/platform/termux.rs @@ -30,10 +30,10 @@ 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, - )) + Some( + env.var("PREFIX") + .map_or_else(|| PathBuf::from(DEFAULT_PREFIX), PathBuf::from), + ) } /// Termux 的 JDK 安装根(`$PREFIX/lib/jvm`)。 From c434dbe335fcec6a0a44dae8a074796208f11691 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 07:50:33 +0800 Subject: [PATCH 06/21] =?UTF-8?q?feat(termux):=20=E6=A8=AA=E5=B1=8F?= =?UTF-8?q?=E8=87=AA=E9=80=82=E5=BA=94=E7=AA=97=E5=8F=A3=E3=80=81=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=90=8E=E6=B8=85=E7=90=86=20apt=20=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8C=BA=E3=80=81=E8=A1=A5=E5=88=86=E6=94=AF=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/boot/termux_window:Termux 上默认窗口按 X 屏幕等比自适应 (横屏铺满、竖屏不溢出,最大 2×;日志 source=termux-screen), ULL_WINDOW_SIZE 覆盖与 1180×760 验收基准不变;5 条单测 - termux::install 成功后清理 $PREFIX/tmp/ull-java-termux(一次安装约 100 MB) - 构建文档补 clippy/rustfmt 组件、cargo-clippy 目标被带成 android 的坑、 门禁命令、横屏行为与 openjdk-17 实测 - 新增 README.termux.md(分支定位/已验证矩阵/快速开始/边界/路线图), 两份主 README 的 Termux 条目链过去 --- README.md | 4 +- README.termux.md | 46 +++++++ README.zh-CN.md | 2 +- crates/app-iced/src/app/boot.rs | 15 ++- crates/app-iced/src/app/boot/termux_window.rs | 115 ++++++++++++++++++ crates/core/src/game/java/termux.rs | 2 + packaging/docs/termux-glibc-build.md | 35 +++++- 7 files changed, 209 insertions(+), 10 deletions(-) create mode 100644 README.termux.md create mode 100644 crates/app-iced/src/app/boot/termux_window.rs diff --git a/README.md b/README.md index c83ceac..1db70e2 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ This list is the reason to believe the rest. None of it is hidden behind marketi - **Install progress is stage-level**, not byte-level, on the content-library install path. - **No native file chooser.** Import takes drag-and-drop or a pasted path. - **Android/Termux is experimental and not promised**: no released asset targets it. A GUI build is - possible from source via the glibc toolchain (`packaging/docs/termux-glibc-build.md`); it was - verified once on CPU rendering only. + possible from source via the glibc toolchain (`packaging/docs/termux-glibc-build.md`, branch notes: + [`README.termux.md`](README.termux.md)); it was verified once on CPU rendering only. ## Download and run diff --git a/README.termux.md b/README.termux.md new file mode 100644 index 0000000..d4175f9 --- /dev/null +++ b/README.termux.md @@ -0,0 +1,46 @@ +# Unlucky Launcher · Termux 适配(`termux-glibc` 分支) + +这条分支把启动器带到 **Termux(Android)**:用 glibc 工具链编出 `aarch64-unknown-linux-gnu` +二进制,在 **termux-x11** 里跑 GUI;同时补上 Termux 平台识别、系统 JDK 安装与中文字体。 + +## 已验证 + +| 项 | 状态 | 证据 | +| --- | --- | --- | +| 源码构建(GUI + CLI) | ✅ | Rust 1.99.1 gnu + clang/lld + glibc 运行时;CPU-only(tiny-skia) | +| 窗口显示 | ✅ | termux-x11 `:0`,截图 1180×760,首帧 ~330 ms | +| 中文 | ✅ | 启动时注册 `/system/fonts/NotoSansCJK-Regular.ttc` | +| 微软登录 id | ✅ | 构建期注入,日志 `client_id=configured source=build` | +| 横屏适配 | ✅ | 默认窗口按 X 屏幕等比自适应(`source=termux-screen`) | +| Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | +| Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | +| GPU(wgpu) | ⏳ | bionic 侧 Vulkan/turnip 已验证;glibc 侧变体待构建验证 | +| Minecraft 运行 | ⏳ | bionic JVM 加载不了 glibc natives;计划走 glibc JVM | + +## 快速开始 + +完整配方(工具链、patchelf、linker、门禁)见 +[`packaging/docs/termux-glibc-build.md`](packaging/docs/termux-glibc-build.md)。最短路径: + +```sh +termux-x11 :0 -ac & +DISPLAY=:0 ./unlucky-launcher-iced --software-renderer +``` + +- 没有 GPU 栈时**必须**加 `--software-renderer`(CPU 渲染)。 +- 窗口尺寸默认按屏幕自适应;`ULL_WINDOW_SIZE=1520x900` 可覆盖。 +- Java:`ull java install 17|21|25` 在 Termux 上默认用系统包(内置镜像 + 隔离 apt 状态)。 + +## 已知边界 + +- 仅单机(OnePlus 15 / Android 17)验证,试验级、不在发布矩阵内。 +- GUI 只验证了 CPU 渲染;GPU 变体需要 glibc 侧 Vulkan 栈。 +- 装好 JDK ≠ 能跑 MC:MC 的 LWJGL natives 是 glibc `.so`,bionic、JVM 加载不了。 +- Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), + PC 的 Linux CI 不受影响。 + +## 后续 + +1. glibc 侧 GPU(wgpu)变体与取证 +2. MC 运行链:glibc JVM(Temurin linux-aarch64 + glibc Mesa)或 proot 方案 +3. 剪贴板/IME 在 termux-x11 下的行为验证 diff --git a/README.zh-CN.md b/README.zh-CN.md index b9b544c..7083627 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -158,7 +158,7 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 文件是被静默覆盖的。 - **内容库的安装进度是阶段级**,不是字节级。 - **没有原生文件选择框。** 导入靠拖拽或粘贴路径。 -- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`),目前只在 CPU 渲染下验证过一次。 +- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`,分支说明见 [`README.termux.md`](README.termux.md)),目前只在 CPU 渲染下验证过一次。 ## 下载与运行 diff --git a/crates/app-iced/src/app/boot.rs b/crates/app-iced/src/app/boot.rs index 182e421..bc93fae 100644 --- a/crates/app-iced/src/app/boot.rs +++ b/crates/app-iced/src/app/boot.rs @@ -30,6 +30,7 @@ use super::{MIN_SIZE, WINDOW_SIZE}; mod diagnostics; mod fallback; +mod termux_window; use fallback::{on_failure, respawn, OnFailure}; @@ -131,10 +132,20 @@ fn launch( // M98:`ULL_WINDOW_SIZE=1520x900` 覆盖尺寸(矩阵的「最小 / 宽屏」两档靠它,且必须走**真外壳** // ——`pages_demo` example 的标题栏是留白带,出不了「三键位置」那张图)。 - let (size, size_source) = acceptance::window_size( + // Termux:没有环境覆盖时,默认尺寸按 X 屏幕等比自适应(横屏优先), + // 失败就退回 1180×760 的验收尺寸。 + let (default_size, default_source) = termux_window::default_size( (WINDOW_SIZE.width, WINDOW_SIZE.height), (MIN_SIZE.width, MIN_SIZE.height), - ); + ) + .unwrap_or(((WINDOW_SIZE.width, WINDOW_SIZE.height), "default")); + let (size, size_source) = + acceptance::window_size(default_size, (MIN_SIZE.width, MIN_SIZE.height)); + let size_source = if size_source == "default" { + default_source + } else { + size_source + }; let settings = diagnostics::window_settings(size); // 两行日志与开窗用的是**同一个** settings(见模块文档纪律 2) diagnostics::report_window(&settings, options.scale_factor, size_source); diff --git a/crates/app-iced/src/app/boot/termux_window.rs b/crates/app-iced/src/app/boot/termux_window.rs new file mode 100644 index 0000000..b75ba4f --- /dev/null +++ b/crates/app-iced/src/app/boot/termux_window.rs @@ -0,0 +1,115 @@ +//! Termux / termux-x11 下的窗口尺寸自适应(横屏优先)。 +//! +//! 验收尺寸(1180×760)是 D30 的硬要求,不能改;但手机显示器可能横屏远大于它 +//! (例如 2066×1272)或竖屏略小。这里只在**运行时**把「默认尺寸」换成按当前 X +//! 屏幕算出的最大等比矩形;`ULL_WINDOW_SIZE` 覆盖仍然优先(见 `acceptance::window_size`)。 +//! +//! 只在 Termux + 有 `DISPLAY` 时探测;`xdpyinfo` 缺失/解析失败一律回默认,不影响其它平台。 + +use std::process::Command; + +use unlucky_core::platform::paths::ProcessEnv; +use unlucky_core::platform::termux; + +/// 屏幕四周留白(像素)。 +const MARGIN: f32 = 32.0; +/// 最大放大倍数:大屏上也不把窗口撑成巨屏。 +const MAX_SCALE: f32 = 2.0; +/// `xdpyinfo` 里尺寸那一行的前缀。 +const DIMENSIONS_PREFIX: &str = "dimensions:"; + +/// Termux 上的默认窗口尺寸:按 X 屏幕等比放大/缩小,失败返回 `None`(用调用方默认)。 +/// +/// `default` / `min` 由调用方传入([`super::WINDOW_SIZE`] / [`super::MIN_SIZE`]), +/// 这里不复制常量。 +#[must_use] +pub(super) fn default_size( + default: (f32, f32), + min: (f32, f32), +) -> Option<((f32, f32), &'static str)> { + if !termux::is_termux(&ProcessEnv) || std::env::var_os("DISPLAY").is_none() { + return None; + } + let screen = x_screen_size()?; + Some((fit_screen(default, min, screen), "termux-screen")) +} + +/// 在 `screen` 里放一个保持 `default` 宽高比的最大矩形(保留留白,不突破 `min`)。 +#[must_use] +pub(super) fn fit_screen(default: (f32, f32), min: (f32, f32), screen: (f32, f32)) -> (f32, f32) { + if screen.0 <= 0.0 || screen.1 <= 0.0 || default.0 <= 0.0 || default.1 <= 0.0 { + return default; + } + let available = ( + (screen.0 - MARGIN * 2.0).max(1.0), + (screen.1 - MARGIN * 2.0).max(1.0), + ); + let scale = (available.0 / default.0) + .min(available.1 / default.1) + .min(MAX_SCALE); + let width = (default.0 * scale).max(min.0).min(screen.0); + let height = (default.1 * scale).max(min.1).min(screen.1); + (width, height) +} + +/// 当前 X 屏幕尺寸(`xdpyinfo` 的 `dimensions: WxH pixels`)。 +fn x_screen_size() -> Option<(f32, f32)> { + let output = Command::new("xdpyinfo").output().ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout); + let line = text + .lines() + .find(|line| line.trim_start().starts_with(DIMENSIONS_PREFIX))?; + let value = line.split_whitespace().nth(1)?; + let (width, height) = value.split_once('x')?; + Some((width.parse().ok()?, height.parse().ok()?)) +} + +#[cfg(test)] +mod tests { + use super::fit_screen; + + const DEFAULT: (f32, f32) = (1180.0, 760.0); + const MIN: (f32, f32) = (880.0, 560.0); + + fn close(actual: (f32, f32), expected: (f32, f32)) { + assert!( + (actual.0 - expected.0).abs() < 0.5 && (actual.1 - expected.1).abs() < 0.5, + "expected {expected:?}, got {actual:?}" + ); + } + + #[test] + fn landscape_screen_scales_up_keeping_aspect() { + // 2066×1272:留白后 2002×1208,高度是瓶颈 → 1875.6×1208。 + close( + fit_screen(DEFAULT, MIN, (2066.0, 1272.0)), + (1875.58, 1208.0), + ); + } + + #[test] + fn portrait_screen_still_fits_width() { + // 1272×2066:宽度是瓶颈 → 1208×778。 + close(fit_screen(DEFAULT, MIN, (1272.0, 2066.0)), (1208.0, 778.03)); + } + + #[test] + fn huge_screen_is_capped_at_two_times() { + close(fit_screen(DEFAULT, MIN, (4000.0, 3000.0)), (2360.0, 1520.0)); + } + + #[test] + fn tiny_screen_never_goes_below_min_or_overflow() { + let (width, height) = fit_screen(DEFAULT, MIN, (800.0, 600.0)); + assert_eq!(width, 800.0, "宽度受屏幕限制"); + assert_eq!(height, 560.0, "高度不小于最小尺寸"); + } + + #[test] + fn invalid_screen_falls_back_to_default() { + assert_eq!(fit_screen(DEFAULT, MIN, (0.0, 0.0)), DEFAULT); + } +} diff --git a/crates/core/src/game/java/termux.rs b/crates/core/src/game/java/termux.rs index d80fab8..fa471c8 100644 --- a/crates/core/src/game/java/termux.rs +++ b/crates/core/src/game/java/termux.rs @@ -127,6 +127,8 @@ fn run_install( match apt_install(prefix, workspace, mirror, package) { Ok(()) => { if let Some(home) = installed_home(prefix, major) { + // 装完把隔离的 lists/cache/archives 清掉(几十到上百 MB,别留在 $PREFIX/tmp)。 + let _ = std::fs::remove_dir_all(workspace); return Ok(home); } failures.push(format!("{mirror}:装完找不到 bin/java")); diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index 0bb732b..3a2a8a7 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -35,12 +35,13 @@ G="$PWD/.gnu" ### 2.2 Rust gnu 工具链 -下载 Rust 1.99.1(或与 `rust-version` 兼容的 stable)的 `rustc` / `rust-std` / `cargo` 三个 -`aarch64-unknown-linux-gnu` 组件。官方源在国内很慢,用 USTC 镜像: +下载 Rust 1.99.1(或与 `rust-version` 兼容的 stable)的 `rustc` / `rust-std` / `cargo`, +以及跑门禁需要的 `clippy` / `rustfmt` 五个 `aarch64-unknown-linux-gnu` 组件。官方源在国内很慢, +用 USTC 镜像: ```sh mkdir -p "$TMPDIR/ull-gnu" && cd "$TMPDIR/ull-gnu" -for c in rustc rust-std cargo; do +for c in rustc rust-std cargo clippy rustfmt; do curl -fLO "https://mirrors.ustc.edu.cn/rust-static/dist/$c-1.99.1-aarch64-unknown-linux-gnu.tar.xz" done ``` @@ -56,11 +57,17 @@ done ```sh LD="$PREFIX/glibc/lib/ld-linux-aarch64.so.1" -for f in "$G/toolchain/bin/rustc" "$G/toolchain/bin/cargo" "$G/toolchain/bin/rustdoc"; do - patchelf --set-interpreter "$LD" --set-rpath "$PREFIX/glibc/lib:$G/toolchain/lib" "$f" +for f in rustc cargo rustdoc clippy-driver cargo-clippy rustfmt cargo-fmt; do + patchelf --set-interpreter "$LD" --set-rpath "$PREFIX/glibc/lib:$G/toolchain/lib" \ + "$G/toolchain/bin/$f" done ``` +> **坑(实测)**:`cargo clippy` 会在 PATH 里找 `cargo-clippy`。如果只装了 Termux 的 Rust, +> 找到的是 android 版 `cargo-clippy`,`aws-lc-sys` 等构建脚本会被当成 +> `--target=aarch64-linux-android` 编译并失败。把 gnu 工具链的 `bin` 放在 PATH 最前面即可; +> `rustc`/`rustfmt` 同理。 + ### 2.4 gcc 运行时片段 `clang -fuse-ld=lld` 仍然需要 `crtbeginS.o` / `crtendS.o` / `libgcc.a`。从 Debian 的 @@ -112,6 +119,18 @@ cargo --config 'source.crates-io.replace-with="ustc"' \ `--no-default-features` 关掉 `gpu`,只编 tiny-skia(CPU)。产物:`.gnu/target/release/unlucky-launcher-iced`。 +门禁(与 CI 同一组命令,环境同上): + +```sh +cargo fmt --all --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace --all-targets --no-fail-fast +``` + +Termux 上有三条**环境差异**失败(不是回归):`launch` 测试读 +`/proc//task//children`(Android 内核没有)、natives 事件的长路径被脱敏成 +``、`settings_bridge` 的 mtime 热重载偶发(flaky)。PC 的 Linux CI 不受影响。 + ## 4. 运行(termux-x11) ```sh @@ -134,6 +153,9 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 中文:GUI 在 Termux 上会把 `/system/fonts/NotoSansCJK-Regular.ttc` 注册进 cosmic-text (fontdb 不扫 Android 的字体目录),无需额外环境变量。 - 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。 +- **横屏优先**:Termux 上默认窗口尺寸会按当前 X 屏幕等比自适应(日志 + `[window] ... source=termux-screen`),横屏铺满、竖屏也不溢出;验收尺寸 1180×760 仍是 + 默认基准,`ULL_WINDOW_SIZE=1520x900` 这类覆盖仍然优先。 ### 4.1 Termux 上的 Java @@ -141,8 +163,11 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序尝试; - `apt-get` 的 lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不改用户的 apt 配置; + 装完自动清理该目录(一次安装约 100 MB 临时占用); - 装完从 `$PREFIX/lib/jvm/java--openjdk` 发现,`ull java list` 直接可见; - `--vendor termux` 只走系统包;`temurin|oracle|microsoft|zulu` 仍走归档下载链。 +- 已实测:`ull java install 17` 从 SAU 镜像装完 `openjdk-17`(含 `openjdk-17-x`), + 约 20 秒;`ull java list` 随后列出 `[system]` 的 17。 ## 5. GPU(未在 glibc 侧验证) From bbb316ca085bd3281e3a56a75ec109f00ec31a58 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 07:57:24 +0800 Subject: [PATCH 07/21] =?UTF-8?q?docs(termux):=20GPU=20=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=E8=90=BD=E5=9C=B0=E2=80=94=E2=80=94glibc=20=E4=BE=A7=E8=A2=AB?= =?UTF-8?q?=E9=A9=B1=E5=8A=A8=E7=89=88=E6=9C=AC=E6=8C=A1=E4=BD=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实测结论: - glibc Vulkan loader + 自写 ICD 能加载 - Termux glibc Mesa 24.2.6 不认识 Adreno 840(chip 0x44050A31 unsupported) - Arch vulkan-freedreno 26.2.6 能加载但 Linux/DRM 构建找不到设备(缺 KGSL, /dev/dri 对 untrusted_app 被 SELinux 拒绝) - GPU 变体会打印具名错误并自动降级 tiny-skia README.termux 同步:GPU 标 ⛔、MC 走 glibc JVM 的结论与后续路线 --- README.termux.md | 16 ++++++++++------ packaging/docs/termux-glibc-build.md | 26 ++++++++++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.termux.md b/README.termux.md index d4175f9..1a58b7b 100644 --- a/README.termux.md +++ b/README.termux.md @@ -14,8 +14,8 @@ | 横屏适配 | ✅ | 默认窗口按 X 屏幕等比自适应(`source=termux-screen`) | | Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | | Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | -| GPU(wgpu) | ⏳ | bionic 侧 Vulkan/turnip 已验证;glibc 侧变体待构建验证 | -| Minecraft 运行 | ⏳ | bionic JVM 加载不了 glibc natives;计划走 glibc JVM | +| GPU(wgpu) | ⛔ | glibc 侧 Mesa 24.2.6 不支持 Adreno 840;Arch 26.2.6 缺 KGSL。CPU 渲染可用 | +| Minecraft 运行 | ⏳ | 无官方 Android natives;计划走 glibc JVM(见下) | ## 快速开始 @@ -34,13 +34,17 @@ DISPLAY=:0 ./unlucky-launcher-iced --software-renderer ## 已知边界 - 仅单机(OnePlus 15 / Android 17)验证,试验级、不在发布矩阵内。 -- GUI 只验证了 CPU 渲染;GPU 变体需要 glibc 侧 Vulkan 栈。 -- 装好 JDK ≠ 能跑 MC:MC 的 LWJGL natives 是 glibc `.so`,bionic、JVM 加载不了。 +- **GPU**:bionic 侧 zink/turnip 可用;glibc 侧被驱动挡住——Termux 的 glibc Mesa 24.2.6 + 不认识 Adreno 840,Arch 的 26.2.6 是 Linux/DRM 构建、缺 KGSL。需要 Mesa 26+ 的 + glibc+KGSL 构建;在那之前用 CPU(tiny-skia)。GPU 变体会自动降级,不会崩。 +- **MC**:Android 原生 natives 走不通(LWJGL 没有官方 Android/bionic 产物,且 GLFW 的 + 窗口模型与 termux-x11 的 X11 不兼容)。可行路线是 glibc JVM(Temurin linux-aarch64 + + glibc Mesa),让 MC 的 Linux natives 在 glibc 进程里正常加载。 - Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), PC 的 Linux CI 不受影响。 ## 后续 -1. glibc 侧 GPU(wgpu)变体与取证 -2. MC 运行链:glibc JVM(Temurin linux-aarch64 + glibc Mesa)或 proot 方案 +1. Mesa 26+ 的 glibc/KGSL 构建(解锁 GPU) +2. MC 运行链:glibc JVM(Temurin linux-aarch64 + glibc Mesa);proot 作为备选 3. 剪贴板/IME 在 termux-x11 下的行为验证 diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index 3a2a8a7..fe8c84f 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -169,23 +169,37 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 已实测:`ull java install 17` 从 SAU 镜像装完 `openjdk-17`(含 `openjdk-17-x`), 约 20 秒;`ull java list` 随后列出 `[system]` 的 17。 -## 5. GPU(未在 glibc 侧验证) +## 5. GPU(已排查,当前被驱动挡在 glibc 侧) termux-x11 的 X 客户端**可以**走硬件:本机 bionic 栈实测 `glxinfo -B` → `zink Vulkan 1.4 (Adreno 840 (MESA_TURNIP))`、`Accelerated: yes`, `vulkaninfo` 中 Adreno 设备带 `VK_KHR_xcb_surface` / `VK_KHR_xlib_surface`。 -要用到 ULL 的 glibc 构建上,还需要 glibc 侧的 Vulkan 栈 -(`vulkan-icd-loader-glibc`、`mesa-vulkan-icd-freedreno-glibc`,可加 `swrast-glibc` 兜底), -并用默认 feature(带 `gpu`)重编: +glibc 侧已把链路搭好并实测到具体阻塞点: + +1. **loader 正常**:`vulkan-icd-loader-glibc` + 自写 ICD JSON 能加载 ICD。 +2. **Termux 的 glibc Mesa 太旧**:`mesa-vulkan-icd-freedreno-glibc` 是 24.2.6(2024), + `vulkaninfo` 报 `chip_id = 44050A31, gpu_id = 6860 is unsupported` + (Adreno 840 不在该版本的支持表里)。 +3. **Arch 的新驱动也走不通**:`vulkan-freedreno 26.2.6`(Arch Linux ARM)能加载、 + 但它是 Linux/DRM 构建,找不到设备——Android 的 GPU 走 KGSL,而 + `/dev/dri` 对 `untrusted_app` 被 SELinux 拒绝(`ls /dev/dri` → Permission denied, + 设备节点本身是 `crw-rw-rw-`)。 + +结论:glibc 侧要用硬件 GPU,需要一个 **Mesa 26+ 的 glibc 构建且带 Android/KGSL 后端**; +现有包都缺一半。在那之前用 CPU(tiny-skia)。 + +GPU 变体仍可构建并验证降级链: ```sh -cargo ... build --release -p unlucky-app-iced # 不加 --no-default-features +cargo ... build --release -p unlucky-app-iced # 默认 feature,带 wgpu VK_ICD_FILENAMES="$PREFIX/glibc/share/vulkan/icd.d/freedreno_icd.aarch64.json" \ +VK_DRIVER_FILES="$VK_ICD_FILENAMES" \ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced --renderer=gpu ``` -这一步尚未在本仓库验证;没配好时用 `--software-renderer` 回退即可。 +没有可用驱动时它会打印具名错误并**自动降级**到 tiny-skia(同一进程不能重建事件循环, +降级走 respawn),所以 GPU 变体在只有 CPU 的环境里也能跑。 ## 6. 已知边界 From 173a18b842464a774fb6b748220c9bca7d7d84a9 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 08:28:36 +0800 Subject: [PATCH 08/21] =?UTF-8?q?feat(termux):=20glibc=20=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E8=87=AA=E5=8A=A8=E5=90=AF=E7=94=A8=E8=87=AA=E7=BC=96?= =?UTF-8?q?=20Mesa=2026=20turnip=20ICD=EF=BC=88GPU=20=E6=89=93=E9=80=9A?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - app/boot/termux_gpu:glibc 构建 + Termux 且随附 ICD 存在时,在探测前设 VK_ICD_FILENAMES/VK_DRIVER_FILES;用户显式设置不覆盖 - 文档:Mesa 26.2.6 + KGSL 的完整构建/安装/验证步骤(含 shebang、 --wrap-mode=nofallback、C++/链接参数三处坑) - 实测:wgpu 选中 Adreno 840(turnip Mesa 26.2.6),截图首帧 61 ms - README.termux:GPU 标 ✅,MC 标 🟡(glibc JRE+AWT 已验证,GLX 待 GL 构建) --- README.termux.md | 16 ++--- crates/app-iced/src/app/boot.rs | 3 + crates/app-iced/src/app/boot/termux_gpu.rs | 58 +++++++++++++++++ packaging/docs/termux-glibc-build.md | 73 +++++++++++++++------- 4 files changed, 118 insertions(+), 32 deletions(-) create mode 100644 crates/app-iced/src/app/boot/termux_gpu.rs diff --git a/README.termux.md b/README.termux.md index 1a58b7b..e893fcf 100644 --- a/README.termux.md +++ b/README.termux.md @@ -14,8 +14,8 @@ | 横屏适配 | ✅ | 默认窗口按 X 屏幕等比自适应(`source=termux-screen`) | | Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | | Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | -| GPU(wgpu) | ⛔ | glibc 侧 Mesa 24.2.6 不支持 Adreno 840;Arch 26.2.6 缺 KGSL。CPU 渲染可用 | -| Minecraft 运行 | ⏳ | 无官方 Android natives;计划走 glibc JVM(见下) | +| GPU(wgpu) | ✅ | 自编 Mesa 26.2.6 + KGSL;wgpu 选中 Adreno 840(Vulkan/turnip) | +| Minecraft 运行 | 🟡 | glibc JRE(Temurin 21.0.13)可跑,AWT/X11 正常;GLX 待 Mesa 26 GL 构建 | ## 快速开始 @@ -34,17 +34,17 @@ DISPLAY=:0 ./unlucky-launcher-iced --software-renderer ## 已知边界 - 仅单机(OnePlus 15 / Android 17)验证,试验级、不在发布矩阵内。 -- **GPU**:bionic 侧 zink/turnip 可用;glibc 侧被驱动挡住——Termux 的 glibc Mesa 24.2.6 - 不认识 Adreno 840,Arch 的 26.2.6 是 Linux/DRM 构建、缺 KGSL。需要 Mesa 26+ 的 - glibc+KGSL 构建;在那之前用 CPU(tiny-skia)。GPU 变体会自动降级,不会崩。 +- **GPU**:已打通,但驱动是自编的(Mesa 26.2.6 + KGSL),装到 + `$PREFIX/glibc/{lib,share/vulkan/icd.d}` 后应用会自动启用;Termux 官方源里的 + glibc Mesa 太旧(24.2.6 不认识 Adreno 840)。驱动缺失时自动降级 CPU,不会崩。 - **MC**:Android 原生 natives 走不通(LWJGL 没有官方 Android/bionic 产物,且 GLFW 的 窗口模型与 termux-x11 的 X11 不兼容)。可行路线是 glibc JVM(Temurin linux-aarch64 + - glibc Mesa),让 MC 的 Linux natives 在 glibc 进程里正常加载。 + glibc Mesa)——JRE 与 AWT/X11 已验证,GLX 还差一份 Mesa 26 的 GL 构建。 - Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), PC 的 Linux CI 不受影响。 ## 后续 -1. Mesa 26+ 的 glibc/KGSL 构建(解锁 GPU) -2. MC 运行链:glibc JVM(Temurin linux-aarch64 + glibc Mesa);proot 作为备选 +1. Mesa 26 GLX(zink/softpipe)构建 → MC 的 LWJGL/GLFW 打通 +2. MC 运行链接线:启动器在 Termux 上改用 glibc JVM(Temurin),bionic java 留给 UI/CLI 3. 剪贴板/IME 在 termux-x11 下的行为验证 diff --git a/crates/app-iced/src/app/boot.rs b/crates/app-iced/src/app/boot.rs index bc93fae..38161ed 100644 --- a/crates/app-iced/src/app/boot.rs +++ b/crates/app-iced/src/app/boot.rs @@ -30,6 +30,7 @@ use super::{MIN_SIZE, WINDOW_SIZE}; mod diagnostics; mod fallback; +mod termux_gpu; mod termux_window; use fallback::{on_failure, respawn, OnFailure}; @@ -40,6 +41,8 @@ pub(crate) fn run(options: Options) -> iced::Result { let boot_started = Instant::now(); // ① 探测(判据取在哪、日志在 `report_boot` 里统一打) + // Termux + glibc 构建:装了我们自编的 turnip ICD 就先生效,auto 也能选到 GPU。 + termux_gpu::enable(); let boot = boot_probe(&options); let decision = boot.decision; diagnostics::report_boot(&options, &boot.probe, &decision); diff --git a/crates/app-iced/src/app/boot/termux_gpu.rs b/crates/app-iced/src/app/boot/termux_gpu.rs new file mode 100644 index 0000000..ee14b8f --- /dev/null +++ b/crates/app-iced/src/app/boot/termux_gpu.rs @@ -0,0 +1,58 @@ +//! Termux 上自动启用随附的 Vulkan ICD(glibc 侧 turnip,`ull_freedreno.json`)。 +//! +//! 背景:Termux 软件源里的 glibc Mesa(24.2.6)不认识 Adreno 840,硬件 GPU 需要用 +//! `packaging/docs/termux-glibc-build.md` §5 里自编的 Mesa 26(KGSL)。驱动装在 +//! `$PREFIX/glibc/share/vulkan/icd.d/ull_freedreno.json` 时,这里在探测之前把它设进 +//! `VK_ICD_FILENAMES` / `VK_DRIVER_FILES`,让 `auto` 也能选到 GPU。 +//! +//! 只在 **glibc 构建**(`target_os = "linux"`)上生效:Termux 原生(bionic)构建的 +//! `target_os = "android"`,glibc 驱动对它没有意义。用户显式设了这两个变量时不覆盖。 + +use std::path::{Path, PathBuf}; + +use unlucky_core::platform::paths::ProcessEnv; +use unlucky_core::platform::termux; + +/// 随附 ICD 的相对路径(相对 `$PREFIX`)。 +const ICD_REL: &str = "glibc/share/vulkan/icd.d/ull_freedreno.json"; + +/// 在 GPU 探测之前调用:装了就启用,没装就什么都不做。 +pub(super) fn enable() { + if !cfg!(target_os = "linux") { + return; + } + if std::env::var_os("VK_ICD_FILENAMES").is_some() + || std::env::var_os("VK_DRIVER_FILES").is_some() + { + return; + } + let Some(prefix) = termux::prefix(&ProcessEnv) else { + return; + }; + let icd = icd_path(&prefix); + if icd.is_file() { + std::env::set_var("VK_ICD_FILENAMES", &icd); + std::env::set_var("VK_DRIVER_FILES", &icd); + } +} + +/// `$PREFIX` 下随附 ICD 的路径(纯函数,测试用)。 +fn icd_path(prefix: &Path) -> PathBuf { + prefix.join(ICD_REL) +} + +#[cfg(test)] +mod tests { + use super::icd_path; + + #[test] + fn icd_path_is_under_the_glibc_prefix() { + let path = icd_path(std::path::Path::new("/data/data/com.termux/files/usr")); + assert_eq!( + path, + std::path::PathBuf::from( + "/data/data/com.termux/files/usr/glibc/share/vulkan/icd.d/ull_freedreno.json" + ) + ); + } +} diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index fe8c84f..ae5df07 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -169,37 +169,62 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 已实测:`ull java install 17` 从 SAU 镜像装完 `openjdk-17`(含 `openjdk-17-x`), 约 20 秒;`ull java list` 随后列出 `[system]` 的 17。 -## 5. GPU(已排查,当前被驱动挡在 glibc 侧) +## 5. GPU(已打通:自编 Mesa 26.2.6 + KGSL) -termux-x11 的 X 客户端**可以**走硬件:本机 bionic 栈实测 -`glxinfo -B` → `zink Vulkan 1.4 (Adreno 840 (MESA_TURNIP))`、`Accelerated: yes`, -`vulkaninfo` 中 Adreno 设备带 `VK_KHR_xcb_surface` / `VK_KHR_xlib_surface`。 +termux-x11 的 X 客户端可以走硬件;glibc 侧的问题在**驱动**,不在 loader: -glibc 侧已把链路搭好并实测到具体阻塞点: +1. `vulkan-icd-loader-glibc` + ICD JSON 本身正常。 +2. Termux 软件源的 glibc Mesa 24.2.6 不认识 Adreno 840 + (`chip_id = 44050A31 is unsupported`)。 +3. Arch 的 `vulkan-freedreno 26.2.6` 能加载,但它是 Linux/DRM 构建,缺 KGSL + (`/dev/dri` 对 `untrusted_app` 被 SELinux 拒绝)。 -1. **loader 正常**:`vulkan-icd-loader-glibc` + 自写 ICD JSON 能加载 ICD。 -2. **Termux 的 glibc Mesa 太旧**:`mesa-vulkan-icd-freedreno-glibc` 是 24.2.6(2024), - `vulkaninfo` 报 `chip_id = 44050A31, gpu_id = 6860 is unsupported` - (Adreno 840 不在该版本的支持表里)。 -3. **Arch 的新驱动也走不通**:`vulkan-freedreno 26.2.6`(Arch Linux ARM)能加载、 - 但它是 Linux/DRM 构建,找不到设备——Android 的 GPU 走 KGSL,而 - `/dev/dri` 对 `untrusted_app` 被 SELinux 拒绝(`ls /dev/dri` → Permission denied, - 设备节点本身是 `crw-rw-rw-`)。 - -结论:glibc 侧要用硬件 GPU,需要一个 **Mesa 26+ 的 glibc 构建且带 Android/KGSL 后端**; -现有包都缺一半。在那之前用 CPU(tiny-skia)。 - -GPU 变体仍可构建并验证降级链: +**解法:给 glibc 自编一份带 KGSL 的 Mesa 26.2.6。** 本机实测通过: ```sh -cargo ... build --release -p unlucky-app-iced # 默认 feature,带 wgpu -VK_ICD_FILENAMES="$PREFIX/glibc/share/vulkan/icd.d/freedreno_icd.aarch64.json" \ -VK_DRIVER_FILES="$VK_ICD_FILENAMES" \ - DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced --renderer=gpu +# 1) 源码 + 依赖 +# mesa-26.2.6.tar.xz(archive.mesa3d.org) +# Debian libstdc++-14-dev 的头($G/gnu-support/usr/include/c++/14) +# meson / ninja(pip 装 meson) +# 2) 把源码里 *.py 的 shebang 从 #!/usr/bin/env python3 改成 Termux python3 +# (Android 没有 /usr/bin/env) +# 3) meson setup(cc-glibc 包装器:-c 不带链接参数、C++ 加 -stdlib=libstdc++、 +# 共享库链接加 -lstdc++;详见 §3) +meson setup build3 --native-file="$G/mesa-native.ini" --wrap-mode=nofallback \ + -Dbuildtype=release -Dstrip=true -Dplatforms=x11 \ + -Dvulkan-drivers=freedreno -Dfreedreno-kmds=kgsl \ + -Dgallium-drivers=zink,softpipe -Dglx=dri -Dopengl=true -Degl=enabled \ + -Dgles1=disabled -Dgles2=enabled -Dllvm=disabled -Dgbm=disabled \ + -Dvideo-codecs= -Dzstd=enabled -Dbuild-tests=false +ninja -C build3 ``` -没有可用驱动时它会打印具名错误并**自动降级**到 tiny-skia(同一进程不能重建事件循环, -降级走 respawn),所以 GPU 变体在只有 CPU 的环境里也能跑。 +`--wrap-mode=nofallback` 是为了跳过 libarchive 子项目(它只被 freedreno 的 decode 工具需要, +且它自带的 `id_t` 兜底宏会和 glibc 头冲突)。 + +安装与启用: + +```sh +cp build3/src/freedreno/vulkan/libvulkan_freedreno.so \ + "$PREFIX/glibc/lib/libvulkan_freedreno_ull.so" +cat > "$PREFIX/glibc/share/vulkan/icd.d/ull_freedreno.json" < ICED_BACKEND=wgpu +[gpu] Selected: AdapterInfo { name: "Adreno (TM) 840", driver: "turnip Mesa driver", + driver_info: "Mesa 26.2.6", backend: Vulkan } +[shot] 1657x1067 renderer=wgpu,首帧 61 ms +``` + +没装驱动/没有 GPU 时仍会打印具名错误并自动降级 tiny-skia(同一进程不能重建事件循环, +降级走 respawn),所以 GPU 变体在纯 CPU 环境也能跑。 ## 6. 已知边界 From 02ed09ae49a881aa4c0e06adbeaeb29f20701b5a Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 08:32:33 +0800 Subject: [PATCH 09/21] =?UTF-8?q?docs(termux):=20=E8=A1=A5=20MC=20?= =?UTF-8?q?=E8=BF=90=E8=A1=8C=E6=97=B6=E7=BB=93=E8=AE=BA=EF=BC=88glibc=20J?= =?UTF-8?q?RE=20+=20LWJGL=20arm64=20+=20softpipe=20GL=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - §4.2:Temurin 21 JRE 在 glibc 下跑通;AWT/X11 正常; LWJGL 3.3.3 arm64 + GLFW + GLX/softpipe 端到端探针通过(GL 3.3) - zink 仍卡 DRI3;softpipe 可用(软件 GL) - README 同步:MC 标 🟡(栈已验证,启动器接线待做),后续改为接线 + DRI3 --- README.termux.md | 12 +++++++----- packaging/docs/termux-glibc-build.md | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/README.termux.md b/README.termux.md index e893fcf..218a5eb 100644 --- a/README.termux.md +++ b/README.termux.md @@ -15,7 +15,7 @@ | Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | | Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | | GPU(wgpu) | ✅ | 自编 Mesa 26.2.6 + KGSL;wgpu 选中 Adreno 840(Vulkan/turnip) | -| Minecraft 运行 | 🟡 | glibc JRE(Temurin 21.0.13)可跑,AWT/X11 正常;GLX 待 Mesa 26 GL 构建 | +| Minecraft 运行 | 🟡 | 栈已验证:glibc JRE + LWJGL arm64 + GLX/softpipe(GL 3.3);启动器接线待做 | ## 快速开始 @@ -38,13 +38,15 @@ DISPLAY=:0 ./unlucky-launcher-iced --software-renderer `$PREFIX/glibc/{lib,share/vulkan/icd.d}` 后应用会自动启用;Termux 官方源里的 glibc Mesa 太旧(24.2.6 不认识 Adreno 840)。驱动缺失时自动降级 CPU,不会崩。 - **MC**:Android 原生 natives 走不通(LWJGL 没有官方 Android/bionic 产物,且 GLFW 的 - 窗口模型与 termux-x11 的 X11 不兼容)。可行路线是 glibc JVM(Temurin linux-aarch64 + - glibc Mesa)——JRE 与 AWT/X11 已验证,GLX 还差一份 Mesa 26 的 GL 构建。 + 窗口模型与 termux-x11 的 X11 不兼容)。有效路线是 glibc JVM——Temurin 21 JRE + AWT、 + LWJGL arm64 + GLFW + GLX/softpipe 已手动跑通(GL 3.3)。启动器接线还差两步: + Linux/aarch64 上把 natives 选成 `natives-linux-arm64`,并给 MC 子进程带 glibc JVM 与 + `MESA_LOADER_DRIVER_OVERRIDE=swrast`。 - Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), PC 的 Linux CI 不受影响。 ## 后续 -1. Mesa 26 GLX(zink/softpipe)构建 → MC 的 LWJGL/GLFW 打通 -2. MC 运行链接线:启动器在 Termux 上改用 glibc JVM(Temurin),bionic java 留给 UI/CLI +1. MC 运行链接线(natives arm64 映射 + glibc JVM 启动 + softpipe env) +2. zink 的 DRI3 问题(硬件 GL;当前 softpipe 可跑但慢) 3. 剪贴板/IME 在 termux-x11 下的行为验证 diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index ae5df07..6a1c571 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -169,6 +169,31 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 已实测:`ull java install 17` 从 SAU 镜像装完 `openjdk-17`(含 `openjdk-17-x`), 约 20 秒;`ull java list` 随后列出 `[system]` 的 17。 +### 4.2 跑 MC 需要的运行时(已手动验证) + +bionic 的 Termux JVM 加载不了 MC 的 glibc natives,所以 MC 要用 **glibc JVM**: + +```sh +# Temurin 21 JRE(linux-aarch64),用 glibc loader 直接跑 +curl -L -o jre21.tar.gz \ + https://api.adoptium.net/v3/binary/latest/21/ga/linux/aarch64/jre/hotspot/normal/eclipse +tar -xzf jre21.tar.gz -C "$G/jre" +JRE="$G/jre/jdk-21.0.13+9-jre" +$PREFIX/glibc/lib/ld-linux-aarch64.so.1 \ + --library-path "$JRE/lib:$JRE/lib/server:$PREFIX/glibc/lib" "$JRE/bin/java" -version +``` + +- AWT/X11:`DISPLAY=:0` 下 Swing 窗口正常(`headless=false`、`swing-ok`)。 +- GL:需要 §5 的 Mesa 26 GL 构建(`-Dgallium-drivers=zink,softpipe -Dglx=dri`)。 + `zink` 目前卡在 `DRI3 not available`;**softpipe 可用**: + `MESA_LOADER_DRIVER_OVERRIDE=swrast LIBGL_ALWAYS_SOFTWARE=1`。 +- 端到端探针:LWJGL 3.3.3 arm64 natives + GLFW + GLX → `GL_VERSION=3.3 (Compatibility + Profile) Mesa 26.2.6`、`GL_RENDERER=softpipe`、`gl-ok`。 + +> 启动器侧还没接线:MC 的 library 清单在 Linux/aarch64 上要选 +> `natives-linux-arm64` 而不是 `natives-linux`(x64),并在启动 MC 的子进程里带上 +> `MESA_LOADER_DRIVER_OVERRIDE=swrast` 与 glibc JVM 的 `JAVA_HOME`。这两步是后续工作。 + ## 5. GPU(已打通:自编 Mesa 26.2.6 + KGSL) termux-x11 的 X 客户端可以走硬件;glibc 侧的问题在**驱动**,不在 loader: From 7eed6c626137ad2bdb1cc80f3fb3d011e172d7cc Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 08:39:25 +0800 Subject: [PATCH 10/21] =?UTF-8?q?feat(termux):=20MC=20=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=8E=A5=E7=BA=BF=E2=80=94=E2=80=94Linux=20arm64=20natives=20+?= =?UTF-8?q?=20softpipe=20=E5=AD=90=E8=BF=9B=E7=A8=8B=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Library::native_classifier:Linux/aarch64 上把 LWJGL3 的 natives-linux 改写成 natives-linux-arm64(Mojang 清单只有 x86_64) - install::libraries:arm64 分类器在清单里没有下载信息时,按 Maven 相对路径 兜底到阿里云镜像(实测 Mojang libraries 404、Aliyun 200); 单测覆盖 arm64 兜底与 x86_64 不变 - launch::defaults:Termux glibc 构建启动 MC 时默认带 MESA_LOADER_DRIVER_OVERRIDE=swrast / LIBGL_ALWAYS_SOFTWARE=1(用户可覆盖) - 文档:JRE patchelf 一次性步骤与接线现状;README 同步 - 验证:patched Temurin java 直接执行;LWJGL arm64+GLFW 探针 gl-ok --- README.termux.md | 8 +- crates/core/src/game/install/libraries.rs | 111 ++++++++++++++++++---- crates/core/src/game/library.rs | 20 +++- crates/core/src/launch/defaults.rs | 47 +++++++++ packaging/docs/termux-glibc-build.md | 19 +++- 5 files changed, 179 insertions(+), 26 deletions(-) diff --git a/README.termux.md b/README.termux.md index 218a5eb..128e624 100644 --- a/README.termux.md +++ b/README.termux.md @@ -15,7 +15,7 @@ | Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | | Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | | GPU(wgpu) | ✅ | 自编 Mesa 26.2.6 + KGSL;wgpu 选中 Adreno 840(Vulkan/turnip) | -| Minecraft 运行 | 🟡 | 栈已验证:glibc JRE + LWJGL arm64 + GLX/softpipe(GL 3.3);启动器接线待做 | +| Minecraft 运行 | 🟡 | 栈已验证;启动器已接线(natives arm64 / softpipe env);待真实 MC 验证 | ## 快速开始 @@ -39,9 +39,9 @@ DISPLAY=:0 ./unlucky-launcher-iced --software-renderer glibc Mesa 太旧(24.2.6 不认识 Adreno 840)。驱动缺失时自动降级 CPU,不会崩。 - **MC**:Android 原生 natives 走不通(LWJGL 没有官方 Android/bionic 产物,且 GLFW 的 窗口模型与 termux-x11 的 X11 不兼容)。有效路线是 glibc JVM——Temurin 21 JRE + AWT、 - LWJGL arm64 + GLFW + GLX/softpipe 已手动跑通(GL 3.3)。启动器接线还差两步: - Linux/aarch64 上把 natives 选成 `natives-linux-arm64`,并给 MC 子进程带 glibc JVM 与 - `MESA_LOADER_DRIVER_OVERRIDE=swrast`。 + LWJGL arm64 + GLFW + GLX/softpipe 已手动跑通(GL 3.3)。启动器已接线: + Linux/aarch64 自动选 `natives-linux-arm64`(阿里云 Maven 兜底),MC 子进程默认带 + softpipe 环境。还差把实例 Java 指到 glibc JRE 后跑一次真实 MC。 - Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), PC 的 Linux CI 不受影响。 diff --git a/crates/core/src/game/install/libraries.rs b/crates/core/src/game/install/libraries.rs index 6638c38..8c867a3 100644 --- a/crates/core/src/game/install/libraries.rs +++ b/crates/core/src/game/install/libraries.rs @@ -79,31 +79,42 @@ pub fn plan( let Some(classifier) = library.native_classifier(environment) else { continue; }; - let Some(artifact) = library.native_artifact(&classifier) else { - warnings.push(format!( - "{} 的 natives 分类器 {classifier} 没有下载信息,已跳过", - library.name - )); - continue; - }; let Ok(native_relative) = library.native_relative_path(&classifier) else { warnings.push(format!("{} 的 natives 路径推导失败,已跳过", library.name)); continue; }; - if !artifact.has_url() { - warnings.push(format!( - "{} 的 natives 构件 {classifier} 没有下载地址,已跳过", - library.name - )); - continue; - } + let dest = layout.libraries_dir().join(&native_relative); + // Linux arm64:Mojang 清单没有 arm64 分类器,兜底到 Maven 镜像(见常量注释)。 + let arm64_fallback = classifier == LINUX_ARM64_CLASSIFIER; + let (url, sha1, size) = match library.native_artifact(&classifier) { + Some(artifact) if artifact.has_url() => ( + artifact.url.clone().unwrap_or_default(), + artifact.sha1.clone(), + artifact.size, + ), + _ if arm64_fallback => (arm64_native_url(&native_relative), None, None), + Some(_) => { + warnings.push(format!( + "{} 的 natives 构件 {classifier} 没有下载地址,已跳过", + library.name + )); + continue; + } + None => { + warnings.push(format!( + "{} 的 natives 分类器 {classifier} 没有下载信息,已跳过", + library.name + )); + continue; + } + }; files.push(PlanFile { role: FileRole::Native, name: format!("{}:{classifier}", library.name), - url: artifact.url.clone().unwrap_or_default(), - sha1: artifact.sha1.clone(), - size: artifact.size, - dest: layout.libraries_dir().join(native_relative), + url, + sha1, + size, + dest, resource: ResourceKind::Library, }); } @@ -141,6 +152,22 @@ fn maven_url(library: &Library, relative: &Path) -> String { } } +/// Linux arm64 的 natives 兜底仓库:Mojang 的 `libraries.minecraft.net` 对 +/// `natives-linux-arm64` 一律 404(实测 3.3.2 的 lwjgl/glfw/opengl/stb/tinyfd/openal), +/// 同一坐标在 Maven Central / 阿里云镜像有。 +pub const LINUX_ARM64_NATIVES_BASE: &str = "https://maven.aliyun.com/repository/public/"; + +/// Linux arm64 的 natives 分类器(见 `Library::native_classifier`)。 +pub const LINUX_ARM64_CLASSIFIER: &str = "natives-linux-arm64"; + +/// arm64 natives 的兜底 URL(按相对路径拼镜像基址)。 +fn arm64_native_url(relative: &Path) -> String { + format!( + "{LINUX_ARM64_NATIVES_BASE}{}", + relative.to_string_lossy().replace('\\', "/") + ) +} + /// 供上层判错用:库展开不产生硬失败,缺地址一律降级成告警。 #[cfg(test)] mod tests { @@ -255,4 +282,52 @@ mod tests { .dest .ends_with("versions/inst/libraries/net/minecraftforge/forge/1.20.1-47.2.0.jar")); } + + #[test] + fn linux_arm64_lwjgl3_natives_fall_back_to_maven_mirror() { + // Mojang 的清单只有 `natives-linux`(x86_64)且没有 arm64 分类器下载信息; + // Linux arm64 要改写成 `natives-linux-arm64` 并兜底到 Maven 镜像。 + let library = library_with_artifact( + r#"{"name":"org.lwjgl:lwjgl:3.3.2","natives":{"linux":"natives-linux"}}"#, + ); + let env = Environment { + os: "linux".to_string(), + os_version: String::new(), + arch: "arm64".to_string(), + ..environment() + }; + let layout = GameLayout::new("/g/.minecraft"); + let (files, warnings) = plan(&layout, "1.20.4", &[library], &env); + assert!(warnings.is_empty(), "不该有告警:{warnings:?}"); + assert_eq!(files.len(), 1); + assert_eq!(files[0].name, "org.lwjgl:lwjgl:3.3.2:natives-linux-arm64"); + assert_eq!( + files[0].url, + "https://maven.aliyun.com/repository/public/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux-arm64.jar" + ); + assert!( + files[0].sha1.is_none(), + "镜像兜底没有 sha1,只能靠大小/存在性" + ); + } + + #[test] + fn linux_x86_64_keeps_the_manifest_classifier() { + let library = library_with_artifact( + r#"{"name":"org.lwjgl:lwjgl:3.3.2","natives":{"linux":"natives-linux"}}"#, + ); + let env = Environment { + os: "linux".to_string(), + os_version: String::new(), + arch: "x86_64".to_string(), + ..environment() + }; + let layout = GameLayout::new("/g/.minecraft"); + let (files, warnings) = plan(&layout, "1.20.4", &[library], &env); + assert!(files.is_empty(), "x86_64 没有清单下载信息,不该凭空拼 URL"); + assert!( + warnings.iter().any(|w| w.contains("natives-linux")), + "{warnings:?}" + ); + } } diff --git a/crates/core/src/game/library.rs b/crates/core/src/game/library.rs index 230c4b9..bb2c06c 100644 --- a/crates/core/src/game/library.rs +++ b/crates/core/src/game/library.rs @@ -172,6 +172,11 @@ impl Library { } /// natives 分类器名:按 OS 查 `natives` 表并展开 `${arch}`(64 位为 `64`)。 + /// + /// **Linux arm64 特例**:Mojang 的 LWJGL3 条目只写 `natives-linux`(x86_64), + /// 但同一个 Maven 坐标有 `natives-linux-arm64`。原样用 `natives-linux` 会下到 x86_64 + /// 的 `.so`,在 aarch64 JVM 里加载失败;这里改成 arm64 分类器(下载地址的兜底见 + /// `install::libraries`)。 pub fn native_classifier(&self, env: &Environment) -> Option { let raw = self.natives.as_ref()?.get(&env.os)?; let arch = if matches!(env.arch.as_str(), "x86_64" | "arm64") { @@ -179,7 +184,20 @@ impl Library { } else { "" }; - Some(raw.replace("${arch}", arch)) + let classifier = raw.replace("${arch}", arch); + if env.os == "linux" + && env.arch == "arm64" + && classifier == "natives-linux" + && self.is_lwjgl3() + { + return Some("natives-linux-arm64".to_owned()); + } + Some(classifier) + } + + /// 是否是 LWJGL 3(`org.lwjgl:`;LWJGL 2 的 group 是 `org.lwjgl.lwjgl`)。 + fn is_lwjgl3(&self) -> bool { + self.name.starts_with("org.lwjgl:") } /// natives 构件下载信息(按分类器查 `downloads.classifiers`)。 diff --git a/crates/core/src/launch/defaults.rs b/crates/core/src/launch/defaults.rs index ffcc157..b60f4cd 100644 --- a/crates/core/src/launch/defaults.rs +++ b/crates/core/src/launch/defaults.rs @@ -174,6 +174,13 @@ pub fn environment(req: &LaunchRequest, os: OsFamily) -> Vec<(String, String)> { set_env(&mut env, "MESA_LOADER_DRIVER_OVERRIDE", "llvmpipe"); set_env(&mut env, "GALLIUM_DRIVER", "llvmpipe"); } + // Termux + glibc 构建:termux-x11 下 glibc Mesa 的 zink 卡 DRI3,softpipe 是当前 + // 可用路径。放在软件渲染块之后、用户自定义之前(用户显式设置仍可覆盖)。 + if cfg!(target_os = "linux") + && crate::platform::termux::is_termux(&crate::platform::paths::ProcessEnv) + { + apply_termux_gl_defaults(&mut env); + } if os == OsFamily::MacOs { if let Some(icd) = &req.renderer.vulkan_icd { set_env(&mut env, "VK_ICD_FILENAMES", &icd.display().to_string()); @@ -201,3 +208,43 @@ fn set_env(env: &mut Vec<(String, String)>, key: &str, value: &str) { env.push((key.to_string(), value.to_string())); } } + +/// Termux(glibc 构建)上的默认 GL 环境:softpipe。 +/// +/// glibc Mesa 26 的 zink 在 termux-x11 下报 `DRI3 not available`,softpipe 是当前 +/// 可用的软件路径;硬件路径等 DRI3 问题解决后再切。 +fn apply_termux_gl_defaults(env: &mut Vec<(String, String)>) { + set_env(env, "MESA_LOADER_DRIVER_OVERRIDE", "swrast"); + set_env(env, "LIBGL_ALWAYS_SOFTWARE", "1"); +} + +#[cfg(test)] +mod tests { + use super::apply_termux_gl_defaults; + + #[test] + fn termux_gl_defaults_force_softpipe() { + let mut env = Vec::new(); + apply_termux_gl_defaults(&mut env); + assert!(env.contains(&( + "MESA_LOADER_DRIVER_OVERRIDE".to_string(), + "swrast".to_string() + ))); + assert!(env.contains(&("LIBGL_ALWAYS_SOFTWARE".to_string(), "1".to_string()))); + } + + #[test] + fn termux_gl_defaults_overwrite_earlier_llvmpipe_choice() { + let mut env = vec![( + "MESA_LOADER_DRIVER_OVERRIDE".to_string(), + "llvmpipe".to_string(), + )]; + apply_termux_gl_defaults(&mut env); + assert_eq!( + env.iter() + .find(|(key, _)| key == "MESA_LOADER_DRIVER_OVERRIDE") + .map(|(_, value)| value.as_str()), + Some("swrast") + ); + } +} diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index 6a1c571..43c8225 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -190,9 +190,22 @@ $PREFIX/glibc/lib/ld-linux-aarch64.so.1 \ - 端到端探针:LWJGL 3.3.3 arm64 natives + GLFW + GLX → `GL_VERSION=3.3 (Compatibility Profile) Mesa 26.2.6`、`GL_RENDERER=softpipe`、`gl-ok`。 -> 启动器侧还没接线:MC 的 library 清单在 Linux/aarch64 上要选 -> `natives-linux-arm64` 而不是 `natives-linux`(x64),并在启动 MC 的子进程里带上 -> `MESA_LOADER_DRIVER_OVERRIDE=swrast` 与 glibc JVM 的 `JAVA_HOME`。这两步是后续工作。 +> **启动器已接线**(本分支): +> - Linux/aarch64 上 LWJGL3 的 natives 自动改写成 `natives-linux-arm64` +> (Mojang 的 `libraries.minecraft.net` 对 arm64 一律 404,下载兜底到阿里云 Maven 镜像); +> - Termux glibc 构建启动 MC 时,子进程默认带 `MESA_LOADER_DRIVER_OVERRIDE=swrast` +> 与 `LIBGL_ALWAYS_SOFTWARE=1`(用户自定义环境变量仍可覆盖)。 +> +> **还差一步**:把实例的 Java 指到 glibc JRE。Temurin 解包后 `bin/java` 默认的 +> interpreter 在 Android 上不存在,先 patchelf 一次即可直接执行: +> +> ```sh +> patchelf --set-interpreter "$PREFIX/glibc/lib/ld-linux-aarch64.so.1" \ +> --set-rpath "$PREFIX/glibc/lib:$JRE/lib:$JRE/lib/server" "$JRE/bin/java" +> "$JRE/bin/java" -version +> ``` +> +> 之后在实例设置里把 Java 指向 `$JRE/bin/java`(或 `--java`)即可。 ## 5. GPU(已打通:自编 Mesa 26.2.6 + KGSL) From 0e7ae44923fab6f35c09266209772e0b4621a981 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 08:45:16 +0800 Subject: [PATCH 11/21] =?UTF-8?q?fix(termux):=20=E7=8E=B0=E4=BB=A3?= =?UTF-8?q?=E6=B8=85=E5=8D=95=E7=9A=84=20LWJGL=20natives=20=E6=9D=A1?= =?UTF-8?q?=E7=9B=AE=E4=B9=9F=E6=94=B9=E5=86=99=E4=B8=BA=20arm64?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.20.x 把 natives 作为独立库条目(org.lwjgl:lwjgl:3.3.2:natives-linux), 不走 natives 表。Linux/aarch64 上按坐标改写成 natives-linux-arm64, URL 兜底到阿里云 Maven(Mojang 对 arm64 一律 404)。 真机 1.20.4 的 dry-run:7 个 natives 全部变成 arm64;x86_64 不变。 --- crates/core/src/game/install/libraries.rs | 78 +++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/crates/core/src/game/install/libraries.rs b/crates/core/src/game/install/libraries.rs index 8c867a3..b5b9f7a 100644 --- a/crates/core/src/game/install/libraries.rs +++ b/crates/core/src/game/install/libraries.rs @@ -39,6 +39,20 @@ pub fn plan( if !library.applies(environment) { continue; } + // 现代清单把 natives 作为独立库条目(`org.lwjgl:lwjgl:3.3.2:natives-linux`), + // 不是 `natives` 表。Linux arm64 上改写坐标并兜底到 Maven 镜像。 + if let Some(rewritten) = linux_arm64_native_entry(library, environment) { + files.push(PlanFile { + role: FileRole::Native, + name: rewritten.name, + url: rewritten.url, + sha1: None, + size: None, + dest: layout.libraries_dir().join(&rewritten.relative), + resource: ResourceKind::Library, + }); + continue; + } if library.coordinate().is_err() { warnings.push(format!("库坐标不合法,已跳过:{}", library.name)); continue; @@ -136,6 +150,41 @@ fn library_dest( } } +/// 现代清单里 LWJGL3 natives 条目的改写结果。 +struct Arm64Native { + name: String, + relative: PathBuf, + url: String, +} + +/// Linux arm64:把 `org.lwjgl:::natives-linux` 改写成 arm64。 +fn linux_arm64_native_entry(library: &Library, env: &Environment) -> Option { + if env.os != "linux" || env.arch != "arm64" || !library.name.starts_with("org.lwjgl:") { + return None; + } + let base = library.name.strip_suffix(":natives-linux")?; + let mut parts = base.split(':'); + let group = parts.next()?; + let artifact = parts.next()?; + let version = parts.next()?; + if parts.next().is_some() { + return None; + } + let relative = PathBuf::from(format!( + "{}/{artifact}/{version}/{artifact}-{version}-natives-linux-arm64.jar", + group.replace('.', "/") + )); + let url = format!( + "{LINUX_ARM64_NATIVES_BASE}{}", + relative.to_string_lossy().replace('\\', "/") + ); + Some(Arm64Native { + name: format!("{base}:natives-linux-arm64"), + relative, + url, + }) +} + /// 按库声明(或默认)的 Maven 基址拼下载地址。 fn maven_url(library: &Library, relative: &Path) -> String { let base = library @@ -311,6 +360,35 @@ mod tests { ); } + #[test] + fn modern_lwjgl_native_entry_is_rewritten_to_arm64_maven() { + // 1.20.x 的清单把 natives 作为独立条目,分类器写在 name 里。 + let library = library_with_artifact( + r#"{"name":"org.lwjgl:lwjgl:3.3.2:natives-linux", + "rules":[{"action":"allow","os":{"name":"linux"}}], + "downloads":{"artifact":{"path":"org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux.jar","url":"https://libraries.minecraft.net/x.jar","sha1":"aa"}}}"#, + ); + let env = Environment { + os: "linux".to_string(), + os_version: String::new(), + arch: "arm64".to_string(), + ..environment() + }; + let layout = GameLayout::new("/g/.minecraft"); + let (files, warnings) = plan(&layout, "1.20.4", &[library], &env); + assert!(warnings.is_empty(), "不该有告警:{warnings:?}"); + assert_eq!(files.len(), 1); + assert_eq!(files[0].role.as_str(), "native"); + assert_eq!(files[0].name, "org.lwjgl:lwjgl:3.3.2:natives-linux-arm64"); + assert_eq!( + files[0].url, + "https://maven.aliyun.com/repository/public/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux-arm64.jar" + ); + assert!(files[0] + .dest + .ends_with("libraries/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux-arm64.jar")); + } + #[test] fn linux_x86_64_keeps_the_manifest_classifier() { let library = library_with_artifact( From 23c2abebc2ccc662c9af95dc5d55ff438dc22321 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 08:53:24 +0800 Subject: [PATCH 12/21] =?UTF-8?q?feat(termux):=20launch=20=E4=BE=A7?= =?UTF-8?q?=E5=90=8C=E6=A0=B7=E6=94=B9=E5=86=99=20arm64=20natives=EF=BC=9B?= =?UTF-8?q?=E7=9C=9F=E6=9C=BA=E8=B7=91=E9=80=9A=20MC=201.20.4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Library::linux_arm64_native_name 收敛改写规则,install 与 launch 共用 - presenter/CLI 的 classpath 组装:1.19+ 的独立 natives 条目按 arm64 坐标 找文件并进 classpath;≤1.18 的 natives 表条目仍归 natives::collect - 真机验证:ull install 1.20.4(740MB)→ glibc Temurin 21 + arm64 natives + softpipe GL,MC 主菜单在 termux-x11 中渲染(窗口 854x480) - 已知非致命:narrator flite 缺失、LWJGL JNI 版本告警、oshi cpuVendor NPE --- crates/cli/src/commands/snapshot.rs | 26 ++++++++++++++++++++--- crates/core/src/game/install/libraries.rs | 8 +++---- crates/core/src/game/library.rs | 13 ++++++++++++ crates/presenter/src/host/launch.rs | 26 ++++++++++++++++++++--- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/crates/cli/src/commands/snapshot.rs b/crates/cli/src/commands/snapshot.rs index b37fd9a..efc01a8 100644 --- a/crates/cli/src/commands/snapshot.rs +++ b/crates/cli/src/commands/snapshot.rs @@ -75,18 +75,38 @@ pub fn prepare(ctx: &Ctx, input: &PlanInput) -> Result { let mut libraries: Vec = Vec::new(); for library in resolved.libraries.as_deref().unwrap_or_default() { - if !library.applies(&environment) || library.native_classifier(&environment).is_some() { + if !library.applies(&environment) { continue; } - let Ok(coordinate) = library.coordinate() else { + // 1.19+ 的 natives 是独立库条目(name 带分类器),必须进 classpath; + // ≤1.18 的 `natives` 表条目归 natives::collect,不进 classpath。 + // Linux arm64:独立条目改写成 natives-linux-arm64(路径与展示名一起改)。 + let arm64 = library.linux_arm64_native_name(&environment); + if arm64.is_none() && library.native_classifier(&environment).is_some() { continue; + } + let (coordinate, name) = match arm64 { + Some(rewritten) => { + let Ok(coordinate) = + unlucky_core::game::library::MavenCoordinate::parse(&rewritten) + else { + continue; + }; + (coordinate, rewritten) + } + None => { + let Ok(coordinate) = library.coordinate() else { + continue; + }; + (coordinate, library.name.clone()) + } }; let path = if library.is_local() { game.version_library(&version_id, &coordinate) } else { game.library(&coordinate) }; - libraries.push(LibraryInput::new(path, library.name.clone())); + libraries.push(LibraryInput::new(path, name)); } // natives 分类器构件(≤1.18.2 才有):收集口径归 core,CLI 只搬运。 diff --git a/crates/core/src/game/install/libraries.rs b/crates/core/src/game/install/libraries.rs index b5b9f7a..1fa0ccb 100644 --- a/crates/core/src/game/install/libraries.rs +++ b/crates/core/src/game/install/libraries.rs @@ -159,10 +159,8 @@ struct Arm64Native { /// Linux arm64:把 `org.lwjgl:::natives-linux` 改写成 arm64。 fn linux_arm64_native_entry(library: &Library, env: &Environment) -> Option { - if env.os != "linux" || env.arch != "arm64" || !library.name.starts_with("org.lwjgl:") { - return None; - } - let base = library.name.strip_suffix(":natives-linux")?; + let name = library.linux_arm64_native_name(env)?; + let base = name.strip_suffix(":natives-linux-arm64")?; let mut parts = base.split(':'); let group = parts.next()?; let artifact = parts.next()?; @@ -179,7 +177,7 @@ fn linux_arm64_native_entry(library: &Library, env: &Environment) -> Option::natives-linux` 改写成 `...:natives-linux-arm64`。 + /// + /// 返回 None 表示不适用(非 Linux/arm64、非 LWJGL3、或不是 natives 条目)。 + #[must_use] + pub fn linux_arm64_native_name(&self, env: &Environment) -> Option { + if env.os != "linux" || env.arch != "arm64" || !self.is_lwjgl3() { + return None; + } + let base = self.name.strip_suffix(":natives-linux")?; + Some(format!("{base}:natives-linux-arm64")) + } + /// natives 构件下载信息(按分类器查 `downloads.classifiers`)。 pub fn native_artifact(&self, classifier: &str) -> Option<&DownloadInfo> { self.downloads.as_ref()?.classifiers.get(classifier) diff --git a/crates/presenter/src/host/launch.rs b/crates/presenter/src/host/launch.rs index dc94f4f..8d65527 100644 --- a/crates/presenter/src/host/launch.rs +++ b/crates/presenter/src/host/launch.rs @@ -134,18 +134,38 @@ pub fn build_request( let mut libraries: Vec = Vec::new(); for library in resolved.libraries.as_deref().unwrap_or_default() { - if !library.applies(&environment) || library.native_classifier(&environment).is_some() { + if !library.applies(&environment) { continue; } - let Ok(coordinate) = library.coordinate() else { + // 1.19+ 的 natives 是独立库条目(name 带分类器),必须进 classpath; + // ≤1.18 的 `natives` 表条目归 natives::collect,不进 classpath。 + // Linux arm64:独立条目改写成 natives-linux-arm64(路径与展示名一起改)。 + let arm64 = library.linux_arm64_native_name(&environment); + if arm64.is_none() && library.native_classifier(&environment).is_some() { continue; + } + let (coordinate, name) = match arm64 { + Some(rewritten) => { + let Ok(coordinate) = + unlucky_core::game::library::MavenCoordinate::parse(&rewritten) + else { + continue; + }; + (coordinate, rewritten) + } + None => { + let Ok(coordinate) = library.coordinate() else { + continue; + }; + (coordinate, library.name.clone()) + } }; let path = if library.is_local() { env.game.version_library(&version_id, &coordinate) } else { env.game.library(&coordinate) }; - libraries.push(LibraryInput::new(path, library.name.clone())); + libraries.push(LibraryInput::new(path, name)); } let natives = unlucky_core::launch::natives::collect( &env.game, From 394faad61160075aa806e54c227370038aba0252 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 08:54:12 +0800 Subject: [PATCH 13/21] =?UTF-8?q?docs(termux):=20MC=201.20.4=20=E7=9C=9F?= =?UTF-8?q?=E6=9C=BA=E8=B7=91=E9=80=9A=E7=BB=93=E8=AE=BA=E4=B8=8E=E5=89=A9?= =?UTF-8?q?=E4=BD=99=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README:MC 标 ✅(glibc JRE + arm64 natives + softpipe);后续收敛为 zink/DRI3、剪贴板/IME、narrator/音频 - 构建文档 §4.2 补真机启动命令、窗口尺寸与日志位置、非致命项 --- README.termux.md | 16 ++++++++-------- packaging/docs/termux-glibc-build.md | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.termux.md b/README.termux.md index 128e624..c77d1cb 100644 --- a/README.termux.md +++ b/README.termux.md @@ -15,7 +15,7 @@ | Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | | Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | | GPU(wgpu) | ✅ | 自编 Mesa 26.2.6 + KGSL;wgpu 选中 Adreno 840(Vulkan/turnip) | -| Minecraft 运行 | 🟡 | 栈已验证;启动器已接线(natives arm64 / softpipe env);待真实 MC 验证 | +| Minecraft 运行 | ✅ | 真机 1.20.4 跑通:glibc JRE + arm64 natives + softpipe GL,主菜单在 X11 渲染 | ## 快速开始 @@ -38,15 +38,15 @@ DISPLAY=:0 ./unlucky-launcher-iced --software-renderer `$PREFIX/glibc/{lib,share/vulkan/icd.d}` 后应用会自动启用;Termux 官方源里的 glibc Mesa 太旧(24.2.6 不认识 Adreno 840)。驱动缺失时自动降级 CPU,不会崩。 - **MC**:Android 原生 natives 走不通(LWJGL 没有官方 Android/bionic 产物,且 GLFW 的 - 窗口模型与 termux-x11 的 X11 不兼容)。有效路线是 glibc JVM——Temurin 21 JRE + AWT、 - LWJGL arm64 + GLFW + GLX/softpipe 已手动跑通(GL 3.3)。启动器已接线: - Linux/aarch64 自动选 `natives-linux-arm64`(阿里云 Maven 兜底),MC 子进程默认带 - softpipe 环境。还差把实例 Java 指到 glibc JRE 后跑一次真实 MC。 + 窗口模型与 termux-x11 的 X11 不兼容)。有效路线是 glibc JVM,已真机跑通 1.20.4: + Temurin 21 JRE(patchelf 后直接执行)+ Linux arm64 natives(启动器自动改写/镜像兜底) + + softpipe GL。非致命项:narrator 的 flite 缺失、LWJGL JNI 版本告警; + softpipe 是软件渲染,帧率有限,zink(硬件 GL)仍卡 DRI3。 - Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), PC 的 Linux CI 不受影响。 ## 后续 -1. MC 运行链接线(natives arm64 映射 + glibc JVM 启动 + softpipe env) -2. zink 的 DRI3 问题(硬件 GL;当前 softpipe 可跑但慢) -3. 剪贴板/IME 在 termux-x11 下的行为验证 +1. zink 的 DRI3 问题(硬件 GL;当前 softpipe 可跑但慢)——这是 MC 帧率的主要瓶颈 +2. 剪贴板/IME 在 termux-x11 下的行为验证 +3. narrator/flite 与音频设备(当前 missing 但不阻断) diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index 43c8225..5860174 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -206,6 +206,12 @@ $PREFIX/glibc/lib/ld-linux-aarch64.so.1 \ > ``` > > 之后在实例设置里把 Java 指向 `$JRE/bin/java`(或 `--java`)即可。 +> +> **真机验证(1.20.4)**:`ull install 1.20.4 --name mc-test`(740 MB)成功后, +> `ull launch mc-test --java "$JRE/bin/java" --account Tester`, +> MC 主菜单在 termux-x11 中渲染(窗口 854×480;日志在实例目录 +> `versions/1.20.4/logs/latest.log`)。非致命项:narrator 的 flite 缺失、 +> LWJGL JNI 版本告警、oshi 的 cpuVendor NPE(都未阻断启动)。 ## 5. GPU(已打通:自编 Mesa 26.2.6 + KGSL) From fe472e36a5d6490ac54a124a3a73e736c584d9e3 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:04:58 +0800 Subject: [PATCH 14/21] =?UTF-8?q?docs(termux):=20README=20=E6=A0=A1?= =?UTF-8?q?=E6=AD=A3=E2=80=94=E2=80=94GPU=20=E9=BB=98=E8=AE=A4=E3=80=81?= =?UTF-8?q?=E6=A8=AA=E5=B1=8F=E6=88=AA=E5=9B=BE=E5=B0=BA=E5=AF=B8=E3=80=81?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=A0=B9=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 构建描述从 CPU-only 改为 GPU 构建(wgpu + CPU 兜底) - 窗口证据改为横屏 1657x1067、首帧 ~60 ms - 快速开始不再强制 --software-renderer:默认 auto,驱动缺失自动降级 - 补数据根说明(Portable 按 exe 目录,--data-dir 暂忽略) --- README.termux.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.termux.md b/README.termux.md index c77d1cb..c874f97 100644 --- a/README.termux.md +++ b/README.termux.md @@ -7,8 +7,8 @@ | 项 | 状态 | 证据 | | --- | --- | --- | -| 源码构建(GUI + CLI) | ✅ | Rust 1.99.1 gnu + clang/lld + glibc 运行时;CPU-only(tiny-skia) | -| 窗口显示 | ✅ | termux-x11 `:0`,截图 1180×760,首帧 ~330 ms | +| 源码构建(GUI + CLI) | ✅ | Rust 1.99.1 gnu + clang/lld + glibc 运行时;GPU 构建(wgpu)默认带 CPU 兜底 | +| 窗口显示 | ✅ | termux-x11 `:0`,横屏 1657×1067,首帧 ~60 ms(GPU) | | 中文 | ✅ | 启动时注册 `/system/fonts/NotoSansCJK-Regular.ttc` | | 微软登录 id | ✅ | 构建期注入,日志 `client_id=configured source=build` | | 横屏适配 | ✅ | 默认窗口按 X 屏幕等比自适应(`source=termux-screen`) | @@ -24,12 +24,16 @@ ```sh termux-x11 :0 -ac & -DISPLAY=:0 ./unlucky-launcher-iced --software-renderer +DISPLAY=:0 ./unlucky-launcher-iced # 默认 auto:有驱动走 GPU,没有则自动降级 CPU ``` -- 没有 GPU 栈时**必须**加 `--software-renderer`(CPU 渲染)。 +- GPU 驱动不随 Termux 源提供,需要按构建文档 §5 自己编一次 Mesa 26 + ICD; + 不装也能跑,只是 `auto` 会落到 tiny-skia(CPU)。 +- 想强制某一侧:`--renderer=gpu` / `--software-renderer`。 - 窗口尺寸默认按屏幕自适应;`ULL_WINDOW_SIZE=1520x900` 可覆盖。 - Java:`ull java install 17|21|25` 在 Termux 上默认用系统包(内置镜像 + 隔离 apt 状态)。 +- **数据根**:Portable 模式按**可执行文件所在目录**生成 `.ULL` / `.minecraft`; + 换目录启动会看到另一套实例(`--data-dir` 目前接受并忽略)。 ## 已知边界 From 41019d9cae9b9d5500e7f4cd73c79ee2616a2eef Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:05:37 +0800 Subject: [PATCH 15/21] =?UTF-8?q?docs(termux):=20=E6=A0=B9=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=96=B0=E5=A2=9E=E9=80=82=E9=85=8D=E6=96=B9=E6=A1=88?= =?UTF-8?q?=E4=B8=8E=E5=90=88=E5=B9=B6=E6=8C=87=E5=8D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TERMUX-ADAPTATION.md:方案(平台识别/JDK/字体/横屏/GPU/MC)、逐文件改动表、 兼容性说明(Termux gate、x86_64 不变)、合并步骤与 CI 预期、验证矩阵、 后续路线与按组回滚策略。README.termux 顶部链过去。 --- README.termux.md | 3 + TERMUX-ADAPTATION.md | 132 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 TERMUX-ADAPTATION.md diff --git a/README.termux.md b/README.termux.md index c874f97..14d54ed 100644 --- a/README.termux.md +++ b/README.termux.md @@ -3,6 +3,9 @@ 这条分支把启动器带到 **Termux(Android)**:用 glibc 工具链编出 `aarch64-unknown-linux-gnu` 二进制,在 **termux-x11** 里跑 GUI;同时补上 Termux 平台识别、系统 JDK 安装与中文字体。 +> 给主线的改动清单、兼容性说明与合并指南见 +> [`TERMUX-ADAPTATION.md`](TERMUX-ADAPTATION.md)。 + ## 已验证 | 项 | 状态 | 证据 | diff --git a/TERMUX-ADAPTATION.md b/TERMUX-ADAPTATION.md new file mode 100644 index 0000000..1048010 --- /dev/null +++ b/TERMUX-ADAPTATION.md @@ -0,0 +1,132 @@ +# Termux 适配:方案、改动清单与合并指南 + +> 分支:`termux-glibc`(基于 `origin/dev`,14 个提交,HEAD `38d3024`) +> 目的:让主线快速了解这组代码改了什么、为什么、怎么验证、如何合并。 + +## TL;DR + +- **构建**:为 Termux 编出 `aarch64-unknown-linux-gnu`(glibc)二进制,在 termux-x11 里跑 GUI。 + Android 原生 target 走不通(winit 的 android 后端需要 NativeActivity),所以不用它。 +- **已跑通**:GUI(CPU 与 GPU 两条渲染路径)、中文、横屏自适应、JDK 发现与系统包安装、 + MC 1.20.4(glibc JRE + arm64 natives + softpipe GL,真机进游戏)。 +- **未收口**:MC 启动参数支持(由主线后续补)、MC 硬件 GL(zink 卡 DRI3)、剪贴板/IME。 +- **对主线的影响**:所有行为都由 **运行时 Termux 识别 / `cfg(target_os = "linux")` / + 文件存在** 三选一 gate 住,Windows / macOS / 普通 Linux 的默认路径不变。 + +## 方案 + +### 1. 平台识别 + +`crates/core/src/platform/termux.rs`:用 `TERMUX_VERSION` 或默认 `PREFIX` +(`/data/data/com.termux/files/usr`)识别 Termux,推导 `$PREFIX` 与 `$PREFIX/lib/jvm`。 +不把路径写死到调用方,也不改 `OsFamily`(Android 上仍按 Linux 家族处理)。 + +### 2. JDK:发现 + 安装 + +- **发现**:`game/java/discover.rs` 的 Unix 扫描根追加 `$PREFIX/lib/jvm`, + 环境可注入(`standard_roots_in`)。补上「Termux 装了 openjdk 但没设 JAVA_HOME/PATH」的盲点。 +- **安装**:新增 `game/java/termux.rs`。Termux 的 openjdk 是 `.deb`(bionic), + 与 Temurin 的 zip/tar.gz 不是同一形态,所以**委托包管理器**: + - 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序 `apt-get update && install`; + - lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不污染用户 apt 配置;装完清理; + - 装完从 `$PREFIX/lib/jvm` 定位,幂等(已装则不走网络)。 +- **vendor 接线**:`JdkVendor::Termux`(17/21/25)。Termux 上缺省安装先试系统包, + 失败再走归档链;`--vendor termux` 只走系统包。设置白名单与契约测试同步。 + +### 3. 字体 + +`crates/app-iced/src/fonts.rs`:Termux 上把 `/system/fonts/NotoSansCJK-Regular.ttc` +注册进 cosmic-text(fontdb 不扫 Android 字体目录),中文不再显示方框。 + +### 4. 窗口(横屏优先) + +`app/boot/termux_window.rs`:Termux 上默认窗口按当前 X 屏幕等比自适应 +(日志 `size_source=termux-screen`),横屏铺满、竖屏不溢出; +`ULL_WINDOW_SIZE` 覆盖与 1180×760 验收基准不变。 + +### 5. GPU(启动器) + +- 驱动是自编的 **Mesa 26.2.6 + KGSL**,装在 `$PREFIX/glibc/{lib,share/vulkan/icd.d}`; + 构建/安装步骤见 `packaging/docs/termux-glibc-build.md` §5。 +- `app/boot/termux_gpu.rs`:glibc 构建 + Termux + ICD 文件存在时,在探测前设 + `VK_ICD_FILENAMES` / `VK_DRIVER_FILES`;用户显式设置不覆盖。 +- 实测 wgpu 选中 Adreno 840(turnip Mesa 26.2.6);无驱动自动降级 tiny-skia。 + +### 6. Minecraft 运行链 + +- **natives**:1.19+ 的清单把 natives 作为独立库条目(`org.lwjgl:...:natives-linux`), + Linux/aarch64 上改写成 `natives-linux-arm64`(`Library::linux_arm64_native_name`, + install 与 launch 共用);Mojang 的 `libraries.minecraft.net` 对 arm64 一律 404, + 下载兜底到阿里云 Maven(`install/libraries.rs`)。 +- **JVM**:用 glibc 的 Temurin JRE(patchelf 后直接执行),bionic 的 Termux JVM + 加载不了 glibc natives。 +- **GL**:glibc Mesa 的 zink 在 termux-x11 下卡 `DRI3 not available`, + 子进程默认带 `MESA_LOADER_DRIVER_OVERRIDE=swrast` + `LIBGL_ALWAYS_SOFTWARE=1` + (`launch/defaults.rs`,用户自定义环境变量可覆盖)。 + +## 改动清单 + +| 文件 | 改动 | 平台隔离 | +| --- | --- | --- | +| `core/platform/termux.rs`(新) | Termux 识别、`$PREFIX`/JVM 根 | 纯函数,按环境判断 | +| `core/platform/mod.rs` | 导出新模块 | — | +| `core/game/java/discover.rs` | `$PREFIX/lib/jvm` 扫描 | Unix + Termux 判断 | +| `core/game/java/termux.rs`(新) | 镜像/隔离 apt 安装、`installed_home` | 仅 Termux 可执行 | +| `core/game/java/vendor.rs` / `source.rs` / `install.rs` | `JdkVendor::Termux` 与安装分派 | Termux 上才启用 | +| `core/setting/keys.rs` + 契约测试 | 白名单加 `termux` | 跨平台可选,非 Termux 明确报错 | +| `cli/commands/java_vendors.rs`、`cli/cli/help.rs`、`presenter/host/java.rs` | 展示与帮助文案 | 展示层 | +| `core/game/library.rs` | `linux_arm64_native_name` / 分类器改写 | 仅 linux+arm64 | +| `core/game/install/libraries.rs` | 现代 natives 条目改写 + Maven 兜底 | 仅 linux+arm64 | +| `core/launch/defaults.rs` | Termux softpipe 环境默认值 | `cfg(linux)` + Termux | +| `presenter/host/launch.rs`、`cli/commands/snapshot.rs` | classpath 用改写后的 arm64 坐标 | 仅 linux+arm64 | +| `app-iced/fonts.rs` | Android CJK 字体注册 | 仅 Termux | +| `app-iced/boot/termux_window.rs`(新) | 横屏自适应 | 仅 Termux | +| `app-iced/boot/termux_gpu.rs`(新) | 自动启用自编 ICD | 仅 glibc+Termux+文件存在 | +| `app-iced/Cargo.toml` | 新增 `unlucky-core` 依赖 | 复用平台识别 | +| `README.termux.md`(新)、`packaging/docs/termux-glibc-build.md`(新) | 分支说明与构建配方 | 文档 | +| `README.md` / `README.zh-CN.md` / 支持矩阵两处 | Termux 行同步 | 文档 | + +## 行为变化与兼容性 + +- 新增 `JdkVendor::Termux` 会改变 `VENDOR_IDS` / `JDK_VENDORS` 的允许值集合 + (契约测试已同步)。非 Termux 上显式选它会在安装时明确报「只在 Termux 里可用」, + 默认回退序**不含** termux,PC 行为不变。 +- Linux/aarch64 的 LWJGL3 natives 会被改写;x86_64、Windows、macOS 不受影响。 +- `app-iced` 直接依赖 `unlucky-core`(此前通过 cli/presenter 间接依赖)。 +- 所有 Termux 专属路径都需要运行时识别或文件存在才会生效;普通 Linux 桌面不会触发。 + +## 合并指南 + +1. `git fetch origin && git rebase origin/dev`(分支基于 `7d8b3b9` 之后的 dev; + dev 若前进,先 rebase 再推)。 +2. PR base:`dev`。CI 门禁:`fmt` / `clippy --workspace --all-targets -D warnings` / + `test --workspace --all-targets` / `cargo deny`(本机没装 deny,PC 上补跑)。 +3. 本机已知、**不影响 PC CI** 的三条失败: + - `unlucky-core --test launch`:`/proc//task//children`(Android 内核没有) + - 同测试:长路径被脱敏成 `` + - `presenter settings_bridge`:mtime 热重载偶发(flaky,单跑 3 次 2 过 1 挂) +4. **不要提交**:`dist/`、`.gnu/`、任何 `ULL_MSA_CLIENT_ID` 值(仓库 Secret)。 +5. 合并后建议真机再跑一次:`--software-renderer` 截图 + `ull java install 17` + MC 启动。 + +## 验证矩阵(本机) + +| 项 | 结果 | +| --- | --- | +| GUI 截图(横屏 1657×1067) | ✅ 中文正常,`client_id=configured source=build` | +| GPU 渲染 | ✅ wgpu 选中 Adreno 840(turnip Mesa 26.2.6);降级路径也验证过 | +| `ull java install 17` | ✅ SAU 镜像约 20 s,装完清理,`java list` 可见 | +| MC 1.20.4 | ✅ 真机安装(740 MB)→ 主菜单 → 建世界 → 进游戏 | +| fmt / clippy / 核心测试 | ✅ 475 条核心测试通过,clippy 零告警 | + +## 后续路线 + +1. **MC 启动参数**(主线功能,另行排期):当前 `ull launch` 不接受自定义 JVM/游戏参数。 +2. **MC 硬件 GL**:修 glibc GLX 的 DRI3(让 zink 生效),或改用 VulkanMod。 +3. 剪贴板 / IME 在 termux-x11 下的行为验证。 +4. narrator/flite 与音频设备(当前缺失但不阻断)。 +5. GPU 驱动分发:目前每次都要按文档自编 Mesa;可考虑脚本化/预编译产物。 + +## 回滚策略 + +全部改动按提交分组(platform → fonts → docs → java → gpu → natives → mc), +可按组 revert;Termux 专属逻辑都有运行时/文件存在 gate,回滚不会波及 PC 路径。 From 161aec52f4d48181b01eaec8885645f68fedf8f3 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:11:41 +0800 Subject: [PATCH 16/21] =?UTF-8?q?fix(app-iced):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=9A=84=20unlucky-core=20=E4=BE=9D=E8=B5=96=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=200.2.2=EF=BC=88=E5=90=88=E5=B9=B6=E5=90=8E?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E5=86=B2=E7=AA=81=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit termux-glibc 分支写这条依赖时 workspace 版本还是 0.2.1;M154 之后其余内部依赖 都已被抬到 0.2.2。caret 约束下 0.2.1 仍能解析到 0.2.2,所以不会构建失败, 但版本对齐是 M154 的既定契约,留一条 0.2.1 会让下一个读到该文件的人以为有例外。 --- crates/app-iced/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/app-iced/Cargo.toml b/crates/app-iced/Cargo.toml index 468ab48..ecba70b 100644 --- a/crates/app-iced/Cargo.toml +++ b/crates/app-iced/Cargo.toml @@ -25,7 +25,7 @@ path = "src/lib.rs" unlucky-design = { path = "../design", version = "0.2.2" } # 平台识别(Termux / $PREFIX 路径)唯一真源:字体注册这类平台适配不再各写一份环境判断。 -unlucky-core = { path = "../core", version = "0.2.1" } +unlucky-core = { path = "../core", version = "0.2.2" } # M85:UI 框架无关的宿主层(DTO / 状态 reducer / host)。**只登记依赖,本 mission 不接线**—— # 页面用它的时机由后续 iced 页面 mission 决定(接线流程见 crates/presenter/src/lib.rs 的 crate 文档)。 From c82e39d0eb7346dc6a5a2d5b7994aaee79e43478 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:19:14 +0800 Subject: [PATCH 17/21] =?UTF-8?q?fix(core):=20Windows=20=E4=B8=8A=20clippy?= =?UTF-8?q?=20=E6=8A=A5=20discover.rs=20=E4=B8=A4=E4=B8=AA=20cfg(unix)=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E5=AF=BC=E5=85=A5=E6=9C=AA=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这两个 Termux 扫描用例挂着 `#[cfg(unix)]`,但它们的 `use`(`standard_roots_in`、 `platform::paths::MapEnv`)写在 mod tests 顶部没跟着 gate: - Termux/Linux 上 cfg(unix) 成立 → 有用例 → 导入有使用者 → clippy 干净(作者本机 475 测试 通过是真的); - Windows 上那两个用例不编译 → 导入无使用者 → `-D warnings` 直接判错 (`unused import` ×2,`could not compile unlucky-core (lib test)`)。 把两个 `use` 同样 gate 成 `#[cfg(unix)]`。代价是这两个用例在 Windows 上不编译不执行, 覆盖由 Linux CI 承担(它们断言的本来就是 Unix 路径)。 --- crates/core/src/game/java/discover.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/core/src/game/java/discover.rs b/crates/core/src/game/java/discover.rs index 7cc2700..bfd9d01 100644 --- a/crates/core/src/game/java/discover.rs +++ b/crates/core/src/game/java/discover.rs @@ -225,8 +225,13 @@ pub fn path_key(path: &Path) -> String { #[cfg(test)] mod tests { - use super::{dedupe, parse_java_home, path_key, standard_roots, standard_roots_in}; + use super::{dedupe, parse_java_home, path_key, standard_roots}; use crate::game::java::probe::{JavaOrigin, JavaRuntime}; + // 这两个只有 Termux 扫描的两个测试用,而那两个测试是 `cfg(unix)`——导入必须同样 gate, + // 否则 Windows 上不编译测试、导入就成了未使用(`-D warnings` 直接判错)。 + #[cfg(unix)] + use super::standard_roots_in; + #[cfg(unix)] use crate::platform::paths::MapEnv; fn runtime(path: &str, origin: JavaOrigin) -> JavaRuntime { From 4dbeb07220428829b6a2431959b56a3a9b59b115 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:19:18 +0800 Subject: [PATCH 18/21] =?UTF-8?q?refactor(core):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E8=B6=8A=20400=20=E8=A1=8C=E7=BA=A2=E7=BA=BF=E7=9A=84=20instal?= =?UTF-8?q?l.rs=20=E4=B8=8E=20install/libraries.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 合并 termux-glibc 后两个文件顶破了「单文件 ≤400 行」(guards.yml 的 file-size): `game/java/install.rs` 379 → 456、`game/install/libraries.rs` 258 → 409。 按步骤拆,纯搬运、零行为变化: - `install/termux_install.rs`(新):Termux 系统包那条路。它与归档链不共用任何一步 (不查版本元数据、不下载、不解压、不校验 sha256),原来只能是一段嵌在 install() 中间的 if。返回 `Outcome::Done`/`Skipped` 并用 Box 装 `InstalledJdk`(否则 large_enum_variant: 392 vs 24 字节)。 - `install/one.rs`(新):回退链里「一家」的机械步骤——下载 → 解压 → 探测 + 幂等复用, 连同 `Prepared` 与 `note()`。 - `install/libraries/arm64_natives.rs`(新):Linux arm64 的 natives 改写与 Maven 兜底 (两个常量 + 改写/拼址两个函数)。用例仍留在 libraries.rs 的 tests 里,它们从 `plan()` 的出口断言,不碰私有步骤。 拆分后:install.rs 319、one.rs 100、termux_install.rs 104、libraries.rs 348、 arm64_natives.rs 72;全仓受检文件(*.rs/*.md/*.toml/*.yml/*.json/*.py/*.sh)复算 最大 400 行,红线内。 --- crates/core/src/game/install/libraries.rs | 53 +----- .../game/install/libraries/arm64_natives.rs | 72 ++++++++ crates/core/src/game/java/install.rs | 174 +++--------------- crates/core/src/game/java/install/one.rs | 100 ++++++++++ .../src/game/java/install/termux_install.rs | 105 +++++++++++ 5 files changed, 302 insertions(+), 202 deletions(-) create mode 100644 crates/core/src/game/install/libraries/arm64_natives.rs create mode 100644 crates/core/src/game/java/install/one.rs create mode 100644 crates/core/src/game/java/install/termux_install.rs diff --git a/crates/core/src/game/install/libraries.rs b/crates/core/src/game/install/libraries.rs index 1fa0ccb..ea92fce 100644 --- a/crates/core/src/game/install/libraries.rs +++ b/crates/core/src/game/install/libraries.rs @@ -18,6 +18,10 @@ use super::super::library::Library; use super::plan::{FileRole, PlanFile}; use crate::download::ResourceKind; +use arm64_natives::{arm64_native_url, linux_arm64_native_entry, LINUX_ARM64_CLASSIFIER}; + +mod arm64_natives; + /// 没写 `url` 的库兜底用的 Maven 基址(Mojang 官方库仓)。 pub const DEFAULT_MAVEN_BASE: &str = "https://libraries.minecraft.net/"; @@ -150,39 +154,6 @@ fn library_dest( } } -/// 现代清单里 LWJGL3 natives 条目的改写结果。 -struct Arm64Native { - name: String, - relative: PathBuf, - url: String, -} - -/// Linux arm64:把 `org.lwjgl:::natives-linux` 改写成 arm64。 -fn linux_arm64_native_entry(library: &Library, env: &Environment) -> Option { - let name = library.linux_arm64_native_name(env)?; - let base = name.strip_suffix(":natives-linux-arm64")?; - let mut parts = base.split(':'); - let group = parts.next()?; - let artifact = parts.next()?; - let version = parts.next()?; - if parts.next().is_some() { - return None; - } - let relative = PathBuf::from(format!( - "{}/{artifact}/{version}/{artifact}-{version}-natives-linux-arm64.jar", - group.replace('.', "/") - )); - let url = format!( - "{LINUX_ARM64_NATIVES_BASE}{}", - relative.to_string_lossy().replace('\\', "/") - ); - Some(Arm64Native { - name, - relative, - url, - }) -} - /// 按库声明(或默认)的 Maven 基址拼下载地址。 fn maven_url(library: &Library, relative: &Path) -> String { let base = library @@ -199,22 +170,6 @@ fn maven_url(library: &Library, relative: &Path) -> String { } } -/// Linux arm64 的 natives 兜底仓库:Mojang 的 `libraries.minecraft.net` 对 -/// `natives-linux-arm64` 一律 404(实测 3.3.2 的 lwjgl/glfw/opengl/stb/tinyfd/openal), -/// 同一坐标在 Maven Central / 阿里云镜像有。 -pub const LINUX_ARM64_NATIVES_BASE: &str = "https://maven.aliyun.com/repository/public/"; - -/// Linux arm64 的 natives 分类器(见 `Library::native_classifier`)。 -pub const LINUX_ARM64_CLASSIFIER: &str = "natives-linux-arm64"; - -/// arm64 natives 的兜底 URL(按相对路径拼镜像基址)。 -fn arm64_native_url(relative: &Path) -> String { - format!( - "{LINUX_ARM64_NATIVES_BASE}{}", - relative.to_string_lossy().replace('\\', "/") - ) -} - /// 供上层判错用:库展开不产生硬失败,缺地址一律降级成告警。 #[cfg(test)] mod tests { diff --git a/crates/core/src/game/install/libraries/arm64_natives.rs b/crates/core/src/game/install/libraries/arm64_natives.rs new file mode 100644 index 0000000..fc0c7f9 --- /dev/null +++ b/crates/core/src/game/install/libraries/arm64_natives.rs @@ -0,0 +1,72 @@ +//! Linux arm64 的 natives 改写与下载兜底。 +//! +//! 两件事实拼出来的特例: +//! 1. Mojang 清单里 LWJGL3 的 natives 只写 `natives-linux`(x86_64 的 `.so`); +//! 直接用它,在 aarch64 JVM 里加载必失败。 +//! 2. 同一个 Maven 坐标确实有 `natives-linux-arm64` 分类器,但 `libraries.minecraft.net` +//! 对它一律 404(实测 3.3.2 的 lwjgl/glfw/opengl/stb/tinyfd/openal),Maven Central 与 +//! 阿里云镜像有。 +//! +//! 所以 Linux arm64 上:分类器改写(`Library::native_classifier` / +//! `Library::linux_arm64_native_name`)+ 地址兜底到镜像。**只在 linux + arm64 生效**, +//! x86_64 / Windows / macOS 的地址一个字节不变。用例仍在 `libraries.rs` 的 tests 里 +//! (它们从 `plan()` 的出口断言,不直接调这里的私有步骤)。 +//! +//! 从 `libraries.rs` 拆出来是为了守住 400 行红线(`guards.yml` 的 `file-size`)。 + +use std::path::{Path, PathBuf}; + +use crate::game::arguments::Environment; +use crate::game::library::Library; + +/// Linux arm64 的 natives 兜底仓库:Mojang 的 `libraries.minecraft.net` 对 +/// `natives-linux-arm64` 一律 404(实测 3.3.2 的 lwjgl/glfw/opengl/stb/tinyfd/openal), +/// 同一坐标在 Maven Central / 阿里云镜像有。 +pub const LINUX_ARM64_NATIVES_BASE: &str = "https://maven.aliyun.com/repository/public/"; + +/// Linux arm64 的 natives 分类器(见 `Library::native_classifier`)。 +pub const LINUX_ARM64_CLASSIFIER: &str = "natives-linux-arm64"; + +/// 现代清单里 LWJGL3 natives 条目的改写结果。 +pub(super) struct Arm64Native { + pub(super) name: String, + pub(super) relative: PathBuf, + pub(super) url: String, +} + +/// Linux arm64:把 `org.lwjgl:::natives-linux` 改写成 arm64。 +pub(super) fn linux_arm64_native_entry( + library: &Library, + env: &Environment, +) -> Option { + let name = library.linux_arm64_native_name(env)?; + let base = name.strip_suffix(":natives-linux-arm64")?; + let mut parts = base.split(':'); + let group = parts.next()?; + let artifact = parts.next()?; + let version = parts.next()?; + if parts.next().is_some() { + return None; + } + let relative = PathBuf::from(format!( + "{}/{artifact}/{version}/{artifact}-{version}-natives-linux-arm64.jar", + group.replace('.', "/") + )); + let url = format!( + "{LINUX_ARM64_NATIVES_BASE}{}", + relative.to_string_lossy().replace('\\', "/") + ); + Some(Arm64Native { + name, + relative, + url, + }) +} + +/// arm64 natives 的兜底 URL(按相对路径拼镜像基址)。 +pub(super) fn arm64_native_url(relative: &Path) -> String { + format!( + "{LINUX_ARM64_NATIVES_BASE}{}", + relative.to_string_lossy().replace('\\', "/") + ) +} diff --git a/crates/core/src/game/java/install.rs b/crates/core/src/game/java/install.rs index 9201d85..358b22c 100644 --- a/crates/core/src/game/java/install.rs +++ b/crates/core/src/game/java/install.rs @@ -19,16 +19,21 @@ use std::path::{Path, PathBuf}; -use super::error::{io_error, JavaError, JavaErrorKind}; +use super::error::{JavaError, JavaErrorKind}; use super::extract; use super::location::{JavaInstallRoot, LocationMode}; -use super::probe::{self, JavaOrigin, JavaRuntime}; -use super::release::{JdkArchive, JdkRelease}; +use super::probe::{self, JavaRuntime}; +use super::release::JdkRelease; use super::source::{self, ChainLog}; use super::target::AdoptiumTarget; use super::vendor::{self, JdkVendor}; use crate::download::{DownloadEngine, DownloadRequest, ResourceKind}; +use one::install_one; + +mod one; +mod termux_install; + /// 归档缓存目录名(在安装根下;托管目录扫描跳过它)。 pub use super::location::CACHE_DIR; @@ -186,75 +191,24 @@ pub async fn install( ) -> Result { let domestic = engine.region().is_domestic(); let priority = request.order(domestic); - let mut chain = ChainLog::new(); let mut reasons: Vec = Vec::new(); // 清掉上次中断留下的 .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(), + // Termux 那一条路(系统包)单独成模块:它与归档链不共用任何一步,见 `termux_install`。 + // 它把 chain 还回来(没走成时),归档链接着往上记。 + let mut chain = match termux_install::attempt( + request.vendor, + request.major, + location, + ChainLog::new(), + &mut reasons, + ) + .await? + { + termux_install::Outcome::Done(installed) => return Ok(*installed), + termux_install::Outcome::Skipped(chain) => chain, }; - 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, reused)) => { - 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: if reused { - None - } else { - 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, // 不提前 `?` 退出——让每一家都记一条降级原因,错误里能看见全貌。 @@ -327,84 +281,6 @@ pub async fn install( .with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME")) } -/// 某一家的中间结果(把七元组元组换成具名结构,读起来才不费劲)。 -struct Prepared { - root: PathBuf, - archive: PathBuf, - runtime: JavaRuntime, - downloaded: Option, - bytes: u64, - files: usize, - extracted: bool, -} - -/// 装某一家的那一份(下载 → 解压 → 探测)。 -async fn install_one( - engine: &DownloadEngine, - location: &JavaInstallRoot, - release: &JdkRelease, -) -> Result { - let root = location.jdk_dir(release.major, release.vendor, &release.version); - let binary = extract::java_binary(&root); - let archive = archive_path(&location.root, release); - - // 幂等第一层:戳记里的 sha256 与本次一致,且 bin/java 在 → 一个字节都不动。 - if extract::is_ready(&root, release.sha256()) { - let runtime = probe::probe(&binary, JavaOrigin::Managed); - if runtime.major == Some(release.major) { - let mut runtime = runtime; - runtime.vendor = Some(release.vendor.id().to_string()); - return Ok(Prepared { - root, - archive, - runtime, - downloaded: None, - bytes: 0, - files: 0, - extracted: false, - }); - } - note(&format!( - "托管目录 {} 里的 java 跑不起来,重装一份", - root.display() - )); - } - - if let Some(parent) = archive.parent() { - std::fs::create_dir_all(parent) - .map_err(|error| io_error("创建归档缓存目录", parent.display(), &error))?; - } - let outcome = engine - .download(archive_request(release, archive.clone())) - .await?; - let report = extract::extract_archive(&archive, release.sha256(), release.major, &root)?; - let mut runtime = probe::probe(&binary, JavaOrigin::Managed); - runtime.vendor = Some(release.vendor.id().to_string()); - if runtime.major != Some(release.major) { - return Err(JavaError::new( - JavaErrorKind::Probe, - format!( - "装好的 JDK 探测结果是 Java {},与请求的 {} 不符({})", - runtime - .major - .map_or_else(|| "未知".to_string(), |major| major.to_string()), - release.major, - binary.display() - ), - ) - .with_hint("删掉该托管目录后重跑 `ull java install`;或手动设 JAVA_HOME")); - } - Ok(Prepared { - root, - archive, - runtime, - downloaded: Some(outcome.source), - bytes: outcome.bytes, - files: report.files, - extracted: report.wrote(), - }) -} - /// 只做规划不落盘:返回将要安装的版本、目标目录与「是否已经装好」。 /// /// # Errors @@ -446,11 +322,3 @@ fn majors_text(vendor: JdkVendor) -> String { .collect::>() .join("/") } - -fn note(message: &str) { - if cfg!(debug_assertions) { - eprintln!("[ull::game::java] {message}"); - } else { - let _ = message; - } -} diff --git a/crates/core/src/game/java/install/one.rs b/crates/core/src/game/java/install/one.rs new file mode 100644 index 0000000..e213e22 --- /dev/null +++ b/crates/core/src/game/java/install/one.rs @@ -0,0 +1,100 @@ +//! 回退链里的「一家」怎么装:下载 → 解压 → 探测,外加幂等复用。 +//! +//! 从 `install.rs` 拆出来是为了守住 400 行红线(`guards.yml` 的 `file-size`): +//! `install.rs` 只保留「按优先级链挑一家 / 记降级原因」的编排,这里放单家的机械步骤。 + +use std::path::PathBuf; + +use super::super::error::{io_error, JavaError, JavaErrorKind}; +use super::super::extract; +use super::super::location::JavaInstallRoot; +use super::super::probe::{self, JavaOrigin, JavaRuntime}; +use super::super::release::JdkRelease; +use super::{archive_path, archive_request}; +use crate::download::DownloadEngine; + +/// 某一家的中间结果(把七元组元组换成具名结构,读起来才不费劲)。 +pub(super) struct Prepared { + pub(super) root: PathBuf, + pub(super) archive: PathBuf, + pub(super) runtime: JavaRuntime, + pub(super) downloaded: Option, + pub(super) bytes: u64, + pub(super) files: usize, + pub(super) extracted: bool, +} + +/// 装某一家的那一份(下载 → 解压 → 探测)。 +pub(super) async fn install_one( + engine: &DownloadEngine, + location: &JavaInstallRoot, + release: &JdkRelease, +) -> Result { + let root = location.jdk_dir(release.major, release.vendor, &release.version); + let binary = extract::java_binary(&root); + let archive = archive_path(&location.root, release); + + // 幂等第一层:戳记里的 sha256 与本次一致,且 bin/java 在 → 一个字节都不动。 + if extract::is_ready(&root, release.sha256()) { + let runtime = probe::probe(&binary, JavaOrigin::Managed); + if runtime.major == Some(release.major) { + let mut runtime = runtime; + runtime.vendor = Some(release.vendor.id().to_string()); + return Ok(Prepared { + root, + archive, + runtime, + downloaded: None, + bytes: 0, + files: 0, + extracted: false, + }); + } + note(&format!( + "托管目录 {} 里的 java 跑不起来,重装一份", + root.display() + )); + } + + if let Some(parent) = archive.parent() { + std::fs::create_dir_all(parent) + .map_err(|error| io_error("创建归档缓存目录", parent.display(), &error))?; + } + let outcome = engine + .download(archive_request(release, archive.clone())) + .await?; + let report = extract::extract_archive(&archive, release.sha256(), release.major, &root)?; + let mut runtime = probe::probe(&binary, JavaOrigin::Managed); + runtime.vendor = Some(release.vendor.id().to_string()); + if runtime.major != Some(release.major) { + return Err(JavaError::new( + JavaErrorKind::Probe, + format!( + "装好的 JDK 探测结果是 Java {},与请求的 {} 不符({})", + runtime + .major + .map_or_else(|| "未知".to_string(), |major| major.to_string()), + release.major, + binary.display() + ), + ) + .with_hint("删掉该托管目录后重跑 `ull java install`;或手动设 JAVA_HOME")); + } + Ok(Prepared { + root, + archive, + runtime, + downloaded: Some(outcome.source), + bytes: outcome.bytes, + files: report.files, + extracted: report.wrote(), + }) +} + +fn note(message: &str) { + if cfg!(debug_assertions) { + eprintln!("[ull::game::java] {message}"); + } else { + let _ = message; + } +} diff --git a/crates/core/src/game/java/install/termux_install.rs b/crates/core/src/game/java/install/termux_install.rs new file mode 100644 index 0000000..62d8ef6 --- /dev/null +++ b/crates/core/src/game/java/install/termux_install.rs @@ -0,0 +1,105 @@ +//! `install` 的 Termux 分支:JDK 由系统包管理器装进 `$PREFIX`,不是可下载的归档。 +//! +//! 单独成模块有两个理由:`.deb` 这条路与归档链**没有共用步骤**(不查版本元数据、不下载、 +//! 不解压、不校验 sha256),留在 `install()` 里只能是一段嵌在中间的 if;而 `install.rs` +//! 贴着 400 行红线(拆分前 456 行,见 `guards.yml` 的 `file-size`)。 + +use super::super::error::{JavaError, JavaErrorKind}; +use super::super::extract; +use super::super::location::JavaInstallRoot; +use super::super::probe::{self, JavaOrigin}; +use super::super::release::{JdkArchive, JdkRelease}; +use super::super::source::ChainLog; +use super::super::vendor::JdkVendor; +use super::InstalledJdk; + +/// Termux 分支的结果。 +pub(super) enum Outcome { + /// 系统包装成了,`install` 直接返回它(装箱只为让枚举别撑到 392 字节, + /// 这条路一次安装才走一次)。 + Done(Box), + /// 不该走这条分支,或者走了但失败(原因已进 `chain` 与 `reasons`)——把 chain 还给调用方, + /// 由它继续跑归档回退链。 + Skipped(ChainLog), +} + +/// 尝试走 Termux 系统包。 +/// +/// 语义(D23「`--vendor` 钉死一家、不自动降级」): +/// - `pinned = Some(Termux)`:只走这条,失败直接冒泡; +/// - `pinned = None`:Termux 上先试系统包,失败记一条降级原因后继续归档链; +/// - 其它平台:`Ok(Skipped(chain))`,一个字节都不动。 +/// +/// # Errors +/// +/// 只有「钉死 termux」且失败时才返回错误;其余失败一律降级成 `reasons` 里的一行。 +pub(super) async fn attempt( + pinned: Option, + major: u32, + location: &JavaInstallRoot, + chain: ChainLog, + reasons: &mut Vec, +) -> Result { + let wanted = match pinned { + Some(JdkVendor::Termux) => true, + Some(_) => false, + None => super::super::termux::available(), + }; + if !wanted { + return Ok(Outcome::Skipped(chain)); + } + if !super::super::termux::available() { + return Err( + JavaError::new(JavaErrorKind::Unsupported, "termux 源只在 Termux 里可用") + .with_hint("普通 Linux/Windows 用 --vendor temurin|oracle|microsoft|zulu"), + ); + } + let mut chain = chain; + match super::super::termux::install(major).await { + Ok((home, reused)) => { + 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(|| major.to_string()); + let release = JdkRelease { + vendor: JdkVendor::Termux, + major, + version, + semver: None, + release_name: format!("Termux openjdk-{major}"), + archive: JdkArchive { + name: format!("openjdk-{major}.deb"), + url: String::new(), + sha256: String::new(), + size: 0, + }, + }; + chain.hit(JdkVendor::Termux, format!("命中 {}", release.summary())); + Ok(Outcome::Done(Box::new(InstalledJdk { + major: release.major, + vendor: JdkVendor::Termux, + release, + root: home, + mode: location.mode, + archive: std::path::PathBuf::new(), + runtime, + downloaded: if reused { + None + } else { + Some("termux".to_string()) + }, + bytes: 0, + files: 0, + extracted: false, + chain, + }))) + } + Err(error) => { + if pinned == Some(JdkVendor::Termux) { + return Err(error); + } + chain.failed(JdkVendor::Termux, error.message().to_string()); + reasons.push(format!("termux:{}", error.message())); + Ok(Outcome::Skipped(chain)) + } + } +} From 88ac9d5aca0fd0303db6bfba8f83c77014c05258 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:19:22 +0800 Subject: [PATCH 19/21] =?UTF-8?q?docs(termux):=20=E9=80=82=E9=85=8D?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=A4=B4=E9=83=A8=E4=B8=8E=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E6=8C=87=E5=8D=97=E5=AF=B9=E9=BD=90=E5=AE=9E=E9=99=85=E5=90=88?= =?UTF-8?q?=E5=B9=B6=EF=BC=8815=20=E6=8F=90=E4=BA=A4=20/=20--no-ff=20/=20?= =?UTF-8?q?=E4=B8=A4=E5=A4=84=20PC=20=E4=BE=A7=E4=BF=AE=E5=A4=8D=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 头部:`14 个提交,HEAD 38d3024` 是写文档那一刻的自我指涉,永远不可能对——改成 `15 个提交,分叉点 87ab878,合并前 HEAD e987e02`。 - 合并指南第 1 步原来写 `git rebase origin/dev`,实际按用户令走 `--no-ff` 合并 (原 15 个提交一字未改地留在历史里,便于按组 revert),改为记录既成事实与 零文本冲突的交集(Cargo.lock / 两个 README / app-iced 的 Cargo.toml)。 - 新增第 6 条:PC 门禁发现、已就地修掉的两处——cfg(unix) 测试导入未 gate 导致 Windows clippy 判错;两个文件越 400 行红线后的拆分。这两条在 Termux 上跑门禁看不出来, 写进合并指南,免得下一个人以为「本机绿了就等于 PC 绿」。 --- TERMUX-ADAPTATION.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/TERMUX-ADAPTATION.md b/TERMUX-ADAPTATION.md index 1048010..441c59a 100644 --- a/TERMUX-ADAPTATION.md +++ b/TERMUX-ADAPTATION.md @@ -1,6 +1,6 @@ # Termux 适配:方案、改动清单与合并指南 -> 分支:`termux-glibc`(基于 `origin/dev`,14 个提交,HEAD `38d3024`) +> 分支:`termux-glibc`(基于 `origin/dev`,15 个提交,分叉点 `87ab878`,合并前 HEAD `e987e02`) > 目的:让主线快速了解这组代码改了什么、为什么、怎么验证、如何合并。 ## TL;DR @@ -97,8 +97,11 @@ ## 合并指南 -1. `git fetch origin && git rebase origin/dev`(分支基于 `7d8b3b9` 之后的 dev; - dev 若前进,先 rebase 再推)。 +1. ~~`git fetch origin && git rebase origin/dev`~~ → **实际按用户令走 `--no-ff` 合并** + (M156):`git merge --no-ff origin/termux-glibc`,15 个原提交一字未改地留在历史里, + 按平台识别 → 字体 → 文档 → Java → GPU → natives → MC 的分组可整组 revert。 + 与 dev 前进的 12 个提交**零文本冲突**(交集只有 `Cargo.lock` / 两个 README / + `crates/app-iced/Cargo.toml`)。 2. PR base:`dev`。CI 门禁:`fmt` / `clippy --workspace --all-targets -D warnings` / `test --workspace --all-targets` / `cargo deny`(本机没装 deny,PC 上补跑)。 3. 本机已知、**不影响 PC CI** 的三条失败: @@ -107,6 +110,15 @@ - `presenter settings_bridge`:mtime 热重载偶发(flaky,单跑 3 次 2 过 1 挂) 4. **不要提交**:`dist/`、`.gnu/`、任何 `ULL_MSA_CLIENT_ID` 值(仓库 Secret)。 5. 合并后建议真机再跑一次:`--software-renderer` 截图 + `ull java install 17` + MC 启动。 +6. **PC 门禁发现的两处、已在 M156 就地修掉**(都只在 Windows/PC 侧才暴露, + 所以在 Termux 上跑门禁看不出来): + - `game/java/discover.rs` 的两个 `#[cfg(unix)]` 测试,导入没跟着 gate + → Windows 上 `-D warnings` 判「未使用导入」。已把两个 `use` 同样 gate 成 `#[cfg(unix)]`; + 代价是这两个用例在 Windows 上不编译(由 Linux CI 覆盖)。 + - 本分支把 `game/java/install.rs`(456 行)与 `game/install/libraries.rs`(409 行) + 顶过了 400 行红线(`guards.yml` 的 `file-size`)。已按步骤拆出 + `install/termux_install.rs`、`install/one.rs`、`install/libraries/arm64_natives.rs`, + 行为不变、用例原样留在原处。 ## 验证矩阵(本机) From cb3ae8556ba0115a7f2308297bea899b0bc35c8e Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:22:46 +0800 Subject: [PATCH 20/21] =?UTF-8?q?docs(core):=20install.rs=20=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E6=96=87=E6=A1=A3=E8=A1=A5=E4=B8=8A=20Termux=20?= =?UTF-8?q?=E5=88=86=E6=94=AF=E4=B8=8E=E5=B9=B3=E5=8F=B0=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E4=B8=8D=E6=97=A9=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 拆分后模块文档只描述归档链,读起来像「Termux 也走这条」;补一行说明 Termux 的系统包 分支在链之前,以及 `AdoptiumTarget::host()` 失败已改成每家一条降级原因(不再整体早退)。 --- crates/core/src/game/java/install.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/core/src/game/java/install.rs b/crates/core/src/game/java/install.rs index 358b22c..6377579 100644 --- a/crates/core/src/game/java/install.rs +++ b/crates/core/src/game/java/install.rs @@ -1,6 +1,7 @@ //! 托管 JDK 安装编排:发行版回退链 → 下载引擎 → 解压 → `java -version` 验证。 //! //! ```text +//! [Termux] termux_install::attempt 系统包(.deb)——与下面这条链不共用任何一步 //! vendor::order(用户优先级, 地区) D23 的尝试顺序 //! → source::fetch(vendor, major) 查到该家最新 GA(Temurin/Oracle/MS/Zulu) //! → location::JavaInstallRoot::jdk_dir 按落点模式算目录 @@ -13,6 +14,14 @@ //! **回退的粒度是发行版**(D23):该家没有这个大版本、镜像全 4xx、sha256 取不到、下载校验失败, //! 都记一条降级原因然后换下一家;只有全部失败才整体报错,错误里带上每一跳的原因。 //! +//! **平台参数(`AdoptiumTarget::host()`)失败不提前退出**:Termux/Android 这类没有归档产物的 +//! 宿主上,早退只会在错误里留一句「不支持该平台」,看不出是哪几家、各卡在哪;现在它降级成 +//! 每家一条原因,与其它失败同形。 +//! +//! 这一步**不拼任何镜像 URL**(Oracle 的哈希段不可推导,见 `oracle` 模块):Temurin 的 +//! GitHub 直链交给 `MirrorTable::expand` 展开成院校镜像,Oracle/MS/Zulu 的链接由各自模块 +//! 从目录页/模板/API 拿到最终形态,引擎只负责「把这个 URL 下下来并校验」。 +//! //! 这一步**不拼任何镜像 URL**(Oracle 的哈希段不可推导,见 `oracle` 模块):Temurin 的 //! GitHub 直链交给 `MirrorTable::expand` 展开成院校镜像,Oracle/MS/Zulu 的链接由各自模块 //! 从目录页/模板/API 拿到最终形态,引擎只负责「把这个 URL 下下来并校验」。 From 3b3430a93e21dd2ab7e8a0be9fc3050196d3db60 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sat, 7 Nov 2026 09:33:10 +0800 Subject: [PATCH 21/21] =?UTF-8?q?docs(termux):=20=E5=AF=B9=E5=A4=96?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E5=8D=87=E7=BA=A7=E2=80=94=E2=80=94CPU=20?= =?UTF-8?q?=E8=BD=AF=E6=B8=B2=E6=9F=93=20+=20=E8=87=AA=E7=BC=96=20Mesa=20G?= =?UTF-8?q?PU=20=E4=B8=A4=E6=9D=A1=E8=B7=AF=E5=BE=84=E5=90=84=E7=9C=9F?= =?UTF-8?q?=E6=9C=BA=E9=AA=8C=E8=AF=81=E4=B8=80=E6=AC=A1=EF=BC=88=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=8B=8D=E6=9D=BF=20A=EF=BC=8C=E5=9B=9B=E5=A4=84?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README.zh-CN.md | 2 +- packaging/docs/compliance-support-boundaries.md | 2 +- packaging/release-notes.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1db70e2..b1d1f3b 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ This list is the reason to believe the rest. None of it is hidden behind marketi - **No native file chooser.** Import takes drag-and-drop or a pasted path. - **Android/Termux is experimental and not promised**: no released asset targets it. A GUI build is possible from source via the glibc toolchain (`packaging/docs/termux-glibc-build.md`, branch notes: - [`README.termux.md`](README.termux.md)); it was verified once on CPU rendering only. + [`README.termux.md`](README.termux.md)); it was verified on-device twice: once on CPU software rendering, once with GPU acceleration via a self-built Mesa/turnip stack (see the evidence in README.termux.md). ## Download and run diff --git a/README.zh-CN.md b/README.zh-CN.md index 7083627..62ca718 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -158,7 +158,7 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 文件是被静默覆盖的。 - **内容库的安装进度是阶段级**,不是字节级。 - **没有原生文件选择框。** 导入靠拖拽或粘贴路径。 -- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`,分支说明见 [`README.termux.md`](README.termux.md)),目前只在 CPU 渲染下验证过一次。 +- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`,分支说明见 [`README.termux.md`](README.termux.md)),已真机验证两条路径各一次:CPU 软渲染,以及自编 Mesa/turnip 栈的 GPU 加速(证据见 `README.termux.md`)。 ## 下载与运行 diff --git a/packaging/docs/compliance-support-boundaries.md b/packaging/docs/compliance-support-boundaries.md index 4e94802..4214261 100644 --- a/packaging/docs/compliance-support-boundaries.md +++ b/packaging/docs/compliance-support-boundaries.md @@ -28,7 +28,7 @@ | Linux arm64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+(`ubuntu-22.04-arm` 原生构建,**与 x64 同地板**)、xkbcommon、fontconfig、libGL、xcb | | Linux arm64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client | | Windows 8.1/8/7 | — | 不支持 | 基线已上调(D21) | -| Android Termux + X11 | — | 试验级(不承诺) | CLI 与 GUI 均需源码自建;GUI 已在单机 CPU 渲染下验证(`packaging/docs/termux-glibc-build.md`),不在本发布矩阵内 | +| Android Termux + X11 | — | 试验级(不承诺) | CLI 与 GUI 均需源码自建;GUI 已在单机真机验证两条路径各一次(CPU 软渲染 + 自编 Mesa/turnip GPU 加速,配方见 `packaging/docs/termux-glibc-build.md`、证据见 `README.termux.md`),不在本发布矩阵内 | `packaging/release-notes.md` 的 "Support matrix" 表与上表逐行对应(英文版同样带 published / not published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。 diff --git a/packaging/release-notes.md b/packaging/release-notes.md index e8d2781..26263af 100644 --- a/packaging/release-notes.md +++ b/packaging/release-notes.md @@ -59,7 +59,7 @@ Published = shipped as a download of **this** release, listed in `SHA256SUMS.txt | Linux arm64, X11 | not built (unverified) | first class (unverified) | glibc 2.35+, `libxkbcommon`, `fontconfig`, `libGL`, `libxcb` | | Linux arm64, Wayland | not built (unverified) | supported (unverified) | the above plus `wayland-client` | | Windows 8.1/8/7 | not built | unsupported | the baseline is Windows 10 | -| Android — Termux + X11 | not built (no CI slot) | experimental, not promised | CLI and GUI from source only; GUI verified once on CPU rendering (see `packaging/docs/termux-glibc-build.md`) | +| Android — Termux + X11 | not built (no CI slot) | experimental, not promised | CLI and GUI from source only; GUI verified on-device twice: CPU software rendering and self-built Mesa/turnip GPU acceleration (see `packaging/docs/termux-glibc-build.md` and `README.termux.md`) | `windows-x64` is the only published slot this release; Windows ARM64 is kept out until its build is green (see Downloads above). Both arm64 rows are formal matrix slots, not the experimental Termux path, and