chore(workspace): M118 —— crates/app 退出默认构建,发布改打 iced bin(Slint 版停发)

M118(D26):Slint 退役树 `crates/app` 不再进 workspace。目录与源码原样保留(不删一个文件),
只是配置上不再可编。

- 根 Cargo.toml:`[workspace] exclude = ["crates/app"]`。摘掉的是整棵 slint 1.18 + usvg 0.48
  依赖树(本机单路冷编 ≈14 分钟;并行超配时表现为 rustc 退 1 且零诊断,见踩坑记录 p2 T7)。
  Cargo.lock 由 cargo 自己重算:8639 → 6343 行(-2389/+93),`name = "slint"` 1→0、
  `name = "i-slint` 11→0、`name = "slint-build"` 1→0。锁必须一起提交:旧锁配 `--locked`
  会直接 `cannot update the lock file`(release.yml 与 ci.yml 的冒烟都带 `--locked`)。
- ci.yml:clippy/test/build 的 `--workspace` 自然不再覆盖 unlucky-app;runtime-smoke(Linux 与
  macOS 探针)打的是 Slint 版 `--bin unlucky-launcher`,该 bin 已不存在 → 换成 iced 版
  `unlucky-launcher-iced`(它的 cli.rs 照抄 Slint 版口径,`--data-dir` 仍被接受)。
- release.yml:**Slint 版停发**。旧写法 `--bin unlucky-launcher` 打的是 crates/app 的 bin,
  也就是说流水线一直在发 Slint 版;改为 `--bin unlucky-launcher-iced --bin ull`,Windows
  导入表自检的 EXE 指向构建产物 `unlucky-launcher-iced.exe`。对外产物文件名不变。
- make_artifacts.py:拆成 `LAUNCHER_BIN = "unlucky-launcher-iced"`(找产物)与
  `LAUNCHER_ASSET = "unlucky-launcher"`(落盘名),tar 内文件名/权限随之按对外名走。
- guards.yml / README(中英)/ packaging/README.md:逐条写明排除后的覆盖口径,历史实现段落
  全部保留并加限定语(不提供构建方式,也不暗示自救)。

门禁:fmt exit 0;clippy --workspace --all-targets -j 4 -- -D warnings exit 0(44.6s);
check --workspace --all-targets exit 0(5m20s,slint 命中 0 行);cargo deny check exit 0。
This commit is contained in:
AskaEth committed 2026-11-04 00:09:38 +08:00
1 parent 3f228ae092
commit cf8c6912ec
9 files changed
+230 -2430

No files matched your search

+26 -8
View File
@@ -3,6 +3,12 @@ name: CI
# 主干门禁:fmt / clippy / test / build 双平台矩阵。
# 架构守门(core 零 UI、文件 ≤400 行、依赖许可)见 guards.yml。
# 平台基线 Win10+(标准工具链产物),平台矩阵见 AGENTS.md §7。
#
# M118(D26):根 Cargo.toml 已 `exclude = ["crates/app"]`(Slint 退役树,源码原样保留)。
# 所以本文件里所有 `--workspace` 都**不再**覆盖 `unlucky-app`:CI 不再编 slint 1.18 + usvg 0.48
# 那棵依赖树(本机单路冷编 ≈14 分钟;并行超配时表现为 `rustc` 退 1 且零诊断,
# 见 DevDocs/00-踩坑记录-p2.md T7)。历史实现只作源码参考,**没有构建方式**(见 README:
# 它的 manifest 整份是 workspace 继承写法,离开 workspace 后连解析都过不了)。
on:
push:
@@ -38,7 +44,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Linux 图形栈构建依赖(winit / femtovg 编译期需要)
- name: Linux 图形栈构建依赖(winit / wgpu 编译期需要)
if: runner.os == 'Linux'
run: |
sudo apt-get update
@@ -49,6 +55,8 @@ jobs:
with:
components: clippy
- uses: Swatinem/rust-cache@v2
# `--workspace` 现在是 8 个 crate,不含 `unlucky-app`(M118 的 workspace exclude),
# 所以这条命令不会拉起 slint / usvg / i-slint-*。
- run: cargo clippy --workspace --all-targets -- -D warnings
test:
@@ -69,6 +77,9 @@ jobs:
libwayland-dev libxcb1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# 同 clippy:`--workspace` 不含 `unlucky-app`(M118),Slint 退役树不再被 `--all-targets` 拉起。
# 也因此**不要**再写 `--exclude unlucky-app`:它已不是成员,那面旗子只会打一条
# `excluded package(s) … not found in workspace` 的警告然后空转(实测 exit 0,不报错)。
- run: cargo test --workspace --all-targets
build:
@@ -89,14 +100,19 @@ jobs:
libwayland-dev libxcb1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
# 同上:`--workspace` 不含 `unlucky-app`(M118)。被摘掉的正是 release 档
# (lto=fat + codegen-units=1)最贵的那棵 slint + usvg 树。
- run: cargo build --workspace --release
# 运行时冒烟(M62):Linux 上**真渲染一帧**,补上「能编译 ≠ 能跑」的空白。
# 上面四个 job 都只证明代码编得过、测得过,从不创建真实图形窗口;本 job 在 Xvfb 虚拟显示里
# 启动 GUI 进程,让 Slint 软件渲染器把当前场景渲进内存像素缓冲并编码成 PNG,再对**像素本身**
# 启动 GUI 进程,让软件渲染器把当前场景渲进内存像素缓冲并编码成 PNG,再对**像素本身**
# 断言(不是只看退出码:窗口起来了但一帧空白同样是「退出码 0」)。
# 诚实边界:证明「Linux 上真能跑出非空白的一帧」,**不**证明视觉正确——视觉靠人眼看 artifact。
# 本 job 绑死 Slint Window::take_snapshot;迁 iced 后必须提供等价的离屏截图通道,否则本 job 静默退化成「能起进程」。
# M118(D26):这里原本绑死 Slint 的 `Window::take_snapshot`,打的是 Slint 版 bin
# `unlucky-launcher`;`crates/app` 退出 workspace 后那个 bin 不再存在,改用 iced 版
# `unlucky-launcher-iced` 自带的 `--screenshot` 通道(`crates/app-iced/src/shot/`)。
# 命令行参数刻意不动:iced 的 `cli.rs` 就是照 Slint 版的口径写的,`--data-dir` 也仍被接受(忽略)。
runtime-smoke:
name: runtime smoke (ubuntu x64, xvfb)
runs-on: ubuntu-latest
@@ -116,13 +132,14 @@ jobs:
- uses: Swatinem/rust-cache@v2
# 用 debug 而不是 release:本 job 验的是「运行时渲染通路能通」,不是优化后二进制的行为;
# release 的 lto="fat" + codegen-units=1 会把这一步拖到十几分钟却不带来额外证据。
- run: cargo build --bin unlucky-launcher --locked
# M118:bin 从 Slint 版 `unlucky-launcher` 换成 iced 版(见上方 job 注释)。
- run: cargo build --bin unlucky-launcher-iced --locked
- name: 在 Xvfb 里跑一帧并截图(退出码非 0 即失败)
run: |
set -o pipefail # 当前没有管道(死行),保留是为将来加管道时先有失败传播
mkdir -p "$RUNNER_TEMP/smoke/data"
xvfb-run -a --server-args="-screen 0 1280x1024x24" \
target/debug/unlucky-launcher --software-renderer \
target/debug/unlucky-launcher-iced --software-renderer \
--screenshot "$RUNNER_TEMP/smoke/runtime-smoke.png" \
--data-dir "$RUNNER_TEMP/smoke/data"
- name: 断言 PNG 存在且非空白(尺寸 / 唯一色数 / 亮度σ / 强边缘占比)
@@ -137,10 +154,11 @@ jobs:
# macOS 槽位探针(M62):**默认不在 PR 上跑**,只有手动 dispatch 才触发。
# 之所以是探针而不是常驻槽位:官方 macOS runner 处于 LaunchDaemon 上下文,没有登录的
# WindowServer 会话,Slint 的 winit backend 创建窗口大概率直接失败;而 worker 禁 push,
# WindowServer 会话,GUI 的 winit backend 创建窗口大概率直接失败;而 worker 禁 push,
# 本机也没有 macOS,这条路径**尚无任何实测证据**——不拿未验证的 job 卡住主干门禁。
# 首跑结论决定去留:绿 → 去掉 if/continue-on-error 升为常驻槽位;红 → 按 D26 归入 iced
# 重写后再规划,并把错误原文贴进 progress 文档(不静默删 job、不吞失败原因)。
# M118:bin 与 Linux 那条一样,从 Slint 版换成 iced 版(`unlucky-launcher-iced`)。
runtime-smoke-macos:
name: runtime smoke (macos arm64, probe)
if: github.event_name == 'workflow_dispatch'
@@ -153,12 +171,12 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo build --bin unlucky-launcher --locked
- run: cargo build --bin unlucky-launcher-iced --locked
- name: 尝试截图(macOS 没有 X 服务器,直接跑;失败即为结论)
run: |
set -o pipefail # 当前没有管道(死行),保留是为将来加管道时先有失败传播
mkdir -p "$RUNNER_TEMP/smoke/data"
target/debug/unlucky-launcher --software-renderer \
target/debug/unlucky-launcher-iced --software-renderer \
--screenshot "$RUNNER_TEMP/smoke/runtime-smoke.png" \
--data-dir "$RUNNER_TEMP/smoke/data"
- name: 断言 PNG 存在且非空白
+18
View File
@@ -7,6 +7,16 @@ name: Guards
# 4. file-size —— 全仓代码/文档单文件 ≤400 行(用户硬约束)
# 5. licenses —— cargo-deny:许可证白名单 + GPL 依赖拦截 + 供应链来源校验
# 6. unsafe-scope —— 全仓 `allow(unsafe_code)` 的文件集合必须恰好只有一个(D36 / M91)
#
# M118(D26):根 Cargo.toml 已 `exclude = ["crates/app"]`(Slint 退役树)。逐条核对过本文件,
# **没有**任何一条守卫以 `--workspace` 为前提,所以排除不会让它们空转:
# ① core-no-ui / presenter-no-ui / design-zero-dep —— 「manifest 文本 + `cargo tree -p <crate>`」
# 口径,被守的 crate 仍是 workspace 成员,依赖树与排除前逐行一致;
# ② file-size / unsafe-scope —— 走 `git ls-files` / `git grep`,与构建图无关,**仍然覆盖
# `crates/app/**`**(源码还在仓库里,就还得守它的行数与 unsafe 落点);
# ③ licenses —— 唯一走 workspace 依赖图的守卫。排除后 Slint 那棵树不再进图(实测 `cargo deny
# list` 里 `slint*` / `i-slint-*` 为 0 个),这与「不再随任何产物发布」一致;代价是它的许可证
# 不再被本门禁审 —— 真要重新纳入发布,得先把它加回 workspace,门禁随之恢复。
on:
push:
@@ -48,6 +58,9 @@ jobs:
# presenter 是 Slint 版(crates/app)与 iced 版(crates/app-iced)共用的唯一业务层(D26)。
# 一旦 slint/iced/winit 被拉进来,分层就废了,而编译与单测仍会全绿 —— 所以必须由 CI 守。
# M118 之后 Slint 版已被 workspace `exclude`,但本条**照样守得住**:它查的是 presenter 自己的
# manifest 与 `cargo tree -p unlucky-presenter`,与「谁在消费 presenter」无关;依赖树里一旦出现
# slint/iced/winit 仍会当场红。名单里的 slint 继续留着,就是为了防「把 UI 依赖偷偷塞进宿主层」。
# 与 core-no-ui 同样是「依赖表 + cargo tree」双查,名单保持一致;脚本刻意复制而非抽公共脚本,
# 理由是 CI 守门脚本自身挂掉的排查成本远高于这几行重复。
presenter-no-ui:
@@ -356,4 +369,9 @@ jobs:
- uses: EmbarkStudios/cargo-deny-action@v2
with:
# 只跑许可/禁用/来源三类,advisories 由后续安全轮次单独接管
# M118:cargo-deny 走 **workspace 依赖图**,`crates/app` 被 exclude 后 Slint 那棵树不再进图
# (实测 `cargo deny list` 里 `slint*` / `i-slint-*` 命中 0 个),所以本门禁不再审 Slint 的
# RF-2.0 许可 —— 与「它不再随任何产物发布」一致。这不是回退:`deny.toml` 里为 Slint 留的
# allow 条目**故意没删**(删它得先确认没有别的路径重新引入),当前等于一条死条目。
# 若将来把 `crates/app` 加回 workspace,本门禁的覆盖会自动恢复。
command: check licenses bans sources
+11 -2
View File
@@ -132,6 +132,13 @@ jobs:
exit 1
fi
# M118(D26):**Slint 版停发**。旧写法 `--bin unlucky-launcher` 打的是 `crates/app`
# (Slint 退役树)的 bin —— 也就是说这条流水线一直在发 Slint 版二进制,而 D26 早已把
# iced 定为产品 UI。现在 GUI 来源只有 `unlucky-launcher-iced`;`crates/app` 也已在根
# Cargo.toml 被 `exclude`,不参与任何 workspace 构建(它连 `--manifest-path` 都编不了)。
# 对外产物文件名**不变**(`unlucky-launcher-<ver>-<slot>.*`):改名发生在
# `packaging/make_artifacts.py` 内部(`LAUNCHER_BIN` 找 iced 产物、落盘用对外名),
# 下载链接 / SHA256SUMS / README 都不用跟着漂。
- name: cargo build --release --locked
shell: bash
env:
@@ -143,14 +150,16 @@ jobs:
if [ -n "$TARGET" ]; then
target_args+=(--target "$TARGET")
fi
cargo build --release --locked "${target_args[@]}" --bin unlucky-launcher --bin ull
cargo build --release --locked "${target_args[@]}" --bin unlucky-launcher-iced --bin ull
- name: Windows 单文件自检(导入表不得出现 VC++ 可再发行 DLL)
if: runner.os == 'Windows'
shell: bash
env:
# 跨槽位的关键点:产物在 target/<triple>/release 而不是 target/release。
EXE: ${{ matrix.build_dir }}/unlucky-launcher.exe
# M118:校验的是**构建产物**(iced bin 的原始文件名)。对外名 `unlucky-launcher`
# 由 packaging/make_artifacts.py 在落盘时才换上,那一步之后才存在。
EXE: ${{ matrix.build_dir }}/unlucky-launcher-iced.exe
run: |
set -euo pipefail
# +crt-static 是否真的生效,只看导入表:出现 VCRUNTIME/MSVCP/msvcr/ucrtbase 说明还得装
Generated
+64 -2360
View File
File diff suppressed because it is too large. Load diff
+11 -2
View File
@@ -4,13 +4,13 @@ members = [
"crates/core",
"crates/agent",
# M85(D26):UI 框架无关的宿主层。Slint 版(crates/app)与 iced 版(crates/app-iced)
# 共用它;Slint 退役后只剩 iced 依赖,本 crate 不受影响。
# 共用它;Slint 退役(M118 起 `crates/app` 已不是 workspace 成员)后只剩 iced 依赖,本 crate 不受影响。
"crates/presenter",
"crates/app",
"crates/cli",
"crates/design",
# P4(D26 iced 重写)第一步:与 Slint 版 `crates/app` 并存的新 UI crate。
# bin 名 `unlucky-launcher-iced`,翻牌时才改名(见 DevDocs/SubAgentsTalk/app-iced-骨架与翻牌.md)。
# M118 起它是发布矩阵里**唯一**的 GUI 来源:发布了 Slint 版的那个 bin 名属于被排除的 `crates/app`。
"crates/app-iced",
# M87(D40 第 2 层):配色取色的解码适配层。**唯一**允许引入 `image` 类依赖的地方,
# 使 `crates/design` 能继续宣称「零第三方依赖」。
@@ -19,6 +19,15 @@ members = [
# 壳层(iced 版现在,别的壳将来)与 presenter 都依赖它,所以它不能认任何一个渲染框架。
"crates/i18n",
]
# M118(D26):`crates/app` 是 Slint 退役树 —— UI 已迁到 iced,目录与源码**原样保留**作历史
# 参考(另一份参考资产见 DevDocs/ops/slint-reference/),但不再进默认构建与发布矩阵。摘出去的
# 是整棵 slint 1.18 + usvg 0.48 依赖树:本机单路冷编 ≈14 分钟,而 CI 的 `--workspace` 每次都会
# 编它,既拖长反馈,又会在并行超配时随机 OOM(见 DevDocs/00-踩坑记录-p2.md T7)。
# 排除 ≠ 删除,但**它确实不再可编**:该 manifest 整份都是 `*.workspace = true` 继承写法,
# 一旦不是 workspace 成员,继承无源、manifest 解析即失败(实测:
# `cargo metadata --manifest-path crates/app/Cargo.toml` → "error inheriting `edition` …")。
# 要读这一版实现请直接读源码,别再尝试构建它。
exclude = ["crates/app"]
[workspace.package]
version = "0.1.0"
+43 -24
View File
@@ -15,8 +15,10 @@ attached to other agent tooling as an MCP server.
- **Single-file native binary**: the launcher itself needs no JVM, no WebView and no sidecar
runtime. The JDK a game needs is downloaded and managed per instance, not as a launcher
dependency.
- **Two UI stacks during the rewrite**: the released UI is Slint (Royalty-Free 2.0); the
next-generation UI is iced 0.14 + Material 3 Expressive and is still being written (see Status).
- **One UI stack in the build**: the UI is iced 0.14 + Material 3 Expressive. The earlier Slint
implementation (`crates/app`) is kept as source-only history: it is excluded from the cargo
workspace, so the workspace never builds, tests or ships it. v0.1.0 — the only tagged release so
far — still carries a Slint build (see Status).
- **CPU software-rendering fallback**: `--software-renderer` keeps the interface rendering
normally on machines with no usable GPU or with broken drivers.
- **AI agent + MCP**: chat, setting edits and crash diagnosis inside the launcher; `ull agent
@@ -34,7 +36,14 @@ attached to other agent tooling as an MCP server.
Two tracks. They are not the same product yet, so read the headings before downloading anything.
### Track A — released: v0.1.0, Slint UI (this is what you can download today)
### Track A — historical: v0.1.0, Slint UI (the only tagged release so far)
> **Historical implementation — no longer in the default build or the release matrix.** The Slint
> tree (`crates/app`) is kept in the repository as source-only reference: it is excluded from the
> cargo workspace, so `cargo build` / `cargo test` / `cargo clippy` never touch it and no artifact is
> built from it. Read the source if you want to see how that UI was put together; do not expect to
> build it. The list below is kept because it records what the v0.1.0 assets actually contain —
> since v0.1.0 the pipeline builds the iced binary and ships it under the same public asset names.
- **GitHub Releases `v0.1.0`** (2026-11-01, flagged pre-release): single-file assets for Windows
x64 (a bare `.exe`), Linux x64 and macOS (arm64 and x64) `.tar.gz`. Every release also carries
@@ -67,16 +76,18 @@ Two tracks. They are not the same product yet, so read the headings before downl
with automatic fallback, and the screenshot/diff channel the visual reviews use).
- **Not done**: the component library (buttons, dialogs, lists, menus, search, and the rest of the
M3 set), page migration, and the i18n pass. Every page in the iced binary is a placeholder.
- **Slint stays until the flip**: `crates/app` remains the reference implementation until the iced
build passes equivalence acceptance; only then is `unlucky-launcher-iced` renamed to
`unlucky-launcher` and the Slint crate retired.
- **Slint is retired from the build, kept as reference**: `crates/app` is no longer a workspace member
(M118) — the source stays for historical reference only: not compiled, not tested, not shipped, and
no longer buildable as a workspace member. What is left of the flip is the *rename*: until the iced
build passes equivalence acceptance its binary is still called `unlucky-launcher-iced`, while the
release pipeline already ships it under the public `unlucky-launcher` asset name.
Binaries in the workspace, so you can tell them apart:
Binaries you may run into, so you can tell them apart:
| Binary | Crate | State |
|--------|-------|-------|
| `unlucky-launcher` | `crates/app` (Slint) | released in v0.1.0; still the usable GUI; also runs every CLI subcommand |
| `unlucky-launcher-iced` | `crates/app-iced` (iced 0.14) | under construction; skeleton only, not shipped; same subcommand dispatch |
| `unlucky-launcher-iced` | `crates/app-iced` (iced 0.14) | the only GUI binary the workspace builds; the release pipeline stages it under the public name `unlucky-launcher`; same subcommand dispatch |
| `unlucky-launcher` | `crates/app` (Slint) | **historical**: that crate is excluded from the workspace, so this binary is no longer built or shipped; the v0.1.0 assets are the last Slint builds |
| `ull` | `crates/cli` | the zero-UI build of the same command set, shipped as its own asset |
`unlucky-launcher` and `ull` are not duplicates. The GUI binary needs the graphics stack when it
@@ -122,8 +133,9 @@ Unlucky-Launcher (cargo workspace)
├── crates/agent # AI agent: ToolSpec registry + built-in client + MCP server (one core, two shapes)
├── crates/design # design tokens: real HCT color / type / shape / spacing / elevation / motion / icons
│ # / component metrics — zero UI dependencies, no third-party crates
├── crates/app # Slint UI (bin `unlucky-launcher`): the released GUI, kept until equivalence acceptance
├── crates/app-iced # next-generation iced UI (bin `unlucky-launcher-iced`): under construction
├── crates/app # HISTORICAL Slint UI (bin `unlucky-launcher`): excluded from the workspace,
│ # source-only reference — never built, tested or shipped any more
├── crates/app-iced # the iced UI (bin `unlucky-launcher-iced`): the only GUI the workspace builds
├── crates/cli # CLI (bin `ull`): headless, zero UI
└── packaging # single-file artifact matrix and release scripts
```
@@ -149,7 +161,7 @@ Prerequisites:
`rustfmt` and `clippy` components)
- Windows: MSVC toolchain (what CI uses) or MinGW-w64 (`x86_64-pc-windows-gnu`)
- Linux: `libxkbcommon-dev libfontconfig1-dev libgl1-mesa-dev libegl1-mesa-dev libwayland-dev
libxcb1-dev` — needed by both UI stacks (Slint's winit + femtovg, iced's winit + wgpu/tiny-skia)
libxcb1-dev` — winit + wgpu/tiny-skia ask for these at build time (the Slint build also needed femtovg)
```bash
cargo fmt --all --check # formatting
@@ -158,24 +170,29 @@ cargo test --workspace # tests
cargo build --release # binaries land in target/release/
```
`--workspace` covers the eight members in the module map. `crates/app` is excluded at the workspace
level (M118), so none of these commands compiles the Slint tree — and there is no build command for
it: that crate is kept as source-only reference and can no longer be built as a workspace member.
What the build produces:
- `unlucky-launcher` — the Slint UI (the released one). It is also the CLI: the same binary answers
every subcommand listed above.
- `unlucky-launcher-iced` — the iced UI skeleton. The name is temporary: it can only become
`unlucky-launcher` at the flip, because two crates in one workspace cannot emit the same binary
name into the same target directory. Same subcommand dispatch.
- `unlucky-launcher-iced` — the GUI (iced). It is also the CLI: the same binary answers every
subcommand listed above. The name is temporary — it becomes `unlucky-launcher` at the flip. The
release artifacts already carry the public name: the pipeline builds this bin and
`packaging/make_artifacts.py` stages it as `unlucky-launcher[.exe]`, so download links never change.
- `unlucky-launcher` — **historical**: no workspace build produces this binary any more; it belonged
to `crates/app`, which is excluded. The v0.1.0 assets are the last builds under this name.
- `ull` — the CLI as a standalone, UI-free binary. Same command set, no graphics stack, smaller, and
the right artifact for a machine that has no display at all.
Rendering switches:
- `unlucky-launcher --software-renderer` (equivalent to `SLINT_BACKEND=winit-software`) forces the
CPU path on the Slint build
- `unlucky-launcher-iced --software-renderer` forces tiny-skia (`ICED_BACKEND=tiny-skia`);
`--screenshot <png>` writes one frame and exits, which is the acceptance channel
- Both stacks fall back to the CPU path when no usable adapter is there: the Slint build through its
own renderer fallback chain, the iced build through its explicit probe
`--screenshot <png>` writes one frame and exits, which is the acceptance channel CI's headless
smoke job uses
- With no usable adapter the iced build's explicit probe falls back to the CPU path on its own
- **Historical** (v0.1.0's Slint build still takes it): `unlucky-launcher --software-renderer`, i.e.
`SLINT_BACKEND=winit-software`, forced the CPU renderer of that build through its own fallback chain
Dependency license check (optional locally, required in CI):
`cargo deny check licenses bans sources`.
@@ -214,8 +231,10 @@ Minimum baseline: **Windows 10**; Windows 8.1 / 8 / 7 are not supported.
[NOTICE](NOTICE).
- **Slint** (<https://slint.dev>), Royalty-Free 2.0: obligation for the Slint build that v0.1.0
ships — an `AboutSlint` widget on the about page (still outstanding) and, until it lands, the
NOTICE file, this section and a "Made with Slint" note on the release page. The obligation is
kept for as long as a Slint-linked binary ships.
NOTICE file, this section and a "Made with Slint" note on the release page. The obligation follows
a **Slint-linked binary**: it applies to the v0.1.0 assets (and to any later build that links Slint
again), not to new iced builds — `crates/app` is excluded from the workspace, so the pipeline no
longer produces a Slint-linked binary.
- **iced** (<https://github.com/iced-rs/iced>), MIT: the framework of the next-generation UI,
together with `winit` and, through it, `wgpu` / `tiny-skia` (Apache-2.0 / MIT).
- **Google Material Color Utilities** (Apache-2.0): the color tokens in `crates/design` are a Rust
+37 -20
View File
@@ -13,8 +13,9 @@ Unlucky Launcher(ULL)是一个用 Rust 写的免费开源 Minecraft: Java
- **单文件原生二进制**:启动器本体不需要 JVM、不需要 WebView、不带旁挂运行时。游戏要的 JDK 由
启动器按实例下载管理,不是启动器的依赖。
- **重写期两套 UI 并存**:已发布版本是 Slint(Royalty-Free 2.0);下一代 UI 是 iced 0.14 +
Material 3 Expressive,目前仍在写(见「当前状态」)。
- **默认构建里只有一套 UI**:UI 是 iced 0.14 + Material 3 Expressive。早先的 Slint 实现
(`crates/app`)作为**历史参考**保留源码:已被 workspace `exclude`,默认构建不编、测试不跑、
发布不发。唯一已发布的 tag `v0.1.0` 发的仍是 Slint 版(见「当前状态」)。
- **CPU 软渲染兜底**:`--software-renderer` 让没有可用 GPU 或驱动有问题的老机也能正常显示界面。
- **AI Agent + MCP**:启动器内可对话、改设置、读崩溃诊断;`ull agent serve` 把同一个内核以 MCP
server 形态(stdio 或 Streamable HTTP)开放给外部 Agent 工具。
@@ -29,7 +30,13 @@ Unlucky Launcher(ULL)是一个用 Rust 写的免费开源 Minecraft: Java
两条轨道,还不是同一个东西——看小标题再决定下载什么。
### 轨道 A —— 已发布:v0.1.0,Slint 版(现在能下载的就是它)
### 轨道 A —— 历史:v0.1.0,Slint 版(目前唯一的 tag)
> **历史实现——已不在默认构建与发布矩阵内。** Slint 那棵树(`crates/app`)作为源码参考留在仓库里:
> 已被 cargo workspace `exclude`,所以 `cargo build` / `cargo test` / `cargo clippy` 都不会碰它,
> 也没有任何产物从它构建。想了解那一版 UI 怎么写的就直接读源码——**不要再尝试构建它**(它的 manifest
> 整份是 workspace 继承写法,离开 workspace 后连解析都过不了)。下面这段原文保留,因为它记录的是
> `v0.1.0` 资产里到底装了什么;从 `v0.1.0` 之后,流水线构建的是 iced 版,对外文件名不变。
- **GitHub Releases `v0.1.0`**(2026-11-01,标记为 pre-release):单文件产物覆盖 Windows x64(裸
`.exe`)、Linux x64、macOS(arm64 与 x64)`.tar.gz`。每个 Release 另挂 `SHA256SUMS.txt` 与
@@ -59,15 +66,16 @@ Unlucky Launcher(ULL)是一个用 Rust 写的免费开源 Minecraft: Java
对比通道)。
- **没完成**:组件库(按钮、对话框、列表、菜单、搜索栏,以及 M3 其余组件)、页面搬迁、i18n 文案。
iced 版里现在每一个页面都是占位。
- **Slint 版在翻牌前不退场**:`crates/app` 作为参考实现保留,直到 iced 版通过等价验收;那时才把
`unlucky-launcher-iced` 改名成 `unlucky-launcher`,并让 Slint 版退役。
- **Slint 版已退出构建,只留源码作参考**:`crates/app` 不再是 workspace 成员(M118)——源码留作历史
参考,不编译、不测试、不发布,也不再有可用的构建方式。翻牌只剩**改名**这一步:在 iced 版通过等价
验收前,它的 bin 还叫 `unlucky-launcher-iced`;发布流水线已经把它按对外名 `unlucky-launcher` 打出去。
工作区里的三个 bin,先认清楚:
可能会遇到的 bin,先认清楚:
| 二进制 | crate | 状态 |
|--------|-------|------|
| `unlucky-launcher` | `crates/app`(Slint) | v0.1.0 已发布,仍是当前能用的图形界面;同时支持全部 CLI 子命令 |
| `unlucky-launcher-iced` | `crates/app-iced`(iced 0.14) | 施工中,仅骨架,不随发布包分发;子命令分发同款 |
| `unlucky-launcher-iced` | `crates/app-iced`(iced 0.14) | workspace 里**唯一**的 GUI bin;发布流水线把它按对外名 `unlucky-launcher` 落盘;子命令分发同款 |
| `unlucky-launcher` | `crates/app`(Slint) | **历史**:该 crate 已被 workspace `exclude`,这个 bin 不再被构建或分发;`v0.1.0` 的资产是最后一批 Slint 版 |
| `ull` | `crates/cli` | 同一套命令的零 UI 构建,作为独立资产随发布分发 |
`unlucky-launcher` 与 `ull` 不是重复品:开窗时 GUI 二进制要用图形栈(X11 / Wayland / GL 走
@@ -109,8 +117,9 @@ Unlucky-Launcher (cargo workspace)
├── crates/agent # AI Agent:ToolSpec 注册表 + 内置客户端 + MCP server(双形态共用一个内核)
├── crates/design # 设计令牌:真 HCT 色彩 / 排版 / 形状 / 间距 / 高程 / 动效 / 图标 / 组件度量
│ # —— 零 UI 依赖、零第三方 crate
├── crates/app # Slint UI(bin `unlucky-launcher`):已发布的图形界面,保留到等价验收通过
├── crates/app-iced # 下一代 iced UI(bin `unlucky-launcher-iced`):施工中
├── crates/app # 历史 Slint UI(bin `unlucky-launcher`):已被 workspace `exclude`,
│ # 仅作源码参考——不再被构建 / 测试 / 发布
├── crates/app-iced # iced UI(bin `unlucky-launcher-iced`):workspace 里唯一的 GUI bin
├── crates/cli # CLI(bin `ull`):headless,零 UI
└── packaging # 单文件产物矩阵与发布脚本
```
@@ -135,28 +144,34 @@ Unlucky-Launcher (cargo workspace)
- Rust stable(`rust-toolchain.toml` 会自动选工具链;门禁还需要 `rustfmt` / `clippy` 组件)
- Windows:MSVC 工具链(CI 用这套)或 MinGW-w64(`x86_64-pc-windows-gnu`)
- Linux:`libxkbcommon-dev libfontconfig1-dev libgl1-mesa-dev libegl1-mesa-dev libwayland-dev
libxcb1-dev`——两套 UI 都要用(Slint 的 winit + femtovg,iced 的 winit + wgpu/tiny-skia)
libxcb1-dev`——winit + wgpu/tiny-skia 编译期要用(Slint 版当年还要 femtovg)
```bash
cargo fmt --all --check # 格式
cargo clippy --workspace --all-targets -- -D warnings # 静态检查(零 warning 门禁)
cargo test --workspace # 测试
cargo build --release # 三个 bin 都落在 target/release/
cargo build --release # bin 落在 target/release/
```
编出来的三个 bin:
`--workspace` 就是模块地图里那八个成员:`crates/app` 已在 workspace 层被排除(M118),上面这些命令
都不会编 Slint 那棵树——而且**它没有构建方式**:该 crate 只作源码参考,作为 workspace 成员已不可编。
- `unlucky-launcher`——Slint 版(已发布的那个)。它同时也是 CLI:同一个二进制认上面列的全部子命令。
- `unlucky-launcher-iced`——iced 版骨架。名字是临时的:只有翻牌时才能改成 `unlucky-launcher`,
因为同一个工作区里两个 crate 不能往同一个 target 目录里产出同名 bin。子命令分发同款。
编出来的 bin:
- `unlucky-launcher-iced`——GUI(iced)。它同时也是 CLI:同一个二进制认上面列的全部子命令。名字是
临时的,翻牌时才改成 `unlucky-launcher`;发布产物**已经**用对外名:流水线编这个 bin,
`packaging/make_artifacts.py` 落盘时改成 `unlucky-launcher[.exe]`,所以下载链接不用变。
- `unlucky-launcher`——**历史**:workspace 构建已不产出它(它属于被排除的 `crates/app`)。
`v0.1.0` 的资产是最后一批用这个名字发出去的构建。
- `ull`——CLI 的独立无 UI 构建:同一套命令、不链接图形栈、体积更小,给完全没有显示设备的机器用。
渲染开关:
- `unlucky-launcher --software-renderer`(等价于 `SLINT_BACKEND=winit-software`)强制 Slint 版走 CPU
- `unlucky-launcher-iced --software-renderer` 强制 tiny-skia(`ICED_BACKEND=tiny-skia`);
`--screenshot <png>` 出一帧图后退出,这是验收通道
- 两套栈在没有可用适配器时都会退到 CPU:Slint 版走它自己的渲染器回退链,iced 版走显式探测
`--screenshot <png>` 出一帧图后退出,这是 CI 无头冒烟 job 用的验收通道
- 没有可用适配器时,iced 版靠显式后端探测自己退到 CPU
- **历史**(`v0.1.0` 的 Slint 版仍认这条):`unlucky-launcher --software-renderer`,即
`SLINT_BACKEND=winit-software`,让那一版走它自己的渲染器回退链
依赖许可检查(本地可选,CI 必跑):`cargo deny check licenses bans sources`。
@@ -188,7 +203,9 @@ cargo build --release # 三个 bin 都落在 ta
- **本项目**:Apache License 2.0,见 [LICENSE](LICENSE);第三方声明见 [NOTICE](NOTICE)。
- **Slint**(<https://slint.dev>),Royalty-Free 2.0:v0.1.0 发布的正是 Slint 版,这项义务继续有效
——关于页放 `AboutSlint` 部件(尚未落地),在此之前由 NOTICE、本节与发布页的「Made with Slint」
说明承担告知义务。只要还有链接 Slint 的产物在发,这条义务就保留。
说明承担告知义务。义务跟着**链接了 Slint 的产物**走:已发布的 `v0.1.0` 资产(以及将来任何重新链接
Slint 的构建)适用;`crates/app` 已被 workspace `exclude`,流水线不再产出链接 Slint 的二进制,
所以新的 iced 版产物不适用这一条。
- **iced**(<https://github.com/iced-rs/iced>),MIT:下一代 UI 的框架;与它同栈的 `winit`、以及经它
引入的 `wgpu` / `tiny-skia`(Apache-2.0 / MIT)。
- **Google Material Color Utilities**(Apache-2.0):`crates/design` 的色彩令牌是它 HCT /
+11 -11
View File
@@ -25,8 +25,8 @@
- 归档解压后是**一个顶层目录** `unlucky-launcher-<ver>-<槽位>/`,用户不会解压出一堆散文件。
- **`ull` 是并列的裸资产,不塞进 tar.gz**:headless 用户要的就是「下一个小文件直接跑」;归档里放
LICENSE/NOTICE/README.md 是许可义务,而这三份本来就作为并列 Release 资产分发,裸 `ull` 不缺条款。
- `unlucky-launcher` 本体也认全部 CLI 子命令(M82),`ull` 不是第二个界面,而是**不链接图形栈**的
同一套命令(差异见 §4 与 §7.4)——两者都不是重复品。
- `unlucky-launcher` 本体也认全部 CLI 子命令(M82),`ull` 不是第二个界面,而是**不链接图形栈**的同一套命令(差异见 §4 与 §7.4),两者不是重复品。
M118:GUI 的**构建产物**名是 `unlucky-launcher-iced`(`crates/app` 已被 workspace `exclude`,不再产出 `unlucky-launcher`);对外文件名仍是 `unlucky-launcher`,改名在 `make_artifacts.py` 落盘时(`LAUNCHER_BIN` 找产物 / `LAUNCHER_ASSET` 定对外名),所以下载链接 / `SHA256SUMS` / 本文矩阵不变。
- `<ver>` 不带前导 `v`(tag 是 `v0.1.0`,文件名是 `0.1.0`)——由脚本统一裁剪。
- UNIX 归档内 `mtime`/`uid`/`gid` 归零、权限固定(二进制 0755、文本 0644),
同源构建的 sha256 不受打包时刻影响。
@@ -51,7 +51,7 @@
### 本地(当前平台)
```bash
cargo build --release --locked # 产出 target/release/{unlucky-launcher,ull,unlucky-launcher-iced}
cargo build --release --locked # 产出 target/release/{unlucky-launcher-iced,ull}(M118:Slint 版不再进 workspace)
cargo build --profile fast --bins # 只是想快点看到画面/跑测试:快档(§6),产物在 target/fast/
python packaging/make_artifacts.py --out dist # 产物落在 dist/,含 SHA256SUMS.txt
python packaging/make_artifacts.py --version v0.1.0 # 顺带校验版本一致性
@@ -67,7 +67,7 @@ macOS x64),其它组合直接报错退出——不在矩阵里的平台不
```bash
rustup target add aarch64-pc-windows-msvc
cargo build --release --locked --target aarch64-pc-windows-msvc \
--bin unlucky-launcher --bin ull # 不用 --bins:发布矩阵只发 GUI + ull,iced 不进产物
--bin unlucky-launcher-iced --bin ull # 不用 --bins:矩阵只发 GUI + ull;GUI 的构建产物名是 iced 的 bin(M118)
python packaging/make_artifacts.py --slot windows-arm64 \
--build-dir target/aarch64-pc-windows-msvc/release --out dist
```
@@ -181,7 +181,7 @@ macOS 产物是**裸二进制**而非 `.app` bundle:符合单文件承诺,
## 5. 软渲染兜底(无 GPU / 老驱动 / 远程桌面)
Slint 默认 `renderer-femtovg`(GL)。两条兜底路径,效果等价,任选其一:
M118(D26):以下是**历史实现(Slint 版,默认 `renderer-femtovg`/GL)**的兜底路径,已不在默认构建与发布矩阵内(`crates/app` 被 workspace `exclude`,源码仅作参考、不编译)。现役 GUI 是 iced 版:`unlucky-launcher-iced --software-renderer`(= `ICED_BACKEND=tiny-skia`)走 tiny-skia 光栅化。v0.1.0 的产物仍认下面两条:
```bash
unlucky-launcher --software-renderer # 启动参数(本仓库解析后设 SLINT_BACKEND)
@@ -190,9 +190,8 @@ SLINT_BACKEND=winit-software unlucky-launcher # 环境变量(Slint
- 参数解析在 `crates/app/src/main.rs` 的 `apply_renderer_flag()`,只认完全相等的 `--software-renderer`。
- 软渲染是 CPU 光栅化,无 GPU 也能出图;代价是动画与大窗口重绘更慢(Metro 纯色界面影响有限)。
- 验收/无交互桌面场景另有进程内截图通道:`--screenshot <png>`(见 `crates/app/src/screenshot.rs`),
它同样能在软渲染下工作,是 CI 与远程机唯一可靠的"真出图"证据。
- 发布冒烟(本机实跑,见 §7)就是用 `--software-renderer --screenshot` 组合证明"无 GPU 也能起来"。
- 截图通道 `--screenshot <png>`:历史实现 `crates/app/src/screenshot.rs`,现役 `crates/app-iced/src/shot/`;
软渲染下照样出图,是 CI 与远程机唯一可靠的"真出图"证据(发布冒烟见 §7,本机实跑 `--software-renderer --screenshot`)。
## 6. Cargo 构建档位(release 体积档 / fast 速度档)
@@ -237,14 +236,14 @@ SLINT_BACKEND=winit-software unlucky-launcher # 环境变量(Slint
① **依赖 opt-level 从 3 退回 `"z"` 又快 29.6% 又小 19~24%**(两臂同为 cu16,唯一自变量是 opt-level)——抬 opt-level 是拿编译时间和体积换运行期;
`cu1 → cu16` 的收益本轮**没有同条件对照**,未归因。② `fast` **冷编译比 `release` 还慢**(24m27s vs 16m01s)、体积 2.3 倍,瓶颈在两个大 UI
crate + 链接 + 机械盘 IO——定位是 **SSD 机器与增量重编**,不是冷编译加速。体积归因:GUI 相对旧基线 +12.9%(13.92 → 15.72 MiB)里
「代码增长」与「依赖 cu16」**未分离**;`ull` 只 +1.7%,在 +1~3% 口径内。体积构成:Slint 运行时(双渲染器 + winit/accesskit/托盘)+ rustls/reqwest + rig/rmcp/axum 为主,`ull` 更小是 D18(CLI 零 UI 依赖)的结果。
「代码增长」与「依赖 cu16」**未分离**;`ull` 只 +1.7%,在 +1~3% 口径内。体积构成(**M118 前的 Slint 版,历史数**):Slint 运行时(双渲染器 + winit/accesskit/托盘)+ rustls/reqwest + rig/rmcp/axum 为主,`ull` 更小是 D18(CLI 零 UI 依赖)的结果。
### 7.2 冒烟(真跑,2026-10-31 夜间 + 11-01 早,本机 Win11 26100)
| 命令 | 实测结果 |
|------|---------|
| `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 --software-renderer --screenshot … --data-dir …`(Slint 版,历史实测) | exit 0,产出 1180×760 PNG(3,588,298 B);看图确认 Metro 实例页(侧栏/搜索/空态/AI 助手面板)正常渲染 → **软渲染路径真的能出图**,不依赖 GL |
| `unlucky-launcher.exe --version` | **无输出**,8 秒后被 timeout 杀掉(exit 124)——GUI 当时不解析 `--version`,会直接开窗口。M82 后已修(走 CLI 分发,见 §10-1)|
### 7.3 打包验证(本机 Windows 槽位真跑)
@@ -280,7 +279,7 @@ CI 的 MSVC `+crt-static` 产物同理(静态 CRT);`release.yml` 里用 `d
总之不允许"悄悄要用户装可再发行包"。
注意:本机只装了 GNU 工具链,MSVC 那个槽位的产物本身**没有在本机验证过**,由 CI 首跑验证。
Linux 侧同一件事的对照(M82 实测:`readelf -d` + `ldd` + `nm -D`,容器内 Debian 12 glibc 2.36):Slint
Linux 侧同一件事的对照(M82 实测:`readelf -d` + `ldd` + `nm -D`,容器内 Debian 12 glibc 2.36;被测的是 **Slint 版 GUI,历史实测**):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` /
@@ -318,6 +317,7 @@ merge 后由 tower 重指 tag 重跑流水线,两槽位进 `SHA256SUMS.txt`
三份文本在 UNIX 归档内、在 Windows 侧作为并列 Release 资产。
- Slint 的 RF 2.0 义务:关于页保留 `AboutSlint` 部件(已在 UI 内),不得移除;
选 RF 分支即不触发 GPL 分支义务(见 `deny.toml` 的许可证白名单注释)。
- **M118 后这条义务只跟着已发布的 `v0.1.0` 资产走**:`crates/app` 已被 workspace `exclude`,发布矩阵里再没有链接 Slint 的二进制,新的 iced 版产物不链接 Slint;`deny.toml` 那条 RF-2.0 allow 因此暂时是死条目(不删,留给将来真有别的路径引入 Slint 时兜底)。
- `README.md`(英文主版)随包分发;中文版 `README.zh-CN.md` 留在仓库不进包(避免包体噪音)。
- 依赖许可门禁不变:`deny.toml` 白名单(Apache-2.0 / MIT / BSD / Zlib / BSL-1.0 / ISC / CC0 / Unlicense /
Unicode-3.0 / CDLA-Permissive-2.0 / Slint-RF-2.0)。发布任务不新增任何依赖。
+9 -3
View File
@@ -54,7 +54,13 @@ import tarfile
from pathlib import Path
from typing import NoReturn
LAUNCHER_BIN = "unlucky-launcher"
# M118(D26):GUI 的**构建产物**名是 iced 版的 `unlucky-launcher-iced`。旧值 `unlucky-launcher`
# 是 `crates/app`(Slint 退役树)的 bin —— 那棵树已被根 Cargo.toml `exclude`,再也不产出它了。
# **对外**文件名仍叫 `unlucky-launcher`:下载链接 / SHA256SUMS / README 都绑着这个名字,
# 改名是「翻牌」那一步的事,不是打包这一步的事。所以刻意拆成两个常量:
# 找文件用 LAUNCHER_BIN,落盘(tar 内文件名 / exe 资产名)用 LAUNCHER_ASSET。
LAUNCHER_BIN = "unlucky-launcher-iced"
LAUNCHER_ASSET = "unlucky-launcher"
CLI_BIN = "ull"
# 随包分发的合规文本(仓库根目录);英文主版 README 是对外分发口径。
COMPLIANCE_DOCS = ("LICENSE", "NOTICE", "README.md")
@@ -216,7 +222,7 @@ def stage_archive(out_dir: Path, tag: str, version: str, launcher: Path) -> Path
if stage.exists():
shutil.rmtree(stage)
stage.mkdir(parents=True)
shutil.copy2(launcher, stage / launcher.name)
shutil.copy2(launcher, stage / LAUNCHER_ASSET)
for doc in COMPLIANCE_DOCS:
source = REPO_ROOT / doc
if not source.is_file():
@@ -232,7 +238,7 @@ def stage_archive(out_dir: Path, tag: str, version: str, launcher: Path) -> Path
info.mtime = 0
info.uid = info.gid = 0
info.uname = info.gname = ""
info.mode = 0o755 if entry.name == launcher.name else 0o644
info.mode = 0o755 if entry.name == LAUNCHER_ASSET else 0o644
with entry.open("rb") as handle:
tar.addfile(info, handle)
shutil.rmtree(stage)