把一个 66k 行的 Windows/MSVC 桌面应用(SpinningMomo,原 xmake + vcpkg)迁到 mcpp,最终构建成功了。过程中遇到 6 个问题,其中 4 个是实现缺陷、2 个是文档与实现不一致。都已绕过,绕行方式一并附上。
环境:mcpp 2026.7.31.1 / xlings 2026.7.28.4 · GitHub Actions windows-latest · MSVC 19.51.36252(VS18 Enterprise,VC tools 14.51.36231)· Windows SDK 10.0.26100.0 · 目标 x86_64-windows-msvc
按影响面排序。
1. build.mcpp 在 Windows + MSVC 下完全不可用
现象:任何带 build.mcpp 的工程在 Windows 上都构建不了,且 build.mcpp 里的代码一行都没机会执行:
error: build.mcpp failed to compile (exit 1):
'C:\Program' is not recognized as an internal or external command,
operable program or batch file.
原因:src/build/build_program.cppm:699 用 capture_exec(compileArgv, ...) 编译 build.mcpp,而 Windows 上 capture_exec 走 cmd.exe,command_from_argv(src/platform/process.cppm:234-240)刻意把 argv[0] 原样保留不加引号:
// The first token (program) is kept RAW on Windows — quoting it would make
// cmd.exe's `/c "..."` strip the outer quotes and mangle the path
#if defined(_WIN32)
std::string cmd = argv[0];
#endif
这个取舍在 argv[0] 不含空格时成立,但 MSVC 的 cl.exe 路径必然含空格(C:\Program Files\Microsoft Visual Studio\...)。
影响:build.mcpp 是构建期逻辑的正规入口(代码生成、资源编译、探测宿主)。在 Windows + MSVC 上它整体不可用,这些逻辑只能移到 mcpp 之外的脚本里,也就失去了重跑缓存和 MCPP_* 环境契约。
绕行:把 build.mcpp 的内容改写成一个外部 prebuild.sh,在 mcpp build 之前跑。
顺带:mcpp:link-lib=<name> 在 build_program.cppm:132 被硬拼成 "-l" + val,MSVC 方言下不可用。即使 #1 修好,这条指令在 msvc 上仍然没有意义。
2. C++/WinRT 投影头目录不在合成的 INCLUDE 里
现象:任何用 C++/WinRT 的工程在 MSVC 上编不过:
fatal error C1083: Cannot open include file: 'winrt/Windows.Foundation.h': No such file or directory
原因:src/toolchain/msvc.cppm:455-461 合成 INCLUDE 时只加了四个目录:
env.push_back({"INCLUDE", join({
tools / "include",
sdk.root / "Include" / sdk.version / "ucrt",
sdk.root / "Include" / sdk.version / "um",
sdk.root / "Include" / sdk.version / "shared",
sdk.root / "Include" / sdk.version / "winrt",
})});
这里的 winrt 是 ABI 头目录(小写的 windows.foundation.h,C 风格接口),不是 C++/WinRT 投影。投影头在 Include/<ver>/cppwinrt/winrt/*.h,而且在部分 SDK 布局下根本不预置,需要 cppwinrt.exe -in local -out <dir> 现场生成。
影响:Windows.Graphics.Capture、WinUI 等一切基于 C++/WinRT 的代码在 mcpp 下开箱不可用。
绕行:prebuild 里探测 Include/<ver>/cppwinrt,没有就用 cppwinrt.exe -in local 生成到项目内,再经 [build] include_dirs 引入。
建议:cppwinrt 目录存在时加进 INCLUDE(与 winrt 并列即可)。
3. 绝对路径的 include_dirs 条目跳过 glob 展开
原因:src/build/plan.cppm:261-270
expand_manifest_include_entry(const std::filesystem::path& root,
const std::filesystem::path& inc)
{
if (inc.is_absolute()) return { inc }; // ← 绝对路径原样返回,不展开
const auto glob = inc.generic_string();
auto expanded = mcpp::modgraph::expand_dir_glob(root, glob);
...
}
相对路径条目会走 expand_dir_glob,绝对路径直接原样返回。
复现:
[build]
include_dirs = ["C:/Program Files (x86)/Windows Kits/10/Include/*/cppwinrt"]
字面量 * 直达 cl.exe。实际观察到的表现是这一条被拆成了三个参数并落到源文件位置:
c1xx: fatal error C1083: Cannot open source file: 'Files'
c1xx: fatal error C1083: Cannot open source file: '(x86)/Windows'
c1xx: fatal error C1083: Cannot open source file: 'Kits/10/Include/*/cppwinrt'
src/build/flags.cppm:238-242 明确对每个 include token 做了 shell_quote_arg,所以拆分这一半我没能定位到具体环节 —— 这部分作为观察到的现象报告,不作断言。跳过 glob 展开那一半是确定的。
绕行:由 prebuild 解析出真实版本目录,在项目内建一个无空格的 junction,manifest 里只写项目相对路径。
4. ldflags 的文件输入不能用工程相对路径,且与 build.mcpp 的行为不一致
现象:
[build]
ldflags = ["gen/app.res"] # rc.exe 产出的资源
LINK : fatal error LNK1181: cannot open input file 'gen\app.res'
原因:ldflags 原样进链接行(src/build/ninja_backend.cppm:409),而 ninja/link.exe 的 cwd 是 target/<triple>/<hash>/,工程相对路径当然解析不到。
不一致点:docs/07-build-mcpp.md:49 明确写着 mcpp:link-search=<dir> 的相对路径按工程根解析。同一件事在两个通道上语义相反,而 manifest 这一侧没有任何说明。
绕行:ldflags = ["../../../gen/app.res"] —— 依赖 target/<triple>/<hash> 这个三层布局,很脆。
建议:要么让 [build] ldflags 里长得像路径的条目按工程根解析(与 link-search 对齐),要么给 [build] 补一个 link_search / link_inputs 字段。
5. 文档在 [build] 下举例了 linkage,但它不是 [build] 的键
现象:
warning: [build] has unsupported key 'linkage' (ignored). Supported keys: sources,
cflags, cxxflags, ldflags, defines, flags, include_dirs, include_dirs_after,
dialect_cxxflags, c_standard, target, static_stdlib, allow_host_libs, cache,
profile, macos_deployment_target.
文档侧:
docs/03-toolchains.md:129 —— `[build] linkage = "dynamic"` opts out
docs/03-toolchains.md:183 —— `[build] linkage = "static"` selects the `/MT` CRT ← 正是 MSVC 那一节
docs/05-mcpp-toml.md:484 则把它放在 [target.<triple>] 下
第二条尤其误导:它在 MSVC 小节里把 [build] linkage 说成选 /MT 的方式,而实际上这个键被静默忽略,所有 TU 仍然编 /MD。
影响:见 #6 的 CRT 问题。绕行:显式 [target.'cfg(windows)'.build] cflags/cxxflags = ["/MT"]。
建议:改掉 03-toolchains.md 的两处;或者在 [build] 下把它实现成 [target.*] 的简写。
6. path 索引的注册落在沙盒 xlings 的配置里,mcpp.toml 的 [indices] 单独不够
这一条最隐蔽 —— 它的表现是本地能跑、CI 不能,而两边的 mcpp.toml 完全一样。
现象:同一个工程,本地首次构建就成功;CI 上(全新 runner)永远失败,且 mcpp 与 xlings 的诊断互相矛盾:
mcpp: 1 index repo configured [sm -> D:/a/SpinningMomo/SpinningMomo/mcpp]
xlings: E_NOT_FOUND: package 'sm:reflectcpp@0.25.0' not found
(hint: searched repos: [xim, mcpplibs])
mcpp index list 在 CI 上也能正确列出:
Project indices (mcpp.toml):
sm ../../mcpp (local path)
原因:mcpp 把 [indices] 物化进 <project>/.mcpp/.xlings.json,但真正解析包的那个沙盒 xlings 以 $MCPP_HOME/registry 为 home(~/.mcpp/config.toml 里 [xlings] home = ""),它读的是 ~/.mcpp/registry/.xlings.json 的 index_repos,不看项目那份。
一个长期存在的本地 checkout 早就被某次 mcpp index add 或某次运行写进去了,所以一直是绿的;CI runner 永远不会有。
绕行:CI 里显式 mcpp index add <name> <path>(它写的正是沙盒那份)。
建议:至少让诊断能对上 —— mcpp 报告「configured」而 xlings 报告「searched repos」里没有它,这组信息把人往查找逻辑上带,实际是注册落点的问题。理想情况下 [indices] 的 path 索引应当在解析前自动注册到沙盒。
附:迁移结果
除上述之外一切顺利。整个迁移只改了两行业务代码(66,156 行 / 180 个 TU):一处 include 路径(vcpkg 的安装布局与上游 tarball 不同),一处 asio 1.30 移除的 cancel(error_code&) 重载。
export using ::HWND; 这类把 Win32 符号从模块重导出回全局命名空间的写法在 MSVC 上工作正常;C++/WinRT 在具名模块的 GMF 里也完全可用(解决 #2 之后)。这两点对 Windows 项目的模块化很关键,可以考虑写进文档。
需要的话我可以把任意一条拆成单独 issue,或者提 PR。
把一个 66k 行的 Windows/MSVC 桌面应用(SpinningMomo,原 xmake + vcpkg)迁到 mcpp,最终构建成功了。过程中遇到 6 个问题,其中 4 个是实现缺陷、2 个是文档与实现不一致。都已绕过,绕行方式一并附上。
环境:mcpp
2026.7.31.1/ xlings2026.7.28.4· GitHub Actionswindows-latest· MSVC 19.51.36252(VS18 Enterprise,VC tools 14.51.36231)· Windows SDK 10.0.26100.0 · 目标x86_64-windows-msvc按影响面排序。
1.
build.mcpp在 Windows + MSVC 下完全不可用现象:任何带
build.mcpp的工程在 Windows 上都构建不了,且build.mcpp里的代码一行都没机会执行:原因:
src/build/build_program.cppm:699用capture_exec(compileArgv, ...)编译build.mcpp,而 Windows 上capture_exec走cmd.exe,command_from_argv(src/platform/process.cppm:234-240)刻意把argv[0]原样保留不加引号:这个取舍在 argv[0] 不含空格时成立,但 MSVC 的
cl.exe路径必然含空格(C:\Program Files\Microsoft Visual Studio\...)。影响:
build.mcpp是构建期逻辑的正规入口(代码生成、资源编译、探测宿主)。在 Windows + MSVC 上它整体不可用,这些逻辑只能移到 mcpp 之外的脚本里,也就失去了重跑缓存和MCPP_*环境契约。绕行:把
build.mcpp的内容改写成一个外部prebuild.sh,在mcpp build之前跑。2. C++/WinRT 投影头目录不在合成的
INCLUDE里现象:任何用 C++/WinRT 的工程在 MSVC 上编不过:
原因:
src/toolchain/msvc.cppm:455-461合成INCLUDE时只加了四个目录:env.push_back({"INCLUDE", join({ tools / "include", sdk.root / "Include" / sdk.version / "ucrt", sdk.root / "Include" / sdk.version / "um", sdk.root / "Include" / sdk.version / "shared", sdk.root / "Include" / sdk.version / "winrt", })});这里的
winrt是 ABI 头目录(小写的windows.foundation.h,C 风格接口),不是 C++/WinRT 投影。投影头在Include/<ver>/cppwinrt/winrt/*.h,而且在部分 SDK 布局下根本不预置,需要cppwinrt.exe -in local -out <dir>现场生成。影响:Windows.Graphics.Capture、WinUI 等一切基于 C++/WinRT 的代码在 mcpp 下开箱不可用。
绕行:prebuild 里探测
Include/<ver>/cppwinrt,没有就用cppwinrt.exe -in local生成到项目内,再经[build] include_dirs引入。建议:
cppwinrt目录存在时加进INCLUDE(与winrt并列即可)。3. 绝对路径的
include_dirs条目跳过 glob 展开原因:
src/build/plan.cppm:261-270相对路径条目会走
expand_dir_glob,绝对路径直接原样返回。复现:
字面量
*直达cl.exe。实际观察到的表现是这一条被拆成了三个参数并落到源文件位置:src/build/flags.cppm:238-242明确对每个 include token 做了shell_quote_arg,所以拆分这一半我没能定位到具体环节 —— 这部分作为观察到的现象报告,不作断言。跳过 glob 展开那一半是确定的。绕行:由 prebuild 解析出真实版本目录,在项目内建一个无空格的 junction,manifest 里只写项目相对路径。
4.
ldflags的文件输入不能用工程相对路径,且与build.mcpp的行为不一致现象:
原因:
ldflags原样进链接行(src/build/ninja_backend.cppm:409),而 ninja/link.exe 的 cwd 是target/<triple>/<hash>/,工程相对路径当然解析不到。不一致点:
docs/07-build-mcpp.md:49明确写着mcpp:link-search=<dir>的相对路径按工程根解析。同一件事在两个通道上语义相反,而 manifest 这一侧没有任何说明。绕行:
ldflags = ["../../../gen/app.res"]—— 依赖target/<triple>/<hash>这个三层布局,很脆。建议:要么让
[build] ldflags里长得像路径的条目按工程根解析(与link-search对齐),要么给[build]补一个link_search/link_inputs字段。5. 文档在
[build]下举例了linkage,但它不是[build]的键现象:
文档侧:
docs/03-toolchains.md:129——`[build] linkage = "dynamic"` opts outdocs/03-toolchains.md:183——`[build] linkage = "static"` selects the `/MT` CRT← 正是 MSVC 那一节docs/05-mcpp-toml.md:484则把它放在[target.<triple>]下第二条尤其误导:它在 MSVC 小节里把
[build] linkage说成选/MT的方式,而实际上这个键被静默忽略,所有 TU 仍然编/MD。影响:见 #6 的 CRT 问题。绕行:显式
[target.'cfg(windows)'.build] cflags/cxxflags = ["/MT"]。建议:改掉
03-toolchains.md的两处;或者在[build]下把它实现成[target.*]的简写。6. path 索引的注册落在沙盒 xlings 的配置里,
mcpp.toml的[indices]单独不够这一条最隐蔽 —— 它的表现是本地能跑、CI 不能,而两边的
mcpp.toml完全一样。现象:同一个工程,本地首次构建就成功;CI 上(全新 runner)永远失败,且 mcpp 与 xlings 的诊断互相矛盾:
mcpp index list在 CI 上也能正确列出:原因:mcpp 把
[indices]物化进<project>/.mcpp/.xlings.json,但真正解析包的那个沙盒 xlings 以$MCPP_HOME/registry为 home(~/.mcpp/config.toml里[xlings] home = ""),它读的是~/.mcpp/registry/.xlings.json的index_repos,不看项目那份。一个长期存在的本地 checkout 早就被某次
mcpp index add或某次运行写进去了,所以一直是绿的;CI runner 永远不会有。绕行:CI 里显式
mcpp index add <name> <path>(它写的正是沙盒那份)。建议:至少让诊断能对上 —— mcpp 报告「configured」而 xlings 报告「searched repos」里没有它,这组信息把人往查找逻辑上带,实际是注册落点的问题。理想情况下
[indices]的 path 索引应当在解析前自动注册到沙盒。附:迁移结果
除上述之外一切顺利。整个迁移只改了两行业务代码(66,156 行 / 180 个 TU):一处 include 路径(vcpkg 的安装布局与上游 tarball 不同),一处 asio 1.30 移除的
cancel(error_code&)重载。export using ::HWND;这类把 Win32 符号从模块重导出回全局命名空间的写法在 MSVC 上工作正常;C++/WinRT 在具名模块的 GMF 里也完全可用(解决 #2 之后)。这两点对 Windows 项目的模块化很关键,可以考虑写进文档。需要的话我可以把任意一条拆成单独 issue,或者提 PR。