feat(release,docs): M149 发布构建烘入微软登录 id(仓库 Secret),文档改为「官方构建开箱即用」

构建侧:release-build.yml 声明 on.workflow_call.secrets.ULL_MSA_CLIENT_ID(required: false),
cargo build 步以 env 注入它,option_env! 把它烘进二进制;release.yml 的 build job 按名显式透传
(与 PUBLISH_TOKEN 同一模式,不用 inherit)。真值放仓库 Secret,不进任何 git 文件;Secret 未录入
或 Actions 之外求值为空串时,client_id.rs 的 blank_env_value_counts_as_missing 保证「空白=没配」,
不会拿空 id 去联网。

文档:README 两版 / help.rs 的 accounts topic / packaging/release-notes.md /
packaging/docs/build-and-ci.md 统一成「官方构建已内置登录所需配置、开箱即用,仍可用环境变量
ULL_MSA_CLIENT_ID 覆盖;自建想自带就构建时注入」的功能性口径 —— 不出现 id 本身,也不出现归属描述。
client_id.rs 的模块注释与测试注释同步(原文写「id 不进仓库(用户定案)」,已随 Secret 方案改写),
并记下「别把 ULL_MSA_CLIENT_ID 导出到跑 cargo test 的 shell」——presenter 的 M132 入口锚会在
已烘焙 id 的构建里故意红。

实测:ULL_MSA_CLIENT_ID=<id> cargo build --bin ull 后,清空 env 跑 ull accounts add msa 打出
用户码与链接(无「未配置」提示);同二进制设一个假 env id 则 AADSTS700016 点名该假 id(env 覆盖生效)。
单测:core auth 28、unlucky-cli 全套、i18n 30、presenter 222 全绿;M132 锚在烘焙构建里按设计红、
在不带 id 的构建里绿。cargo fmt --all --check 通过。
This commit is contained in:
AskaEth committed 2026-11-06 21:08:53 +08:00
1 parent 1c53c29f76
commit 5597a4cc18
8 files changed
+76 -32

No files matched your search

+14
View File
@@ -29,6 +29,15 @@ on:
type: string type: string
required: false required: false
default: "" default: ""
# (M149)微软登录用的 Azure 公共客户端 id(公共客户端 id 随二进制一起分发,不按高密级处理,但**不进 git
# 文件**:真值放仓库 Secret,用户在仓库设置里录入)。`required: false` 是刻意的——调用方没传(本地、
# 没有 runner 的镜像站、以及 Secret 尚未录入时)求值为空串,`client_id.rs` 把空白当「没配」,
# 于是非官方构建不烘 id,官方构建才烘。**调用方必须显式传**(`secrets: inherit` 或按名传);
# 没传时这里是空,不会报错。
secrets:
ULL_MSA_CLIENT_ID:
description: "Azure 公共客户端 id,烘进发布二进制(构建期 option_env!);留空 = 不烘"
required: false
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -119,10 +128,15 @@ jobs:
# M118(D26):**Slint 版停发** —— GUI 只有 `unlucky-launcher-iced`(`crates/app` 已 exclude,不进 workspace)。 # M118(D26):**Slint 版停发** —— GUI 只有 `unlucky-launcher-iced`(`crates/app` 已 exclude,不进 workspace)。
# 产物名由 make_artifacts.py 的 `asset_stem()` 生成 `<名字>_<版本号>_<平台>_<架构>`;不传 `--target`(M140 起全原生)。 # 产物名由 make_artifacts.py 的 `asset_stem()` 生成 `<名字>_<版本号>_<平台>_<架构>`;不传 `--target`(M140 起全原生)。
# (M149)`ULL_MSA_CLIENT_ID` 经 `option_env!` 烘进二进制(`crates/core/src/auth/microsoft/client_id.rs`):
# 官方产物自带微软登录所需的配置,装完即可登录;运行期同名环境变量仍优先于它,可覆盖。
# 值来自仓库 Secret(不进 git 文件);Secret 没配时这里是空串,client_id.rs 把空白当「没配」,
# 于是非官方构建不烘 id。想自己烘一份就从源码构建:`ULL_MSA_CLIENT_ID=<id> cargo build`。
- name: cargo build --release --locked - name: cargo build --release --locked
shell: bash shell: bash
env: env:
RUSTFLAGS: ${{ inputs.rustflags }} RUSTFLAGS: ${{ inputs.rustflags }}
ULL_MSA_CLIENT_ID: ${{ secrets.ULL_MSA_CLIENT_ID }}
run: | run: |
set -euo pipefail set -euo pipefail
cargo build --release --locked --bin unlucky-launcher-iced --bin ull cargo build --release --locked --bin unlucky-launcher-iced --bin ull
+6
View File
@@ -103,6 +103,12 @@ jobs:
os: ${{ matrix.os }} os: ${{ matrix.os }}
build_dir: ${{ matrix.build_dir }} build_dir: ${{ matrix.build_dir }}
rustflags: ${{ matrix.rustflags }} rustflags: ${{ matrix.rustflags }}
secrets:
# (M149)微软登录的公共客户端 id:值来自仓库 Secret,被调方把它烘进发布二进制(`option_env!` 注入)。
# 可复用工作流拿不到调用方未显式传入的 secret,故必须按名传(与下面 PUBLISH_TOKEN 同一模式,
# 不用 `inherit`:那会把仓库全部 secret 一起透进被调方)。Secret 未录入时求值为空串,被调方把空白
# 当「没配」——产物只是不带 id,不会拿空 id 去联网。
ULL_MSA_CLIENT_ID: ${{ secrets.ULL_MSA_CLIENT_ID }}
release: release:
name: GitHub Release name: GitHub Release
+15 -11
View File
@@ -83,16 +83,17 @@ Three tracks on one page:
- **Third-party community auth servers** (authlib-injector style). - **Third-party community auth servers** (authlib-injector style).
- **Local offline profiles**, for isolated testing. - **Local offline profiles**, for isolated testing.
**Microsoft sign-in needs an Azure public client id, and no release binary carries one.** The id is **Microsoft sign-in works out of the box in official builds.** Release binaries carry the Azure public
not in this repo and CI injects none, so set `ULL_MSA_CLIENT_ID=<id>` before launching, or build with client id they need (a public client id, not a credential — it ships inside the binary), so there is
it (`ULL_MSA_CLIENT_ID=<id> cargo build`, which bakes it in via `option_env!`). Without one, sign-in nothing to configure before signing in. Setting `ULL_MSA_CLIENT_ID=<id>` still overrides it: the
stops before any network call and the alert says exactly what is missing: the title "Microsoft variable is read at startup, so restart the launcher after changing it. Building from source bakes in
sign-in is not configured", a body naming `ULL_MSA_CLIENT_ID=<id>` (set it, or build with it, then whatever that variable holds at build time (`ULL_MSA_CLIENT_ID=<id> cargo build`). A build with no id
restart the launcher — a running process does not pick up a new variable), and anywhere stops before any network call, and its alert says exactly what is missing: the title
`Code: msa_not_configured`. It offers no Retry button, because retrying in that same process cannot "Microsoft sign-in is not configured", a body naming `ULL_MSA_CLIENT_ID=<id>` (set it, or build with
help. Every other sign-in failure keeps the older shape: one category line plus its own `Code:` it, then restart), and `Code: msa_not_configured`. It offers no Retry button, because retrying in that
line, with the full reason only in the launcher log. Community auth servers and local profiles need same process cannot help. Every other sign-in failure keeps the older shape: one category line plus
no id. its own `Code:` line, with the full reason only in the launcher log. Community auth servers and local
profiles need no id.
### Look and language ### Look and language
@@ -243,7 +244,7 @@ upstream cargo. On Linux, X11 is the preferred path today and Wayland is the lon
Capability differences are feature gates with runtime probing, never a compile-time "this platform Capability differences are feature gates with runtime probing, never a compile-time "this platform
gets less". gets less".
First run with a Microsoft account? Set `ULL_MSA_CLIENT_ID` first — see First run with a Microsoft account? Nothing to configure — official builds carry the client id; see
[Accounts](#accounts). Nothing else needs configuring. [Accounts](#accounts). Nothing else needs configuring.
## Build from source ## Build from source
@@ -266,6 +267,9 @@ cargo deny check licenses bans sources # dependency licences (re
What the build produces: What the build produces:
- A build from source carries no Microsoft client id by default. Set `ULL_MSA_CLIENT_ID` at build time
to bake one in (`ULL_MSA_CLIENT_ID=<id> cargo build`); official release binaries already carry one
— see [Accounts](#accounts).
- `unlucky-launcher-iced` — the GUI, and the CLI in the same file: it answers every subcommand listed - `unlucky-launcher-iced` — the GUI, and the CLI in the same file: it answers every subcommand listed
above. `iced` is cargo's bin name, not a download name — `packaging/make_artifacts.py` stages that above. `iced` is cargo's bin name, not a download name — `packaging/make_artifacts.py` stages that
same build as `unlucky-launcher_<version>_<platform>_<arch>` and names the binary same build as `unlucky-launcher_<version>_<platform>_<arch>` and names the binary
+10 -7
View File
@@ -68,12 +68,13 @@ Unlucky Launcher 是一个用 Rust 写的免费开源 **Minecraft: Java 版**启
- **第三方社区认证服**(authlib-injector 一类)。 - **第三方社区认证服**(authlib-injector 一类)。
- **本地离线档案**,用于隔离测试。 - **本地离线档案**,用于隔离测试。
**微软登录需要一个 Azure 公共客户端 id,而发布版二进制里没有。** 这个 id 不在仓库里,CI 也不注入,所以 **微软登录在官方构建里开箱即用。** 发布版二进制已内置它需要的 Azure 公共客户端 id(公共客户端 id 不是
启动前设 `ULL_MSA_CLIENT_ID=<id>`(或构建时注入:`ULL_MSA_CLIENT_ID=<id> cargo build`,它通过 凭据,本来就随二进制一起分发),所以登录前不需要任何配置。设 `ULL_MSA_CLIENT_ID=<id>` 仍可覆盖内置值:
`option_env!` 烧进二进制)。没有它时点登录**不会联网**,弹窗直接说清是这一项没配:标题「微软登录未配置」, 变量在启动时读取,改完要**重启启动器**。从源码构建时该变量有什么就烘什么(`ULL_MSA_CLIENT_ID=<id> cargo build`,
正文点名 `ULL_MSA_CLIENT_ID=<id>`(设好或构建时注入后要**重启启动器**——已在运行的进程读不到新设的环境 经 `option_env!` 烧进二进制)。两处都没有 id 的构建点登录**不会联网**,弹窗直接说清是这一项没配:标题
变量),并带 `代码:msa_not_configured`;它**不给重试按钮**,因为在同一个进程里重试必然是同一个错。其它 「微软登录未配置」,正文点名 `ULL_MSA_CLIENT_ID=<id>`(设好或构建时注入后重启),并带
登录失败仍是老样子:一句分类文案 + `代码:<码>`,完整原因只在启动器日志里。第三方社区服与本地档案不需要 id。 `代码:msa_not_configured`;它**不给重试按钮**,因为在同一个进程里重试必然是同一个错。其它登录失败仍是
老样子:一句分类文案 + `代码:<码>`,完整原因只在启动器日志里。第三方社区服与本地档案不需要 id。
### 外观与语言 ### 外观与语言
@@ -197,7 +198,7 @@ Linux x64 / arm64(基线判定在 `crates/core/src/platform/os_info.rs`)。W
(MSVC / GNU)构建,构建方式与上游 cargo 一致。Linux 上 X11 是当前首选路径,Wayland 是大方向。能力差异 (MSVC / GNU)构建,构建方式与上游 cargo 一致。Linux 上 X11 是当前首选路径,Wayland 是大方向。能力差异
一律用 feature gate + 运行期探测表达,绝不做成「这个平台天生少一块」的编译期裁剪。 一律用 feature gate + 运行期探测表达,绝不做成「这个平台天生少一块」的编译期裁剪。
第一次用微软账户?先设 `ULL_MSA_CLIENT_ID`——见[账户](#账户)。其它都不需要配置。 第一次用微软账户?不用配置——官方构建已内置客户端 id,见[账户](#账户)。其它都不需要配置。
## 从源码构建 ## 从源码构建
@@ -218,6 +219,8 @@ cargo deny check licenses bans sources # 依赖许可(CI 必
编出来的 bin: 编出来的 bin:
- 从源码构建默认不带微软客户端 id:想让产物自带,就在构建时设 `ULL_MSA_CLIENT_ID`(`ULL_MSA_CLIENT_ID=<id>
cargo build`);官方发布产物已内置,见[账户](#账户)。
- `unlucky-launcher-iced`——GUI,同时也是 CLI:同一个文件认上面列的全部子命令。名字里的 `iced` 是 cargo - `unlucky-launcher-iced`——GUI,同时也是 CLI:同一个文件认上面列的全部子命令。名字里的 `iced` 是 cargo
的 bin 名,不是下载名——`packaging/make_artifacts.py` 把同一次构建落成 的 bin 名,不是下载名——`packaging/make_artifacts.py` 把同一次构建落成
`unlucky-launcher_<版本>_<平台>_<架构>`,归档里的二进制名写成 `unlucky-launcher`。 `unlucky-launcher_<版本>_<平台>_<架构>`,归档里的二进制名写成 `unlucky-launcher`。
+4 -4
View File
@@ -133,10 +133,10 @@ accessToken 一律脱敏(输出里出现的是 <redacted:token>)。",
**分文件**存放,先写凭据后写元数据、原子写、凭据文件 POSIX 侧 0600。 **分文件**存放,先写凭据后写元数据、原子写、凭据文件 POSIX 侧 0600。
凭据永不打印:list 只输出名称/轨道/UUID。 凭据永不打印:list 只输出名称/轨道/UUID。
add msa 走微软 device-code(用户码打在 stderr,去 https://www.microsoft.com/link 输码)。 add msa 走微软 device-code(用户码打在 stderr,去 https://www.microsoft.com/link 输码)。
它需要一个 Azure 公共客户端 id —— 登录链路是完整的,缺的只是这一项配置;id 不进仓库: 微软登录所需配置已内置在官方构建里,装完即可用;仍可用环境变量覆盖(优先于内置值):
ULL_MSA_CLIENT_ID=<id> %prog% accounts add msa 每次运行时给(环境变量优先) ULL_MSA_CLIENT_ID=<id> %prog% accounts add msa 每次运行时给(优先于内置值)
ULL_MSA_CLIENT_ID=<id> cargo build 构建时注入,二进制自带 ULL_MSA_CLIENT_ID=<id> cargo build 自己从源码构建时注入,二进制自带
两处都没有时命令立刻报「微软登录未配置」(含怎么配),**不会去联网**。 两处都没有时(只有自建且不自带 id 才会这样)命令立刻报「微软登录未配置」(含怎么配),**不会去联网**。
authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CLI 不开这条通道)。", authlib-injector 账户的密码登录留给 GUI(密码进 argv 会泄露,CLI 不开这条通道)。",
), ),
"sessions" => Some("%prog% sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"), "sessions" => Some("%prog% sessions list [--limit N] 列出会话索引(按开始时间倒序,缺省 20 条)"),
+10 -3
View File
@@ -2,8 +2,9 @@
//! //!
//! ## 为什么单独一块(M117) //! ## 为什么单独一块(M117)
//! //!
//! 这个 id 不进仓库(用户定案),所以它可以来自两处:运行期环境变量 [`CLIENT_ID_ENV`], //! 这是**公共客户端 id**(非机密,随二进制一起分发),可以来自两处:运行期环境变量 [`CLIENT_ID_ENV`],
//! 或构建期 `option_env!` 注入([`CLIENT_ID_BUILD`])。**两处都没有**时登录是「没配置」, //! 或构建期 `option_env!` 注入([`CLIENT_ID_BUILD`])——官方发布构建在构建时烘入它(M149),
//! 所以官方产物开箱即用;环境变量仍优先,可覆盖。**两处都没有**时登录是「没配置」,
//! 不是「没实现」——这两件事在用户眼里完全不同,文案与日志都必须分清: //! 不是「没实现」——这两件事在用户眼里完全不同,文案与日志都必须分清:
//! //!
//! | 现象 | 该说什么 | //! | 现象 | 该说什么 |
@@ -20,6 +21,9 @@
pub const CLIENT_ID_ENV: &str = "ULL_MSA_CLIENT_ID"; pub const CLIENT_ID_ENV: &str = "ULL_MSA_CLIENT_ID";
/// 构建期可注入的 client id(`ULL_MSA_CLIENT_ID=<id> cargo build`)。 /// 构建期可注入的 client id(`ULL_MSA_CLIENT_ID=<id> cargo build`)。
///
/// 官方发布构建就是这么烘的(`.github/workflows/release-build.yml` 给构建步设了同名环境变量,M149;
/// 值来自仓库 Secret,不进 git 文件),所以官方产物走到这里恒为 `Some`;从源码自建时才可能为空。
pub const CLIENT_ID_BUILD: Option<&str> = option_env!("ULL_MSA_CLIENT_ID"); pub const CLIENT_ID_BUILD: Option<&str> = option_env!("ULL_MSA_CLIENT_ID");
/// 两处都没配时给用户看的那句话(CLI / GUI / 启动日志共用,**不含任何真实 id**)。 /// 两处都没配时给用户看的那句话(CLI / GUI / 启动日志共用,**不含任何真实 id**)。
@@ -180,7 +184,10 @@ mod tests {
assert_eq!(message, MISSING_CLIENT_ID_MESSAGE); assert_eq!(message, MISSING_CLIENT_ID_MESSAGE);
assert!(!message.contains("未支持")); assert!(!message.contains("未支持"));
} }
// 构建期注入过 id 时唯一合法(本仓库 CI/本地都不注入,走不到这里)。 // 构建期注入过 id 时唯一合法(`ULL_MSA_CLIENT_ID` 也设进 `cargo test` 的环境就会走到这里;
// 官方发布构建即烘了 id,但门禁里的 `cargo test` 不带该变量,故常态走上面那条。
// 注意:把该变量导出到跑测试的 shell 会让 presenter 的 M132 入口锚**故意红**
// (它要求「两处都没有 id」的构建,见 crates/presenter/src/host/accounts/tests.rs)。
Ok(_) => assert!( Ok(_) => assert!(
CLIENT_ID_BUILD.is_some_and(|value| !value.trim().is_empty()), CLIENT_ID_BUILD.is_some_and(|value| !value.trim().is_empty()),
"没配 id 却构造成功" "没配 id 却构造成功"
+8
View File
@@ -75,6 +75,14 @@ python packaging/make_artifacts.py --slot windows-arm64 \
逐字搬进 `.github/workflows/release-publish.yml`(同 `workflow_call`,`secrets.GITHUB_TOKEN` 改为调用方显式透传 逐字搬进 `.github/workflows/release-publish.yml`(同 `workflow_call`,`secrets.GITHUB_TOKEN` 改为调用方显式透传
的 secret)。触发(`on`)、`env`、顶层 `permissions`、`concurrency` 与 `plan` 的槽位表**一字未动**;改槽位仍只改 的 secret)。触发(`on`)、`env`、顶层 `permissions`、`concurrency` 与 `plan` 的槽位表**一字未动**;改槽位仍只改
`release.yml` 那一处(`PUBLISH_SLOTS` 白名单在 `release-publish.yml`,两个 arm64 修复时要改的仍是那两行)。 `release.yml` 那一处(`PUBLISH_SLOTS` 白名单在 `release-publish.yml`,两个 arm64 修复时要改的仍是那两行)。
- **M149 微软登录 id 的烘焙**:`release-build.yml` 的 `cargo build` 步带 `env.ULL_MSA_CLIENT_ID`(值
`${{ secrets.ULL_MSA_CLIENT_ID }}`),`option_env!` 把它烘进二进制,于是发布产物装完即可登录。id **不进
git 文件**:真值放仓库 Secret,由 `release.yml` 的 `build` job 按名显式透传(可复用工作流拿不到未显式传入的
secret;这里刻意不用 `secrets: inherit`——那会把仓库全部 secret 一起透进被调方)。Secret 未录入、或在整个
Actions 之外求值时该表达式为空串,`client_id.rs` 把空白当「没配」⇒ 非官方构建只是不烘 id,不会拿空 id 去
联网;本地自烘仍走 `ULL_MSA_CLIENT_ID=<id> cargo build`。**别把这个 env 导出到跑 `cargo test` 的 shell**:
presenter 的 M132 入口锚会在「已烘焙 id」的构建里**故意红**(它要求一个两处都没有 id 的构建)。
- 发布说明模板(`release-notes.md`)的 "Known limitations" 里,微软登录一条已改为「开箱即用、env 可覆盖」。
Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21), Windows 为什么用 MSVC 静态 CRT 而不是本机这套 GNU 工具链:两者都属"标准工具链"(D21),
但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是 但发布产物要**不依赖 VC++ 可再发行包**,`+crt-static` 把 CRT 静态链进 exe,Win10+ 的 UCRT 本身是
+9 -7
View File
@@ -100,14 +100,16 @@ Get-FileHash .\unlucky-launcher_{{VERSION}}_windows_x64.exe -Algorithm SHA256
## Known limitations of this build ## Known limitations of this build
- Microsoft (MSA) sign-in needs an Azure public client id, and release binaries do not carry one: set - Microsoft (MSA) sign-in works out of the box: this build carries the Azure public client id it needs
`ULL_MSA_CLIENT_ID=<id>` before launching, or build with that variable set. Without it sign-in stops (a public client id, not a credential — it ships inside the binary), so there is nothing to configure
before signing in. Setting `ULL_MSA_CLIENT_ID=<id>` before launching still overrides it; the variable
is read at startup, so restart the launcher after changing it. A build with no id anywhere stops
before any network call, and the GUI banner says so plainly: the title "Microsoft sign-in is not before any network call, and the GUI banner says so plainly: the title "Microsoft sign-in is not
configured", a body naming `ULL_MSA_CLIENT_ID=<id>` (set it, or build with it, then restart the configured", a body naming `ULL_MSA_CLIENT_ID=<id>` (set it, or build with it, then restart) and
launcher — a running process does not pick up a new variable) and `Code: msa_not_configured`, with `Code: msa_not_configured`, with no Retry button. Every other sign-in failure keeps the generic
no Retry button. Every other sign-in failure keeps the generic banner plus its own `Code:` line, banner plus its own `Code:` line, with the full reason only in the launcher log. `ull accounts add
with the full reason only in the launcher log. `ull accounts add msa` prints the same fix on the msa` prints the same fix on the command line. Offline profiles and authlib-injector servers need no
command line. Offline profiles and authlib-injector servers need no id. id.
- Binaries are not code-signed / notarized yet: Windows SmartScreen and macOS Gatekeeper warn on first - Binaries are not code-signed / notarized yet: Windows SmartScreen and macOS Gatekeeper warn on first
launch. The checksums above are the integrity reference for now. launch. The checksums above are the integrity reference for now.
- When the macOS slot ships it is the bare binary (single-file promise), not a `.app` bundle: no Dock - When the macOS slot ships it is the bare binary (single-file promise), not a `.app` bundle: no Dock