Repository navigation
build: 迁到 mcpp + C++23 具名模块(#2 的精简版) - #3
Draft
Sunrisepeak wants to merge 5 commits into
Draft
Sunrisepeak wants to merge 5 commits into
Sunrisepeak wants to merge 5 commits into
Conversation
把 C++ 后端从 xmake + vcpkg + 头文件/PCH 迁到 mcpp + 官方包索引 + C++23 具名模块。 ## 源码形态 317 个 .hpp + 180 个 .cpp + 1 个 PCH,变成 263 个 .cppm + 180 个 .cpp + 52 个 .hpp。 .hpp 全部留在 src/vendor/ 下,一个都不在别处。接口只放导出的声明,实现全部在同名 .cpp 里 —— 接口占总行数 17.4%,里面没有一个普通函数体。 vendor 层分两种形态,分界线是**宏**:真正的第三方库(xxhash / webp / sqlite / spdlog / rfl / uWebSockets / dkm)转成 .cppm 封装模块,头文件只在那里解析一次; Windows SDK、WIL、WebView2 仍是头文件,由各模块在自己的全局模块片段里包含 —— 宏不进 BMI,而这棵树用了约 600 处(FAILED 295、SUCCEEDED 49、IID_PPV_ARGS 42、 WM_* 约 100)。把它们也变成模块要为每个宏造 constexpr 替身并改写全部调用点, 那是一次与模块化无关的语义重构。 ## 依赖 全部来自官方 mcpp 包索引,工程不带私有索引。索引缺的 8 个包与 asio 的导出面扩展 已经贡献并合入上游(mcpplibs/mcpp-index #213-#216)。 asio 以 `import asio;` 消费,不是 `#include <asio.hpp>` —— 头文件形态下 asio::detail::service_registry::use_service 的特化会在每个导入方重新实例化, MSVC 无法调和(fatal error C1116)。 Windows 工具链因此从原生 cl.exe 换成 llvm(targeting MSVC ABI):cl.exe 无法把 asio 的 io_context::service(类内声明、类外定义的嵌套类)经 BMI 往返 —— asio/io_context.hpp(1041): error C2039: 'service' is not a member of 'asio::io_context'。llvm 不是新引入的东西,它是 mcpp 在 Windows 的默认工具链。 ## 十七条机器强制的架构不变量 scripts/check-cpp-architecture.py + check-module-graph.py,加起来不到一秒, 在 CI 的 preflight (linux) job 里先跑,Windows 构建 needs 它。每一条都是一次 Windows 构建换来的,也都反向验证过(注入违规,确认会红)。 其中一条贯穿全程 —— **reachable ≠ visible**:`import` 不传递,用到的实体必须由 看得见的模块导出。头文件时代传递包含免费提供了这条依赖,所以它从来没有被写下来。 限定名与非限定名两半都按实体判定(Ns::Enum::Enumerator 要看枚举而不是枚举项), 再加上没有命名空间可认的那一半:Win32 的裸全局类型与 WebView2 的标识符。 模块图必须是 DAG 这一条,暴露了头文件的 include guard 一直藏着的一个真实的环: AppState 的 29 个前向声明换成 import 后,app_state -> ui.notification_window.state -> …types -> core.notifications.types -> app_state 闭合。第三条回边是真的 —— 一个 core 层的 types 模块依赖了应用根对象。改掉了。 ## 构建与 CI - mcpp-windows.yml:preflight (linux) + Windows 全量构建 + mcpp test + 产物核对 - build-release.yml / scenario-tests.yml:原有 CI 原样保留,只把 xmake 段换成 mcpp - 三个 workflow 共用 .github/actions/setup-mcpp,一份引导,不会各自漂移 tests/ 适配 mcpp test:它把 tests/**/*.cpp 每个文件编成独立二进制、按退出码判定, 框架无关。于是两个测试文件各自带一个 main 和一个计数器,doctest 依赖去掉了。 场景测试的窗口夹具从 tests/ 移到 scenarios/ —— 它不是测试,mcpp test 会去运行它。 产物路径从 build/windows/x64/release/ 变成 target/<triple>/<fingerprint>/bin/, fingerprint 覆盖所有会改变输出的东西,所以没有稳定软链接,脚本一律现找 (scripts/mcpp-artifact.js)。
删掉 XMAKE_GLOBALDIR 之后 build-release.yml 与 scenario-tests.yml 的 job 上留下 一个没有任何键的 env:。通用 YAML 解析器把它读成空映射,GitHub 的 workflow schema 不接受 —— 表现是两个 workflow 连 job 都没建起来,run 名字直接显示成文件路径。
沙箱里装什么由 .xlings.json + mcpp.toml 决定,而这两个本来就在缓存键里, 所以按 workflow 分隔的后缀什么也没买到,只是把一次冷依赖构建变成了三次。 场景测试的 60 分钟是按 xmake + 预编译 vcpkg 包定的;冷的 mcpp 依赖构建加上 全量模块构建装不进去。
mcpp test 的两个测试二进制都链接失败: lld-link: error: undefined symbol: WinMain `[build] ldflags` 会到达这个包里的**每一次链接**,而 mcpp 没有 per-target ldflags —— 所以 `-Wl,/subsystem:windows` 也落到了 mcpp test 构建的测试二进制上, 它们于是要 WinMain 而不是 main。 改成在入口翻译单元里写链接器指令:它只到达包含它的那个二进制,lld-link 从 `.drectve` 里读出来,和从命令行读是一回事。两个 GUI 程序(SpinningMomo 与 SpinningMomoScenarioWindow)各自声明,测试二进制拿默认的 console。 顺带按评审意见收拾 mcpp.toml:依赖合并成一张 `[dependencies]` 表、用 `ns.libname` 写法;注释只留与本工程相关的理由,去掉迁移过程与上游 PR 的叙述。 190 行 -> 114 行。
场景测试报「相同内容没有得到相同 hash」。不是 hash 逻辑的问题 ——
gallery 扫描器有一条 debug 捷径:
if (core::build_config::is_debug_build()) {
return hash(file_path.string()); // 路径哈希,不读内容
}
而 is_debug_build() 只看 NDEBUG。mcpp 的 release profile 设优化级别,不管这个宏,
所以 `mcpp build --release` 出来的产物仍然认为自己是 debug,两份相同内容的文件
拿到不同的 hash,去重于是静默失效。
这不止是测试的事。全树有 14 个使用点,还决定 WebView2 指向 Vite dev server
还是内嵌资源、日志级别、调试 HTTP 路由是否注册 —— 也就是说在这个修复之前,
`mcpp build --release` 产出的是一个**错的程序**,而不是一次失败的构建。
[profile.*] 没有 defines 键,所以走文档写明的 cxxflags 透传。它会到达
sm.core.build_config 自己的编译,而所有消费者都从那个 BMI 里读结论 ——
整个构建不可能像头文件时代那样自己跟自己不一致。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
把 C++ 后端从 xmake + vcpkg + 头文件/PCH 迁到 mcpp + 官方包索引 + C++23 具名模块。
这是 #2 的精简版:只留下「以 mcpp + 模块化作为构建」所必需的改动。#2 里为了双构建并存而做的东西 —— xmake 侧的 asio 模块单元、三个 xmake 补丁脚本、两个构建一致性检查、以及 12 个用来定位工具链行为的临时探针工程 —— 在这里全部不带。#2 保留,作为完整的迁移记录与证据链。
.hpp+ 180.cpp,1 个 PCH.cppm+ 180.cpp+ 52.hpp(.hpp全在src/vendor/)关键决定
import asio;,不是#include <asio.hpp>。 头文件形态下asio::detail::service_registry::use_service的特化会在每个导入方重新实例化,MSVC 无法调和(fatal error C1116)。模块形态让它只实例化一次。Windows 工具链因此从原生
cl.exe换成 llvm(targeting MSVC ABI)。 cl.exe 无法把 asio 的io_context::service(类内声明、类外定义的嵌套类)经 BMI 往返:llvm 不是新引入的东西 —— 它是 mcpp 在 Windows 的默认工具链,也是本项目此前 xmake 一直用的
clang-cl[llvm]背后的同一套 LLVM。cxx_runtime = "self-contained"一行取代分散的/MT,并且传导到依赖包的编译行 —— 这是「工程可以直接消费官方索引」的前提。vendor 层分两种形态,分界线是宏。 真正的第三方库(xxhash / webp / sqlite / spdlog / rfl / uWebSockets / dkm)转成
.cppm封装模块,头文件只在那里解析一次;Windows SDK / WIL / WebView2 仍是头文件,由各模块在自己的全局模块片段里包含 —— 宏不进 BMI,而这棵树用了约 600 处(FAILED295、SUCCEEDED49、IID_PPV_ARGS42、WM_*~100)。把它们也变成模块,等于为每个宏造一个constexpr替身再改写全部调用点,那是一次与模块化无关的语义重构。依赖:官方索引,工程不带私有索引
索引缺的 8 个包与 asio 的导出面扩展已经贡献并合入上游:mcpp-index#213 / #214 / #215 / #216。
WebView2 是唯一没进索引的:它是 NuGet 二进制分发,索引的形态分类里没有这一档。它继续走 prebuild,经
include_dirs/ldflags到达构建 —— 和迁移前一样。17 条机器强制的架构不变量
scripts/check-cpp-architecture.py+check-module-graph.py,加起来不到一秒,在 CI 的preflight (linux)job 里先跑,Windows 构建needs它。每一条都是一次 Windows 构建换来的,也都反向验证过(注入违规,确认会红)。其中一条贯穿全程 —— reachable ≠ visible:
import不传递,用到的实体必须由看得见的模块导出。头文件时代传递包含免费提供了这条依赖,所以它从来没有被写下来;「能编」的时长等于「碰巧有别的东西把它拉进来」的时长。限定名与非限定名两半都按实体判定(Ns::Enum::Enumerator要看枚举而不是枚举项),再加上没有命名空间可认的那一半:Win32 的裸全局类型(SIZE/RECT/HWND…)与 WebView2 的标识符。模块图必须是 DAG 这一条,暴露了头文件的 include guard 一直藏着的一个真实的环:
AppState的 29 个前向声明换成import之后,闭合。前两条回边是冗余 import,第三条是真的 —— 一个 core 层的 types 模块依赖了应用根对象。改掉了(见
src/core/notifications/types.cppm)。迁移过程中发现的一个真实缺陷:release 构建缺
NDEBUG场景测试报「相同内容没有得到相同 hash」。不是 hash 逻辑的问题 —— gallery 扫描器有一条 debug 捷径:
而
is_debug_build()只看NDEBUG。mcpp 的 release profile 设优化级别,不管这个宏,所以mcpp build --release出来的产物仍然认为自己是 debug。这不止是测试的事。 全树 14 个使用点,还决定 WebView2 指向 Vite dev server 还是内嵌资源、日志级别、调试 HTTP 路由是否注册 —— 在这个修复之前,release 产物是一个错的程序,而不是一次失败的构建。
修法是
[profile.release] cxxflags = ["-DNDEBUG"]([profile.*]没有defines键,走文档写明的透传)。它会到达sm.core.build_config自己的编译,所有消费者从那个 BMI 里读结论 —— 整个构建不可能像头文件时代那样自己跟自己不一致。这一条也适用于 #2,那边尚未修(按要求保持不动)。
构建与 CI
mcpp-windows.ymlpreflight (linux)+ Windows 全量构建 +mcpp test+ 产物核对build-release.ymlscenario-tests.ymldeploy-docs.yml三个 workflow 共用
.github/actions/setup-mcpp—— 一份引导,不会各自漂移。测试:
mcpp test把tests/**/*.cpp每个文件编成独立二进制、按退出码判定,框架无关。于是两个测试文件各自带一个main和一个计数器,doctest 依赖去掉了。场景测试的窗口夹具从tests/移到scenarios/—— 它不是测试,mcpp test会真的去运行它。产物路径从
build/windows/x64/release/变成target/<triple>/<fingerprint>/bin/。fingerprint 覆盖所有会改变输出的东西(profile、工具链、feature 集),所以没有稳定软链接,脚本一律现找(scripts/mcpp-artifact.js)。CI
mcpp-windowsmcpp test+ 产物核对Build Releasedist/组装 + 便携版 ZIPScenario Testsdeploy-docsdocs/**push 上触发,本 PR 不涉及