Compare commits
15
Commits
dev
...
termux-glibc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e987e02755 | ||
|
|
38d302428c | ||
|
|
7d8b3b99e6 | ||
|
|
5bd566f92f | ||
|
|
aa963345bd | ||
|
|
147280d12a | ||
|
|
29e49dd7a4 | ||
|
|
64fe489a04 | ||
|
|
aaba339499 | ||
|
|
7cbb354972 | ||
|
|
38f4682dba | ||
|
|
c9efdf0fd1 | ||
|
|
16c61ca8b7 | ||
|
|
76ddc9b3e9 | ||
|
|
898e052c2d |
No files matched your search
Generated
+1
@@ -5003,6 +5003,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
"unlucky-cli",
|
"unlucky-cli",
|
||||||
|
"unlucky-core",
|
||||||
"unlucky-design",
|
"unlucky-design",
|
||||||
"unlucky-i18n",
|
"unlucky-i18n",
|
||||||
"unlucky-presenter",
|
"unlucky-presenter",
|
||||||
|
|||||||
@@ -193,8 +193,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.
|
report lists every conflict so nothing is overwritten silently.
|
||||||
- **Install progress is stage-level**, not byte-level, on the content-library install path.
|
- **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.
|
- **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
|
- **Android/Termux is experimental and not promised**: no released asset targets it. A GUI build is
|
||||||
unverified there.
|
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
|
## 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
@@ -157,7 +157,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 渲染下验证过一次。
|
||||||
|
|
||||||
## 下载与运行
|
## 下载与运行
|
||||||
|
|
||||||
|
|||||||
@@ -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/<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 启动。
|
||||||
|
|
||||||
|
## 验证矩阵(本机)
|
||||||
|
|
||||||
|
| 项 | 结果 |
|
||||||
|
| --- | --- |
|
||||||
|
| 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):颜色/排版/形状/间距/图标几何全部只从这里取。
|
# 令牌唯一真源(M61):颜色/排版/形状/间距/图标几何全部只从这里取。
|
||||||
unlucky-design = { path = "../design", version = "0.2.1" }
|
unlucky-design = { path = "../design", version = "0.2.1" }
|
||||||
|
|
||||||
|
# 平台识别(Termux / $PREFIX 路径)唯一真源:字体注册这类平台适配不再各写一份环境判断。
|
||||||
|
unlucky-core = { path = "../core", version = "0.2.1" }
|
||||||
|
|
||||||
# M85:UI 框架无关的宿主层(DTO / 状态 reducer / host)。**只登记依赖,本 mission 不接线**——
|
# M85:UI 框架无关的宿主层(DTO / 状态 reducer / host)。**只登记依赖,本 mission 不接线**——
|
||||||
# 页面用它的时机由后续 iced 页面 mission 决定(接线流程见 crates/presenter/src/lib.rs 的 crate 文档)。
|
# 页面用它的时机由后续 iced 页面 mission 决定(接线流程见 crates/presenter/src/lib.rs 的 crate 文档)。
|
||||||
# 与 Slint 版共用同一套 host 与状态语义,这是 D26「重写只换渲染」的落点。
|
# 与 Slint 版共用同一套 host 与状态语义,这是 D26「重写只换渲染」的落点。
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ use super::{MIN_SIZE, WINDOW_SIZE};
|
|||||||
|
|
||||||
mod diagnostics;
|
mod diagnostics;
|
||||||
mod fallback;
|
mod fallback;
|
||||||
|
mod termux_gpu;
|
||||||
|
mod termux_window;
|
||||||
|
|
||||||
use fallback::{on_failure, respawn, OnFailure};
|
use fallback::{on_failure, respawn, OnFailure};
|
||||||
|
|
||||||
@@ -39,6 +41,8 @@ pub(crate) fn run(options: Options) -> iced::Result {
|
|||||||
let boot_started = Instant::now();
|
let boot_started = Instant::now();
|
||||||
|
|
||||||
// ① 探测(判据取在哪、日志在 `report_boot` 里统一打)
|
// ① 探测(判据取在哪、日志在 `report_boot` 里统一打)
|
||||||
|
// Termux + glibc 构建:装了我们自编的 turnip ICD 就先生效,auto 也能选到 GPU。
|
||||||
|
termux_gpu::enable();
|
||||||
let boot = boot_probe(&options);
|
let boot = boot_probe(&options);
|
||||||
let decision = boot.decision;
|
let decision = boot.decision;
|
||||||
diagnostics::report_boot(&options, &boot.probe, &decision);
|
diagnostics::report_boot(&options, &boot.probe, &decision);
|
||||||
@@ -131,10 +135,20 @@ fn launch(
|
|||||||
|
|
||||||
// M98:`ULL_WINDOW_SIZE=1520x900` 覆盖尺寸(矩阵的「最小 / 宽屏」两档靠它,且必须走**真外壳**
|
// M98:`ULL_WINDOW_SIZE=1520x900` 覆盖尺寸(矩阵的「最小 / 宽屏」两档靠它,且必须走**真外壳**
|
||||||
// ——`pages_demo` example 的标题栏是留白带,出不了「三键位置」那张图)。
|
// ——`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),
|
(WINDOW_SIZE.width, WINDOW_SIZE.height),
|
||||||
(MIN_SIZE.width, MIN_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);
|
let settings = diagnostics::window_settings(size);
|
||||||
// 两行日志与开窗用的是**同一个** settings(见模块文档纪律 2)
|
// 两行日志与开窗用的是**同一个** settings(见模块文档纪律 2)
|
||||||
diagnostics::report_window(&settings, options.scale_factor, size_source);
|
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)
|
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 的第一级)。
|
/// 同 [`resolve`],但把配置里 `ui.language` 的显式值喂给中文策略(D28 的第一级)。
|
||||||
pub fn resolve_with(setting: Option<&str>) -> Resolved {
|
pub fn resolve_with(setting: Option<&str>) -> Resolved {
|
||||||
|
register_termux_fonts();
|
||||||
|
|
||||||
let platform = Platform::current();
|
let platform = Platform::current();
|
||||||
let faces = system_faces();
|
let faces = system_faces();
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ pub const fn general() -> &'static str {
|
|||||||
config set <key> <value> 写配置(走 setting schema 校验)
|
config set <key> <value> 写配置(走 setting schema 校验)
|
||||||
java list 列出可用 Java(含每个 JDK 的实际落点)
|
java list 列出可用 Java(含每个 JDK 的实际落点)
|
||||||
java install <major> [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版;
|
java install <major> [--vendor X] 安装托管 JDK(缺省按地区回退链选发行版;
|
||||||
--vendor temurin|oracle|microsoft|zulu 钉死一家)
|
--vendor temurin|oracle|microsoft|zulu|termux 钉死一家)
|
||||||
java vendors 列出四家发行版的镜像/支持版本/当前优先级
|
java vendors 列出各发行版的镜像/支持版本/当前优先级
|
||||||
agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP;
|
agent serve [--stdio] [--port N] 起对外 MCP server(默认 127.0.0.1 的 Streamable HTTP;
|
||||||
[--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读)
|
[--allow-write] --stdio 给 llama.cpp 内置 client 用;默认只读)
|
||||||
agent config [--port N] 打印 Claude Code / Cursor 的配置样例
|
agent config [--port N] 打印 Claude Code / Cursor 的配置样例
|
||||||
@@ -157,7 +157,7 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL
|
|||||||
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
%prog% java list 探测 JAVA_HOME / PATH / 托管目录 / 实例覆盖里的 Java
|
||||||
并打印每个 JDK 的实际落点(managed 项带目录)
|
并打印每个 JDK 的实际落点(managed 项带目录)
|
||||||
%prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
%prog% java install <major> [--vendor X] 安装托管 JDK(D13/D23):回退链查版本 → 下载 → 解压 → 验证
|
||||||
%prog% java vendors 列出四家发行版的镜像/支持版本/当前优先级顺序
|
%prog% java vendors 列出各发行版的镜像/支持版本/当前优先级顺序
|
||||||
|
|
||||||
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
|
落点(D23 三模式,随 core.data_mode 与设置键 java.installRoot 走):
|
||||||
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
portable(默认) <数据根>/.ULL/data/java-managed/jdk-<major>-<vendor>/
|
||||||
@@ -168,7 +168,9 @@ authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CL
|
|||||||
发行版回退链(D23):国内 temurin(TUNA) → oracle(华为云→南大) → microsoft(aka.ms)
|
发行版回退链(D23):国内 temurin(TUNA) → oracle(华为云→南大) → microsoft(aka.ms)
|
||||||
→ zulu;国外 temurin → microsoft → oracle → zulu。设置 java.vendorPriority 可改顺序
|
→ zulu;国外 temurin → microsoft → oracle → zulu。设置 java.vendorPriority 可改顺序
|
||||||
(只写几家也行,其余按默认序补齐);--vendor 钉死一家、不自动降级。
|
(只写几家也行,其余按默认序补齐);--vendor 钉死一家、不自动降级。
|
||||||
每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进 --json 的 chain 块与人读的「降级:」行。",
|
Termux 上缺省先用系统包(pkg install openjdk-<major>,支持 17/21/25),失败再走归档链;
|
||||||
|
--vendor termux 只走系统包。每家失败的原因(版本不存在 / 镜像全 4xx / 校验失败)进
|
||||||
|
--json 的 chain 块与人读的「降级:」行。",
|
||||||
),
|
),
|
||||||
"agent" => Some(
|
"agent" => Some(
|
||||||
"\
|
"\
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use unlucky_core::download::MirrorTable;
|
use unlucky_core::download::MirrorTable;
|
||||||
use unlucky_core::game::java::{vendor, JdkVendor};
|
use unlucky_core::game::java::{termux, vendor, JdkVendor};
|
||||||
|
|
||||||
use super::java::{region_profile, JavaSettings};
|
use super::java::{region_profile, JavaSettings};
|
||||||
use crate::context::Ctx;
|
use crate::context::Ctx;
|
||||||
@@ -23,7 +23,11 @@ use crate::output::Report;
|
|||||||
pub fn vendors(ctx: &Ctx) -> Result<Report, CliError> {
|
pub fn vendors(ctx: &Ctx) -> Result<Report, CliError> {
|
||||||
let settings = JavaSettings::load(ctx);
|
let settings = JavaSettings::load(ctx);
|
||||||
let domestic = region_profile(ctx).is_domestic();
|
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 table = MirrorTable::builtin();
|
||||||
let rows: Vec<Value> = order
|
let rows: Vec<Value> = order
|
||||||
.iter()
|
.iter()
|
||||||
@@ -114,6 +118,10 @@ fn vendor_json(table: &MirrorTable, item: JdkVendor, index: usize) -> Value {
|
|||||||
.zulu()
|
.zulu()
|
||||||
.map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)])
|
.map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)])
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
JdkVendor::Termux => termux::MIRRORS
|
||||||
|
.iter()
|
||||||
|
.map(|mirror| format!("{mirror}(系统包:pkg install openjdk-<major>)"))
|
||||||
|
.collect(),
|
||||||
};
|
};
|
||||||
json!({
|
json!({
|
||||||
"rank": index + 1,
|
"rank": index + 1,
|
||||||
|
|||||||
@@ -75,18 +75,38 @@ pub fn prepare(ctx: &Ctx, input: &PlanInput) -> Result<Plan, CliError> {
|
|||||||
|
|
||||||
let mut libraries: Vec<LibraryInput> = Vec::new();
|
let mut libraries: Vec<LibraryInput> = Vec::new();
|
||||||
for library in resolved.libraries.as_deref().unwrap_or_default() {
|
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;
|
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;
|
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() {
|
let path = if library.is_local() {
|
||||||
game.version_library(&version_id, &coordinate)
|
game.version_library(&version_id, &coordinate)
|
||||||
} else {
|
} else {
|
||||||
game.library(&coordinate)
|
game.library(&coordinate)
|
||||||
};
|
};
|
||||||
libraries.push(LibraryInput::new(path, library.name.clone()));
|
libraries.push(LibraryInput::new(path, name));
|
||||||
}
|
}
|
||||||
|
|
||||||
// natives 分类器构件(≤1.18.2 才有):收集口径归 core,CLI 只搬运。
|
// natives 分类器构件(≤1.18.2 才有):收集口径归 core,CLI 只搬运。
|
||||||
|
|||||||
@@ -39,6 +39,20 @@ pub fn plan(
|
|||||||
if !library.applies(environment) {
|
if !library.applies(environment) {
|
||||||
continue;
|
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() {
|
if library.coordinate().is_err() {
|
||||||
warnings.push(format!("库坐标不合法,已跳过:{}", library.name));
|
warnings.push(format!("库坐标不合法,已跳过:{}", library.name));
|
||||||
continue;
|
continue;
|
||||||
@@ -79,31 +93,42 @@ pub fn plan(
|
|||||||
let Some(classifier) = library.native_classifier(environment) else {
|
let Some(classifier) = library.native_classifier(environment) else {
|
||||||
continue;
|
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 {
|
let Ok(native_relative) = library.native_relative_path(&classifier) else {
|
||||||
warnings.push(format!("{} 的 natives 路径推导失败,已跳过", library.name));
|
warnings.push(format!("{} 的 natives 路径推导失败,已跳过", library.name));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if !artifact.has_url() {
|
let dest = layout.libraries_dir().join(&native_relative);
|
||||||
warnings.push(format!(
|
// Linux arm64:Mojang 清单没有 arm64 分类器,兜底到 Maven 镜像(见常量注释)。
|
||||||
"{} 的 natives 构件 {classifier} 没有下载地址,已跳过",
|
let arm64_fallback = classifier == LINUX_ARM64_CLASSIFIER;
|
||||||
library.name
|
let (url, sha1, size) = match library.native_artifact(&classifier) {
|
||||||
));
|
Some(artifact) if artifact.has_url() => (
|
||||||
continue;
|
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 {
|
files.push(PlanFile {
|
||||||
role: FileRole::Native,
|
role: FileRole::Native,
|
||||||
name: format!("{}:{classifier}", library.name),
|
name: format!("{}:{classifier}", library.name),
|
||||||
url: artifact.url.clone().unwrap_or_default(),
|
url,
|
||||||
sha1: artifact.sha1.clone(),
|
sha1,
|
||||||
size: artifact.size,
|
size,
|
||||||
dest: layout.libraries_dir().join(native_relative),
|
dest,
|
||||||
resource: ResourceKind::Library,
|
resource: ResourceKind::Library,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,6 +150,39 @@ fn library_dest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 现代清单里 LWJGL3 natives 条目的改写结果。
|
||||||
|
struct Arm64Native {
|
||||||
|
name: String,
|
||||||
|
relative: PathBuf,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linux arm64:把 `org.lwjgl:<module>:<version>:natives-linux` 改写成 arm64。
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// 按库声明(或默认)的 Maven 基址拼下载地址。
|
/// 按库声明(或默认)的 Maven 基址拼下载地址。
|
||||||
fn maven_url(library: &Library, relative: &Path) -> String {
|
fn maven_url(library: &Library, relative: &Path) -> String {
|
||||||
let base = library
|
let base = library
|
||||||
@@ -141,6 +199,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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -255,4 +329,81 @@ mod tests {
|
|||||||
.dest
|
.dest
|
||||||
.ends_with("versions/inst/libraries/net/minecraftforge/forge/1.20.1-47.2.0.jar"));
|
.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:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 写的,新版发行版通常没有→容错跳过) |
|
//! | 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/*` |
|
//! | Linux | `/usr/lib/jvm/*`、`/usr/java/*` |
|
||||||
|
//! | Termux | `$PREFIX/lib/jvm/*`(`java-17-openjdk` 这类;`$PREFIX` 由 [`crate::platform::termux`] 识别) |
|
||||||
//! | macOS | `/Library/Java/JavaVirtualMachines/*.jdk/Contents/Home` |
|
//! | macOS | `/Library/Java/JavaVirtualMachines/*.jdk/Contents/Home` |
|
||||||
//!
|
//!
|
||||||
//! 三个纪律:
|
//! 三个纪律:
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use crate::platform::paths::{EnvSource, ProcessEnv};
|
||||||
|
use crate::platform::termux;
|
||||||
|
|
||||||
use super::extract::java_binary_name;
|
use super::extract::java_binary_name;
|
||||||
use super::probe::{self, discover_managed_roots, system_candidates, JavaOrigin, JavaRuntime};
|
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` 的父目录的父目录)。
|
/// 候选 JDK 根目录(`bin/java` 的父目录的父目录)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn standard_roots() -> Vec<PathBuf> {
|
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();
|
let mut roots = Vec::new();
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
for variable in ["ProgramW6432", "ProgramFiles", "ProgramFiles(x86)"] {
|
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"] {
|
for base in ["/usr/lib/jvm", "/usr/java"] {
|
||||||
roots.extend(child_dirs(Path::new(base)));
|
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.sort();
|
||||||
roots.dedup();
|
roots.dedup();
|
||||||
@@ -211,8 +225,9 @@ pub fn path_key(path: &Path) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
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::game::java::probe::{JavaOrigin, JavaRuntime};
|
||||||
|
use crate::platform::paths::MapEnv;
|
||||||
|
|
||||||
fn runtime(path: &str, origin: JavaOrigin) -> JavaRuntime {
|
fn runtime(path: &str, origin: JavaOrigin) -> JavaRuntime {
|
||||||
JavaRuntime {
|
JavaRuntime {
|
||||||
@@ -323,4 +338,41 @@ mod tests {
|
|||||||
let roots = standard_roots();
|
let roots = standard_roots();
|
||||||
assert!(roots.iter().all(|root| !root.as_os_str().is_empty()));
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ use super::error::{io_error, JavaError, JavaErrorKind};
|
|||||||
use super::extract;
|
use super::extract;
|
||||||
use super::location::{JavaInstallRoot, LocationMode};
|
use super::location::{JavaInstallRoot, LocationMode};
|
||||||
use super::probe::{self, JavaOrigin, JavaRuntime};
|
use super::probe::{self, JavaOrigin, JavaRuntime};
|
||||||
use super::release::JdkRelease;
|
use super::release::{JdkArchive, JdkRelease};
|
||||||
use super::source::{self, ChainLog};
|
use super::source::{self, ChainLog};
|
||||||
use super::target::AdoptiumTarget;
|
use super::target::AdoptiumTarget;
|
||||||
use super::vendor::{self, JdkVendor};
|
use super::vendor::{self, JdkVendor};
|
||||||
@@ -184,7 +184,6 @@ pub async fn install(
|
|||||||
location: &JavaInstallRoot,
|
location: &JavaInstallRoot,
|
||||||
request: InstallRequest,
|
request: InstallRequest,
|
||||||
) -> Result<InstalledJdk, JavaError> {
|
) -> Result<InstalledJdk, JavaError> {
|
||||||
let target = AdoptiumTarget::host()?;
|
|
||||||
let domestic = engine.region().is_domestic();
|
let domestic = engine.region().is_domestic();
|
||||||
let priority = request.order(domestic);
|
let priority = request.order(domestic);
|
||||||
let mut chain = ChainLog::new();
|
let mut chain = ChainLog::new();
|
||||||
@@ -192,6 +191,75 @@ pub async fn install(
|
|||||||
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
// 清掉上次中断留下的 .staging-* 与空壳(只碰我们自己的形状,见 location 模块)。
|
||||||
let _ = super::location::cleanup_orphans(&location.root);
|
let _ = super::location::cleanup_orphans(&location.root);
|
||||||
|
|
||||||
|
// Termux:JDK 由系统包管理器装进 `$PREFIX`,不是可下载的归档。
|
||||||
|
// 默认(没钉发行版)时优先走它;`--vendor termux` 时只走它。
|
||||||
|
let want_termux = match request.vendor {
|
||||||
|
Some(JdkVendor::Termux) => true,
|
||||||
|
Some(_) => false,
|
||||||
|
None => super::termux::available(),
|
||||||
|
};
|
||||||
|
if want_termux {
|
||||||
|
if !super::termux::available() {
|
||||||
|
return Err(
|
||||||
|
JavaError::new(JavaErrorKind::Unsupported, "termux 源只在 Termux 里可用")
|
||||||
|
.with_hint("普通 Linux/Windows 用 --vendor temurin|oracle|microsoft|zulu"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
match super::termux::install(request.major).await {
|
||||||
|
Ok((home, 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,
|
||||||
|
// 不提前 `?` 退出——让每一家都记一条降级原因,错误里能看见全貌。
|
||||||
|
let target = AdoptiumTarget::host();
|
||||||
|
|
||||||
for candidate in &priority {
|
for candidate in &priority {
|
||||||
let vendor = *candidate;
|
let vendor = *candidate;
|
||||||
if !vendor.supports(request.major) {
|
if !vendor.supports(request.major) {
|
||||||
@@ -205,6 +273,15 @@ pub async fn install(
|
|||||||
reasons.push(format!("{}:{reason}", vendor.id()));
|
reasons.push(format!("{}:{reason}", vendor.id()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
let target = match &target {
|
||||||
|
Ok(target) => *target,
|
||||||
|
Err(error) => {
|
||||||
|
let message = error.message().to_string();
|
||||||
|
chain.failed(vendor, message.clone());
|
||||||
|
reasons.push(format!("{}:{message}", vendor.id()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
let release = match source::fetch(engine, vendor, request.major, target).await {
|
let release = match source::fetch(engine, vendor, request.major, target).await {
|
||||||
Ok(release) => release,
|
Ok(release) => release,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ pub mod release;
|
|||||||
pub mod select;
|
pub mod select;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
pub mod target;
|
pub mod target;
|
||||||
|
pub mod termux;
|
||||||
pub mod vendor;
|
pub mod vendor;
|
||||||
pub mod zulu;
|
pub mod zulu;
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ pub async fn fetch(
|
|||||||
})?;
|
})?;
|
||||||
zulu::fetch_latest(client, table, major, target).await
|
zulu::fetch_latest(client, table, major, target).await
|
||||||
}
|
}
|
||||||
|
JdkVendor::Termux => Err(JavaError::new(
|
||||||
|
super::error::JavaErrorKind::Unsupported,
|
||||||
|
"Termux 的 JDK 由系统包管理器安装,不走归档下载链",
|
||||||
|
)
|
||||||
|
.with_hint("在 Termux 里用 `ull java install <major>`(默认走 termux 源)")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)四个模块里。
|
//! `zulu` / `api`(Temurin)四个模块里。
|
||||||
|
|
||||||
/// 设置键 `java.vendorPriority` 的允许值(顺序即默认序的覆盖来源)。
|
/// 设置键 `java.vendorPriority` 的允许值(顺序即默认序的覆盖来源)。
|
||||||
pub const VENDOR_IDS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
|
///
|
||||||
|
/// `termux` 只在 Termux 宿主上可用(系统包管理器安装),默认序里不出现;
|
||||||
|
/// 显式写进优先级或 `--vendor termux` 才走它。
|
||||||
|
pub const VENDOR_IDS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||||
|
|
||||||
/// 一家 JDK 发行版。
|
/// 一家 JDK 发行版。
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
@@ -28,11 +31,19 @@ pub enum JdkVendor {
|
|||||||
Microsoft,
|
Microsoft,
|
||||||
/// Azul Zulu(第四备选,API 元数据 + CDN 直链)。
|
/// Azul Zulu(第四备选,API 元数据 + CDN 直链)。
|
||||||
Zulu,
|
Zulu,
|
||||||
|
/// Termux 的系统 openjdk(`pkg install openjdk-<major>`;只在 Termux 里可用)。
|
||||||
|
Termux,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JdkVendor {
|
impl JdkVendor {
|
||||||
/// 全部发行版(枚举顺序,不是优先级顺序)。
|
/// 全部发行版(枚举顺序,不是优先级顺序)。
|
||||||
pub const ALL: [Self; 4] = [Self::Temurin, Self::Oracle, Self::Microsoft, Self::Zulu];
|
pub const ALL: [Self; 5] = [
|
||||||
|
Self::Temurin,
|
||||||
|
Self::Oracle,
|
||||||
|
Self::Microsoft,
|
||||||
|
Self::Zulu,
|
||||||
|
Self::Termux,
|
||||||
|
];
|
||||||
|
|
||||||
/// 稳定 id(目录名段、`--vendor` 取值、设置值、JSON 字段)。
|
/// 稳定 id(目录名段、`--vendor` 取值、设置值、JSON 字段)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -42,6 +53,7 @@ impl JdkVendor {
|
|||||||
Self::Oracle => "oracle",
|
Self::Oracle => "oracle",
|
||||||
Self::Microsoft => "microsoft",
|
Self::Microsoft => "microsoft",
|
||||||
Self::Zulu => "zulu",
|
Self::Zulu => "zulu",
|
||||||
|
Self::Termux => "termux",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +65,7 @@ impl JdkVendor {
|
|||||||
Self::Oracle => "Oracle OpenJDK",
|
Self::Oracle => "Oracle OpenJDK",
|
||||||
Self::Microsoft => "Microsoft OpenJDK",
|
Self::Microsoft => "Microsoft OpenJDK",
|
||||||
Self::Zulu => "Azul Zulu",
|
Self::Zulu => "Azul Zulu",
|
||||||
|
Self::Termux => "Termux OpenJDK",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +79,7 @@ impl JdkVendor {
|
|||||||
Some(Self::Microsoft)
|
Some(Self::Microsoft)
|
||||||
}
|
}
|
||||||
"zulu" | "azul" => Some(Self::Zulu),
|
"zulu" | "azul" => Some(Self::Zulu),
|
||||||
|
"termux" | "termux-openjdk" | "pkg" => Some(Self::Termux),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +94,7 @@ impl JdkVendor {
|
|||||||
Self::Temurin | Self::Zulu => major >= 8,
|
Self::Temurin | Self::Zulu => major >= 8,
|
||||||
Self::Oracle => major >= 9,
|
Self::Oracle => major >= 9,
|
||||||
Self::Microsoft => matches!(major, 11 | 17 | 21 | 25),
|
Self::Microsoft => matches!(major, 11 | 17 | 21 | 25),
|
||||||
|
Self::Termux => matches!(major, 17 | 21 | 25),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +106,7 @@ impl JdkVendor {
|
|||||||
Self::Oracle => &[17, 21, 25],
|
Self::Oracle => &[17, 21, 25],
|
||||||
Self::Microsoft => &[11, 17, 21, 25],
|
Self::Microsoft => &[11, 17, 21, 25],
|
||||||
Self::Zulu => &[8, 11, 17, 21, 25],
|
Self::Zulu => &[8, 11, 17, 21, 25],
|
||||||
|
Self::Termux => &[17, 21, 25],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ impl Library {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// natives 分类器名:按 OS 查 `natives` 表并展开 `${arch}`(64 位为 `64`)。
|
/// 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> {
|
pub fn native_classifier(&self, env: &Environment) -> Option<String> {
|
||||||
let raw = self.natives.as_ref()?.get(&env.os)?;
|
let raw = self.natives.as_ref()?.get(&env.os)?;
|
||||||
let arch = if matches!(env.arch.as_str(), "x86_64" | "arm64") {
|
let arch = if matches!(env.arch.as_str(), "x86_64" | "arm64") {
|
||||||
@@ -179,7 +184,33 @@ impl Library {
|
|||||||
} else {
|
} 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`)。
|
/// 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, "MESA_LOADER_DRIVER_OVERRIDE", "llvmpipe");
|
||||||
set_env(&mut env, "GALLIUM_DRIVER", "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 os == OsFamily::MacOs {
|
||||||
if let Some(icd) = &req.renderer.vulkan_icd {
|
if let Some(icd) = &req.renderer.vulkan_icd {
|
||||||
set_env(&mut env, "VK_ICD_FILENAMES", &icd.display().to_string());
|
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()));
|
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 paths;
|
||||||
pub mod receipt;
|
pub mod receipt;
|
||||||
pub mod redirect;
|
pub mod redirect;
|
||||||
|
pub mod termux;
|
||||||
pub mod theme_probe;
|
pub mod theme_probe;
|
||||||
pub mod vk_probe;
|
pub mod vk_probe;
|
||||||
pub mod wallpaper;
|
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 层
|
/// 与 `crate::game::java::vendor::VENDOR_IDS` 必须是同一份集合——setting 层不依赖 game 层
|
||||||
/// (分层纪律),一致性由 `tests/setting/java_install_keys.rs` 的契约测试固定。
|
/// (分层纪律),一致性由 `tests/setting/java_install_keys.rs` 的契约测试固定。
|
||||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu"];
|
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||||
|
|
||||||
/// launcher.json 当前 schema 版本。
|
/// launcher.json 当前 schema 版本。
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -175,7 +175,10 @@ fn sha256_texts_are_parsed_the_same_way_for_every_upstream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vendor_ids_are_the_setting_whitelist() {
|
fn vendor_ids_are_the_setting_whitelist() {
|
||||||
assert_eq!(VENDOR_IDS, &["temurin", "oracle", "microsoft", "zulu"]);
|
assert_eq!(
|
||||||
|
VENDOR_IDS,
|
||||||
|
&["temurin", "oracle", "microsoft", "zulu", "termux"]
|
||||||
|
);
|
||||||
for id in VENDOR_IDS {
|
for id in VENDOR_IDS {
|
||||||
assert_eq!(JdkVendor::parse(id).map(|vendor| vendor.id()), Some(*id));
|
assert_eq!(JdkVendor::parse(id).map(|vendor| vendor.id()), Some(*id));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ fn sources_of(table: &unlucky_core::download::MirrorTable, vendor: JdkVendor) ->
|
|||||||
.zulu()
|
.zulu()
|
||||||
.map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)])
|
.map(|zulu| vec![format!("{}({})", zulu.api, zulu.measured_speed)])
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
JdkVendor::Termux => unlucky_core::game::java::termux::MIRRORS
|
||||||
|
.iter()
|
||||||
|
.map(|mirror| format!("{mirror}(系统包:pkg install openjdk-<major>)"))
|
||||||
|
.collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,18 +134,38 @@ pub fn build_request(
|
|||||||
|
|
||||||
let mut libraries: Vec<LibraryInput> = Vec::new();
|
let mut libraries: Vec<LibraryInput> = Vec::new();
|
||||||
for library in resolved.libraries.as_deref().unwrap_or_default() {
|
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;
|
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;
|
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() {
|
let path = if library.is_local() {
|
||||||
env.game.version_library(&version_id, &coordinate)
|
env.game.version_library(&version_id, &coordinate)
|
||||||
} else {
|
} else {
|
||||||
env.game.library(&coordinate)
|
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(
|
let natives = unlucky_core::launch::natives::collect(
|
||||||
&env.game,
|
&env.game,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
| Linux arm64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+(`ubuntu-22.04-arm` 原生构建,**与 x64 同地板**)、xkbcommon、fontconfig、libGL、xcb |
|
| Linux arm64 X11 | ❌ 不构建 | 一等(未真机验证) | glibc 2.35+(`ubuntu-22.04-arm` 原生构建,**与 x64 同地板**)、xkbcommon、fontconfig、libGL、xcb |
|
||||||
| Linux arm64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client |
|
| Linux arm64 Wayland | ❌ 不构建 | 支持 | 上述 + wayland-client |
|
||||||
| Windows 8.1/8/7 | — | 不支持 | 基线已上调(D21) |
|
| 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
|
`packaging/release-notes.md` 的 "Support matrix" 表与上表逐行对应(英文版同样带 published / not
|
||||||
published 一列,Termux 行标注 experimental)——改一边必须同步另一边,否则标题里的"一致"就是假话。
|
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, 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` |
|
| 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 |
|
| 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
|
`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
|
(see Downloads above). Both arm64 rows are formal matrix slots, not the experimental Termux path, and
|
||||||
|
|||||||
Reference in new issue
Block a user