feat(cli): GUI 二进制内建 CLI 子命令分发,ull 纳入发布资产(M82)

一个二进制两种用法:两个 GUI 的 main.rs 在进 UI 之前分发子命令,命中就把控制权交给
unlucky_cli::run —— 不建窗口、不探后端、不扫字体、不 dlopen 图形库。

分发判据只有一条,子命令表不重复:crates/cli 的解析器是唯一真相源。
- 首个 token 是裸词 → 子命令位置,交 CLI(未知子命令由 CLI 报错 + 提示 --help,退出 1,
  不静默开窗)
- 首个 token 是 flag → 只有 CLI 解析出真命令才算 CLI 调用;--data-dir X 解析成 Welcome、
  --screenshot / --renderer / --diff 等 GUI 参数解析失败,一律仍走 GUI(CI 脚本零改动)

顺带:unlucky-launcher --version 以前会直接开窗,现在走 CLI 分发报版本。

packaging:ull 作为独立裸资产随每个槽位发布(make_artifacts.py + release.yml 的 assets
数组与 SHA256SUMS glob),矩阵完整性检查改成 GUI/CLI 各查一次。

app-iced 的 x11 / wayland / linux-theme-detection 收进 target 专属的 cfg(target_os) 段。

门禁实跑:cargo fmt --all --check 通过;cargo clippy --workspace --all-targets -D warnings
通过(exit 0);cargo test --workspace 通过(51 个测试二进制 / 1369 个用例,含本次新增的
5 + 5 条分发判据用例)。
This commit is contained in:
AskaEth committed 2026-11-02 08:08:09 +08:00
1 parent ef3212021a
commit 1c7285cea9
11 files changed
+457 -53

No files matched your search

+23 -7
View File
@@ -14,6 +14,7 @@
| `linux-arm64` | `unlucky-launcher-<ver>-linux-arm64.tar.gz` | 单二进制归档(原生 ARM64 runner 构建) | 同上 |
| `macos-arm64` | `unlucky-launcher-<ver>-macos-arm64.tar.gz` | 单二进制归档 | 同上 |
| `macos-x64` | `unlucky-launcher-<ver>-macos-x64.tar.gz` | 单二进制归档 | 同上 |
| 每槽位 | `ull-<ver>-<槽位>[.exe]` | **裸 CLI 文件**(独立资产,不进 tar.gz) | 零 UI 依赖的 `ull` 本体,进同一份 `SHA256SUMS.txt` |
| 全平台 | `SHA256SUMS.txt` | 校验清单 | 每行 `<sha256>␠␠<文件名>`(`sha256sum -c` 可直吃) |
约定细节:
@@ -22,6 +23,10 @@
归档里额外放三份合规文本是许可义务要求,不是运行时依赖;Windows 因为"一个 exe 装不下文本",
把 `LICENSE` / `NOTICE` / `README.md` 作为 Release 的并列资产分发,条款同样随下载走。
- 归档解压后是**一个顶层目录** `unlucky-launcher-<ver>-<槽位>/`,用户不会解压出一堆散文件。
- **`ull` 是并列的裸资产,不塞进 tar.gz**:headless 用户要的就是「下一个小文件直接跑」;归档里放
LICENSE/NOTICE/README.md 是许可义务,而这三份本来就作为并列 Release 资产分发,裸 `ull` 不缺条款。
- `unlucky-launcher` 本体也认全部 CLI 子命令(M82),`ull` 不是第二个界面,而是**不链接图形栈**的
同一套命令(差异见 §4 与 §7.6)——两者都不是重复品。
- `<ver>` 不带前导 `v`(tag 是 `v0.1.0`,文件名是 `0.1.0`)——由脚本统一裁剪。
- UNIX 归档内 `mtime`/`uid`/`gid` 归零、权限固定(二进制 0755、文本 0644),
同源构建的 sha256 不受打包时刻影响。
@@ -148,6 +153,11 @@ Windows ARM64 原生库(Mojang/LWJGL 只发 x64/x86),实际游玩仍走系
Debian/Ubuntu 一行:`sudo apt install libxkbcommon0 fontconfig libgl1 libegl1 libxcb1 libx11-6 libwayland-client0`。
运行期**不需要** `-dev` 包、不需要 Java(Java 是启动游戏时才由启动器自己管理的东西)。
上表是**开窗**要用的库,不是进程启动时就要的:实测(§7.1)除 `libfontconfig.so.1` 外它们**都不在** ELF
的 `DT_NEEDED` 里,由 `dlopen` 在创建窗口那一刻才加载。所以同一台没有 X11 / Wayland / GL 的机器上
`unlucky-launcher instances list` 照样跑得起来(GUI 二进制先分发后开窗),而 `ull` 连 fontconfig 都不需要。
arm64 侧是同一套(同一份依赖树、同一个 winit/femtovg 组合)。
### Linux arm64(aarch64)
依赖与 x64 完全同一套(`libxkbcommon` / `fontconfig` / `libGL` / `libxcb` / `libwayland-client`),
@@ -233,7 +243,7 @@ LTO 也就只有一个线程可跑(`fat` 本身只管"跨 crate 全量参与"
|------|---------|
| `ull.exe --version` | 输出 `ull 0.1.0(unlucky-cli)` + `os: Windows 11 · 10.0.26100(x86_64)· 满足最低要求`,exit 0 —— 版本与 workspace `0.1.0` 一致 |
| `unlucky-launcher.exe --software-renderer --screenshot … --data-dir …` | exit 0,产出 1180×760 PNG(3,588,298 B);看图确认 Metro 实例页(侧栏/搜索/空态/AI 助手面板)正常渲染 → **软渲染路径真的能出图**,不依赖 GL |
| `unlucky-launcher.exe --version` | **无输出**,8 秒后被 timeout 杀掉(exit 124)——GUI 目前不解析 `--version`,会直接开窗口。已作为 finding 上报,见 §10-1 |
| `unlucky-launcher.exe --version` | **无输出**,8 秒后被 timeout 杀掉(exit 124)——GUI 当时不解析 `--version`,会直接开窗口。M82 后已修(走 CLI 分发,见 §10-1)|
### 7.3 打包验证(本机 Windows 槽位真跑)
@@ -268,6 +278,13 @@ CI 的 MSVC `+crt-static` 产物同理(静态 CRT);`release.yml` 里用 `d
总之不允许"悄悄要用户装可再发行包"。
注意:本机只装了 GNU 工具链,MSVC 那个槽位的产物本身**没有在本机验证过**,由 CI 首跑验证。
Linux 侧同一件事的对照(M82 实测:`readelf -d` + `ldd` + `nm -D`,容器内 Debian 12 glibc 2.36):Slint
GUI 的 ELF `DT_NEEDED` 只有 `libfontconfig.so.1` / `libgcc_s.so.1` / `libm.so.6` / `libc.so.6` /
`ld-linux-x86-64.so.2`;`libX11` / `libxcb` / `libxkbcommon` / `libwayland-client` / `libGL` / `libEGL`
**都不在**导入表里,而是以字符串形式由 `dlopen` 在开窗那一刻加载(`nm -D` 可见 `U dlopen@GLIBC_2.34` /
`dlsym` / `dlclose`)。所以 §4 那张「开窗要的库」不是启动前提,缺了也不影响进程起来跑 CLI;
`ull` 的 `DT_NEEDED` 连 fontconfig 都没有,只剩 glibc 那一组。
### 7.5 arm64 槽位的本机验证(脚本层取证,不假装跑得动 arm64 二进制)
本机没有 ARM 机器,也不具备 MSVC ARM64 链接库组合,所以只做**能真实证明的东西**(`python` 直接加载
@@ -322,16 +339,15 @@ experimental / not part of this release matrix)——改一边必须同步另
## 10. 已知边界 / 后续
1. **GUI 无 `--version`**:`unlucky-launcher.exe` 目前只解析 `--software-renderer` / `--screenshot*` /
`--data-dir` / `--mock`,`--version` 会当成普通参数忽略并直接开窗口。版本自证目前靠:
①产物名里的 `<ver>` ②`ull --version`(同 workspace 版本)③tag 交叉校验。
加一个 `--version` 分支属于 `crates/app` 范围,已作为 finding 上报,不在本 mission 内改。
1. **GUI 的 `--version` 已解决**(M82):`unlucky-launcher --version` 现在命中子命令分发,输出与
`ull --version` 同一条版本 + 系统摘要后退出,不再开窗。§7.2 里「无输出、8 秒后被 timeout 杀掉」那行
是**改之前**的实测记录,留作对照。版本自证的另外两条(产物名里的 `<ver>`、tag 交叉校验)不变。
2. **签名/公证**:Windows 无 Authenticode、macOS 无 codesign/notarize(都要证书/开发者账号),
当前以 SHA256SUMS + Release 说明作为完整性参考。
3. **macOS `.app` bundle / `.dmg`**:与单文件承诺冲突,暂不做;若用户要 Dock 图标再单开 mission。
4. **Linux musl 静态版**:可彻底摆脱 glibc 地板,但 Slint 在 musl 上需额外验证,未纳入本矩阵。
5. **`ull` CLI 未随包**:本次矩阵只发 GUI 单文件(用户核心卖点是启动器本体);headless 用户可从
源码 `cargo build --release --bin ull`,或在后续把 `ull` 加进同一矩阵(脚本已按槽位参数化,改动很小)。
5. **`ull` 已随包**(M82 起):每个槽位与 GUI 并列发一份裸 `ull`,进同一份 `SHA256SUMS.txt`,矩阵
完整性检查按「GUI 与 CLI 各自都要有」分开查(`release.yml`)。等 CI 首跑确认这条在真流水线上有效。
6. **本机 GNU 链接器的已知无害告警**:`cargo build --release`(GNU 工具链)会打一行
`warning: linker stderr: corrupt .drectve at end of def file`,来自 MinGW `ld` 对 `.drectve` 段的
老式抱怨,不影响产物(体积/导入表/冒烟均已实测正常);CI 的 MSVC 槽位不会出现这条。
+28 -3
View File
@@ -9,6 +9,7 @@ Release matrix (see `packaging/README.md` for the full contract):
Linux arm64 unlucky-launcher-<ver>-linux-arm64.tar.gz
macOS arm64 unlucky-launcher-<ver>-macos-arm64.tar.gz
macOS x64 unlucky-launcher-<ver>-macos-x64.tar.gz
every slot ull-<ver>-<slot>[.exe] (bare CLI binary, no archive)
all SHA256SUMS.txt
`--version` is taken from the cargo workspace so a tag can never drift away from
@@ -17,6 +18,15 @@ The archives carry the launcher binary plus LICENSE / NOTICE / README.md, which
what the license obligations ask for; Windows ships the bare `.exe` (the single-file
selling point) and the same three documents are attached to the Release next to it.
The CLI ships as its **own bare asset** on every platform rather than inside the tar.gz:
the point of `ull` is a small single file you download and run on a machine with no
display, and an archive it has to be extracted from adds a step for that user. The
launcher is the archive because it needs the license texts beside it; the Release
carries LICENSE / NOTICE / README.md anyway, so a bare `ull` loses nothing.
The launcher binary also answers every CLI subcommand, so `ull` is not a second
interface — it is the version of it that links no graphics stack at all.
Slot and build directory are overridable because a cross build's binaries live in
`target/<triple>/release` and the build host (x64) is not the artifact (arm64): CI
passes both from its matrix, and the executable header is read back so a binary can
@@ -189,6 +199,16 @@ def write_sums(out_dir: Path, assets: list[Path]) -> Path:
return sums
def stage_cli(out_dir: Path, version: str, slot: str, cli: Path) -> Path:
"""Copy the CLI out as its own bare asset (`ull-<ver>-<slot>[.exe]`)."""
name = f"{CLI_BIN}-{version}-{slot}" + (".exe" if cli.suffix == ".exe" else "")
asset = out_dir / name
shutil.copy2(cli, asset)
# 裸文件下载即可运行:Windows 宿主机上源文件没有 x 位,这里补上(UNIX 侧才有意义)。
asset.chmod(0o755)
return asset
def stage_archive(out_dir: Path, tag: str, version: str, launcher: Path) -> Path:
"""tar.gz with one top-level dir holding the binary + compliance docs."""
stem = f"unlucky-launcher-{version}-{tag}"
@@ -259,10 +279,15 @@ def main() -> int:
shutil.copy2(launcher, asset)
else:
asset = stage_archive(out_dir, slot, version, launcher)
# CLI 与 GUI 同一矩阵槽位、各自独立资产:架构同样回读文件头校验,不给 arm64 槽位混进 x64 的 ull。
cli = binary(CLI_BIN, build_dir, family)
verify_arch(cli, slot)
cli_asset = stage_cli(out_dir, version, slot, cli)
sums = write_sums(out_dir, [asset])
size_mb = asset.stat().st_size / (1024 * 1024)
print(f"artifact: {asset.name} {size_mb:.2f} MiB sha256={sha256(asset)}")
sums = write_sums(out_dir, [asset, cli_asset])
for item in (asset, cli_asset):
size_mb = item.stat().st_size / (1024 * 1024)
print(f"artifact: {item.name} {size_mb:.2f} MiB sha256={sha256(item)}")
print(f"sums: {sums.name}")
return 0
+13 -1
View File
@@ -19,6 +19,18 @@ No WebView, no bundled runtime, no JRE download needed to start the launcher its
this release next to the `.exe` (a lone exe cannot carry them), so the license terms travel
with the download on every platform.
Every slot also ships the command line as its own small asset — `ull-{{VERSION}}-<slot>`
(`.exe` on Windows):
```bash
./ull-{{VERSION}}-linux-x64 instances list # servers / CI / Termux: no graphics stack needed
unlucky-launcher instances list # desktops: the GUI binary answers the same commands
```
`ull` runs `instances`, `install`, `launch`, `accounts`, `sessions`, `logs`, `diag`, `config`,
`java` and `agent`, and links no graphics library at all. The GUI binary accepts the same set
before it ever opens a window, so on a machine with a desktop you do not need `ull`.
## Support matrix
| Platform | Level | Runtime requirements |
@@ -31,7 +43,7 @@ with the download on every platform.
| Linux arm64, X11 | first class | glibc 2.35+ (built on `ubuntu-22.04-arm`, same floor as x64), `libxkbcommon`, `fontconfig`, `libGL`, `libxcb` |
| Linux arm64, Wayland | supported | the above plus `wayland-client` |
| Windows 8.1/8/7 | unsupported | baseline was raised to Win10 (decision D21) |
| Android — Termux + X11 | experimental, not promised | CLI only (build from source), GUI unverified; not part of this release matrix |
| Android — Termux + X11 | experimental, not promised | CLI only (the `ull` asset, or build from source), GUI unverified; not part of this release matrix |
Both arm64 rows are **formally released slots** (the assets ship in this release and in
`SHA256SUMS.txt`), not the experimental Termux path: Linux arm64 is built and smoke-tested on a