diff --git a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md index c05b740..4dd1ca1 100644 --- a/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md +++ b/.agents/docs/2026-09-26-deps-vcpkg-rules-qt-plan.md @@ -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 为 `-linux-libcxx`,经 `VCPKG_CHAINLOAD_TOOLCHAIN_FILE` 以 mcpp 的 clang 编译端口,再接 vcpkg 自身的 Linux 工具链;clang 的配置文件给出 libc++ 与 mcpp 链接的 C 库 | +| 每个 triplet 一个 vcpkg 安装 | vcpkg 的清单模式从安装中移除其余 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 回归验证。 diff --git a/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md b/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md new file mode 100644 index 0000000..1a2338d --- /dev/null +++ b/.agents/docs/2026-09-26-plugins-0.15-architecture-plan.md @@ -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 为生成的 `-linux-libcxx`,端口以 mcpp 的 clang 编译。 +- **每个 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 自身的 `/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/`(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。 diff --git a/.github/scripts/check-deps-and-qt.sh b/.github/scripts/check-deps-and-qt.sh index 3b087be..c73eb98 100644 --- a/.github/scripts/check-deps-and-qt.sh +++ b/.github/scripts/check-deps-and-qt.sh @@ -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" @@ -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 @@ -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 @@ -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=: `, 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bda882c..9a6bace 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1453,7 +1453,7 @@ jobs: # catch, so it must not introduce one. # THE MEMBERS THAT CARRY SOURCES, READ OUT OF THE MANIFEST. From 0.13.0 a # member family may hold features that add a payload and no source - # (`rules-qt-xim`, `dist-apk-kotlin`): they compile nothing this fixture + # (`dist-apk-kotlin`): they compile nothing this fixture # could miss, and naming them here would download an SDK on every host. # So the requirement is one-sided -- every source-carrying member of the # four families is named -- and the other side only checks spelling: @@ -1638,7 +1638,7 @@ jobs: # # vcpkg keeps everything it reuses in one per-user directory -- its # binary cache (`archives/`), its registry cache (`registries/`) and, - # through `mcpp-deps`, its downloads -- so caching that directory is what + # through the member's `--downloads-root`, its downloads -- so caching that directory is what # makes the second run of a manifest a restore rather than a compile. - name: Cache vcpkg's per-user directory uses: actions/cache@v4 @@ -1650,6 +1650,13 @@ jobs: - name: deps-vcpkg installs a manifest as an action, and planning installs nothing run: bash .github/scripts/check-deps-and-qt.sh vcpkg-consumer + # Linux has two C++ standard libraries. Under a libc++ toolchain the + # ports build with mcpp's clang, and the two toolchains' prefixes coexist. + - name: deps-vcpkg and deps-cmake build with mcpp's clang under a libc++ toolchain + run: | + "$MCPP" toolchain install llvm 22.1.8 + bash .github/scripts/check-deps-and-qt.sh vcpkg-libcxx + - name: deps-archive extracts a zip the project keeps and deploys its tree beside the program run: bash .github/scripts/check-deps-and-qt.sh archive-consumer @@ -1659,12 +1666,17 @@ jobs: - name: deps-cmake builds a CMake subproject as an action and links it run: bash .github/scripts/check-deps-and-qt.sh cmake-consumer - # A console program: QtCore, which is what Linux is served for. QtGui - # loads libdbus, which the ecosystem does not publish, so the Widgets - # fixture runs on Windows only. - - name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK from xim:qt-base + - name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK the project declares run: bash .github/scripts/check-deps-and-qt.sh qt-consumer + # The payload carries QtGui's runtime closure (libdbus, fontconfig, xcb, + # ...), so a Widgets program links, runs offscreen and packs on Linux. + - name: rules-qt builds a Widgets program with a .ui form, runs it offscreen and packs it + run: bash .github/scripts/check-deps-and-qt.sh qt-widgets-consumer + + - name: rules-qt takes the SDK from the project's payload, QT_ROOT_DIR or options::root + run: bash .github/scripts/check-deps-and-qt.sh qt-sdk-consumer + # ── THE SAME RULE ON THE OTHER TWO PLATFORMS ──────────────────────────────── # # `rules-spirv` is the only member whose payload this ecosystem publishes for @@ -1837,7 +1849,7 @@ jobs: # three hosts is the class of difference this job exists to catch. # THE MEMBERS THAT CARRY SOURCES, READ OUT OF THE MANIFEST. From 0.13.0 a # member family may hold features that add a payload and no source - # (`rules-qt-xim`, `dist-apk-kotlin`): they compile nothing this fixture + # (`dist-apk-kotlin`): they compile nothing this fixture # could miss, and naming them here would download an SDK on every host. # So the requirement is one-sided -- every source-carrying member of the # four families is named -- and the other side only checks spelling: @@ -2337,11 +2349,14 @@ jobs: - name: deps-cmake builds a CMake subproject as an action and links it run: bash .github/scripts/check-deps-and-qt.sh cmake-consumer - - name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK from xim:qt-base + - name: rules-qt runs moc, rcc, lupdate and lrelease, with the SDK the project declares run: bash .github/scripts/check-deps-and-qt.sh qt-consumer - # Windows and macOS: QtGui's dependencies are the system's there. On Linux - # QtGui loads libdbus, which the ecosystem does not publish, so the Linux - # job runs the console fixture only. - - name: rules-qt builds a Widgets program with a .ui form, and it runs offscreen + # Windows: the payload carries the VC++ runtime Qt's DLLs import, so the + # program starts on a machine without the VC++ Redistributable. macOS: + # Qt loads system frameworks only. + - name: rules-qt builds a Widgets program with a .ui form, runs it offscreen and packs it run: bash .github/scripts/check-deps-and-qt.sh qt-widgets-consumer + + - name: rules-qt takes the SDK from the project's payload, QT_ROOT_DIR or options::root + run: bash .github/scripts/check-deps-and-qt.sh qt-sdk-consumer diff --git a/README.md b/README.md index 2d282cc..7f041ff 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,17 @@ # mcpp-plugins The build plugins the mcpp project maintains, published as one package, -`mcpp:plugins`. A consumer selects the members it needs through features and -imports each one from `build.mcpp` under the module name the member declares. +`mcpp:plugins`. mcpp is a general engine with a framework for build plugins; +plugins come from this package, from third parties, or from the project +itself, and `build.mcpp` is where a project uses them. A consumer selects the +members it needs through features, imports each one from `build.mcpp` under the +module name the member declares, and configures it there. ```toml [build-dependencies.mcpp] -plugins = { version = "0.8.0", features = ["rules-spirv"], host-module = true } +plugins = { version = "0.15.0", features = ["rules-spirv"], host-module = true } ``` -`[build-dependencies]`, not `[dependencies]`. The two keys answer separate -questions: `host-module = true` says which build-time product is wanted, and -the section says whether the package reaches the target. A rule package answers -"no" to the second -- its library must never be linked into the artifact while -its rule is still needed -- which is the case docs/05 section 2.6.1 exists for. -Writing it under `[dependencies]` also works, which is precisely why the -distinction has to be stated rather than left to a failure to teach. - ```cpp // build.mcpp import std; @@ -30,1074 +25,78 @@ int main() { } ``` +The edge is a `[build-dependencies]` entry, not a `[dependencies]` entry: +`host-module = true` states which build-time product is wanted, and the section +states that the package does not reach the target. A rule's library is never +linked into the artifact (mcpp docs/05 §2.6.1). + ## Naming -| family | module name | purpose | +| family | module | answers | |---|---|---| -| rules | `mcpp.rules.` | how one kind of translation unit is compiled by a compiler mcpp does not drive: the spelling of its flags, the probe of its toolkit, the actions it submits | -| tools | `mcpp.tools.` | a build-time utility independent of any compiler; see `tools/README.md` | -| dist | `mcpp.dist.` | what comes out of the link, and in what form a user installs it: an `.msi`, an AppImage, a signed `.app` | -| deps | `mcpp.deps.` | where a library comes from: a prefix a program mcpp does not drive installs (vcpkg, CMake), mapped into the build | +| rules | `mcpp.rules.` | how one kind of translation unit is compiled by a compiler mcpp does not drive | +| tools | `mcpp.tools.` | what the build program does itself (see [`tools/README.md`](tools/README.md)) | +| dist | `mcpp.dist.` | what comes out of the link, and in what form a user installs it | +| deps | `mcpp.deps.` | where a library comes from | | identity | `mcpp.plugins` | the lib root, compiled before every member; it states the collection's version | -The four families answer four different questions, and the prefix is which -one a member answers: - -``` -rules-* how is this translation unit compiled -tools-* what does the build program need to do itself -dist-* what comes out of the link, and in what form a user installs it -deps-* where does a library come from -``` - -A `dist-*` member fits neither of the first two definitions: it does not -compile a translation unit and it does not do its work while the build program -runs. It consumes **link outputs** through a `role = "artifact"` action, reached -with `mcpp pack --format ` (mcpp 2026.9.11.1+). The prefix matters because -the taxonomy is load-bearing -- a consumer reading `rules-wix` would expect a -compiler it does not drive and a translation unit, and there is neither. - -A `deps-*` member compiles none of the project's translation units and does not -do its work while the build program runs: the installation is a `prepare` -action (mcpp's SPEC-007, docs/specs/build-plugins.md), which fills a declared -output directory and which the package's compile and link edges wait for, and -whose command is `mcpp-deps`, a program built from this package. The build -program refers to the prefix by name -- include directory, libraries by full -path, runtime search directory -- and never by what is in it, so a plan made -before the installation is the plan made after it. - -The `mcpp.` prefix is reserved for this package: mcpp warns when a module under -it is declared by a package outside the `mcpp` namespace. `mcpp.build.*` is the -engine's own module family and is not used here. +The `mcpp.` prefix is reserved for this package. The full account of the +families, and of how the engine routes a file to a rule, is in +[docs/engine-and-rules.md](docs/engine-and-rules.md). ## Members -| feature | module | since mcpp | what it needs | -|---|---|---|---| -| `rules-ascendc` | `mcpp.rules.ascendc` | 2026.9.6.6 | `[build] accel = "ascend8.5+{dav-c220}"`, a constrained glob for `*.asc`. Compiles with BiSheng in MIXED mode, so the object carries the device binary and a host-callable launcher and joins the ordinary link -- no registration file and no device-link step. Its own engine needs are `.asc` in the device-source table and `mcpp::link_flag` for the `-rpath-link` the toolkit's shared libraries require, both 2026.9.6.5 | -| `rules-cuda` | `mcpp.rules.cuda` | 2026.9.6.6 | `[build] accel = "cuda…"`, a constrained glob for `*.cu`; the clang route with an LLVM toolchain, the nvcc route with a GCC one | -| `rules-hip` | `mcpp.rules.hip` | 2026.9.6.6 | `[build] accel = "hip, cuda12.9+{sm_89}"`, a constrained glob for `*.hip`. On the NVIDIA platform HIP is a header layer over the CUDA runtime, so the compiler is the project's own clang and there is no ROCm on the machine | -| `rules-metal` | `mcpp.rules.metal` | 2026.9.8.1 | the Metal toolchain of the macOS host's Xcode, located rather than installed: Xcode is not redistributable, so no payload is declared. `.metal` sources the project names on a macOS or iOS row become one `xcrun --sdk metal` action per shader (`-MMD`, so an edited `#include` recompiles the shaders that include it) and one `xcrun --sdk metallib` action per library, placed beside the program with `mcpp::deploy` under `metallib/`, which `dist-apple` maps into the bundle's resources. `compile(shaders)` compiles one source several times with definitions of its own, one library per `shader`; `options::library` links every shader into one library (`default` is the one `newDefaultLibrary` finds). Before planning anything the rule asks `xcrun --sdk --show-sdk-path` and `--find metal` / `--find metallib`, and refuses naming the command that answered nothing, because a missing SDK and a missing compiler have different remedies (Xcode 26 installs the Metal toolchain as a separate component). A shader on any other row is refused naming the row. CI compiles the fixture on `macos-15` and checks each library's magic, and that a header edit recompiles only the shaders that include it | -| `rules-qt` | `mcpp.rules.qt` | 2026.9.26.2 (mcpp#702) | a Qt 6 SDK: `rules-qt-xim` declares `xim:qt` 6.11.1 on the target axis, `rules-qt-xim-base` declares `xim:qt-base` instead (qtbase and qttools with the QtQml library lupdate loads, about a third of the download; 0.14.0), `rules-qt-xim-addons` adds `xim:qt-addons` (the additional libraries) as a second prefix, and `options::root` names an SDK from elsewhere. From 0.13.0. `moc` for every header under the package root that declares `Q_OBJECT`, `Q_GADGET` or `Q_NAMESPACE` and for a source that includes its own `.moc`; `uic` for `.ui`, `rcc` for `.qrc`, `lrelease` for `.ts` (named in `[build] sources` or in the options), each a `role = "source"` action with declared inputs. The modules are linked by full path, and the SDK's `bin/` (Windows) or `lib/` is a runtime search directory: the program's run path, `mcpp run`'s load path, `mcpp pack`'s closure, and on Windows the Qt DLLs the program imports placed beside it by the engine. The plugin directories `deploy_plugins` names are deployed beside the program. See [`deps-vcpkg`, `deps-cmake`, `deps-archive` and `rules-qt`](#deps-vcpkg-deps-cmake-deps-archive-and-rules-qt) | -| `rules-slang` | `mcpp.rules.slang` | 2026.9.7.1 | `[build] accel = "vulkan1.2"`, a constrained glob for `*.slang`. Slang is a different language from GLSL rather than a second driver for it -- its own module system, generics, and targets beyond SPIR-V -- so it is a rule of its own. `.slang` is **not** in the engine's device-source table: this feature declares `device_extensions = [".slang"]` and `rule_module = "mcpp.rules.slang"`, and the engine routes it from there. That is the criterion for the whole arrangement -- a new device language costs no engine release. Since 0.7.0 it has the same `options::storage` axis as `rules-spirv` (header / object / sidecar), `options::extra_args` for the arguments the rule has no field for, and `options::per_file` for what one shader gets that the others do not -- a project with a `-fvk-use-gl-layout` and one shader needing `-emit-spirv-via-glsl` writes both without leaving one `compile()` call | -| `rules-spirv` | `mcpp.rules.spirv` | 2026.9.6.6 | `[build] accel = "vulkan1.2"`, a constrained glob for the shader stages; compiles each shader through a `role = "source"` action and states which of the two compilers produced it | -| `rules-swift` | `mcpp.rules.swift` | 2026.9.8.1 | the Swift compiler of the macOS host's Xcode or Command Line Tools, located rather than installed, as `rules-metal` locates its toolchain. From 0.12.0. The `.swift` sources a project names on a macOS or iOS row compile as one module named after the package: one whole-module `xcrun --sdk swiftc -wmo -emit-object -target ` action whose role is `object`, so the object joins every image of the package, and one `swiftc -typecheck -emit-objc-header-path` action whose role is `source`, whose directory `mcpp::include_dir` adds, so the package's C and C++ sources include `-Swift.h`. `options::bridging_header` names a C header Swift sees without an import. The link receives the toolchain's `usr/lib/swift/` and the SDK's `usr/lib/swift` as search directories and `/usr/lib/swift` as a run path through `mcpp::link_flag`. Before planning anything the rule asks `xcrun --sdk --show-sdk-path` and `--find swiftc`, and refuses naming the command that answered nothing; a Swift source on any other row is refused naming the row. Not supported: a Swift `import` of another package's module, and another package's C++ including this package's generated header, which both need an engine channel that publishes a package's interface directory to its dependents; and SwiftPM dependencies. CI builds `tests/swift-consumer` on `macos-15` -- a C++ program calling a `@_cdecl` Swift function that calls back into C -- and runs it | -| `rules-sycl` | `mcpp.rules.sycl` | 2026.9.6.6 | `[build] accel = "sycl"` or `"sycl, cuda12.9+{sm_89}"`, a constrained glob for `*.sycl`, and `compat:sycl-runtime` so the artifact can reach `libsycl.so.9` at run time. Its own engine need is `.sycl` in the device-source table, 2026.9.6.1 | -| `tools-embed` | `mcpp.tools.embed` | 2026.9.5.4 | nothing beyond mcpp: it reads a file and writes a header while the build program runs. The floor is the release whose fast path compares a declared file input, without which an edit to the data does not reach the binary | -| `tools-island` | `mcpp.tools.island` | 2026.9.7.1 | nothing beyond mcpp: it reads marked entry points out of an island's own source and writes the `extern "C"` boundary header its compiler reads and the module the C++ side imports. Not a device rule -- it claims no extension, and a project calls it from its own `build.mcpp` | -| `dist-appimage` | `mcpp.dist.appimage` | 2026.9.11.1 | `xim:appimagetool`, which this feature declares on the `cfg(linux)` axis. Linux only. Turns the tree `mcpp pack` staged into one AppImage: the staged bundle is already an AppDir bar three files, so the member writes an `AppRun`, a `.desktop` entry and an icon into it and invokes one tool -- it never copies or re-lays-out a tree that can be hundreds of megabytes. From 0.11.1 `options::icon` may be an SVG as well as a PNG: the image carries `.svg`, because the desktop entry names the icon without an extension and a reader finds it by the file's, and any other format is refused by name | -| `dist-wix` | `mcpp.dist.wix` | 2026.9.11.1 | `xim:wix` 5.0.2-1, which this feature declares on the Windows target axis; the .NET 6 runtime the tool needs is a Windows component the payload does not carry, and `wix --version` names it when it is missing. Windows only. `--format msi` renders a `.wxs` and passes the program in as a preprocessor variable, because a bind path that resolves to nothing is silent. From 0.10.0 `--format setup` is a Burn bundle chaining that MSI, with WiX's stock bootstrapper application (`bal:WixStandardBootstrapperApplication`, theme `hyperlinkLicense`, `options::license_url` its link) loaded through the `WixToolset.BootstrapperApplications.wixext` extension the 5.0.2-1 payload carries (`options::extension` names another). The bundle is written as `-.exe` beside the MSI, with an UpgradeCode of its own, and `options::bundle_output` naming `setup.exe` is refused before `wix` runs, because `wix` refuses that name (WIX0388). A project with its own bootstrapper application supplies `options::bundle_wxs`, which receives the MSI as `$(Msi)`. From 0.10.1 the MSI installs the staged tree, not the program alone: every file `mcpp pack` stages beside the program (a deployed file, a resolved DLL) is named file by file in a `StagedFiles` component group written beside the definition -- enumerated while the tree exists, never harvested from a directory -- placed at its staged path under `INSTALLFOLDER`, and declared an input of the action; the generated definition references the group, and a project's own `options::wxs` installs it with ``. `options::inputs` and `options::bundle_inputs` declare the files a project's own definitions name beyond that (an icon, a bootstrapper application and its payloads). CI builds the bundle on `windows-2022` and compares the MSI `wix burn extract` takes out of it with the one the first action wrote, and installs the MSI administratively and compares both the program and a deployed file | -| `dist-apple` | `mcpp.dist.apple` | 2026.9.14.2 (0.10.0); 2026.9.11.2 (macOS) and 2026.9.12.3 (iOS) before it | the base macOS install (`ditto`, `codesign`, `hdiutil`), and `xim:macapp-run` for `mcpp run` on macOS, which this feature declares with `when = "run"`. macOS: `Contents/`-shaped, as always. iOS (`aarch64-ios-sim`, `aarch64-ios`): a flat bundle at the same call site -- no separate feature, no separate module -- with `MinimumOSVersion` from `mcpp::min_platform_version()` (#622 A11), `CFBundleSupportedPlatforms` read from `env == "sim"`, `UIDeviceFamily`, `LSRequiresIPhoneOS`, and a directory of flat PNGs listed under `CFBundleIcons` in place of macOS's single `.icns` file. Signing is skipped on the simulator row (`options::identity` is ignored, with a `mcpp::warning` naming why), and the device row signs only with an identity. The iOS row is measured end to end on `macos-15`: a real `mcpp build`, `mcpp pack --format app` and `mcpp run` against `aarch64-ios-sim`, through `xim:apple-simulator-tools`' `simctl-run`. **The macOS floor is one release higher than its siblings** and the reason is not this member: under 2026.9.11.1 `mcpp pack` staged before dispatching and let a staging failure fail the command, so on a Mach-O program -- which the built-in closure walk refuses, because it uses `LD_TRACE_LOADED_OBJECTS` and dyld answers that by running the program -- every dispatched format was unreachable, including one that reads no staged tree. 2026.9.11.2 makes staging a service to the provider. From 0.9.2 the staged tree's deployed files (`bin//...`, which the engine stages for a Mach-O program before the closure walk since the release for mcpp#630) land at the bundle's resource destination -- `Contents/Resources//...` on macOS, the bundle root on iOS -- and the launcher alone goes to the executable directory, so `CFBundleExecutable` names a file that is where it says. The iOS fixture declares `llvm.libcxx` and `llvm.compiler-rt-builtins` under `cfg(os = "ios")`, which is what an application that imports `std` on those rows declares. From 0.10.0, with mcpp 2026.9.14.2: the dylibs the engine stages beside a Mach-O program, which the stage manifest's `needs` lines name, go to `Contents/Frameworks/` (`Frameworks/` on iOS) and not to the resources; the program is linked with the rpath that finds them there (`@executable_path/../Frameworks`, `@executable_path/Frameworks` on iOS) through `mcpp::link_flag`, so no file is edited after the link; a macOS bundle without `options::identity` is signed ad hoc, frameworks first and the bundle second, which `codesign --verify --deep --strict` requires of a bundle that carries a framework; an incomplete closure is a `mcpp::warning` naming the unresolved libraries; every refusal is a `mcpp::warning` as well, because the engine discards a build program's output when it exits 0. On macOS the member supplies the runner named `app` (`macapp-run`), so `mcpp run --format app` runs the bundle's executable in the foreground and returns its status with no runner in the manifest; a manifest runner of that name wins. `--format dmg` stages the bundle beside an `Applications` link and writes a UDZO image with `hdiutil create` (`options::volume_name`, `options::dmg`); it is refused on iOS. An engine below 2026.9.14.2 stages no `needs` lines, so the bundle carries no framework, anchors the rpath to the package directory, and hands the bundle directory to the kernel under `mcpp run --format app` unless `--runner app` is typed. CI measures the bundle on `macos-15`: the load command, the signature, the program with and without its framework (exit 7, then "Library not loaded"), `mcpp run --format app` with and without `--runner app`, and `hdiutil verify` and an attached image. From 0.11.0: a project's own Info.plist entries (`options::info_plist`), an iOS device bundle's provisioning profile (`options::provisioning_profile`), and `devicectl-run` (`xim:apple-device-tools`) as the device row's runner named `app` -- see [`dist-apple`: a project's Info.plist, and an iOS device](#dist-apple-a-projects-infoplist-and-an-ios-device). From 0.12.0 `options::omit_keys` leaves out a key the member only defaults -- see [`dist-apple`: a project's Info.plist, and an iOS device](#dist-apple-a-projects-infoplist-and-an-ios-device). From 0.12.0 a package in the resolved graph contributes Info.plist entries through `[package.metadata.dist-apple]`, applied before the application's own | -| `dist-web` | `mcpp.dist.web` | 2026.9.13.1, the release that carries `${mcpp.self}` and `mcpp stage`'s argument shape as an engine contract (`stage --verify content --output `) -- what lets this member's copy run on every host mcpp does, Windows included, in place of the `cp` this member used through 0.8.0 | nothing beyond mcpp: `wasm32-emscripten` only. Copies `${mcpp.stage_dir}/bin/` -- the `.js` launcher, the implicit `.wasm`, the `.data` when present, and every `mcpp::deploy`'d file, all of which #622 A5 and A4 already stage there -- to `/web/`, dropping the `bin/` prefix a browser has no use for, and writes an `index.html` rendered from a project template or a built-in default that loads the script with a plain `