Repository navigation
feat: mcpp emit build-database —— 不写工程目录的 S1 构建数据库(模块图、标准库模块、监视输入) #636
Description
Activity
lsp-mcpp 消费端的进展与契约补充
2026-09-14 更新:按维护者的答复(只输出等级 2、set 按包划分、
visible-sets列出其余所有 set)调整了下文中等级与 set 的描述,issue 正文同步修订。lsp-mcpp 这一侧已按本 issue 的契约实现消费端,在 mcpp 实现命令之前先用模拟生产方验证,CI 在 Linux、macOS、Windows 上通过(Sunrisepeak/lsp-mcpp-private#1)。实现与测试中确认了几处契约细节,补充如下,供实现本命令时参考。
消费端现状
- 服务端先读
mcpp --protocol-version:kinds中有mcpp.build-database,且emit build-database的效应中没有write-project时,运行mcpp emit build-database --format json,取data.database与data.watch。数据库只写入服务端自己的缓存目录。 - 旧版 mcpp 没有该 kind 时,仍回退到
--configure-only,状态中给出不降级的提示producer-writes-project。 - 模拟生产方是
src/tools/mockmcpp.cpp,它按夹具中记录的mcpp-mock.json输出信封。下列夹具的数据已用 S1 Schema 与本 issue 的契约(等级 2、set 按包划分、每个 set 看见其余所有 set)校验,可以作为 mcpp 契约测试的参考输出:
夹具 断言 mcpp-emitmcpp 输出等级 2,S1 库补全后模型为等级 3;跳转、悬停、补全、引用、诊断,测试文件经 hello:test跨 set 导入;工作区文件不变mcpp-emit-package-stdstd、std.compat是mcpp:std中来自依赖包的翻译单元,工具链不带stdlib.module-metadatamcpp-emit-broken命令失败时,状态显示 mcpp 自己的诊断;不回退到 --configure-onlymcpp-emit-watchwatch中的输入变化后重新运行命令;再次失败时保留上次的数据库并提示可能过期;命令恢复后回到正常状态契约补充
1. 失败时的输出。 解析失败时,标准输出仍然只有一个信封:
diagnostics中至少一条severity为error,带稳定的code与面向用户的message,日志只写标准错误。消费端对支持本命令的 mcpp 不再回退到--configure-only:回退会写工程目录,还会掩盖真正的原因。因此失败信封是用户看到的唯一解释,状态中原样显示为<code>: <message>。2. 耗时上限。 消费端给命令 5 分钟上限。依赖需要联网而网络不可用时,建议尽快返回失败信封,或用本地缓存完成解析并附带
warning诊断,不要长时间阻塞。3.
watch的语义。- 消费端把条目注册为编辑器的文件监视:相对条目以工作区根为基准,绝对条目以其父目录为基准;编辑器不支持动态注册时,每 2 秒轮询一次。任一条目变化后,防抖 1.5 秒重新运行本命令。
- 由此有一条约束:命令不得写入任何列在
watch中的文件,否则会反复触发运行。例如mcpp.lock需要更新时,应当用诊断说明,而不是由本命令写入(与“不写工程目录”一致)。 - 工作区之外、但会改变结果的输入,例如路径依赖包的
mcpp.toml与build.mcpp,请用绝对路径列出。 - glob 语法按 LSP 3.17:
*、**、?、{a,b}、[...]、[!...]。Windows 与 macOS 上大小写不敏感。
4. 重复运行的耗时。 如果
watch列出源文件模式(例如src/**/*.cppm),用户每保存一次源文件就会触发一次运行。新增的import会改变模块图,这样列出本身是必要的。结果与上次相同时,消费端只替换模型,不重建索引与计划;但进程启动与解析本身的耗时用户仍能感到。建议inputs-fingerprint未变时直接复用上次的解析结果,目标是在 mcpp 仓库规模上数百毫秒内返回。验收标准 7 可以补充“输入未变时再次运行的耗时”。5. 路径写法一致。 所有路径为绝对路径,同一文档内同一文件只用一种写法:macOS 上不要混用
/var/...与/private/var/...,Windows 上不要混用 8.3 短文件名与长文件名。消费端会规范化,但两种写法混用会让watch匹配与编辑器缓冲区的对应变得脆弱。6.
requires的准确性。 消费端按provides与requires建立模块图,并据此并行准备模块。在 mcpp 仓库上,首次跨模块跳转在 32 线程机器上由逐个构建时的 38 秒降到约 25 秒。requires应来自 mcpp 自己的扫描结果(含条件编译与scan_overrides),分区写全名;与实际导入不一致会让准备顺序出错。7. 依赖包提供的
std。 本命令落地前,消费端临时从 mcpp 的 std 构建缓存读取std-module.json(schema 1),取得std、std.compat的源文件与命令。这依赖 mcpp 的内部布局。本命令已在 mcpp 2026.9.15.1 中把这两个文件作为mcpp:std的翻译单元输出;由于工程可以通过.xlings.json固定更早的 mcpp,这条临时路径保留,只在回退到--configure-only时使用(见下方 2026-09-15 的评论)。验收方式的补充建议
mcpp-emit夹具本身是一个真实的 mcpp 工程(llvm@22.1.8),可以直接用真实的 mcpp 运行:删除场景中的server-arguments(服务端会使用 PATH 上的 mcpp),然后运行lsp-mcpp-conformance run --server <lsp-mcpp> --payload <payload> --fixture conformance/fixtures/mcpp-emit期望同样是模型等级 3(mcpp 输出等级 2,由 S1 库补全)、工作区不变,以及各项语言功能通过。依赖包提供
std的情形可以用夹具self-lsp-mcpp(lsp-mcpp 自己的仓库,依赖 openkal-llvm-runtime)验证。mcpp-emit-package-std、mcpp-emit-broken与mcpp-emit-watch依赖模拟生产方记录的数据,不适用于真实的 mcpp。后续需求的补充数据
target 的选择仍是后续需求。lsp-mcpp 自己的仓库在 Windows 主机上需要
--target x86_64-windows-gnu才能构建,而 IDE 目前没有渠道把 target 传给 mcpp,所以 lsp-mcpp 的 nightly 暂不在 Windows 上做自举测试。- 服务端先读
- changed the title
[-]feat: mcpp emit build-database —— 不写工程目录的 S1 构建数据库(模块图、标准库清单、结构化选项)[/-][+]feat: mcpp emit build-database —— 不写工程目录的 S1 构建数据库(模块图、标准库模块、监视输入)[/+]on Sep 14, 2026 - added 15 commits that reference this issue
on Sep 14, 2026 已落地:mcpp 2026.9.15.1
mcpp emit build-database随 mcpp 2026.9.15.1 发布,实现见 #639,规则见 SPEC-005(docs/specs/build-database.md)。命令与文档
mcpp emit build-database输出 S1「C++ Build Database: IDE Profile」0.2.0 文档。--format json输出mcpp.build-database信封,文档在data.database,同时给出data.watch与data.inputs-fingerprint。--spec compile-commands改为输出compile_commands.json的条目。-o <file>原子写入文件。- 选择器与
mcpp build相同。
保证
- 命令不写工程目录。规划写到
$MCPP_HOME/cache/build-database/<key>。 - 标准库模块只描述、不编译。
mcpp.lock从工程读取,从不写回;与规划结果不一致时给出MCPP_LOCK_WOULD_CHANGE。--protocol-version为该命令声明的效应不含write-project。
文档内容
文档满足 S1 等级 2:
- 每个包一个集合,另有
<包>:test与mcpp:std,visible-sets列出其余所有集合。 ide.role取自扫描器读到的声明形式。arguments与compile_commands.json取自同一条记录。baseline-arguments为集合内最长公共前缀,local-arguments为各单元余下的部分。private为false。config-files列出驱动在命令行之外读取的文件。- 标准库模块单元的命令从 mcpp 实际运行的构建命令中还原,对工具链自带的 std 与依赖包提供的
std.cppm规则相同。
发布与验证
- mcpp 2026.9.15.1 与 xlings 2026.9.14.1 均已发布并镜像到 GitHub 与 GitCode;xim-pkgindex 的
latest已指向这两个版本(bump(xlings): track 2026.9.14.1 as latest openxlings/xim-pkgindex#841、#842)。 - 在
xlings subos use verify-636 --sandbox中配置 CN mirror 后,对已发布的 mcpp 做生态验证:18 项通过,0 项失败(Windows 一节由 CI 的 e2e 687 覆盖)。- 全新 home 安装工具链、构建依赖
mcpplibs.cmdline的工程后,store 中没有包目录混入其他下载物。 - 两个工程的
emit build-database输出符合 SPEC-005,工程目录不变。 xlings self doctor能指出人为制造的污染包目录,--fix后恢复。
- 全新 home 安装工具链、构建依赖
与 lsp-mcpp 的对照
- lsp-mcpp 的
specs/tools/validate.py(s1_semantics、mcpp_contract)与 S1 schema 对三份真实输出无失败:GCC 16 工程、LLVM 22 工程、lsp-mcpp 仓库自身(11 个集合,std 来自 openkal-llvm-runtime)。 - 用这个 mcpp 替换
lsp-mcpp-mock-mcpp运行 conformance:mcpp-emit12/12;mcpp-emit-package-std13/13;- 基于真实清单的 watch 场景 10/10:改动清单触发重载;清单损坏时模型保留为
model-stale,诊断为MCPP_BUILD_DATABASE_PLAN_FAILED;修复后恢复。
实现中发现并一并修复
- 未被
sourcesglob 匹配的目标入口(发现的测试、glob 之外的main)此前按行首import读取,注释与原始字符串中的import被当作导入。lsp-mcpp 的tests/test_scan.cpp因此被规划为导入三个不存在的模块。现在入口由扫描器读取。 - Windows 上每条命令打印 "The system cannot find the path specified." 的问题一并修复(Windows 使用由 xlings 安装的 mcpp 执行
mcpp build|test的时候提示The system cannot find the path specified.openxlings/xlings#543)。版本探针改为以参数向量运行,vendored xlings 从此能按 pin 更新。 - 内置 xlings 升至 2026.9.14.1:没有
install()的包只得到自己归档的内容(fix(xim,doctor): a package without an install hook receives its own archive, not the download directory (2026.9.14.1) openxlings/xlings#596)。已被污染的 registry 用XLINGS_HOME=<registry> xlings self doctor --fix修复。
- added a commit that references this issue
on Sep 14, 2026 lsp-mcpp 侧用 mcpp 2026.9.15.1 做的闭环验证
lsp-mcpp 的 CI、nightly 与发布流程已固定到 mcpp 2026.9.15.1,并在三个主机上用真实的 mcpp 分别验证了生产方与消费方两侧(Sunrisepeak/lsp-mcpp-private#1,CI run 34883539883)。
验证内容
方面 做法 结果 生产方输出 每个主机对该主机的 mcpp 夹具运行 mcpp emit build-database --format json,把信封交给specs/tools/validate.py。检查项:S2 信封 Schema、S1 Schema、S1 语义规则,以及本 issue 的契约(等级 2、set 按包划分、每个 set 看见其余所有 set、std在mcpp:std中、<package>:test为测试 set)Linux( gcc@16.1.0、llvm@22.1.8)、macOS(llvm@22.1.8)、Windows(msvc@system,以及llvm@22.1.8的x86_64-windows-msvc)全部通过消费方 mcpp-gcc、mcpp-llvm、mcpp-msvc、mcpp-llvm-msvc断言:S1 库补全后模型为等级 3;跨模块跳转、悬停、补全、引用正常;测试文件经hello:test跨 set 跳转;工程目录不变通过 监视 新夹具 mcpp-watch:新增模块接口后重新加载;写坏mcpp.toml后保留上次的模型,状态为 degraded,并显示MCPP_BUILD_DATABASE_PLAN_FAILED;修复后恢复 ready三个主机通过,Linux 另以轮询模式通过 自举 lsp-mcpp 仓库自身:11 个 set, std为 openkal-llvm-runtime 提供的mcpp:std单元本地通过 耗时(开发机):示例工程 0.55–0.60 秒,lsp-mcpp 仓库 1.6 秒。运行前后,工程目录的文件清单与内容哈希不变。lsp-mcpp 的模拟夹具也已改为录制这个版本的真实输出。
两点观察(不需要改动 mcpp,供参考)
- 标准库单元的
local-arguments带有输入与输出(--precompile <源文件> -o <BMI>)。S1 把local-arguments定义为“不在baseline-arguments中的参数”,这样写符合规范。lsp-mcpp 的 S1 库原先把源文件当作语义参数,已按这种写法修正。 .xlings.json固定的版本。 mcpp 仓库根目录的.xlings.json固定"mcpp": "2026.9.14.3",xlings 在仓库目录中运行的是这个版本。因此在编辑器中打开 mcpp 仓库时,仍会回退到--configure-only并写入compile_commands.json与target/;固定版本升到 2026.9.15.1 或以后即可避免。固定的版本没有安装时,xlings 对所有命令返回 1;lsp-mcpp 现在会把 xlings 的说明原样显示在状态中。
对之前评论的更正
之前的评论说,命令落地后删除从
std-module.json读取std的临时路径。考虑到工程可以通过.xlings.json固定更早的 mcpp,这条路径保留,只在回退到--configure-only时使用,支持本命令的 mcpp 不会走到这里。- 标准库单元的
概述
本需求新增一个命令,按公开格式输出 mcpp 构建计划中与 IDE 相关的事实:模块图、工具链与标准库模块,以及需要监视的输入。输出是 S1 构建数据库 IDE Profile 0.2.0 的等级 2 文档,放在机器输出协议 v1 的信封里。等级 3 所需的结构化语义选项不由 mcpp 输出,由消费方的 S1 库从参数得到(见“结构化语义选项”)。
该命令与
mcpp build --configure-only做同样的解析,区别是不写工程目录:不写compile_commands.json、target/与mcpp.lock,数据库只出现在标准输出中。消费方是 lsp-mcpp,一个编译器无关的 C++ 模块语言服务器,以 clangd 23.1 为语义引擎,把各编译器的工程归一化成 clangd 能理解的输入。lsp-mcpp 的设计把 mcpp 作为第一个生产方。相关材料:
specs/src/project/mcpp.cpp动机
编译数据库之外的事实
lsp-mcpp 目前以
mcpp build --configure-only(#387)生成的compile_commands.json作为 mcpp 工程的数据来源,在 Linux 与 macOS 主机上通过一致性测试。编译数据库只说明每个文件怎样编译,下表中其余的事实只能由服务端反推。这些事实 mcpp 在BuildPlan、SourceUnit与工具链指纹中都已经算出。scan_overrides与 P1689 扫描结果无法取得SourceUnit.providesInterface的判定不一致-dumpmachine、-print-library-module-manifest-path等查询-nostdinc++ -isystem <前缀>/include/c++/v1显式选择 libc++ 时,驱动查询答不出清单,只能从包含目录推断<前缀>/lib/libc++.modules.jsonmcpp.toml、mcpp.lock、**/*.cppm等模式工程目录的写入
按
docs/01-getting-started.md,--configure-only是配置操作,会更新compile_commands.json、构建目录元数据与锁文件元数据。lsp-mcpp 的设计要求不在用户工程目录中写文件;目前打开一个 mcpp 工程,工程根目录就会出现compile_commands.json与target/。与现有决定的关系
--format json信封,kind为mcpp.build-database,不另建ide.*契约。mcpp metadata: 本命令输出构建数据库,属于“按外部格式产出文档”,与mcpp emit xpkg同类,因此放在emit下。如果维护者倾向并入mcpp metadata,下文的输出契约不变,只改命令名。--configure-only(feat: generate compile database without building #387): 保持不变,继续负责compile_commands.json的兼容输出。设计思路
命令与选择器
选择器与
mcpp build相同。同一组参数下,解析出的构建计划必须与mcpp build --configure-only一致。信封
{ "schemaVersion": 1, "kind": "mcpp.build-database", "kindVersion": 1, "effects": ["read-project"], "mcpp": { "version": "…", "protocol": { "min": 1, "max": 1 } }, "data": { "database": { /* S1 0.2.0 文档,见下节 */ }, "watch": ["mcpp.toml", "mcpp.lock", "src/**/*.cppm"], "inputs-fingerprint": "…" }, "diagnostics": [] }data.databasebuild_database.jsondata.watchdata.inputs-fingerprint效应
mcpp --protocol-version在kinds中声明"mcpp.build-database": 1。commands中声明该命令可能产生的全部效应:read-project、network、write-global-cache、exec-build-script。解析与构建相同,可能下载依赖、安装工具链、运行依赖的构建程序,所以按docs/50对解析类命令的约定如实列出。write-project,这是本命令存在的理由。每次实际发生的效应写在信封的effects中。--format值按协议输出到标准错误并以 2 退出。IDE 只在受信任的工作区中运行本命令,不受信任时不运行 mcpp。
S1 文档内容(等级 2)
规范正文:S1。Schema:
s1-build-database.schema.json。按本 issue 形状写出的信封示例:s2-envelope.json(gcc@16.1.0,set 为hello、hello:test、mcpp:std)。S1 的顶层结构兼容 P2977R2,IDE 相关字段都放在各层的ide对象中。文档与工具链
version、revision1、0ide.profile-version"0.2.0"ide.generator{ "name": "mcpp", "version": <mcpp 版本> }ide.toolchains.<id>…familygcc、clang或msvc;clang++ 构建 MSVC ABI 时为clang…version、…build-id…driver…targetx86_64-pc-windows-msvc…sysroot…stdlibname:libstdc++、libc++或msvc-stl;version;module-metadata:标准库模块清单的绝对路径std或std.compat时必须…config-files--no-default-config时为空数组各工具链的
stdlib.module-metadata:stdlib.module-metadatagcc@X-print-file-name=libstdc++.modules.json的结果llvm@X,显式-nostdinc++ -isystem <前缀>/include/c++/v1<前缀>/lib[/<target>]/libc++.modules.jsonmsvc@system、msvc@<toolset>,以及 clang 以 MSVC STL 构建<tools>\modules\modules.json。它不是 P3286 形状,而是{"version": 1, "revision": 0, "library": "microsoft/STL", "module-sources": ["std.ixx", "std.compat.ixx"]}(windows-2022 上的 14.44.35207 实测);lsp-mcpp 会让 S1 同时接受这种形状,mcpp 不需要另写文件mcpp 构建的标准库模块作为翻译单元输出在 set
mcpp:std中:provides为std、std.compat,arguments为 mcpp 构建它们时的实际命令。stdlib.module-metadata。单元与清单同时给出同一模块时,消费方以单元为准(S1 6.1 节,S1-6.1-4)。stdlib.module-metadata省略。例如 openkal-llvm-runtime 0.9.5 只带llvm-generated/std.cppm与std.compat.cppm,mcpp 以stdModuleFlags构建它们。S1 0.2.0 已接受这种写法。lsp-mcpp 打开它自己的仓库时实测:编译数据库只有
-fmodule-file=std=...pcm,服务端找不到std的源文件,109 个单元中 102 个无法解析std。set
set 按包划分,不按构建目标划分:每个包(包括依赖包)一个 set,包的测试另成一个
<package>:test,mcpp 构建的标准库模块成一个mcpp:std。依赖包也作为 set 输出,IDE 才能跳转到依赖的模块源码。name<package>、<package>:test、mcpp:stdfamily-namemcpp:std为mcppvisible-setsbaseline-argumentside.toolchainide.configurationdev、releaseide.kindexecutable,其余包为library;<package>:test为test;mcpp:std为libraryide.module-metadata翻译单元
source、work-directoryargumentscompile_commands.json中对应条目一致local-argumentsbaseline-arguments中的参数objectprivateprovidesSourceUnit.provides映射到计划中的 BMI 路径;没有计划 BMI 时为空字符串requiresSourceUnit.requires_,分区写全名M:Pide.roleide.roleexport module M;module-interfaceprovides有值,providesInterface == true,名字不含:export module M:P;module-partition-interface:module M:P;module-partition-implementationprovidesInterface == falsemodule M;module-implementationnon-moduleunknownprovidesInterface为nullopt,例如来自scan_overrides;不猜测结构化语义选项:交给 S1 库
mcpp 不写 set 与单元的
ide.options。等级 3 由消费方的 S1 库完成:set 的options由baseline-arguments结构化得到,没有baseline-arguments时取所有单元共有的参数;单元的差量取自local-arguments,没有时取它比 set 多出的参数。构建专用的参数(输出、依赖文件、优化、调试信息、警告、BMI 定位)被丢弃,没有结构化字段的参数按原顺序放进raw-semantic-arguments。这样得到的选项只是参数的重述,消费方仍以arguments编译。结构化规则只维护一份,GCC、Clang、MSVC 三种方言不需要在每个生产方各实现一遍。S1 库的实现是 lsp-mcpp 的
lspmcpp.spec.options,S1 第 9 节与 11.1 节写明了这种做法。因此对 mcpp 的要求落在参数上:arguments与compile_commands.json中对应条目一致;baseline-arguments与local-arguments如实拆分:两者拼起来,就是arguments去掉驱动、-c、输入与输出之后的参数;-fmodule-file=、-fprebuilt-module-path=、/reference、/ifcOutput等)只出现在arguments中。第二阶段:进度流
lsp-mcpp 的 S2 发现协议 0.1 采用“标准输入一个请求、标准输出 JSONL 消息、生产方写数据库文件”的形式,与协议 v1 的约定不一致。lsp-mcpp 将把 S2 升到 0.2,增加单文档模式:发现命令输出一个信封,数据库内联在
data.database中。本 issue 的第一阶段只需要--format json。大工程上解析耗时较长时,第二阶段启用协议保留的
--format ndjson:每行一个信封,若干kind: "mcpp.progress",最后一行是mcpp.build-database,或者一个带诊断的失败信封。非目标
compile_commands.json与--configure-only。验收标准
data.database通过 S1 Schema 校验并满足等级 2:examples/中的示例、带tests/与 dev-dependency 的工程、多成员工作区。ide.profile-version与ide.toolchains,每个 set 有ide.toolchain,每个单元有ide.role,工程的全部翻译单元都在文档中。不写ide.options。<package>、<package>:test与mcpp:std;每个 set 的visible-sets列出其余所有 set。mcpp build --configure-only生成的compile_commands.json逐条对应,arguments相同。provides中的 BMI 路径允许为空字符串。stdlib.module-metadata指向真实存在的清单(std由依赖包提供时省略),std、std.compat是mcpp:std的翻译单元:gcc@16.1.0、llvm@22.1.8,以及依赖 openkal-llvm-runtime 的工程;llvm@22.1.8;gcc@16.1.0(x86_64-windows-gnu)、msvc@system、llvm@22.1.8(x86_64-windows-msvc)。mcpp --protocol-version声明了该 kind 与命令的效应,效应中没有write-project;不支持的--format按协议以 2 退出。--configure-only同一量级。涉及模块
src/cli.cppm:emit下新增build-database子命令与选择器。src/build/:从BuildPlan写出 S1 文档,可与compile_commands.cppm共用单元遍历。src/modgraph/graph.cppm:读取SourceUnit的provides、providesInterface、requires_。src/toolchain/:工具链指纹与标准库清单定位。src/wire.cppm、docs/50-machine-output.md:新 kind、效应声明、字段说明。tests/:单元测试、契约测试与 e2e。lsp-mcpp 侧的配合
mcpp --protocol-version,kinds中有mcpp.build-database时调用本命令,否则回退到--configure-only。specs/tools/validate.py对每份模拟数据检查:不含ide.options;set 只有<package>、<package>:test与mcpp:std;每个 set 看见其余所有 set;std、std.compat是mcpp:std的单元;<package>:test的ide.kind为test。mcpp-emit夹具另有测试文件经hello:test跨 set 导入包的模块。mcpp-gcc、mcpp-llvm、mcpp-msvc、mcpp-llvm-msvc在真实 mcpp 支持本命令后,将断言模型等级 3 与工程目录不变。emit build-database或并入mcpp metadata)以维护者意见为准。后续需求
IDE 打开工程时不知道应当使用哪个 target,只能得到主机默认目标的数据库。例如 lsp-mcpp 自身在 Windows 主机上以
--target x86_64-windows-gnu构建,而主机默认目标是x86_64-windows-msvc。docs/04-mcpp-toml.md中没有找到工程级默认 target 的写法,这一点留待第一阶段落地后讨论。相关:#371、#379、#385、#387。
修订记录
2026-09-14:按 mcpp 维护者的答复修订三处。
visible-sets列出其余所有 set;原文要求按目标写传递闭包。<package>:test与mcpp:std;原文按构建目标划分。S1 规范相应增加了说明性文字(第 7、9、11.1 节,无规范性改动),lsp-mcpp 的模拟数据与校验已随之调整(Sunrisepeak/lsp-mcpp-private#1)。