Skip to content

feat(ide): add versioned project snapshots and pre-build CDB - #372

Draft
wellwei wants to merge 26 commits into
mcpp-community:mainfrom
wellwei:codex/ide-snapshot
Draft

feat(ide): add versioned project snapshots and pre-build CDB#372
wellwei wants to merge 26 commits into
mcpp-community:mainfrom
wellwei:codex/ide-snapshot

Conversation

@wellwei

@wellwei wellwei commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

  • add a versioned, read-only IDE project snapshot for workspace/member discovery, artifact state and structured diagnostics
  • add an IDE configure operation that resolves the real build plan and publishes a clangd compile database before ordinary source compilation or linking
  • define and document the IDE-client lifecycle while keeping workspace fan-out, freshness watching and stale fallback in the plugin

Closes #371

Protocol surface

mcpp ide snapshot --format json

  • emits schema version 1 with protocol capability negotiation
  • reports workspace members, package targets, selectors, CDB locations and aggregate partial/stale/configured state
  • reports manifest and artifact failures as structured diagnostics
  • performs read-only inspection with workspace path-containment checks
  • treats configured as metadata plus regular-file existence, not active freshness or JSON validation

mcpp ide configure --format ndjson

  • resolves the same BuildPlan and compile flags used by a normal build
  • emits ordered operation-started, snapshot-published and operation-finished events with stable operation correlation
  • publishes project, configuration and snapshot identities plus content-addressed reply CDB metadata
  • includes discovered tests/**/*.cpp translation units and enables their development-dependency context
  • stages standard-library and cache-hit module prerequisites needed by emitted compile commands
  • publishes before compiling ordinary project TUs or linking final targets, so syntactically incomplete projects can still obtain clangd configuration
  • closes the NDJSON lifecycle with a structured failure instead of mixing human progress into stdout

Publication and compatibility

  • writes content-addressed replies under .mcpp/ide/replies/
  • records the active configured snapshot in .mcpp/ide/current.json
  • maintains root compile_commands.json as a compatibility projection for existing language-server clients
  • serializes concurrent publication and distinguishes lock setup/I/O failures from actual lock contention in human diagnostics
  • uses replacement-based fresh CDB publication on POSIX and Windows, retaining or restoring the previous usable root CDB on normal failures
  • validates publication paths remain inside the physical project root

Build integration

  • extracts shared test-target discovery so IDE CDBs and mcpp test use the same names, member scope and per-glob flags
  • preserves best-effort mcpp test --list inventory for invalid manifests while real build/configure remains strict in prepare_build()
  • keeps index-recovery notices off protocol stdout and updates the bootstrap pin used by this branch
  • does not add a custom-toolchain feature or a new build backend

IDE client responsibilities and current limits

The new protocol documentation records that clients must:

  • obtain workspace trust before configure, because resolution may install tools/dependencies, execute project or dependency build.mcpp, and update lock/cache/resolution state
  • watch relevant inputs, debounce changes and avoid retriggering on files written by the active configure operation
  • fan out workspaces through workspace.members[].workspacePath and one --package configure per member
  • serialize operations per member, correlate seq/operationId, retain the previous usable CDB on failure and mark it stale
  • treat the reply CDB as authoritative and not parse human error messages as stable categories
  • represent missing uncached module BMIs as pending and offer an explicit normal build/test instead of silently building during discovery

Version 1 intentionally does not provide daemon mode, cancellation, configure --workspace fan-out, active freshness recomputation, a ready-artifact phase or cross-toolchain BMI compatibility guarantees.

Test plan

  • self-build: mcpp build --no-color
  • fresh binary: mcpp test --no-color (65 passed; 0 failed)
  • tests/e2e/159_test_list.sh
  • tests/e2e/196_ide_snapshot.sh
  • tests/e2e/197_ide_snapshot_read_only.sh
  • tests/e2e/198_ide_configure.sh
  • tests/e2e/199_ide_configure_windows.sh on native Windows/MSVC CI
  • git diff --check origin/main
  • required GitHub Actions on Linux, macOS and Windows

wellwei added 26 commits August 7, 2026 18:30
@speak-agent

speak-agent commented Aug 8, 2026

Copy link
Copy Markdown
Member

深度 review 完这个 PR 和 #371,以及结合 mcpp-community/mcpp-vscode#5 / mcpp-community/mcpp-vscode#8 的实际需求,结论如下:

  1. A 部分(CDB + 测试发现 + 原子写)拆成独立 PR,尽快合入并发版。
  2. mcpp-vscode 侧重新适配 —— 改为消费 mcpp build --configure-only(spawn + 退出码 + 读 CDB),扩展代码更少,且不再被这条协议的发版链路阻塞。
  3. 本 PR 不继续推进,协议部分的方向收敛到 #379

下面是理由,都附了可复现的验证方式,不用采信我的断言。


一、核心前提需要复核:CDB 早就在 ninja 之前写了

src/build/ninja_backend.cppm:1546:

std::expected<BuildResult, BuildError> NinjaBackend::build(...) {
    ...
    write_file(ninja_path, manifest);
    auto flags = compute_flags(plan);
    write_compile_commands(plan, flags);   // ← 这里
    ...                                     //   之后才 spawn ninja

实测(mcpp 2026.8.6.3,即本分支的 pin 版本):

mkdir -p probe/src && cd probe
printf '[package]\nname = "probe"\nversion = "1.0.0"\n' > mcpp.toml
printf '#include <cstdio>\n\nint main( {\n    return missing_symbol;\n}\n' > src/main.cpp
mcpp build --no-color   # → rc=1, "3 errors generated"
ls -l compile_commands.json
# → 1624 bytes,entry 完整:
#   -std=c++23 -fprebuilt-module-path=... --no-default-config -nostdinc++ -isystem .../c++/v1

也就是说,#371 的 "mcpp currently writes a useful compile database only after a successful build" 不成立。真正的门槛不是"编译成功",而是 prepare_build() 成功 —— 而 configure_project() 走的是同一个 prepare_build()(src/ide/configure.cppm:189)。

推论:在"源码编不过时还能拿到 CDB"这件事上,ide configuremcpp build 能力完全相同。 tests/e2e/198_ide_configure.sh 精心构造的 int main( { 场景,mcpp build 一样能过。README 新增的那句 "so clangd can analyze a project whose source currently has build errors" 描述的是 main 上已有的行为。

剥掉这条之后,真实增量剩四条,其中只有第一条是核心:

  1. 不必为拿 CDB 付一次完整编译 + 链接(分钟级 → 秒级)← 唯一的核心理由
  2. CDB 覆盖 tests/** 及其 dev-dependency 上下文(merge_compile_commands 已部分缓解)
  3. 退出码能区分"CDB 没写"和"CDB 写了但编译失败"
  4. stdout 纯净

这四条,mcpp build --configure-only 全部满足,约 150 行。

补充一点:stage_cached_module_prerequisites 的增量也比预期小 —— 它只 stage std BMI 和缓存命中的依赖 BMI,而 std BMI 在 prepare_build 里就 ensure_built 了(build 路径同样有);项目自身的模块 BMI 两条路都不构建(本 PR 文档 §6 也承认 "module completion may remain pending")。

二、按"只有 mcpp 能做的才让 mcpp 做"这条线算账

组件 插件侧实现 mcpp 侧实现 判定
CDB flags / --configure-only / test 发现 / BMI staging 不可能 已有或 ~150 行 mcpp 做 ✅
CDB 失效输入清单 不可能 ~20 行(数据全在 ctx.fp+plan) mcpp 做,但本 PR 没做
self env --format json 要重实现三级 home 回退 + xlings shim 检测 ~15 行(值已在打印) mcpp 做,但本 PR 没做
workspace 成员发现 [workspace].members ~60 行 inspect.cppm 433 行 插件做
NDJSON 事件协议 spawn + 等 exit + 读文件 ~10 行 events.cppm 106 + 事件构造 + seq/opId 体系 插件做
reply/current.json/发布锁/回滚 copy 旧的,失败 copy 回来 ~20 行 publish.cppm 333 行 插件做

后三行不是"复杂度相当",是 mcpp 做明显更贵。原因是 mcpp 必须额外承担插件根本不需要的负担:只读性保证(e2e 197 要用整棵树 inventory 验证)、路径 containment(member 逃逸 / 符号链接逃逸)、选择器语义、诊断降级。插件在自己进程里读文件,单进程、私有、可随时改,这些负担都不存在。

把功能移进 mcpp,不是把复杂度搬过去,是把复杂度放大 —— 因为 mcpp 是多进程、公开契约、要向后兼容的。

另外两条实际成本:

三、两个 P0(即使不继续推进,也值得记录)

P0-1:NDJSON stdout 还有一条没堵住的污染源

src/pm/package_fetcher.cppm:1036 硬编码 /*quiet=*/false:

mcpp::xlings::ensure_official_package_index_fresh(
    xlEnv, parsed.packageName, cfg_.searchTtlSeconds, /*quiet=*/false);

往下追:

  • xlings.cppm:1583print_status("Refreshing", "package index — X not found locally")std::println 到 stdout
  • xlings.cppm:1585update_index(env, /*quiet=*/false)update_index_unguardedrun_streaming(cmd, [quiet](line){ if(!quiet) std::println("{}", line); }),而 cmdxlings update 2>&1xlings 全量输出逐行进 stdout

关键在于 xlings::print_status(src/xlings.cppm:395)不看 mcpp::ui::is_quiet(),只受调用方传下来的 bool 控制。QuietGuard 设的是 ui::set_quiet(true),对它无效。

触发条件不罕见:任一 xim: 包(含工具链)在本地索引缺失且未命中 debounce —— 新装机器、新增依赖、切换 toolchain 版本都会命中。

e2e 198 结构性覆盖不到:它用 _inherit_toolchain.sh 把宿主已装工具链复制进临时 MCPP_HOME,索引永远命中,这条路径一次都走不到。

根因是 stdout 归属分散在多个 bool 参数里 —— PR 已经逐点堵了 5 处(load_or_init 链、c++fly 摘要、tool build verbose、Kept/Restored ×2),这是第 6 处。建议的修法不是加第 7 个 bool,而是让 xlings::print_statusmcpp::ui::status(它有 g_quiet 门),或引入 ui::stdout_is_protocol() 一票否决。

P0-2:文档指定的客户端流程在 rooted workspace 的根成员上必然失败

docs/11-ide-protocol.md §7.2 要求客户端:

use workspace.members[].workspacePath and run one configure --package <workspacePath> operation per member

src/ide/inspect.cppm:248 对 rooted workspace 产出 workspacePath == "."(tests/unit/test_ide_snapshot.cppRootedWorkspaceSelectsRoot 明确断言 members[0].workspacePath == ".")。

. 喂回 configure:

configure_project (configure.cppm:182)
  → discover_test_targets(*root, ".")
    → project::resolve_member_dir(m, root, ".")     [project.cppm:140]
      → 遍历 [workspace].members,比 mp 与 path(mp).filename()
      → "." 两者都不匹配
      → unexpected("workspace member '.' not found in [workspace].members")
→ exit 3

根因是 member selector 语法在三处独立推导:project.cppm::resolve_member_dir / prepare.cppm:951(内联复制的同一份逻辑) / ide/inspect.cppm::matches_selector。前两处一致,第三处多了 . 别名。

现有测试恰好绕开:e2e 198 的 workspace 是 virtual(根无 [package],不产生 . 成员);单测里的 workspace 要么 virtual,要么 name / dir / workspacePath 三者相同。mcpp 自己也不是 workspace,dogfood 同样碰不到。

顺带一个相关的表示不一致:canonical_member_selector(model.cppm:128)对根包返回 nullopt,inspect 侧用 "." —— 同一件事两种编码。

四、A 部分:建议拆出来的清单

这些有独立价值,和协议无关,建议单独开 PR 尽快合并:

内容 理由
src/build/test_targets.cppm(81 行) 真正的收敛。把 tests/** 发现从 run_tests 提取出来,消除了"IDE CDB 的发现规则"和"mcpp test 的发现规则"在 member 选择 / 命名 / per-glob flags 上漂移的可能
write_fresh_compile_commands + platform::fs::replace_file + FileLock(dir, ec) 顺带修一个既有缺陷:write_compile_commands() 目前是 ofstream 截断写,非原子、不检查失败,clangd 可能读到半截 JSON。FileLock 区分"竞争"与"I/O 失败"也是净收益
prepare.cppm 的 stdout 纪律修复 load_or_init(quiet=is_quiet()) 这条链堵得对,fresh MCPP_HOME 的 bootstrap 文本确实该跟随 quiet
test_compile_commands / test_platform_fs / test_test_options 单测 全部保留
新写:mcpp build --configure-only 替代 ide configure。复用 mcpp build-p/--profile/--target/--features/--cap 语义、复用人类 stdout、复用既有 CDB 写路径。~150 行
:CDB 失效输入清单 `invalidatedBy: [{path,hash}

src/ide/model.cppmDiagnostic/Position/Range/Severitysrc/ide/snapshot.cppm 的 envelope 构造 + 内容寻址 ID(合计约 150 行)设计质量很高,建议捞进 #379 提升为 mcpp.wire,服务所有 JSON 出口而不只是 IDE。

不建议合入的:inspect.cppm(433) / publish.cppm(333) / events.cppm(106) / cmd_ide.cppm(119) / ide 两个子命令 / IdePhase·SnapshotState·ArtifactState(生产代码零使用者) / describe_std_module(仅 test_toolchain_stdmod.cpp:84 调用) / .xlings.json pin bump(与本 PR 无关,且会制造合并冲突) / docs/superpowers/ 目录树(本仓库设计文档惯例是 .agents/docs/,159 个文件在那)。

五、mcpp-vscode 侧的影响是正向的

改为:

spawn `mcpp build --configure-only --format json`
  → 读退出码
  → 读 compile_commands.json
  → 按 invalidatedBy 清单设置 watcher

相比解析 NDJSON 事件流(seq 校验 / operationId 关联 / 超期操作隔离 / 事件类型状态机),扩展侧代码更少。而且 mcpp-community/mcpp-vscode#5 里那些 TODO —— multi-root watcher、mcpp.path 变化重配、避免 build/configure 重复执行 —— 本来就在扩展侧,不受影响。

更重要的是解除阻塞:--configure-only 是个小改动,可以很快合入并发版;不必等一套 6562 行走完 review → 发布 → 索引传播 → pin 更新的全链路。

另外 mcpp-community/mcpp-vscode#5 里"明确首次打开工程是执行完整 build,还是只执行 IDE configure,避免重复工作"这个纠结,在前提复核之后就清楚了:两者能力相同,差别只在成本。所以答案是首次打开只跑 --configure-only,用户显式要求完整语义(模块 BMI)时才 mcpp build

六、这个 PR 里做得好、值得保留到后续工作的部分

不想让这些被一起丢掉,几处明显高于平均水平:

  • tests/e2e/198 用 FIFO 验证 operation-started 在 prepare 完成前就 flush(第 274-293 行),而且脚本里写明了理由:"若只在进程退出后读取整个文件,事件即使被错误地移到末尾也会假通过"。这是真正的抗假绿设计,同样的思路应该用在 --configure-only 的测试上。
  • find target -name '*.o' + find target -path '*/bin/*' 直接验证"没编译普通 TU、没链接",而不是验证代理指标。
  • tests/e2e/197 用整棵树 inventory(size + mtime + content)验证只读性。
  • write_fresh_compile_commands 里先 is_regular_file 再开流 —— Linux 上把目录交给 ifstream 会抛异常而非设 failbit,这是踩过才知道的。
  • replace_file 在 Windows 用 MoveFileExW 并直接用 path::c_str(),避免 noexceptwstring 临时分配抛出触发 terminate
  • FileLock::try_acquire(dir, ec) 区分"锁竞争"和"I/O 失败",并且有专门的单测。
  • tests/fixtures/ide/snapshot-v1.json 这种 golden fixture 的做法值得推广到 RFC: 统一机器可读输出协议 —— envelope + destructive + --format 归一 + 老命令永久兼容 #379 的每个 kind
  • 文档诚实:明确写了 configured 只表示"元数据指向存在的常规文件"而非新鲜度、明确写了 configure 不是 read-only 需要 workspace trust。这两点在 RFC: 统一机器可读输出协议 —— envelope + destructive + --format 归一 + 老命令永久兼容 #379 里会被固化成机器可读的字段(destructive)。

七、下一步

  1. 本 PR 保持开启但不再推进(便于后续引用),或转 draft —— 由你决定。
  2. A 部分开新 PR。如果你希望我来开,说一声。
  3. 协议方向的讨论移到 #379,那里 @ 了你一组具体问题。
  4. feat: integrate mcpp build --configure-only into clangd workflow mcpp-vscode#5 的实现分支需要返工这一点,抱歉 —— 但返工方向是变简单,而且能提前发版。

@Sunrisepeak

Copy link
Copy Markdown
Member

@wellwei 可以把A部分拆解个新PR, 然后其他的可以 在下面RFC讨论

@wellwei
wellwei marked this pull request as draft August 8, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: provide a versioned IDE model and pre-build compile database

3 participants