Merge branch 'feat/m156-termxmerge-merge-termux-glibc-into'
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Dev Guards / 架构与合规守门(同 main 口径) (push) Canceled after 0s
Dev Guards / fmt (rustfmt --check) (push) Canceled after 0s
Dev Guards / clippy (ubuntu) (push) Canceled after 0s
This commit is contained in:
commit
d68523b02a
34 files changed
+1732
-132
No files matched your search
Generated
+1
@@ -5003,6 +5003,7 @@ dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"unlucky-cli",
|
||||
"unlucky-core",
|
||||
"unlucky-design",
|
||||
"unlucky-i18n",
|
||||
"unlucky-presenter",
|
||||
|
||||
@@ -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`, branch notes:
|
||||
[`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
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Unlucky Launcher · Termux 适配(`termux-glibc` 分支)
|
||||
|
||||
这条分支把启动器带到 **Termux(Android)**:用 glibc 工具链编出 `aarch64-unknown-linux-gnu`
|
||||
二进制,在 **termux-x11** 里跑 GUI;同时补上 Termux 平台识别、系统 JDK 安装与中文字体。
|
||||
|
||||
> 给主线的改动清单、兼容性说明与合并指南见
|
||||
> [`TERMUX-ADAPTATION.md`](TERMUX-ADAPTATION.md)。
|
||||
|
||||
## 已验证
|
||||
|
||||
| 项 | 状态 | 证据 |
|
||||
| --- | --- | --- |
|
||||
| 源码构建(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`) |
|
||||
| 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 运行 | ✅ | 真机 1.20.4 跑通:glibc JRE + arm64 natives + softpipe GL,主菜单在 X11 渲染 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
完整配方(工具链、patchelf、linker、门禁)见
|
||||
[`packaging/docs/termux-glibc-build.md`](packaging/docs/termux-glibc-build.md)。最短路径:
|
||||
|
||||
```sh
|
||||
termux-x11 :0 -ac &
|
||||
DISPLAY=:0 ./unlucky-launcher-iced # 默认 auto:有驱动走 GPU,没有则自动降级 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` 目前接受并忽略)。
|
||||
|
||||
## 已知边界
|
||||
|
||||
- 仅单机(OnePlus 15 / Android 17)验证,试验级、不在发布矩阵内。
|
||||
- **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,已真机跑通 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. zink 的 DRI3 问题(硬件 GL;当前 softpipe 可跑但慢)——这是 MC 帧率的主要瓶颈
|
||||
2. 剪贴板/IME 在 termux-x11 下的行为验证
|
||||
3. narrator/flite 与音频设备(当前 missing 但不阻断)
|
||||
+1
-1
@@ -158,7 +158,7 @@ unlucky-launcher --version # CLI,版本 + 系统摘要
|
||||
文件是被静默覆盖的。
|
||||
- **内容库的安装进度是阶段级**,不是字节级。
|
||||
- **没有原生文件选择框。** 导入靠拖拽或粘贴路径。
|
||||
- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产,那一侧的 GUI 也未验证。
|
||||
- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`,分支说明见 [`README.termux.md`](README.termux.md)),已真机验证两条路径各一次:CPU 软渲染,以及自编 Mesa/turnip 栈的 GPU 加速(证据见 `README.termux.md`)。
|
||||
|
||||
## 下载与运行
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
# Termux 适配:方案、改动清单与合并指南
|
||||
|
||||
> 分支:`termux-glibc`(基于 `origin/dev`,15 个提交,分叉点 `87ab878`,合并前 HEAD `e987e02`)
|
||||
> 目的:让主线快速了解这组代码改了什么、为什么、怎么验证、如何合并。
|
||||
|
||||
## 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`~~ → **实际按用户令走 `--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** 的三条失败:
|
||||
- `unlucky-core --test launch`:`/proc/<pid>/task/<pid>/children`(Android 内核没有)
|
||||
- 同测试:长路径被脱敏成 `<opaque>`
|
||||
- `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`,
|
||||
行为不变、用例原样留在原处。
|
||||
|
||||
## 验证矩阵(本机)
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| 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 路径。
|
||||
@@ -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.2" }
|
||||
|
||||
# M85:UI 框架无关的宿主层(DTO / 状态 reducer / host)。**只登记依赖,本 mission 不接线**——
|
||||
# 页面用它的时机由后续 iced 页面 mission 决定(接线流程见 crates/presenter/src/lib.rs 的 crate 文档)。
|
||||
# 与 Slint 版共用同一套 host 与状态语义,这是 D26「重写只换渲染」的落点。
|
||||
|
||||
@@ -30,6 +30,8 @@ use super::{MIN_SIZE, WINDOW_SIZE};
|
||||
|
||||
mod diagnostics;
|
||||
mod fallback;
|
||||
mod termux_gpu;
|
||||
mod termux_window;
|
||||
|
||||
use fallback::{on_failure, respawn, OnFailure};
|
||||
|
||||
@@ -39,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);
|
||||
@@ -131,10 +135,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);
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ pub const fn general() -> &'static str {
|
||||
config set <key> <value> 写配置(走 setting schema 校验)
|
||||
java list 列出可用 Java(含每个 JDK 的实际落点)
|
||||
java install <major> [--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 <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
||||
%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序
|
||||
%prog% java vendors 列出各发行版的镜像/支持版本/当前优先级顺序
|
||||
|
||||
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
|
||||
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
||||
@@ -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-<major>,支持 17/21/25),失败再走归档链;
|
||||
--vendor termux 只走系统包。每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进
|
||||
--json 的 chain 块与人读的「降级:」行。",
|
||||
),
|
||||
"agent" => Some(
|
||||
"\
|
||||
|
||||
@@ -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;
|
||||
@@ -23,7 +23,11 @@ use crate::output::Report;
|
||||
pub fn vendors(ctx: &Ctx) -> Result<Report, CliError> {
|
||||
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<Value> = order
|
||||
.iter()
|
||||
@@ -114,6 +118,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-<major>)"))
|
||||
.collect(),
|
||||
};
|
||||
json!({
|
||||
"rank": index + 1,
|
||||
|
||||
@@ -75,18 +75,38 @@ pub fn prepare(ctx: &Ctx, input: &PlanInput) -> Result<Plan, CliError> {
|
||||
|
||||
let mut libraries: Vec<LibraryInput> = 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 只搬运。
|
||||
|
||||
@@ -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/";
|
||||
|
||||
@@ -39,6 +43,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;
|
||||
@@ -79,31 +97,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,
|
||||
});
|
||||
}
|
||||
@@ -255,4 +284,81 @@ 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 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(
|
||||
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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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:<module>:<version>:natives-linux` 改写成 arm64。
|
||||
pub(super) fn linux_arm64_native_entry(
|
||||
library: &Library,
|
||||
env: &Environment,
|
||||
) -> Option<Arm64Native> {
|
||||
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('\\', "/")
|
||||
)
|
||||
}
|
||||
@@ -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<PathBuf> {
|
||||
/// 候选 JDK 根目录(`bin/java` 的父目录的父目录)。
|
||||
#[must_use]
|
||||
pub fn standard_roots() -> Vec<PathBuf> {
|
||||
standard_roots_in(&ProcessEnv)
|
||||
}
|
||||
|
||||
/// 同 [`standard_roots`],但环境变量可注入(测试 / 便携化封装用)。
|
||||
#[must_use]
|
||||
pub fn standard_roots_in(env: &dyn EnvSource) -> Vec<PathBuf> {
|
||||
let mut roots = Vec::new();
|
||||
if cfg!(windows) {
|
||||
for variable in ["ProgramW6432", "ProgramFiles", "ProgramFiles(x86)"] {
|
||||
@@ -99,6 +109,10 @@ pub fn standard_roots() -> Vec<PathBuf> {
|
||||
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();
|
||||
@@ -213,6 +227,12 @@ pub fn path_key(path: &Path) -> String {
|
||||
mod tests {
|
||||
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 {
|
||||
JavaRuntime {
|
||||
@@ -323,4 +343,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();
|
||||
}
|
||||
}
|
||||
@@ -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,22 +14,35 @@
|
||||
//! **回退的粒度是发行版**(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 下下来并校验」。
|
||||
|
||||
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::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;
|
||||
|
||||
@@ -184,14 +198,31 @@ pub async fn install(
|
||||
location: &JavaInstallRoot,
|
||||
request: InstallRequest,
|
||||
) -> Result<InstalledJdk, JavaError> {
|
||||
let target = AdoptiumTarget::host()?;
|
||||
let domestic = engine.region().is_domestic();
|
||||
let priority = request.order(domestic);
|
||||
let mut chain = ChainLog::new();
|
||||
let mut reasons: Vec<String> = Vec::new();
|
||||
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
||||
let _ = super::location::cleanup_orphans(&location.root);
|
||||
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 归档发行版需要 Adoptium 的平台参数;Termux/Android 上这里会 Unsupported,
|
||||
// 不提前 `?` 退出——让每一家都记一条降级原因,错误里能看见全貌。
|
||||
let target = AdoptiumTarget::host();
|
||||
|
||||
for candidate in &priority {
|
||||
let vendor = *candidate;
|
||||
if !vendor.supports(request.major) {
|
||||
@@ -205,6 +236,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) => {
|
||||
@@ -250,84 +290,6 @@ pub async fn install(
|
||||
.with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME"))
|
||||
}
|
||||
|
||||
/// 某一家的中间结果(把七元组元组换成具名结构,读起来才不费劲)。
|
||||
struct Prepared {
|
||||
root: PathBuf,
|
||||
archive: PathBuf,
|
||||
runtime: JavaRuntime,
|
||||
downloaded: Option<String>,
|
||||
bytes: u64,
|
||||
files: usize,
|
||||
extracted: bool,
|
||||
}
|
||||
|
||||
/// 装某一家的那一份(下载 → 解压 → 探测)。
|
||||
async fn install_one(
|
||||
engine: &DownloadEngine,
|
||||
location: &JavaInstallRoot,
|
||||
release: &JdkRelease,
|
||||
) -> Result<Prepared, JavaError> {
|
||||
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
|
||||
@@ -369,11 +331,3 @@ fn majors_text(vendor: JdkVendor) -> String {
|
||||
.collect::<Vec<_>>()
|
||||
.join("/")
|
||||
}
|
||||
|
||||
fn note(message: &str) {
|
||||
if cfg!(debug_assertions) {
|
||||
eprintln!("[ull::game::java] {message}");
|
||||
} else {
|
||||
let _ = message;
|
||||
}
|
||||
}
|
||||
@@ -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<String>,
|
||||
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<Prepared, JavaError> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<InstalledJdk>),
|
||||
/// 不该走这条分支,或者走了但失败(原因已进 `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<JdkVendor>,
|
||||
major: u32,
|
||||
location: &JavaInstallRoot,
|
||||
chain: ChainLog,
|
||||
reasons: &mut Vec<String>,
|
||||
) -> Result<Outcome, JavaError> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,7 @@ pub mod release;
|
||||
pub mod select;
|
||||
pub mod source;
|
||||
pub mod target;
|
||||
pub mod termux;
|
||||
pub mod vendor;
|
||||
pub mod zulu;
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ pub async fn fetch(
|
||||
})?;
|
||||
zulu::fetch_latest(client, table, major, target).await
|
||||
}
|
||||
JdkVendor::Termux => Err(JavaError::new(
|
||||
super::error::JavaErrorKind::Unsupported,
|
||||
"Termux 的 JDK 由系统包管理器安装,不走归档下载链",
|
||||
)
|
||||
.with_hint("在 Termux 里用 `ull java install <major>`(默认走 termux 源)")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
//! Termux 系统 JDK(`openjdk-<major>` 包)的安装与定位。
|
||||
//!
|
||||
//! Termux 的 openjdk 是 `.deb`(bionic),由 apt/dpkg 装进 `$PREFIX`,与 Temurin 的
|
||||
//! zip/tar.gz 归档不是同一形态,所以**不走** `source::fetch` 的归档链。这里委托包管理器:
|
||||
//!
|
||||
//! 1. 用内置镜像生成一份**隔离**的 sources.list;
|
||||
//! 2. `apt-get update && install` 的 lists/cache 都指向临时目录,不污染用户的 apt 状态;
|
||||
//! 3. 装完在 `$PREFIX/lib/jvm` 下找 `java-<major>-openjdk`,用 `bin/java` 是否存在做判据。
|
||||
//!
|
||||
//! 镜像顺序是实测可用的国内源;全部失败才报错,错误里带每一跳的原因。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::platform::paths::ProcessEnv;
|
||||
use crate::platform::termux as host;
|
||||
|
||||
use super::error::{JavaError, JavaErrorKind};
|
||||
|
||||
/// 内置镜像(termux-main 仓库根)。顺序即尝试顺序。
|
||||
pub const MIRRORS: &[&str] = &[
|
||||
"https://mirrors.sau.edu.cn/termux/apt/termux-main",
|
||||
"https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main",
|
||||
"https://mirrors.ustc.edu.cn/termux/apt/termux-main",
|
||||
"https://packages.termux.dev/apt/termux-main",
|
||||
];
|
||||
|
||||
/// 当前宿主是不是 Termux。
|
||||
#[must_use]
|
||||
pub fn available() -> bool {
|
||||
host::is_termux(&ProcessEnv)
|
||||
}
|
||||
|
||||
/// 大版本 → Termux 包名(主源只有 17 / 21 / 25)。
|
||||
#[must_use]
|
||||
pub fn package_for(major: u32) -> Option<String> {
|
||||
matches!(major, 17 | 21 | 25).then(|| format!("openjdk-{major}"))
|
||||
}
|
||||
|
||||
/// `$PREFIX/lib/jvm` 下与 `major` 对应的 JDK 根(`bin/java` 必须存在)。
|
||||
#[must_use]
|
||||
pub fn installed_home(prefix: &Path, major: u32) -> Option<PathBuf> {
|
||||
let root = prefix.join("lib").join("jvm");
|
||||
let entries = std::fs::read_dir(root).ok()?;
|
||||
let dashed = format!("java-{major}-openjdk");
|
||||
let plain = format!("openjdk-{major}");
|
||||
let mut found: Vec<PathBuf> = entries
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir() && path.join("bin").join("java").is_file())
|
||||
.filter(|path| {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name == dashed || name == plain)
|
||||
})
|
||||
.collect();
|
||||
found.sort();
|
||||
found.into_iter().next()
|
||||
}
|
||||
|
||||
/// 一条 `deb` 行(测试与安装共用同一份拼法)。
|
||||
#[must_use]
|
||||
pub fn sources_line(mirror: &str) -> String {
|
||||
format!("deb {} stable main\n", mirror.trim_end_matches('/'))
|
||||
}
|
||||
|
||||
/// 装一个 Termux JDK,返回它的根目录。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 当前不在 Termux、主源没有该大版本、所有镜像都失败、装完仍找不到 `bin/java` 时返回错误。
|
||||
///
|
||||
/// 返回 `(JDK 根, 是否复用了已装)`:包管理器刚装完与「本来就有」对调用方是两种结果
|
||||
/// (一个要报安装,一个要报「未走网络」)。
|
||||
pub async fn install(major: u32) -> Result<(PathBuf, bool), JavaError> {
|
||||
let Some(package) = package_for(major) else {
|
||||
return Err(JavaError::new(
|
||||
JavaErrorKind::Unsupported,
|
||||
format!("Termux 主源没有 Java {major}(只有 17 / 21 / 25)"),
|
||||
)
|
||||
.with_hint("`pkg search openjdk` 看可用包;其它大版本请用归档发行版"));
|
||||
};
|
||||
let Some(prefix) = host::prefix(&ProcessEnv) else {
|
||||
return Err(
|
||||
JavaError::new(JavaErrorKind::Unsupported, "termux 源只在 Termux 里可用")
|
||||
.with_hint("普通 Linux/Windows 用 Temurin / Oracle / MS / Zulu"),
|
||||
);
|
||||
};
|
||||
|
||||
// 幂等:已经装了同一大版本就不碰网络。
|
||||
if let Some(home) = installed_home(&prefix, major) {
|
||||
return Ok((home, true));
|
||||
}
|
||||
|
||||
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}"),
|
||||
)
|
||||
})?
|
||||
.map(|home| (home, false))
|
||||
}
|
||||
|
||||
/// 逐镜像尝试 `apt-get`(阻塞,跑在 `spawn_blocking` 里)。
|
||||
fn run_install(
|
||||
prefix: &Path,
|
||||
workspace: &Path,
|
||||
package: &str,
|
||||
major: u32,
|
||||
) -> Result<PathBuf, JavaError> {
|
||||
let mut failures: Vec<String> = Vec::new();
|
||||
for mirror in MIRRORS {
|
||||
match apt_install(prefix, workspace, mirror, package) {
|
||||
Ok(()) => {
|
||||
if let Some(home) = installed_home(prefix, major) {
|
||||
// 装完把隔离的 lists/cache/archives 清掉(几十到上百 MB,别留在 $PREFIX/tmp)。
|
||||
let _ = std::fs::remove_dir_all(workspace);
|
||||
return Ok(home);
|
||||
}
|
||||
failures.push(format!("{mirror}:装完找不到 bin/java"));
|
||||
}
|
||||
Err(error) => failures.push(format!("{mirror}:{error}")),
|
||||
}
|
||||
}
|
||||
Err(JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
format!(
|
||||
"Termux 包安装失败(试过 {} 个镜像):{}",
|
||||
MIRRORS.len(),
|
||||
failures.join(";")
|
||||
),
|
||||
)
|
||||
.with_hint("确认 apt 可用、磁盘空间足够,或手动 `pkg install openjdk-<major>`"))
|
||||
}
|
||||
|
||||
/// 对单个镜像跑 `apt-get update` + `install`(lists/cache 隔离在 workspace 内)。
|
||||
fn apt_install(prefix: &Path, workspace: &Path, mirror: &str, package: &str) -> Result<(), String> {
|
||||
let sources = workspace.join("sources.list");
|
||||
std::fs::write(&sources, sources_line(mirror))
|
||||
.map_err(|error| format!("写 sources.list 失败:{error}"))?;
|
||||
|
||||
let lists = workspace.join("lists");
|
||||
let cache = workspace.join("cache");
|
||||
let archives = workspace.join("archives");
|
||||
for dir in [&lists, &cache, &archives] {
|
||||
std::fs::create_dir_all(dir).map_err(|error| format!("建临时目录失败:{error}"))?;
|
||||
}
|
||||
|
||||
let options = [
|
||||
format!("Dir::Etc::sourcelist={}", sources.display()),
|
||||
"Dir::Etc::sourceparts=-".to_string(),
|
||||
format!("Dir::State::lists={}", lists.display()),
|
||||
format!("Dir::Cache={}", cache.display()),
|
||||
format!("Dir::Cache::archives={}", archives.display()),
|
||||
"Acquire::ForceIPv4=true".to_string(),
|
||||
"Acquire::Retries=2".to_string(),
|
||||
"Acquire::http::Timeout=30".to_string(),
|
||||
"Acquire::https::Timeout=30".to_string(),
|
||||
];
|
||||
|
||||
run_apt(prefix, &options, &["update"])?;
|
||||
run_apt(
|
||||
prefix,
|
||||
&options,
|
||||
&["install", "-y", "--no-install-suggests", package],
|
||||
)
|
||||
}
|
||||
|
||||
/// 执行一次 `apt-get`(`-o` 选项在前,子命令在后)。
|
||||
fn run_apt(prefix: &Path, options: &[String], args: &[&str]) -> Result<(), String> {
|
||||
let apt = prefix.join("bin").join("apt-get");
|
||||
let mut command = Command::new(&apt);
|
||||
command.env("DEBIAN_FRONTEND", "noninteractive");
|
||||
for option in options {
|
||||
command.arg("-o").arg(option);
|
||||
}
|
||||
command.args(args);
|
||||
let output = command
|
||||
.output()
|
||||
.map_err(|error| format!("执行 {} 失败:{error}", apt.display()))?;
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let last = stderr
|
||||
.lines()
|
||||
.rev()
|
||||
.find(|line| !line.trim().is_empty())
|
||||
.unwrap_or("(无 stderr 输出)");
|
||||
Err(format!(
|
||||
"apt-get {} 退出 {}:{last}",
|
||||
args.first().copied().unwrap_or("?"),
|
||||
output.status.code().unwrap_or(-1)
|
||||
))
|
||||
}
|
||||
|
||||
/// 临时工作区(按大版本区分,避免两次安装互相踩 lists)。
|
||||
fn workspace_dir(prefix: &Path, major: u32) -> PathBuf {
|
||||
prefix
|
||||
.join("tmp")
|
||||
.join("ull-java-termux")
|
||||
.join(format!("jdk-{major}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{installed_home, package_for, sources_line, MIRRORS};
|
||||
|
||||
#[test]
|
||||
fn only_the_majors_that_termux_ships_are_installable() {
|
||||
assert_eq!(package_for(17).as_deref(), Some("openjdk-17"));
|
||||
assert_eq!(package_for(21).as_deref(), Some("openjdk-21"));
|
||||
assert_eq!(package_for(25).as_deref(), Some("openjdk-25"));
|
||||
assert_eq!(package_for(8), None);
|
||||
assert_eq!(package_for(11), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sources_line_has_one_deb_and_keeps_the_mirror_root() {
|
||||
let line = sources_line("https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main/");
|
||||
assert_eq!(
|
||||
line,
|
||||
"deb https://mirrors.tuna.tsinghua.edu.cn/termux/apt/termux-main stable main\n"
|
||||
);
|
||||
assert!(MIRRORS.iter().all(|mirror| mirror.starts_with("https://")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installed_home_requires_bin_java_and_matching_major() {
|
||||
let base = std::env::temp_dir().join(format!("ull-termux-home-{}", std::process::id()));
|
||||
let good = base.join("lib").join("jvm").join("java-21-openjdk");
|
||||
let empty = base.join("lib").join("jvm").join("java-17-openjdk");
|
||||
std::fs::create_dir_all(good.join("bin")).unwrap();
|
||||
std::fs::write(good.join("bin").join("java"), b"").unwrap();
|
||||
std::fs::create_dir_all(&empty).unwrap();
|
||||
|
||||
assert_eq!(installed_home(&base, 21), Some(good));
|
||||
assert_eq!(installed_home(&base, 17), None, "没有 bin/java 的目录不算");
|
||||
assert_eq!(installed_home(&base, 25), None);
|
||||
|
||||
std::fs::remove_dir_all(&base).ok();
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,10 @@
|
||||
//! `zulu` / `api`(Temurin)四个模块里。
|
||||
|
||||
/// 设置键 `java.vendorPriority` 的允许值(顺序即默认序的覆盖来源)。
|
||||
pub const VENDOR_IDS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
|
||||
///
|
||||
/// `termux` 只在 Termux 宿主上可用(系统包管理器安装),默认序里不出现;
|
||||
/// 显式写进优先级或 `--vendor termux` 才走它。
|
||||
pub const VENDOR_IDS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||
|
||||
/// 一家 JDK 发行版。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -28,11 +31,19 @@ pub enum JdkVendor {
|
||||
Microsoft,
|
||||
/// Azul Zulu(第四备选,API 元数据 + CDN 直链)。
|
||||
Zulu,
|
||||
/// Termux 的系统 openjdk(`pkg install openjdk-<major>`;只在 Termux 里可用)。
|
||||
Termux,
|
||||
}
|
||||
|
||||
impl JdkVendor {
|
||||
/// 全部发行版(枚举顺序,不是优先级顺序)。
|
||||
pub const ALL: [Self; 4] = [Self::Temurin, Self::Oracle, Self::Microsoft, Self::Zulu];
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::Temurin,
|
||||
Self::Oracle,
|
||||
Self::Microsoft,
|
||||
Self::Zulu,
|
||||
Self::Termux,
|
||||
];
|
||||
|
||||
/// 稳定 id(目录名段、`--vendor` 取值、设置值、JSON 字段)。
|
||||
#[must_use]
|
||||
@@ -42,6 +53,7 @@ impl JdkVendor {
|
||||
Self::Oracle => "oracle",
|
||||
Self::Microsoft => "microsoft",
|
||||
Self::Zulu => "zulu",
|
||||
Self::Termux => "termux",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +65,7 @@ impl JdkVendor {
|
||||
Self::Oracle => "Oracle OpenJDK",
|
||||
Self::Microsoft => "Microsoft OpenJDK",
|
||||
Self::Zulu => "Azul Zulu",
|
||||
Self::Termux => "Termux OpenJDK",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +79,7 @@ impl JdkVendor {
|
||||
Some(Self::Microsoft)
|
||||
}
|
||||
"zulu" | "azul" => Some(Self::Zulu),
|
||||
"termux" | "termux-openjdk" | "pkg" => Some(Self::Termux),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -80,6 +94,7 @@ impl JdkVendor {
|
||||
Self::Temurin | Self::Zulu => major >= 8,
|
||||
Self::Oracle => major >= 9,
|
||||
Self::Microsoft => matches!(major, 11 | 17 | 21 | 25),
|
||||
Self::Termux => matches!(major, 17 | 21 | 25),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +106,7 @@ impl JdkVendor {
|
||||
Self::Oracle => &[17, 21, 25],
|
||||
Self::Microsoft => &[11, 17, 21, 25],
|
||||
Self::Zulu => &[8, 11, 17, 21, 25],
|
||||
Self::Termux => &[17, 21, 25],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> {
|
||||
let raw = self.natives.as_ref()?.get(&env.os)?;
|
||||
let arch = if matches!(env.arch.as_str(), "x86_64" | "arm64") {
|
||||
@@ -179,7 +184,33 @@ 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:<module>`;LWJGL 2 的 group 是 `org.lwjgl.lwjgl`)。
|
||||
fn is_lwjgl3(&self) -> bool {
|
||||
self.name.starts_with("org.lwjgl:")
|
||||
}
|
||||
|
||||
/// 现代清单(1.19+)把 natives 作为独立库条目:Linux arm64 上把
|
||||
/// `org.lwjgl:<module>:<ver>:natives-linux` 改写成 `...:natives-linux-arm64`。
|
||||
///
|
||||
/// 返回 None 表示不适用(非 Linux/arm64、非 LWJGL3、或不是 natives 条目)。
|
||||
#[must_use]
|
||||
pub fn linux_arm64_native_name(&self, env: &Environment) -> Option<String> {
|
||||
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`)。
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ pub const JAVA_INSTALL_ROOT_UNSET: &str = "";
|
||||
///
|
||||
/// 与 `crate::game::java::vendor::VENDOR_IDS` 必须是同一份集合——setting 层不依赖 game 层
|
||||
/// (分层纪律),一致性由 `tests/setting/java_install_keys.rs` 的契约测试固定。
|
||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
|
||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||
|
||||
/// launcher.json 当前 schema 版本。
|
||||
///
|
||||
|
||||
@@ -175,7 +175,10 @@ fn sha256_texts_are_parsed_the_same_way_for_every_upstream() {
|
||||
|
||||
#[test]
|
||||
fn vendor_ids_are_the_setting_whitelist() {
|
||||
assert_eq!(VENDOR_IDS, &["temurin", "oracle", "microsoft", "zulu"]);
|
||||
assert_eq!(
|
||||
VENDOR_IDS,
|
||||
&["temurin", "oracle", "microsoft", "zulu", "termux"]
|
||||
);
|
||||
for id in VENDOR_IDS {
|
||||
assert_eq!(JdkVendor::parse(id).map(|vendor| vendor.id()), Some(*id));
|
||||
}
|
||||
|
||||
@@ -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-<major>)"))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -134,18 +134,38 @@ pub fn build_request(
|
||||
|
||||
let mut libraries: Vec<LibraryInput> = 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,
|
||||
|
||||
@@ -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 软渲染 + 自编 Mesa/turnip GPU 加速,配方见 `packaging/docs/termux-glibc-build.md`、证据见 `README.termux.md`),不在本发布矩阵内 |
|
||||
|
||||
`packaging/release-notes.md` 的 "Support matrix" 表与上表逐行对应(英文版同样带 published / not
|
||||
published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
# 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`,
|
||||
以及跑门禁需要的 `clippy` / `rustfmt` 五个 `aarch64-unknown-linux-gnu` 组件。官方源在国内很慢,
|
||||
用 USTC 镜像:
|
||||
|
||||
```sh
|
||||
mkdir -p "$TMPDIR/ull-gnu" && cd "$TMPDIR/ull-gnu"
|
||||
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
|
||||
```
|
||||
|
||||
解包后按组件布局合并到 `$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 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 的
|
||||
`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> # 可选;烘微软登录 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`。
|
||||
|
||||
门禁(与 CI 同一组命令,环境同上):
|
||||
|
||||
```sh
|
||||
cargo fmt --all --check
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo test --workspace --all-targets --no-fail-fast
|
||||
```
|
||||
|
||||
Termux 上有三条**环境差异**失败(不是回归):`launch` 测试读
|
||||
`/proc/<pid>/task/<pid>/children`(Android 内核没有)、natives 事件的长路径被脱敏成
|
||||
`<opaque>`、`settings_bridge` 的 mtime 热重载偶发(flaky)。PC 的 Linux CI 不受影响。
|
||||
|
||||
## 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 已烘入。
|
||||
- **横屏优先**:Termux 上默认窗口尺寸会按当前 X 屏幕等比自适应(日志
|
||||
`[window] ... source=termux-screen`),横屏铺满、竖屏也不溢出;验收尺寸 1180×760 仍是
|
||||
默认基准,`ULL_WINDOW_SIZE=1520x900` 这类覆盖仍然优先。
|
||||
|
||||
### 4.1 Termux 上的 Java
|
||||
|
||||
`ull java install <major>` 在 Termux 上**默认先用系统包**(`openjdk-17/21/25`):
|
||||
|
||||
- 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序尝试;
|
||||
- `apt-get` 的 lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不改用户的 apt 配置;
|
||||
装完自动清理该目录(一次安装约 100 MB 临时占用);
|
||||
- 装完从 `$PREFIX/lib/jvm/java-<major>-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。
|
||||
|
||||
### 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`。
|
||||
|
||||
> **启动器已接线**(本分支):
|
||||
> - 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`)即可。
|
||||
>
|
||||
> **真机验证(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)
|
||||
|
||||
termux-x11 的 X 客户端可以走硬件;glibc 侧的问题在**驱动**,不在 loader:
|
||||
|
||||
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 拒绝)。
|
||||
|
||||
**解法:给 glibc 自编一份带 KGSL 的 Mesa 26.2.6。** 本机实测通过:
|
||||
|
||||
```sh
|
||||
# 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
|
||||
```
|
||||
|
||||
`--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" <<EOF
|
||||
{"ICD":{"api_version":"1.4.335","library_path":"$PREFIX/glibc/lib/libvulkan_freedreno_ull.so"},"file_format_version":"1.0.0"}
|
||||
EOF
|
||||
```
|
||||
|
||||
应用启动时(glibc 构建 + Termux + 该 JSON 存在)会自动把它设进
|
||||
`VK_ICD_FILENAMES` / `VK_DRIVER_FILES`(`app/boot/termux_gpu.rs`);用户显式设了这两个
|
||||
变量时不覆盖。实测日志:
|
||||
|
||||
```text
|
||||
[probe] adapters=1 [Adreno (TM) 840 [IntegratedGpu]] hardware=true :: request=auto -> 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. 已知边界
|
||||
|
||||
- 没有面向 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 文件**。
|
||||
@@ -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 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
|
||||
|
||||
Reference in new issue
Block a user