diff --git a/README.md b/README.md index c83ceac..1db70e2 100644 --- a/README.md +++ b/README.md @@ -195,8 +195,8 @@ This list is the reason to believe the rest. None of it is hidden behind marketi - **Install progress is stage-level**, not byte-level, on the content-library install path. - **No native file chooser.** Import takes drag-and-drop or a pasted path. - **Android/Termux is experimental and not promised**: no released asset targets it. A GUI build is - possible from source via the glibc toolchain (`packaging/docs/termux-glibc-build.md`); it was - verified once on CPU rendering only. + possible from source via the glibc toolchain (`packaging/docs/termux-glibc-build.md`, branch notes: + [`README.termux.md`](README.termux.md)); it was verified once on CPU rendering only. ## Download and run diff --git a/README.termux.md b/README.termux.md new file mode 100644 index 0000000..d4175f9 --- /dev/null +++ b/README.termux.md @@ -0,0 +1,46 @@ +# Unlucky Launcher · Termux 适配(`termux-glibc` 分支) + +这条分支把启动器带到 **Termux(Android)**:用 glibc 工具链编出 `aarch64-unknown-linux-gnu` +二进制,在 **termux-x11** 里跑 GUI;同时补上 Termux 平台识别、系统 JDK 安装与中文字体。 + +## 已验证 + +| 项 | 状态 | 证据 | +| --- | --- | --- | +| 源码构建(GUI + CLI) | ✅ | Rust 1.99.1 gnu + clang/lld + glibc 运行时;CPU-only(tiny-skia) | +| 窗口显示 | ✅ | termux-x11 `:0`,截图 1180×760,首帧 ~330 ms | +| 中文 | ✅ | 启动时注册 `/system/fonts/NotoSansCJK-Regular.ttc` | +| 微软登录 id | ✅ | 构建期注入,日志 `client_id=configured source=build` | +| 横屏适配 | ✅ | 默认窗口按 X 屏幕等比自适应(`source=termux-screen`) | +| Java 发现 | ✅ | `JAVA_HOME`/PATH + `$PREFIX/lib/jvm` 扫描 | +| Java 自动安装 | ✅ | `ull java install 17` 由内置镜像装完 openjdk-17(约 20 s) | +| GPU(wgpu) | ⏳ | bionic 侧 Vulkan/turnip 已验证;glibc 侧变体待构建验证 | +| Minecraft 运行 | ⏳ | bionic JVM 加载不了 glibc natives;计划走 glibc JVM | + +## 快速开始 + +完整配方(工具链、patchelf、linker、门禁)见 +[`packaging/docs/termux-glibc-build.md`](packaging/docs/termux-glibc-build.md)。最短路径: + +```sh +termux-x11 :0 -ac & +DISPLAY=:0 ./unlucky-launcher-iced --software-renderer +``` + +- 没有 GPU 栈时**必须**加 `--software-renderer`(CPU 渲染)。 +- 窗口尺寸默认按屏幕自适应;`ULL_WINDOW_SIZE=1520x900` 可覆盖。 +- Java:`ull java install 17|21|25` 在 Termux 上默认用系统包(内置镜像 + 隔离 apt 状态)。 + +## 已知边界 + +- 仅单机(OnePlus 15 / Android 17)验证,试验级、不在发布矩阵内。 +- GUI 只验证了 CPU 渲染;GPU 变体需要 glibc 侧 Vulkan 栈。 +- 装好 JDK ≠ 能跑 MC:MC 的 LWJGL natives 是 glibc `.so`,bionic、JVM 加载不了。 +- Termux 上三条测试失败是环境差异(`/proc/*/children`、长路径脱敏、mtime flaky), + PC 的 Linux CI 不受影响。 + +## 后续 + +1. glibc 侧 GPU(wgpu)变体与取证 +2. MC 运行链:glibc JVM(Temurin linux-aarch64 + glibc Mesa)或 proot 方案 +3. 剪贴板/IME 在 termux-x11 下的行为验证 diff --git a/README.zh-CN.md b/README.zh-CN.md index b9b544c..7083627 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -158,7 +158,7 @@ unlucky-launcher --version # CLI,版本 + 系统摘要 文件是被静默覆盖的。 - **内容库的安装进度是阶段级**,不是字节级。 - **没有原生文件选择框。** 导入靠拖拽或粘贴路径。 -- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`),目前只在 CPU 渲染下验证过一次。 +- **Android/Termux 是试验级、不承诺**:发布矩阵里没有面向它的资产。GUI 可以经 glibc 工具链源码自建(见 `packaging/docs/termux-glibc-build.md`,分支说明见 [`README.termux.md`](README.termux.md)),目前只在 CPU 渲染下验证过一次。 ## 下载与运行 diff --git a/crates/app-iced/src/app/boot.rs b/crates/app-iced/src/app/boot.rs index 182e421..bc93fae 100644 --- a/crates/app-iced/src/app/boot.rs +++ b/crates/app-iced/src/app/boot.rs @@ -30,6 +30,7 @@ use super::{MIN_SIZE, WINDOW_SIZE}; mod diagnostics; mod fallback; +mod termux_window; use fallback::{on_failure, respawn, OnFailure}; @@ -131,10 +132,20 @@ fn launch( // M98:`ULL_WINDOW_SIZE=1520x900` 覆盖尺寸(矩阵的「最小 / 宽屏」两档靠它,且必须走**真外壳** // ——`pages_demo` example 的标题栏是留白带,出不了「三键位置」那张图)。 - let (size, size_source) = acceptance::window_size( + // Termux:没有环境覆盖时,默认尺寸按 X 屏幕等比自适应(横屏优先), + // 失败就退回 1180×760 的验收尺寸。 + let (default_size, default_source) = termux_window::default_size( (WINDOW_SIZE.width, WINDOW_SIZE.height), (MIN_SIZE.width, MIN_SIZE.height), - ); + ) + .unwrap_or(((WINDOW_SIZE.width, WINDOW_SIZE.height), "default")); + let (size, size_source) = + acceptance::window_size(default_size, (MIN_SIZE.width, MIN_SIZE.height)); + let size_source = if size_source == "default" { + default_source + } else { + size_source + }; let settings = diagnostics::window_settings(size); // 两行日志与开窗用的是**同一个** settings(见模块文档纪律 2) diagnostics::report_window(&settings, options.scale_factor, size_source); diff --git a/crates/app-iced/src/app/boot/termux_window.rs b/crates/app-iced/src/app/boot/termux_window.rs new file mode 100644 index 0000000..b75ba4f --- /dev/null +++ b/crates/app-iced/src/app/boot/termux_window.rs @@ -0,0 +1,115 @@ +//! Termux / termux-x11 下的窗口尺寸自适应(横屏优先)。 +//! +//! 验收尺寸(1180×760)是 D30 的硬要求,不能改;但手机显示器可能横屏远大于它 +//! (例如 2066×1272)或竖屏略小。这里只在**运行时**把「默认尺寸」换成按当前 X +//! 屏幕算出的最大等比矩形;`ULL_WINDOW_SIZE` 覆盖仍然优先(见 `acceptance::window_size`)。 +//! +//! 只在 Termux + 有 `DISPLAY` 时探测;`xdpyinfo` 缺失/解析失败一律回默认,不影响其它平台。 + +use std::process::Command; + +use unlucky_core::platform::paths::ProcessEnv; +use unlucky_core::platform::termux; + +/// 屏幕四周留白(像素)。 +const MARGIN: f32 = 32.0; +/// 最大放大倍数:大屏上也不把窗口撑成巨屏。 +const MAX_SCALE: f32 = 2.0; +/// `xdpyinfo` 里尺寸那一行的前缀。 +const DIMENSIONS_PREFIX: &str = "dimensions:"; + +/// Termux 上的默认窗口尺寸:按 X 屏幕等比放大/缩小,失败返回 `None`(用调用方默认)。 +/// +/// `default` / `min` 由调用方传入([`super::WINDOW_SIZE`] / [`super::MIN_SIZE`]), +/// 这里不复制常量。 +#[must_use] +pub(super) fn default_size( + default: (f32, f32), + min: (f32, f32), +) -> Option<((f32, f32), &'static str)> { + if !termux::is_termux(&ProcessEnv) || std::env::var_os("DISPLAY").is_none() { + return None; + } + let screen = x_screen_size()?; + Some((fit_screen(default, min, screen), "termux-screen")) +} + +/// 在 `screen` 里放一个保持 `default` 宽高比的最大矩形(保留留白,不突破 `min`)。 +#[must_use] +pub(super) fn fit_screen(default: (f32, f32), min: (f32, f32), screen: (f32, f32)) -> (f32, f32) { + if screen.0 <= 0.0 || screen.1 <= 0.0 || default.0 <= 0.0 || default.1 <= 0.0 { + return default; + } + let available = ( + (screen.0 - MARGIN * 2.0).max(1.0), + (screen.1 - MARGIN * 2.0).max(1.0), + ); + let scale = (available.0 / default.0) + .min(available.1 / default.1) + .min(MAX_SCALE); + let width = (default.0 * scale).max(min.0).min(screen.0); + let height = (default.1 * scale).max(min.1).min(screen.1); + (width, height) +} + +/// 当前 X 屏幕尺寸(`xdpyinfo` 的 `dimensions: WxH pixels`)。 +fn x_screen_size() -> Option<(f32, f32)> { + let output = Command::new("xdpyinfo").output().ok()?; + if !output.status.success() { + return None; + } + let text = String::from_utf8_lossy(&output.stdout); + let line = text + .lines() + .find(|line| line.trim_start().starts_with(DIMENSIONS_PREFIX))?; + let value = line.split_whitespace().nth(1)?; + let (width, height) = value.split_once('x')?; + Some((width.parse().ok()?, height.parse().ok()?)) +} + +#[cfg(test)] +mod tests { + use super::fit_screen; + + const DEFAULT: (f32, f32) = (1180.0, 760.0); + const MIN: (f32, f32) = (880.0, 560.0); + + fn close(actual: (f32, f32), expected: (f32, f32)) { + assert!( + (actual.0 - expected.0).abs() < 0.5 && (actual.1 - expected.1).abs() < 0.5, + "expected {expected:?}, got {actual:?}" + ); + } + + #[test] + fn landscape_screen_scales_up_keeping_aspect() { + // 2066×1272:留白后 2002×1208,高度是瓶颈 → 1875.6×1208。 + close( + fit_screen(DEFAULT, MIN, (2066.0, 1272.0)), + (1875.58, 1208.0), + ); + } + + #[test] + fn portrait_screen_still_fits_width() { + // 1272×2066:宽度是瓶颈 → 1208×778。 + close(fit_screen(DEFAULT, MIN, (1272.0, 2066.0)), (1208.0, 778.03)); + } + + #[test] + fn huge_screen_is_capped_at_two_times() { + close(fit_screen(DEFAULT, MIN, (4000.0, 3000.0)), (2360.0, 1520.0)); + } + + #[test] + fn tiny_screen_never_goes_below_min_or_overflow() { + let (width, height) = fit_screen(DEFAULT, MIN, (800.0, 600.0)); + assert_eq!(width, 800.0, "宽度受屏幕限制"); + assert_eq!(height, 560.0, "高度不小于最小尺寸"); + } + + #[test] + fn invalid_screen_falls_back_to_default() { + assert_eq!(fit_screen(DEFAULT, MIN, (0.0, 0.0)), DEFAULT); + } +} diff --git a/crates/core/src/game/java/termux.rs b/crates/core/src/game/java/termux.rs index d80fab8..fa471c8 100644 --- a/crates/core/src/game/java/termux.rs +++ b/crates/core/src/game/java/termux.rs @@ -127,6 +127,8 @@ fn run_install( match apt_install(prefix, workspace, mirror, package) { Ok(()) => { if let Some(home) = installed_home(prefix, major) { + // 装完把隔离的 lists/cache/archives 清掉(几十到上百 MB,别留在 $PREFIX/tmp)。 + let _ = std::fs::remove_dir_all(workspace); return Ok(home); } failures.push(format!("{mirror}:装完找不到 bin/java")); diff --git a/packaging/docs/termux-glibc-build.md b/packaging/docs/termux-glibc-build.md index 0bb732b..3a2a8a7 100644 --- a/packaging/docs/termux-glibc-build.md +++ b/packaging/docs/termux-glibc-build.md @@ -35,12 +35,13 @@ G="$PWD/.gnu" ### 2.2 Rust gnu 工具链 -下载 Rust 1.99.1(或与 `rust-version` 兼容的 stable)的 `rustc` / `rust-std` / `cargo` 三个 -`aarch64-unknown-linux-gnu` 组件。官方源在国内很慢,用 USTC 镜像: +下载 Rust 1.99.1(或与 `rust-version` 兼容的 stable)的 `rustc` / `rust-std` / `cargo`, +以及跑门禁需要的 `clippy` / `rustfmt` 五个 `aarch64-unknown-linux-gnu` 组件。官方源在国内很慢, +用 USTC 镜像: ```sh mkdir -p "$TMPDIR/ull-gnu" && cd "$TMPDIR/ull-gnu" -for c in rustc rust-std cargo; do +for c in rustc rust-std cargo clippy rustfmt; do curl -fLO "https://mirrors.ustc.edu.cn/rust-static/dist/$c-1.99.1-aarch64-unknown-linux-gnu.tar.xz" done ``` @@ -56,11 +57,17 @@ done ```sh LD="$PREFIX/glibc/lib/ld-linux-aarch64.so.1" -for f in "$G/toolchain/bin/rustc" "$G/toolchain/bin/cargo" "$G/toolchain/bin/rustdoc"; do - patchelf --set-interpreter "$LD" --set-rpath "$PREFIX/glibc/lib:$G/toolchain/lib" "$f" +for f in rustc cargo rustdoc clippy-driver cargo-clippy rustfmt cargo-fmt; do + patchelf --set-interpreter "$LD" --set-rpath "$PREFIX/glibc/lib:$G/toolchain/lib" \ + "$G/toolchain/bin/$f" done ``` +> **坑(实测)**:`cargo clippy` 会在 PATH 里找 `cargo-clippy`。如果只装了 Termux 的 Rust, +> 找到的是 android 版 `cargo-clippy`,`aws-lc-sys` 等构建脚本会被当成 +> `--target=aarch64-linux-android` 编译并失败。把 gnu 工具链的 `bin` 放在 PATH 最前面即可; +> `rustc`/`rustfmt` 同理。 + ### 2.4 gcc 运行时片段 `clang -fuse-ld=lld` 仍然需要 `crtbeginS.o` / `crtendS.o` / `libgcc.a`。从 Debian 的 @@ -112,6 +119,18 @@ cargo --config 'source.crates-io.replace-with="ustc"' \ `--no-default-features` 关掉 `gpu`,只编 tiny-skia(CPU)。产物:`.gnu/target/release/unlucky-launcher-iced`。 +门禁(与 CI 同一组命令,环境同上): + +```sh +cargo fmt --all --check +cargo clippy --workspace --all-targets -- -D warnings +cargo test --workspace --all-targets --no-fail-fast +``` + +Termux 上有三条**环境差异**失败(不是回归):`launch` 测试读 +`/proc//task//children`(Android 内核没有)、natives 事件的长路径被脱敏成 +``、`settings_bridge` 的 mtime 热重载偶发(flaky)。PC 的 Linux CI 不受影响。 + ## 4. 运行(termux-x11) ```sh @@ -134,6 +153,9 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 中文:GUI 在 Termux 上会把 `/system/fonts/NotoSansCJK-Regular.ttc` 注册进 cosmic-text (fontdb 不扫 Android 的字体目录),无需额外环境变量。 - 启动日志里 `[auth] msa client_id=configured source=build` 表示构建期 id 已烘入。 +- **横屏优先**:Termux 上默认窗口尺寸会按当前 X 屏幕等比自适应(日志 + `[window] ... source=termux-screen`),横屏铺满、竖屏也不溢出;验收尺寸 1180×760 仍是 + 默认基准,`ULL_WINDOW_SIZE=1520x900` 这类覆盖仍然优先。 ### 4.1 Termux 上的 Java @@ -141,8 +163,11 @@ DISPLAY=:0 .gnu/target/release/unlucky-launcher-iced \ - 内置 SAU / TUNA / USTC / 官方四个 termux-main 镜像,按顺序尝试; - `apt-get` 的 lists/cache 隔离在 `$PREFIX/tmp/ull-java-termux/`,不改用户的 apt 配置; + 装完自动清理该目录(一次安装约 100 MB 临时占用); - 装完从 `$PREFIX/lib/jvm/java--openjdk` 发现,`ull java list` 直接可见; - `--vendor termux` 只走系统包;`temurin|oracle|microsoft|zulu` 仍走归档下载链。 +- 已实测:`ull java install 17` 从 SAU 镜像装完 `openjdk-17`(含 `openjdk-17-x`), + 约 20 秒;`ull java list` 随后列出 `[system]` 的 17。 ## 5. GPU(未在 glibc 侧验证)