Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,20 @@ GalTranslPP 在 0.13.1 上构建成功后仍有两个手动步骤(解压嵌入

实测:库包中的 `artifact` 输出经 `mcpp::deploy` 传播到依赖它的程序的 `bin/` 与 `mcpp pack` 目录;依赖包中无下游
消费者的 `artifact` action 随下游构建执行。

## 7. 0.14.1:Linux 上依赖的 C++ 标准库与程序一致

Windows 上 MSVC ABI 的编译器共用 Microsoft 的标准库,macOS 上共用 libc++;Linux 上主机编译器使用
libstdc++,mcpp 的 clang 使用 libc++,两者的 `std::` 符号互不链接。0.14.1 在 Linux 的 libc++ 工具链下:

| 交付 | 机制 |
|---|---|
| `deps-vcpkg` 生成的 triplet | 默认 triplet 为 `<arch>-linux-libcxx`,经 `VCPKG_CHAINLOAD_TOOLCHAIN_FILE` 以 mcpp 的 clang 编译端口,再接 vcpkg 自身的 Linux 工具链;clang 的配置文件给出 libc++ 与 mcpp 链接的 C 库 |
| 每个 triplet 一个 vcpkg 安装 | vcpkg 的清单模式从安装中移除其余 triplet 的包;前缀改为 `<install root>/<triplet>/<triplet>`,两个工具链的前缀并存,切换不重装 |
| `deps-cmake` | 未经 `cache_args` 指定编译器或工具链文件时,传入 mcpp 的 clang |

判据(Linux CI `vcpkg-libcxx`):llvm 下 `libfmt.a` 含 `std::__1::` 符号且程序运行;默认工具链构建后 libc++
前缀仍在;切回 llvm 不重跑安装;deps-cmake 子项目的 `CMAKE_CXX_COMPILER` 为 mcpp 的 clang。

GalTranslPP 上游仅支持 Windows(README 所述,代码直接调用 WinAPI),Linux 的判据由本仓库的 fixture 承担;
GalTranslPP PR2 作为 Windows 回归验证。
186 changes: 186 additions & 0 deletions .agents/docs/2026-09-26-plugins-0.15-architecture-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# mcpp-plugins 0.15.0:职责分层、以 build.mcpp 为中心的配置与多平台运行时闭包

> 2026-09-26 · 第 3 版(吸收第 1、2 版评审)· 承接 `2026-09-26-deps-vcpkg-rules-qt-{design,plan}.md`

## 0. 总纲

核心体系由两部分组成:mcpp 通用引擎及其构建插件框架;官方、第三方或项目内的构建插件。`build.mcpp` 是这一机制的
核心使用点:项目在其中导入插件模块、配置插件并调用它。

评审提出的问题:

1. Linux 上 vcpkg 端口与程序的 C++ 标准库不一致。
2. Linux 上 QtGui 不可用。
3. 插件与依赖库绑定:Qt 版本固定,运行时库写在插件的 feature 中。
4. feature 应是通用机制;依赖的选择由插件与 build.mcpp 组合配置实现。
5. 内部实现(`mcpp-deps`)不得成为消费方契约。
6. 缺少插件开发规范;README 过长。

约束:
- GalTranslPP 上游仅支持 Windows。Linux 与 macOS 的判据由插件仓库的 fixture 承担,GalTranslPP PR2 作为 Windows 回归。
- mcpp 最新版本为 2026.9.26.2。
- 0.14.1 与 0.15.0 合并为一个版本 0.15.0。每个仓库一个 PR,仅在无法同时验证时拆分(§8)。

## 1. 职责分层与决定

| 层 | 负责 | 不负责 |
|---|---|---|
| mcpp | 构建图、动作与戳、运行时闭包检查、部署、打包、工具链信息 | 任何具体库、SDK 或工具 |
| 插件 | 机制:输入变为声明了输入输出的动作;前缀或 SDK 映射进构建 | 版本;运行时闭包;绕过引擎缺口 |
| xlings 包 | 载荷及其在每个平台上的运行时闭包 | 构建逻辑 |
| 项目 | 策略:库、SDK、版本、路径、选项 | — |

- **D1 feature 只表达机制。** feature 只能声明机制自身执行的工具(`xim:vcpkg`、`xim:cmake`),不得声明链接进程序的库或
SDK。`rules-qt-xim`、`rules-qt-xim-base`、`rules-qt-xim-addons` 在 0.15.0 **直接删除**(评审决定,不保留别名)。
- **D2 运行时闭包归提供它的包,逐平台成立**(§4)。
- **D3 内部实现不进入契约。** 契约面只有 feature 名、`options` 字段、返回类型和文档所列的环境变量。

## 2. 配置:以 build.mcpp 为中心

每项配置取第一个给出值的层级:

| 级 | 位置 | 说明 |
|---|---|---|
| 1 | `build.mcpp` 中的 `options` | 项目配置的主要位置,值可以计算;插件的返回值(前缀、SDK 根、部署的文件)是编程扩展点 |
| 2 | 约定的环境变量 | 只采用生态中已有的名字(`QT_ROOT_DIR`),变化时重新规划 |
| 3 | 项目在 `[xlings]` 中声明的载荷 | 版本由项目声明 |

生效层级以 `mcpp::fact` 记录。第 2 版中的 `[package.metadata.<成员>]` 一级按评审意见删除:vcpkg 的库列表属于
build.mcpp,而不是 mcpp.toml。

```toml
[build-dependencies.mcpp]
plugins = { version = "0.15.0", features = ["rules-qt", "deps-vcpkg"], host-module = true }

[target.'cfg(any(windows, linux, macos))'.xlings.workspace]
"xim:qt-base" = "6.11.1.1"
```

```cpp
// build.mcpp
import mcpp.rules.qt;
import mcpp.deps.vcpkg;
int main() {
mcpp::deps::vcpkg::options v;
v.libraries = { "fmt", "spdlog" };
mcpp::rules::qt::options q;
q.modules = { "Core", "Widgets" };
return mcpp::deps::vcpkg::use(v) && mcpp::rules::qt::compile(q) ? 0 : 1;
}
```

rules-qt 的 SDK 查找顺序为 `options::root` → `QT_ROOT_DIR`(不采用 `QTDIR`)→ 项目声明的 `xim:qt` 或 `xim:qt-base`(附带
`xim:qt-addons`)。找不到时的警告给出可直接复制的两行声明。

## 3. C++ 标准库对齐

- **deps-vcpkg**:在 Linux 的 libc++ 工具链下,默认 triplet 为生成的 `<arch>-linux-libcxx`,端口以 mcpp 的 clang 编译。
- **每个 triplet 独立安装**:前缀为 `<install root>/<triplet>/<triplet>`。
- **deps-cmake**:同样条件下传入 mcpp 的 clang。
- **rules-qt**:在 libc++ 工具链下使用 Qt 官方 Linux 构建时给出警告。

以上已完成:PR #32 的这部分三平台 CI 通过,本机 `vcpkg-libcxx` 判据通过。

## 4. 多平台运行时闭包

判据:只装有操作系统的机器上,`mcpp pack` 的产物能够启动。

| 平台 | Qt 加载的外部库 | 0.15.0 |
|---|---|---|
| Linux | glib、zstd、zlib、libdbus、fontconfig、freetype、X11、xkbcommon、EGL/GL、xcb 系列 | `xim:qt` 与 `xim:qt-base` 声明这些包和 `xim:glibc`。libxpkg 的加载器谓词在安装后改写载荷中的每个 ELF:可执行文件使用 xlings 加载器,RUNPATH 为这些包与载荷自身 `lib/` 的闭包。工具与程序共用一个加载器和 libc |
| Windows | 系统 DLL;VC++ 运行时(MSVCP140、VCRUNTIME140、VCRUNTIME140_1) | windows-x86_64 载荷从微软可再分发包(xlings-res/msvc 镜像,与 `xim:msvc` 14.44.35207 同一 vsix)取出 `Microsoft.VC143.CRT` 放入 `bin/`。windows-aarch64 暂缺镜像,列为后续项 |
| macOS | 系统 framework 与 libc++ | 无新增 |

- **新增 `xim:dbus` 1.16.2**:由 conda-forge 重新打包到 xlings-res/dbus(双端逐字节一致),依赖只有 libc 与 libpthread;构建
前缀已重定位到 `/`。
- **后续项**:QtNetwork 所需的 krb5、brotli;windows-aarch64 的 CRT;xlings glibc 的 UTF-8 locale。

本机验证(手工模拟改写 RUNPATH):Linux 上 console、Widgets offscreen 两个程序均运行。Windows 的 CRT 放置由 CI 验证(V1)。

## 5. mcpp-deps:删除

它原是 deps 系列动作的命令,负责四件事:
- vcpkg 的环境变量;
- 安装根的锁;
- 短路径的临时目录;
- CMake 三步串联,以及解压前清空目录。

它经 `tools = ["mcpp-deps"]` 进入了消费方契约,也是 mcpp#705、#707 性能问题的来源。0.15.0 以不依赖新引擎能力的方式替代:

| 职责 | 替代 |
|---|---|
| `VCPKG_ROOT`、`VCPKG_DISABLE_METRICS` | vcpkg 参数 `--vcpkg-root`、`--disable-metrics` |
| 锁 | vcpkg 自身的 `<root>/vcpkg/vcpkg-running.lock`;workspace fixture 两成员共用一个根,本机通过(V2) |
| 短路径临时目录 | `--x-buildtrees-root`、`--x-packages-root`、`--downloads-root`,路径在规划时计算 |
| CMake 三步、清空并解压 | 构建程序写出的脚本,由 `cmake -P` 执行;参数以方括号参数书写,保留空格与分号 |

- `tools/deps_main.cpp` 与 `[targets.mcpp-deps]` 直接删除,与 D1 的处理一致,不保留弃用目标。
- 依赖边写有 `tools = ["mcpp-deps"]` 的消费方须删除该项,这记入 §7。
- 本机 vcpkg-consumer、cmake-consumer、archive-consumer、vcpkg-workspace、vcpkg-libcxx 五个判据全部通过。

交给 mcpp 的只有通用能力,都不阻塞本版:
- E1:动作支持环境变量与工作目录;
- E2:feature 自带其所需的包内工具。`tools-embed` 的 `mcpp-embed` 仍经 `tools` 暴露,待 E2 支持后收回。

## 6. 规范与文档

- **README**:精简为约 100 行(原 1113 行),开篇陈述 §0 的总纲;各成员细节移至 `docs/<成员>.md`。
- **`docs/plugin-development.md`(新增)**:包含职责分层表和十条规则:
1. 机制而非策略;
2. feature 只表达机制;
3. build.mcpp 为先的三级配置;
4. 运行时闭包归载荷;
5. 内部实现不进入契约;
6. 工作即动作;
7. 缺失即警告;
8. 引擎缺口提交 issue;
9. 每条行为对应判据;
10. 文档简洁。

第三方插件与项目内插件同样适用。

## 7. 兼容性(0.15.0 的破坏性变更)

| 变化 | 消费方需做的修改 |
|---|---|
| 删除 `rules-qt-xim*` | 改为 `features = ["rules-qt"]`,并在 `[xlings]` 中声明 `xim:qt-base` 或 `xim:qt` 及版本 |
| 删除 `mcpp-deps` | 从依赖边删除 `tools = ["mcpp-deps"]` |
| vcpkg 前缀多一层 triplet | 改用返回的 `prefix`,不硬编码 `vcpkg_installed/<triplet>`(GalTranslPP `gpp.build` 第 182 行) |
| 插件不再提供 glib 目录 | 使用外部 Qt 的 Linux 项目自行处理闭包 |

载荷的更新方式:xlings 以磁盘上的载荷与安装记录判定"已安装",不会对已存在的载荷调用配方的 `installed()`。因此含运行时闭包的载荷以新版本号
`6.11.1.1` 发布(xim-pkgindex#888,沿用 fontconfig 2.15.0.1、libglvnd 1.7.0.1 的做法),项目声明 `"xim:qt-base" = "6.11.1.1"`。
这一点由插件 CI 实测发现:缓存恢复了旧的 6.11.1 载荷,Linux 程序缺少 `libzstd.so.1`。

## 8. 任务依赖与 PR

```
xlings-res/dbus(已发布)
xim-pkgindex#885:dbus(单独拆出:CI 从已发布索引解析依赖,新包与其首个依赖方无法同时验证)
│ 合入 → 索引发布
xim-pkgindex#884:qt、qt-base 的 Linux 闭包与 Windows CRT
│ 合入 → 索引发布
mcpp-plugins#32 → 0.15.0(Linux Widgets 与 Windows CRT 判据依赖上一步)
│ 三平台 CI 全绿 → tag、GitHub release、gtc 上传 GitCode
mcpp-index:登记 0.15.0
│
GalTranslPP PR2:按 §7 迁移;Windows 回归(含 VC++ 运行时)
并行:mcpp issue E1、E2
```

## 9. 总体评审

| 维度 | 结论 |
|---|---|
| 架构 | 四层职责不重叠;插件中的平台逻辑均由引擎提供的工具链信息推导,没有写死的值 |
| 稳定性 | 动作的输入输出完整;运行时闭包在规划、链接、运行、打包阶段分别由引擎与载荷保证 |
| 简洁 | 删除三个 feature、一个包内工具和 metadata 一级;新增的机制只有 `cmake -P` 脚本 |
| 易用性 | 配置集中在 build.mcpp;依赖边不再有 `tools`;Qt 选择是两行显式声明 |
| 兼容性 | 破坏性变更集中在 §7,每项修改为一到两行;载荷修订经新版本号到达已安装旧载荷的机器 |
| 跨平台 | 三个平台的闭包逐一论证;windows-aarch64 的 CRT 为已知缺口 |
| 一致性 | 三级配置、fact 记录、警告优先,对所有成员相同 |
| 测试 | 新增 `qt-sdk-consumer`(三个层级)、Linux Widgets 与打包、Windows CRT、`vcpkg-libcxx` |

遗留:
- V1:Windows 的 CRT 是否被引擎放置到程序旁;
- 后续项:krb5、brotli、windows-aarch64 的 CRT、UTF-8 locale。
99 changes: 93 additions & 6 deletions .github/scripts/check-deps-and-qt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ vcpkg_consumer() {

if is_windows; then
# The pack collects it from the runtime search directory.
ls target/vcpkg_installed/x64-windows/bin/fmt.dll > /dev/null || fail "x64-windows built no fmt.dll"
ls target/vcpkg_installed/x64-windows/x64-windows/bin/fmt.dll > /dev/null || fail "x64-windows built no fmt.dll"
run_directly vcpkg-consumer | tee target/ci/direct.log
grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/direct.log ||
fail "started from the build directory, the program did not find fmt.dll"
Expand All @@ -83,6 +83,51 @@ vcpkg_consumer() {
fi
}

# LINUX UNDER A libc++ TOOLCHAIN. The host compiler vcpkg and CMake find uses
# libstdc++, whose `std::` symbols a libc++ program cannot link. The default
# triplet is then the generated `x64-linux-libcxx`, whose ports build with
# mcpp's clang; fmt's interface returns `std::string`, so the link itself is the
# criterion. The prefix is then shown to survive the default toolchain's own
# installation, which vcpkg would remove if the two triplets shared one.
vcpkg_libcxx() {
local llvm="${MCPP_LLVM:-llvm@22.1.8}" gen=x64-linux-libcxx
[ "$(uname -m)" = aarch64 ] && gen=arm64-linux-libcxx
cd "$ROOT/tests/vcpkg-consumer"
rm -rf target vcpkg_installed
mkdir -p target/ci
"$MCPP" build --toolchain "$llvm" 2>&1 | tee target/ci/libcxx-build.log
"$MCPP" run --toolchain "$llvm" | tee target/ci/libcxx-run.log
grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/libcxx-run.log || fail "the libc++ program did not print through fmt"
local lib; lib=$(find target/vcpkg_installed -path "*/$gen/$gen/lib/libfmt.a" | head -1)
[ -n "$lib" ] || fail "no $gen prefix with libfmt.a"
grep -q 'std::__1::' <(nm -C "$lib") || fail "$lib is not built against libc++"
echo "ok: under $llvm the ports build with mcpp's clang and the program links them"

"$MCPP" build 2>&1 | tee target/ci/default-build.log
"$MCPP" run | tee target/ci/default-run.log
grep -qE '^vcpkg-consumer: fmt [0-9]+ says 42$' target/ci/default-run.log || fail "the default toolchain's program did not print through fmt"
[ -f "$lib" ] || fail "the default toolchain's installation removed the $gen prefix"
local stamp; stamp=$(find target -path '*deps-vcpkg*' -name "$gen.stamp" | head -1)
[ -n "$stamp" ] || fail "no $gen installation stamp"
touch -r "$stamp" target/ci/before-switch-back
sleep 1
"$MCPP" build --toolchain "$llvm" --profile dev > target/ci/switch-back.log 2>&1 ||
{ cat target/ci/switch-back.log; fail "the build switched back to $llvm failed"; }
[ -z "$(find "$stamp" -newer target/ci/before-switch-back)" ] ||
fail "switching back to $llvm re-ran its installation"
echo "ok: the two toolchains' prefixes coexist, and switching back installs nothing"

# deps-cmake takes the same compilers.
cd "$ROOT/tests/cmake-consumer"
rm -rf target
mkdir -p target/ci
"$MCPP" build --toolchain "$llvm" 2>&1 | tee target/ci/libcxx-build.log
"$MCPP" run --toolchain "$llvm" | grep -q '^cmake-consumer: greet says 42$' || fail "the libc++ cmake-consumer did not run"
grep -rqs 'CMAKE_CXX_COMPILER:[A-Z]*=.*xim-x-llvm.*/clang++' target --include=CMakeCache.txt ||
fail "deps-cmake configured the subproject without mcpp's clang"
echo "ok: deps-cmake configures the subproject with mcpp's clang under $llvm"
}

vcpkg_workspace() {
cd "$ROOT/tests/vcpkg-workspace"
rm -rf target app-a/target app-b/target vcpkg_installed
Expand All @@ -106,10 +151,7 @@ cmake_consumer() {
"$MCPP" run | tee target/ci/run.log
grep -q '^cmake-consumer: greet says 42$' target/ci/run.log || fail "the program did not call the subproject's library"
assert_not_rerun "$(stamp_of deps-cmake)"
# The builds after an edit are planned as well. The subproject lies inside
# this repository, the tree mcpp stamps for the plugins' host tool
# (mcpp#705), so an edit also rebuilds `mcpp-deps`; the build that is
# expected to re-run the installation absorbs that rebuild.
# The builds after an edit are planned as well.
touch greet/greet.c
"$MCPP" build --profile dev > target/ci/third-build.log 2>&1 || { cat target/ci/third-build.log; fail "the rebuild failed"; }
# The installed library is the product of the rebuild, whatever the engine
Expand Down Expand Up @@ -234,15 +276,60 @@ qt_widgets_consumer() {
find target/dist -ipath '*platforms/qoffscreen.dll' | grep -q . ||
fail "the packed tree carries no platforms/qoffscreen.dll"
echo "ok: the packed tree carries the Qt modules and the platform plugins"
# The VC++ runtime Qt's DLLs import travels with them, so the program
# does not depend on the target machine's VC++ Redistributable.
for dll in msvcp140.dll vcruntime140.dll vcruntime140_1.dll; do
find target -path '*/bin/*' -iname "$dll" | grep -q . || fail "$dll was not placed beside the program"
find target/dist -iname "$dll" | grep -q . || fail "the packed tree carries no $dll"
done
echo "ok: the VC++ runtime is beside the program and in the packed tree"
fi
if ! is_windows && ! is_macos; then
# QtGui's runtime closure comes from the payload: the program runs
# under the ecosystem's loader, which reads no host library directory.
"$MCPP" pack --format dir | tee target/ci/pack.log
for so in libQt6Widgets.so.6 libdbus-1.so.3 libxkbcommon.so.0 libfontconfig.so.1; do
find target/dist -name "$so*" | grep -q . || fail "the packed tree carries no $so"
done
echo "ok: the packed tree carries Qt and QtGui's runtime closure"
fi
}

# The SDK at each level `rules-qt` consults, read back from the fact the rule
# records (`rules-qt.sdk=<level>: <root>`, in the build program's cache).
qt_sdk_consumer() {
cd "$ROOT/tests/qt-sdk-consumer"
rm -rf target
mkdir -p target/ci
sdk_fact() { grep -h -o 'rules-qt\.sdk=[^"]*' target/.build-mcpp/build.mcpp.cache | tail -1; }
"$MCPP" build 2>&1 | tee target/ci/build.log
"$MCPP" run | tee target/ci/run.log
grep -qE '^qt-sdk-consumer: Qt 6\.' target/ci/run.log || fail "the program did not load QtCore"
local fact root; fact=$(sdk_fact)
case "$fact" in "rules-qt.sdk=xlings: "*) ;; *) fail "the project's payload was not the SDK: $fact" ;; esac
root=${fact#rules-qt.sdk=xlings: }
echo "ok: the SDK is the payload the project declares ($root)"

# The payload's root, named for one machine, then by the build program.
QT_ROOT_DIR="$root" "$MCPP" build > target/ci/env-build.log 2>&1 || { cat target/ci/env-build.log; fail "the build under QT_ROOT_DIR failed"; }
fact=$(sdk_fact)
[ "$fact" = "rules-qt.sdk=QT_ROOT_DIR: $root" ] || fail "QT_ROOT_DIR was not the SDK: $fact"
echo "ok: QT_ROOT_DIR names the SDK, and a change re-plans the build"
QT_SDK_CONSUMER_ROOT="$root" QT_ROOT_DIR=/nonexistent "$MCPP" build > target/ci/options-build.log 2>&1 ||
{ cat target/ci/options-build.log; fail "the build under options::root failed"; }
fact=$(sdk_fact)
[ "$fact" = "rules-qt.sdk=options: $root" ] || fail "options::root was not the SDK: $fact"
echo "ok: options::root names the SDK ahead of QT_ROOT_DIR"
}

case "${1:-}" in
vcpkg-consumer) vcpkg_consumer ;;
vcpkg-libcxx) vcpkg_libcxx ;;
archive-consumer) archive_consumer ;;
vcpkg-workspace) vcpkg_workspace ;;
cmake-consumer) cmake_consumer ;;
qt-consumer) qt_consumer ;;
qt-widgets-consumer) qt_widgets_consumer ;;
*) echo "usage: $0 vcpkg-consumer|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer"; exit 2 ;;
qt-sdk-consumer) qt_sdk_consumer ;;
*) echo "usage: $0 vcpkg-consumer|vcpkg-libcxx|archive-consumer|vcpkg-workspace|cmake-consumer|qt-consumer|qt-widgets-consumer|qt-sdk-consumer"; exit 2 ;;
esac
Loading
Loading