fix(构建): dart compile exe 已被 Dart 3.13 打断,构建方式收敛到 scripts/build_cli.sh 一处

## 根因(不是我们的代码,是 SDK 行为变了)
```
$ dart compile exe bin/pmon_install.dart
'dart compile' does not support build hooks, use 'dart build' instead.
Packages with build hooks: objective_c.
```
`objective_c` 是 `file_picker` 的传递依赖。于是 README 与历史脚本里的
`dart compile exe ...` **全部失效**,用户照抄必失败 —— 典型"文档在说谎"。

替代命令:`dart build cli -t bin/<x>.dart -o <dir>`,产物布局也变了:
`<dir>/bundle/bin/<x>` + `<dir>/bundle/lib/<libs>`(多一层 `bundle/`,多出动态库)。

## 1) "怎么构建"收敛到一处:`client/scripts/build_cli.sh`
- 默认构建 `pmon_agent` + `pmon_install`,可追加其它入口(`pmon_viewer` 等)。
- 权威产物是 **bundle**(exe 与 lib/ 必须一起用),同时产出**扁平副本**
  `build/pmon-agent` / `build/pmon-install`,并把 `bundle/lib/` 的动态库平铺到 `build/`。
- 构建命令不再出现在别处:`client/README.md:98`、`:765-766` 已改为
  `bash scripts/build_cli.sh`,根 `README.md:916` 的描述也改为指向脚本;
  `bin/pmon_agent.dart:13`、`bin/pmon_install.dart:6` 的注释同改,
  `pmon_install.dart` 找不到可执行文件时的提示也改为指向脚本。

## 2) 安装器落位要带运行期库
`_install` 新增 `_runtimeLibsNextTo()`:覆盖两种产物形态
- bundle:`<bundle>/bin/<exe>` + `<bundle>/lib/<libs>`
- 扁平:exe 与库同目录(脚本会把库平铺到 `build/`)
只按扩展名收 `.dylib/.so/.dll`,**不整目录拷贝**,并排除核心库(它单独落位,避免拷两遍)。

**依据(实测,不是猜)**:
- `otool -L build/cli/pmon_agent/bundle/bin/pmon_agent` 里**没有** `objective_c.dylib`
  —— 它不是链接期依赖,而是运行期按需 dlopen 的库。
- 把 exe 单独拷到临时目录(不带该 dylib)后跑 `--help` 与
  `--collector --duration 2`:**都 exit 0,日志无 dlopen/缺库错误**
  ⇒ 当前 CLI 已走到的代码路径**并不需要**它。
- 结论:拷贝是**自包含保险**(bundle/lib 是 SDK 为该 exe 声明的运行期依赖集),
  不是修复一个正在发生的失败。代价一次文件拷贝;收益是将来若某条代码路径用到
  ObjC 桥接,不会在运行期才报 dlopen 错、且错得与"装不上"完全不像。
第一版只在"exe 的 ../lib"里找库,实测**扁平形态装出来没有 objective_c.dylib**
(脚本已把库拷到 build/,但 exe 的 ../lib 指向 client/lib);已补上"exe 同目录"这一支。

## 3) `build/pmon-install`:**保留**,继续由脚本产出
理由不是"兼容旧习惯",而是:GUI 的常驻采集卡按候选链找 CLI,
**第一候选就是 `build/pmon-install`**(`lib/host/resident_service.dart`)。
若它不存在,候选链只剩 `dart run` —— 而**最终用户机器上没有 Dart SDK**,
那条候选在真实安装里等同不可用,卡片会永远显示"找不到 CLI"。
故让脚本继续产出该路径,候选链才有意义。

## 4) 完整验证:新命令构建 → 临时 HOME 安装 → 落位后真跑
```
$ client/scripts/build_cli.sh
   dart: .../flutter/bin/dart   版本: Dart SDK 3.13.3
   -- pmon_agent --   ✓ bundle: build/cli/pmon_agent/bundle    ✓ 扁平副本: build/pmon-agent
   -- pmon_install -- ✓ bundle: build/cli/pmon_install/bundle  ✓ 扁平副本: build/pmon-install

$ ./build/pmon-install --help                      → exit 0
$ HOME=$TMP ./build/pmon-install install --no-start --label com.pmon.collector.flat
  ✓ 已落位: $TMP/Library/Application Support/pmon_client/bin/pmon-agent
  ✓ 已落位: .../bin/libpmon_core.dylib
  ✓ 已落位运行期库: .../bin/objective_c.dylib
  ✓ 配置: .../config.json
  ✓ 已写入: $TMP/Library/LaunchAgents/com.pmon.collector.flat.plist
  ✗ launchctl bootstrap → exit 5 Input/output error   ← 本环境受限(老事实),失败语义正确非零
$ <落位目录>/pmon-agent --print-plan               → exit 0,打出完整 plist 计划(核心库加载成功)
$ HOME=$TMP ./build/pmon-install status --json      → 路径全部指向临时 HOME,running=false
$ HOME=$TMP ./build/pmon-install uninstall --label ...flat → exit 0,plist 已摘除
```

## 5) e2e 跟着更新(`tests/installer-clean-e2e.js` 34 → 37 项)
- 采集端候选**优先取 bundle 形态**(`build/cli/pmon_agent/bundle/bin/`),
  否则"落位要不要带运行期库"这条测不出来。
- 新增断言:`bundle 的运行期库已一并落位(objective_c.dylib)`。
- 新增断言:**落位后的可执行文件真的能跑** —— `--help`(exit 0) 与
  `--print-plan`(exit 0 且输出含 `install_commands`,证明 exe + 核心库 + 运行期库三件齐)。

## 验证
- `dart analyze lib bin`:**No issues found!**
- `flutter test`:**574/0**
- `cargo test --release`:**61/0**
- `bash tests/run-all.sh`:**32 套件 31 通过 / 1 失败**(唯一红仍是隐私扫描,未碰)
- `node tests/installer-clean-e2e.js`:**37 通过 / 0 失败 / 1 跳过**(跳过=launchd 受限)
- `node tests/cli-exit-code-e2e.js`:**11/0**
- `guard-structure-test.js`:**44/0**

## 未做(边界外,需他人跟进)
`client/lib/host/resident_service.dart:317/351/414` 与
`client/test/resident_service_test.dart:299` 里仍写着 `dart compile exe ...` 的提示文案
(那是 GUI 侧文件,本轮不碰)。建议替换为:
`请先执行 client/scripts/build_cli.sh(产物 build/pmon-install)`。
另 `client/lib/**` 里"纯 Dart 才能 dart compile exe"的说法是**能力描述**(仍然成立),
未改。
This commit is contained in:
PMon Dev committed 2026-10-27 23:50:15 +08:00
1 parent e6d1438864
commit 5a909f14fd
7 files changed
+215 -13

No files matched your search

+1 -1
View File
@@ -913,7 +913,7 @@ socket I/O、平台差异落地、界面。
| 组成 | 说明 |
|---|---|
| `bin/pmon_agent.dart` | 无界面常驻采集进程(方案 8.3「采集核心作为独立进程」),可 `dart compile exe` 成小可执行文件 |
| `bin/pmon_agent.dart` | 无界面常驻采集进程(方案 8.3「采集核心作为独立进程」),可编成小可执行文件(构建命令见 `client/scripts/build_cli.sh`) |
| `bin/pmon_viewer.dart` | ★ 无界面**查看端** CLI:连真实网关、走真实配对、订阅并打印快照/实时帧/断点/能力集,退出码反映结果 |
| `lib/host/link.dart` | ★ 宿主 socket 适配层:真 socket 驱动 Rust 状态机(喂事件、执行动作) |
| `lib/host/viewer.dart` | ★ 查看端宿主层:独立 socket + 配对 REST + 60 帧窗口 + 断点判定 + 能力集 + 离线明示(**纯 Dart**) |
+6 -3
View File
@@ -95,7 +95,9 @@ dart run bin/pmon_agent.dart --duration 15 # 跑 15 秒后退出
dart run bin/pmon_agent.dart --print-plan # 只打印自启注册计划
# 编译成独立可执行文件(供 LaunchAgent / systemd 调用)
dart compile exe bin/pmon_agent.dart -o build/pmon-agent
# ⚠️ 构建命令只有一处:scripts/build_cli.sh(Dart 3.13 起 `dart compile exe`
# 遇到带 build hooks 的包会直接失败,见脚本文件头)
bash scripts/build_cli.sh
```
### ⚠️ 联调必须清代理变量
@@ -762,8 +764,9 @@ plist 的**内容**由 Rust 核心 `pmon_keepalive_plan_autostart()` 生成,
```bash
# 1) 编译两个独立可执行文件(非沙箱,才能写 LaunchAgents 与调 launchctl)
dart compile exe bin/pmon_agent.dart -o build/pmon-agent
dart compile exe bin/pmon_install.dart -o build/pmon-install
# 构建命令只此一处,别在这里再抄一遍(Dart 3.13 起 `dart compile exe` 会因
# build hooks 失败;脚本会同时产出 build/pmon-agent 与 build/pmon-install)
bash scripts/build_cli.sh
# 2) 注册自启(落位到 ~/Library/Application Support/pmon_client/bin,写 plist,bootstrap)
./build/pmon-install install --server http://127.0.0.1:3000 --device-id my-mac
+4 -1
View File
@@ -10,7 +10,10 @@
/// dart run bin/pmon_agent.dart --print-plan # 只打印本机自启注册计划(不联网)
///
/// 编译成独立可执行文件(可放进 LaunchAgent / systemd / 计划任务):
/// dart compile exe bin/pmon_agent.dart -o build/pmon-agent
/// client/scripts/build_cli.sh # 产出 build/pmon-agent 与 build/pmon-install
///
/// ⚠️ 不要再照抄 `dart compile exe`:Dart 3.13 起遇到带 build hooks 的包
/// (本项目的 `objective_c`)会直接报错并退出,见 `scripts/build_cli.sh` 的文件头。
library;
import 'dart:async';
+53 -2
View File
@@ -3,7 +3,8 @@
/// 为什么安装器是独立 CLI 而不是 GUI 的一部分:
/// macOS 沙箱应用**写不了 `~/Library/LaunchAgents/`**(该目录不在应用容器内)。
/// 故自启注册必须由**非沙箱**进程完成 —— 就是这个 CLI,
/// 由 `dart compile exe` 编译成独立可执行文件后运行。
/// 编译成独立可执行文件后运行(构建方式见 `scripts/build_cli.sh`,
/// **不要**照抄历史上的 `dart compile exe`:Dart 3.13 起它会因 build hooks 直接失败)。
/// GUI 只负责显示状态。
///
/// 自启产物的**内容**(plist 文本、安装/卸载命令)由 Rust 核心生成
@@ -98,7 +99,8 @@ Future<int> _install(Map<String, Object> opts, String label) async {
p.join(Directory.current.path, 'build', 'pmon-agent');
if (!File(srcAgent).existsSync()) {
stderr.writeln('找不到采集端可执行文件: $srcAgent');
stderr.writeln('请先执行: dart compile exe bin/pmon_agent.dart -o build/pmon-agent');
stderr.writeln('请先执行: client/scripts/build_cli.sh '
'(产物 build/pmon-agent;Dart 3.13 起 `dart compile exe` 已因 build hooks 失效)');
return 1;
}
@@ -120,6 +122,22 @@ Future<int> _install(Map<String, Object> opts, String label) async {
stdout.writeln(' ✓ 已落位: $installedAgentPath');
stdout.writeln(' ✓ 已落位: $libDst');
// 1b) 采集端自带的**运行期动态库**:`dart build cli` 的产物布局是
// `<bundle>/bin/<exe>` + `<bundle>/lib/<libs>`(Dart 3.13 起 `dart compile exe`
// 因 build hooks 失效,见 scripts/build_cli.sh 的文件头)。
// 只拷 exe 会让缺库失败**推迟到运行期**,而且报的是 dlopen 错,
// 与"装不上"完全不像 —— 这种"很晚才炸"的失败正是要提前消灭的。
// 依据:bundle/lib 是 SDK 为该可执行文件声明的运行期依赖集(不是构建残留目录),
// 故按扩展名收动态库,**不整目录拷贝**(避免把无关产物带进安装目录)。
// 实测(2026-10-27):当前两个 CLI 已走到的代码路径并不 dlopen objective_c.dylib,
// 所以这一步是"自包含保险",不是修复一个正在发生的失败 —— 代价只是一次拷贝。
final runtimeLibs = _runtimeLibsNextTo(srcAgent);
for (final lib in runtimeLibs) {
final dst = p.join(installBinDir, p.basename(lib.path));
await lib.copy(dst);
stdout.writeln(' ✓ 已落位运行期库: $dst');
}
// 2) 配置(自启项只传 --collector,网关与设备标识从配置读)
final prev = await ConfigStore.load();
final merged = AppConfig(
@@ -628,6 +646,39 @@ Future<AgentProcessProbe> probeAgentProcess() async {
}
}
/// 取「与该可执行文件配套的运行期动态库」。
///
/// 两种产物形态都要覆盖(见 `scripts/build_cli.sh`):
/// ① **bundle 形态**(权威):`<bundle>/bin/<exe>` + `<bundle>/lib/<libs>`
/// ② **扁平形态**(GUI 候选链用的 `build/pmon-agent`):exe 与库平铺在同一目录
/// 只认动态库扩展名 —— **不整目录拷贝**:安装目录里每多一个来路不明的文件,
/// 日后排查就多一个干扰项。核心库([coreLibName])由调用方单独落位,这里排除掉,
/// 免得同一次安装把它拷两遍、日志里出现两条"已落位"。
List<File> _runtimeLibsNextTo(String exePath) {
final exeDir = Directory(p.dirname(exePath));
final dirs = <Directory>[
Directory(p.join(exeDir.path, '..', 'lib')), // bundle 布局
exeDir, // 扁平布局
];
const exts = ['.dylib', '.so', '.dll'];
final core = coreLibName;
final seen = <String>{};
final out = <File>[];
for (final dir in dirs) {
if (!dir.existsSync()) continue;
for (final entity in dir.listSync()) {
if (entity is! File) continue;
final lower = entity.path.toLowerCase();
if (!exts.any(lower.endsWith)) continue;
final base = p.basename(entity.path);
if (base == core) continue;
if (!seen.add(base)) continue;
out.add(entity);
}
}
return out;
}
/// 默认设备标识:主机名(与服务端一眼能对上是哪台机器)
String _defaultDeviceId() => Platform.localHostname;
+111
View File
@@ -0,0 +1,111 @@
#!/usr/bin/env bash
# 构建墨观客户端 CLI 可执行文件 —— **"怎么构建 CLI"的唯一入口**
#
# 为什么要有这个脚本(2026-10-27):
# Dart 3.13 起 `dart compile exe` 遇到带 build hooks 的包会直接失败:
# 'dart compile' does not support build hooks, use 'dart build' instead.
# Packages with build hooks: objective_c.
# `objective_c` 是 `file_picker` 的传递依赖,**不是我们代码的问题**,
# 但 README 与历史脚本里那些 `dart compile exe ...` 命令因此全部失效 ——
# 用户照抄必失败,属于"文档在说谎"。
#
# 所以构建命令只写在这里,README 指过来;**不要**在 README / 其他脚本里再抄一份
# (本仓库"同一份知识写两处"已翻车多次:指标字段、配对码契约、显示目录、安装路径)。
#
# ── 产物布局(`dart build cli` 的约定)────────────────────────────
# build/cli/<name>/bundle/bin/<name> ← 可执行文件
# build/cli/<name>/bundle/lib/<libs> ← 该可执行文件的运行期动态库
# **bundle 是权威产物**:exe 与它的库是一个整体,别只拿走 bin/ 里的文件。
#
# 同时产出扁平副本(兼容既有路径,见下):
# build/pmon-agent build/pmon-install
# 并把 bundle/lib/ 里的动态库一并拷到 build/ 下,保证扁平副本也是自包含的。
#
# 为什么保留 `build/pmon-install`:
# GUI 的常驻采集卡按候选链找 CLI,**第一候选就是 `build/pmon-install`**
# (`lib/host/resident_service.dart`)。没有它就只剩"dart run"这一条 ——
# 而最终用户机器上**没有 Dart SDK**,那条候选在真实安装里等同不可用。
# 故本脚本继续产出该路径,让候选链有意义。
#
# 用法:
# client/scripts/build_cli.sh # 默认构建 pmon_agent + pmon_install
# client/scripts/build_cli.sh pmon_viewer # 追加构建其它入口(可多个)
# PMON_DART=/path/to/dart client/scripts/build_cli.sh
set -euo pipefail
cd "$(dirname "$0")/.." # → client/
DART_BIN="${PMON_DART:-}"
if [ -z "$DART_BIN" ]; then
if [ -x "$HOME/development/flutter/bin/dart" ]; then
DART_BIN="$HOME/development/flutter/bin/dart"
elif command -v dart >/dev/null 2>&1; then
DART_BIN="$(command -v dart)"
else
echo "✗ 找不到 dart(可用 PMON_DART=/path/to/dart 指定)" >&2
exit 1
fi
fi
# 默认构建两个"会被安装/被常驻卡拉起"的入口;其它按参数追加。
TARGETS=("$@")
if [ ${#TARGETS[@]} -eq 0 ]; then
TARGETS=(pmon_agent pmon_install)
fi
# Dart 入口名 → 扁平产物名(历史路径用连字符,保持与 README/候选链一致)
flat_name() {
case "$1" in
pmon_agent) echo "pmon-agent" ;;
pmon_install) echo "pmon-install" ;;
*) echo "${1//_/-}" ;;
esac
}
echo "== 构建 CLI(dart build cli)=="
echo " dart: $DART_BIN"
echo " 版本: $("$DART_BIN" --version 2>&1 | head -1)"
for t in "${TARGETS[@]}"; do
src="bin/${t}.dart"
if [ ! -f "$src" ]; then
echo "✗ 找不到入口: $src" >&2
exit 1
fi
out="build/cli/${t}"
echo ""
echo "-- ${t} --"
rm -rf "$out"
"$DART_BIN" build cli -t "$src" -o "$out"
bundle="${out}/bundle"
exe="${bundle}/bin/${t}"
if [ ! -x "$exe" ]; then
echo "✗ 构建未产出可执行文件: $exe" >&2
exit 1
fi
# 扁平副本:兼容既有路径(GUI 候选链 / README 历史写法)
flat="build/$(flat_name "$t")"
cp "$exe" "$flat"
chmod +x "$flat"
echo " ✓ bundle: ${bundle}"
# 扁平副本的运行期库:拷到扁平 exe 同目录(bundle 布局里 lib/ 与 bin/ 同级,
# 扁平化后只能平铺)。当前这两个 CLI 实测不 dlopen 这些库,属自包含保险。
if [ -d "${bundle}/lib" ]; then
for lib in "${bundle}/lib"/*; do
[ -f "$lib" ] || continue
case "$lib" in
*.dylib|*.so|*.dll) cp "$lib" "build/" ;;
*) echo " · 跳过非动态库: $(basename "$lib")" ;;
esac
done
fi
echo " ✓ 扁平副本: ${flat}"
done
echo ""
echo "== 完成 =="
echo " 权威产物: build/cli/<name>/bundle/(exe 与 lib/ 一起用)"
echo " 扁平副本: build/pmon-agent、build/pmon-install(供 GUI 候选链与既有文档)"
+37 -3
View File
@@ -101,14 +101,27 @@ function expectedInstallRoot(home) {
}
function findAgentBin() {
// **优先 bundle 形态**(`<bundle>/bin/<exe>` + `<bundle>/lib/<libs>`):
// 那才是 `dart build cli` 的权威产物,也是"落位要不要带上运行期库"能被验证的形态。
// 扁平 `build/pmon-agent` 放在最后 —— 它没有同级 ../lib,测不出这条。
const cands = [
process.env.PMON_AGENT_BIN,
path.join(CLIENT, 'build', 'pmon-agent'),
path.join(CLIENT, 'build', 'cli', 'pmon_agent', 'bundle', 'bin', 'pmon_agent'),
path.join(CLIENT, 'build', 'agent-bundle', 'bundle', 'bin', 'pmon_agent'),
path.join(CLIENT, 'build', 'pmon-agent'),
].filter(Boolean);
return cands.find((c) => fs.existsSync(c)) || null;
}
/** 该可执行文件配套的运行期动态库(Dart CLI bundle 布局:<bundle>/bin + <bundle>/lib) */
function runtimeLibsFor(exePath) {
const libDir = path.join(path.dirname(exePath), '..', 'lib');
if (!fs.existsSync(libDir)) return [];
return fs.readdirSync(libDir)
.filter((f) => /\.(dylib|so|dll)$/i.test(f))
.map((f) => path.join(libDir, f));
}
/**
* 依赖缺失就**明确跳过**(返回成功),且不打印汇总行。
*
@@ -135,8 +148,7 @@ if (!fs.existsSync(CORE_LIB_SRC)) {
const AGENT_BIN = findAgentBin();
if (!AGENT_BIN) {
bail('找不到采集端可执行文件(先执行 cd client && '
+ 'dart build cli -t bin/pmon_agent.dart -o build/agent-bundle,'
bail('找不到采集端可执行文件(先执行 client/scripts/build_cli.sh,'
+ '或用 PMON_AGENT_BIN=/path/to/pmon-agent 指定)');
}
@@ -281,6 +293,28 @@ if (fs.existsSync(binDir)) {
ok('采集端已置为可执行(mode & 0o111)', (mode & 0o111) !== 0,
`mode=${(mode & 0o777).toString(8)}`);
}
// 运行期依赖库必须跟着落位:源是 bundle 形态时,缺库失败会**推迟到运行期**,
// 报的是 dlopen 错,与"装不上"完全不像(正是要提前消灭的"很晚才炸")。
const srcLibs = runtimeLibsFor(AGENT_BIN).map((x) => path.basename(x));
if (srcLibs.length > 0) {
ok(`bundle 的运行期库已一并落位(${srcLibs.join(', ')})`,
srcLibs.every((n) => files.includes(n)), `落位=${files.join(', ')}`);
} else {
skipped('运行期库落位校验', '所用采集端是扁平产物(无同级 ../lib),该形态本来不带库');
}
// ★ 落位后的可执行文件**真的能跑**("构建成功"不算完)
const installedExe = path.join(binDir, agentName);
const helpRun = spawnSync(installedExe, ['--help'], { encoding: 'utf8', timeout: 30000 });
ok('落位后的采集端能真实执行(--help 退出码 0)', helpRun.status === 0,
`exit=${helpRun.status} ${(helpRun.stderr || '').slice(0, 200)}`);
// --print-plan 会加载 Rust 核心库并调 FFI —— 证明"exe + 核心库 + 运行期库"三件齐了
const planRun = spawnSync(installedExe, ['--print-plan'],
{ encoding: 'utf8', timeout: 60000 });
ok('落位后的采集端能加载核心库并出计划(--print-plan 退出码 0)',
planRun.status === 0 && /install_commands/.test(planRun.stdout || ''),
`exit=${planRun.status} ${(planRun.stderr || '').slice(0, 300)}`);
} else {
ok('安装目录 bin/ 已创建', false, binDir);
}
+3 -3
View File
@@ -23,7 +23,7 @@
# [E] 段守「全新设备首次接入」(认领即注册,防死锁回归);
# [F] 段守「指标/心跳通道不得绕过设备令牌」(防只守 hello 的漏洞回归)。
# 查看端端到端 21 项需业务服务 + dart + 已构建的 Rust 核心。
# 安装器干净目录 34 项(E10a)需 dart + Rust 核心产物 + 采集端可执行文件;
# 安装器干净目录 37 项(E10a)需 dart + Rust 核心产物 + 采集端可执行文件;
# 全程在临时 HOME 内跑,不碰真实家目录;缺依赖时明确跳过并返回成功。
# CLI 进程退出码 11 项需 dart + Rust 核心产物:守"失败语义真的到达进程边界"
# (本 SDK 上 main 的返回值不设退出码,只有 exitCode=/exit() 生效)。
@@ -124,7 +124,7 @@ run "FFI 冒烟(79 项)" ${PMON_PYTHON:-python3} ffi-smoke.py
# 缺 dart / 缺 Rust 核心产物 / 缺采集端可执行文件时,脚本自己打印 ⚠ 并返回成功。
DART_BIN="${PMON_DART:-$HOME/development/flutter/bin/dart}"
if [ -x "$DART_BIN" ] && [ -f ../rust-core/target/release/libpmon_core.dylib ]; then
run "安装器干净目录 e2e(34 项)" node installer-clean-e2e.js
run "安装器干净目录 e2e(37 项)" node installer-clean-e2e.js
# CLI **进程退出码** e2e:本 SDK 上 main 的返回值不设退出码(实测 int/Future<int>
# 返回 3 → 进程 0),只有显式 exitCode=/exit() 才生效。这套用**真实子进程**断言
# 四种必须 0 的场景(帮助/print-plan/抢不到锁/duration 到期)与两种必须非零的场景
@@ -132,7 +132,7 @@ if [ -x "$DART_BIN" ] && [ -f ../rust-core/target/release/libpmon_core.dylib ];
run "CLI 进程退出码 e2e(11 项)" node cli-exit-code-e2e.js
else
echo ""
echo -e "${YELLOW}⚠ 缺 dart 或 Rust 核心产物:跳过安装器/退出码 e2e(34 + 11 项)。${NC}"
echo -e "${YELLOW}⚠ 缺 dart 或 Rust 核心产物:跳过安装器/退出码 e2e(37 + 11 项)。${NC}"
echo " 需 (cd rust-core && cargo build --release) 与 dart。"
fi