Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2350e2b
docs: design read-only IDE snapshot protocol
wellwei Aug 5, 2026
9933126
docs: plan read-only IDE snapshot implementation
wellwei Aug 5, 2026
210a31b
feat(ide): define snapshot protocol model
wellwei Aug 5, 2026
3885a63
fix(ide): align snapshot fixture contract
wellwei Aug 5, 2026
9759acf
feat(ide): inspect workspaces without side effects
wellwei Aug 5, 2026
1ad07f9
fix(ide): restrict workspace package selector aliases
wellwei Aug 5, 2026
5336e9f
fix(ide): harden workspace inspection paths
wellwei Aug 5, 2026
68e1bbb
fix(ide): contain workspace inspection paths
wellwei Aug 6, 2026
1b6b6a1
feat(ide): serialize versioned workspace snapshots
wellwei Aug 6, 2026
4e99879
feat(cli): add read-only ide snapshot command
wellwei Aug 6, 2026
fee82fe
test(ide): prove snapshot is read only
wellwei Aug 6, 2026
c58bdf4
docs(ide): define universal project model and early CDB plan
wellwei Aug 6, 2026
e8dbd2c
feat(ide): define configuration and event protocol
wellwei Aug 6, 2026
0c9c2e4
feat(build): add fresh compile database publication
wellwei Aug 6, 2026
54faf3e
feat(ide): publish compile database before build
wellwei Aug 6, 2026
1e8f1f4
fix(ide): stage module prerequisites before publishing cdb
wellwei Aug 6, 2026
67587e3
test(ide): gate llvm configure e2e and track release version
wellwei Aug 6, 2026
18f41e8
docs(ide): record configure validation boundaries
wellwei Aug 6, 2026
b2d092b
fix(ide): keep configure events machine-readable
wellwei Aug 7, 2026
9ae6d9f
fix(ide): make configure publication portable
wellwei Aug 7, 2026
458c1a0
chore(release): bootstrap pin -> 2026.8.6.3
wellwei Aug 7, 2026
bd3843d
fix(ide): harden configured snapshot publication
wellwei Aug 7, 2026
50daf50
fix(test): preserve best-effort test inventory
wellwei Aug 7, 2026
10c8142
fix(ide): distinguish publication lock failures
wellwei Aug 7, 2026
0e519e0
docs(ide): document protocol and client lifecycle
wellwei Aug 7, 2026
07bbc16
docs(ide): clarify client trust and protocol limits
wellwei Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(ide): harden configured snapshot publication
  • Loading branch information
wellwei committed Aug 7, 2026
commit bd3843dec91ae4029b46abe2eb104454005a1a1d
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,9 @@ import mcpplibs.cmdline;
- Fingerprinted BMI cache: hashed by compiler/flags/standard library, shared across projects
- Ninja backend: auto-generated build.ninja, parallel compilation
- compile_commands.json generated automatically (ready for clangd / ccls)
- `mcpp ide configure` publishes a fresh CDB before ordinary TU compilation,
- `mcpp ide configure` publishes a fresh CDB for ordinary and `tests/**/*.cpp`
translation units (including test dev-dependency flags) before compilation,
and persists the configured snapshot
so clangd can analyze a project whose source currently has build errors
- First-class C support: `.c` files auto-detected, mixed C/C++ projects
- User-defined cflags / cxxflags / ldflags / c_standard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,28 @@ artifact 状态包括 `pending`、`ready`、`stale`、`missing`、`unavailable`

回复文件写入临时文件后关闭并校验,再使用同目录 rename 发布。`index-<generation>.json` 只引用完整回复文件;客户端先读 index,再按引用读取对象。这借鉴 CMake File API 的 reply index 和不可变 reply 文件,避免读到半个 JSON。

截至 2026-08-07,configured 阶段先实现以下可恢复子集:

```text
<project-root>/.mcpp/ide/
current.json
replies/
snapshot-<contentHash>.json
compile_commands-<cdbHash>.json
```

`current.json` 是最后一个原子切换点,记录 `projectId`、`configurationId`、
`snapshotId`、toolchain 和内容寻址 CDB。`ide snapshot` 会读取并验证该记录;无
mcpp metadata 的根 CDB 仍为 `stale/unverified`。按 configuration 分目录的 index、
ready 和 last-known-good 仍由后续 prepare/publish 阶段实现。

configured publisher 会在替换根 compatibility CDB 前保存旧内容;若随后
`current.json` 发布失败,则尝试原子恢复旧根 CDB。内容寻址 reply 可以保留为不可见文件。
根 CDB 与 `current.json` 是两个独立路径,操作系统不能为它们提供跨文件事务;如果外部
进程在回滚期间持续占用文件,发布结果会返回复合错误,调用方必须把它视为需要人工恢复的
不一致状态。发布过程持有 `.mcpp/ide/.lock`,多个 configure 可以并行解析,但不能交错
切换 mutable 指针。

### 7.2 CDB 发布时序

```text
Expand Down Expand Up @@ -380,20 +402,33 @@ Node 单元测试和 `clangd --check` 不能替代真实 Extension Development H

### 当前实现的验证边界

截至 2026-08-06,早期 CDB 实现还有两个 IDE 路径的测试缺口,不能据此推断核心 Ninja 构建后端存在回归:
截至 2026-08-07,早期 CDB 实现仍有一个工具链矩阵缺口,不能据此推断核心 Ninja 构建后端存在回归:

1. cached dependency BMI 的发布前 staging 已有 helper 单测和 macOS/Clang E2E,但尚缺真实 GCC `.gcm`、MSVC `.ifc` 缓存命中工程。核心 Ninja 后端仍通过自己的 `stage_file` edges 物化缓存产物;此缺口的直接风险是 IDE CDB 引用了未物化或路径漂移的 BMI,而不是普通构建无法完成。

1. 尚未通过真实 `configure_project()` 流程制造 staging 失败,并断言已有根 CDB 保持不变。普通 `mcpp build/test` 继续使用既有 `write_compile_commands()` 和 Ninja 执行路径,不经过 IDE 的 fresh CDB 发布与回退逻辑。完整 last-known-good 状态仍属于后续 snapshot publish 层,不应把它与当前测试缺口混为已实现能力。
2. cached dependency BMI 的发布前 staging 已有 helper 单测和 macOS/Clang E2E,但尚缺真实 GCC `.gcm`、MSVC `.ifc` 缓存命中工程。核心 Ninja 后端仍通过自己的 `stage_file` edges 物化缓存产物;此缺口的直接风险是 IDE CDB 引用了未物化或路径漂移的 BMI,而不是普通构建无法完成。
该缺口仍跨越 `BuildPlan::cachedBmi`、`bmi_traits()`、`stage_file()` 和 Ninja BMI 命名等共享契约。应按任务 3 将目标路径收敛到 `mcpp.build.artifact_layout`,由 Ninja 与 IDE 共同消费,再补真实工具链矩阵。GCC/MSVC E2E 只证明路径和产物契约,不代表 clangd 能消费 `.gcm` 或 `.ifc`;该能力仍按工具链 capability 明确表达。

第二项仍跨越 `BuildPlan::cachedBmi`、`bmi_traits()`、`stage_file()` 和 Ninja BMI 命名等共享契约。应按任务 3 将目标路径收敛到 `mcpp.build.artifact_layout`,由 Ninja 与 IDE 共同消费,再补真实工具链矩阵。GCC/MSVC E2E 只证明路径和产物契约,不代表 clangd 能消费 `.gcm` 或 `.ifc`;该能力仍按工具链 capability 明确表达。
真实 CLI E2E 已覆盖 `build.mcpp` 失败,并逐字节断言已有根 CDB 和
`current.json` 不变;configure 还会把选中 member 的 `tests/**/*.cpp` 合成为与
`mcpp test` 相同的 targets,并在发现测试时自动启用 dev-dependencies,使测试 CDB
包含实际测试构建所需的 include/define。workspace selector 与 member 目录调用使用物理
workspace root 和规范化 member path,产生相同的 `projectId + configurationId`。

当前 `configure` 已在解析前发送带 `operationId` 的 `operation-started`,并在成功或
失败的全部后续事件中沿用该 ID;失败序列为
`diagnostic -> operation-finished(status=failed)`。解析前尚无 resolved
`configurationId`,因此早期 started 事件不携带该字段,客户端应以 `operationId`
关联整个生命周期。阶段化 `progress`、取消、迟到事件过滤,以及
index/current/last-known-good 发布仍属于后续 publish/prepare 任务。现阶段不能据此
宣称完整的长任务管理和失败回退协议已经完成。
关联整个生命周期。阶段化 `progress`、取消、迟到事件过滤,以及按 configuration
的 index、ready 和 last-known-good 发布仍属于后续 prepare 任务。configured
`current.json` 已原子发布,但现阶段仍不能据此宣称完整的长任务管理和失败回退协议
已经完成。

`ide snapshot` 当前验证 metadata schema、工程内路径约束和 CDB 文件存在性,但不会
在只读路径重新执行 resolved build fingerprint。manifest、lockfile、source 集或工具链
输入变化后,插件必须触发一次 `ide configure` 才能得到新 snapshot;在此之前
`configured` 仅表示“最后一次已发布 configured snapshot 仍可读取”,不表示所有输入
已重新证明为 fresh。验收标准中的主动 stale 判定仍属于后续 configuration index/freshness
工作。

当前 `configured_snapshot_id` 已绑定 configuration、BuildContext fingerprint 和
CDB 内容;完整 resolved module graph、artifact metadata 和输入 provenance 尚未
Expand Down
15 changes: 4 additions & 11 deletions src/build/compile_commands.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import std;
import mcpp.build.plan;
import mcpp.build.flags;
import mcpp.libs.json;
import mcpp.platform.fs;

export namespace mcpp::build {

Expand Down Expand Up @@ -281,17 +282,9 @@ write_fresh_compile_commands(const std::filesystem::path& path, std::string_view
}
}

std::filesystem::rename(temp, path, ec);
std::error_code typeEc;
const bool replaceableFile = std::filesystem::is_regular_file(path, typeEc);
if (ec && replaceableFile && !typeEc) {
// Windows rename 不覆盖已有普通文件;只对文件执行 remove + rename,
// 不能把同名目录等其他对象当成可替换的旧 CDB。
std::error_code removeEc;
std::filesystem::remove(path, removeEc);
ec.clear();
std::filesystem::rename(temp, path, ec);
}
// POSIX 使用 rename,Windows 使用 MoveFileEx(REPLACE_EXISTING);两者
// 都不会先删除旧文件,因此失败时 last-known-good CDB 仍然存在。
mcpp::platform::fs::replace_file(temp, path, ec);
if (ec) {
const auto publishError = ec;
std::error_code cleanupEc;
Expand Down
70 changes: 8 additions & 62 deletions src/build/execute.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export module mcpp.build.execute;
import std;
import mcpp.build.build_program; // #359 glob inputs the mtime sweep cannot see
import mcpp.build.prepare;
import mcpp.build.test_targets;
import mcpp.diag;
import mcpp.build.plan;
import mcpp.build.backend;
Expand Down Expand Up @@ -930,72 +931,17 @@ export int run_tests(std::span<const std::string> passthrough,
return 2;
}

// Workspace scoping: discovery must run against the MEMBER, not the
// workspace root — otherwise `tests/**/*.cpp` globs every member's tests
// together (two `tests/main.cpp` → "duplicate test name 'main'"). When a
// member is selected (via -p, threaded as package_filter), glob from its
// dir; prepare_build below resolves the SAME member, so the two agree.
// (--workspace fans out over members at the cmd layer, one call per member.)
auto testRoot = *root;
if (auto rm = mcpp::manifest::load(*root / "mcpp.toml"); rm) {
auto member = mcpp::project::resolve_member_dir(*rm, *root, overrides.package_filter);
if (!member) { mcpp::ui::error(member.error()); return 2; }
if (!member->empty()) testRoot = *member;
}

// 1. Discover test files (scoped to the member/package).
auto testFiles = mcpp::modgraph::expand_glob(testRoot, "tests/**/*.cpp");
if (testFiles.empty()) {
// 测试命令和 IDE configure 共用同一发现规则,避免 CDB 与真实测试
// 编译在 member 选择、名称和 `[build].flags` 上发生漂移。
auto discovery = discover_test_targets(*root, overrides.package_filter);
if (!discovery) { mcpp::ui::error(discovery.error()); return 2; }
auto testRoot = std::move(discovery->root);
auto testTargets = std::move(discovery->targets);
if (testTargets.empty()) {
std::println("no tests found in tests/");
return 0;
}

// [build].flags globs also cover tests: a glob names files — whether they
// are scanned sources or test TUs is orthogonal. Matched entries ride the
// per-target flag channel (issue #131) on the synthesized test target.
// (Feature-folded entries are prepare-time state; tests take the base
// [build].flags — sufficient for per-test compile options.)
struct TestGlobFlags {
mcpp::manifest::GlobFlags gf;
std::set<std::filesystem::path> files;
};
std::vector<TestGlobFlags> testGlobFlags;
if (auto mm = mcpp::manifest::load(testRoot / "mcpp.toml")) {
for (auto const& gf : mm->buildConfig.globFlags) {
auto hits = mcpp::modgraph::expand_glob(testRoot, gf.glob);
testGlobFlags.push_back({gf, {hits.begin(), hits.end()}});
}
}

// 2. Synthesize a Target for each test file.
// Name = path relative to tests/, extension dropped, '/' separators —
// so tests/00-a/0.cpp and tests/01-b/0.cpp coexist as '00-a/0' and
// '01-b/0' (stems alone would collide). Flat layouts keep their old
// names ('tests/smoke.cpp' → 'smoke').
std::vector<mcpp::manifest::Target> testTargets;
std::set<std::string> seenNames;
for (auto& f : testFiles) {
auto rel = std::filesystem::relative(f, testRoot / "tests");
auto name = rel.replace_extension("").generic_string();
if (!seenNames.insert(name).second) {
mcpp::ui::error(std::format(
"duplicate test name '{}' (two test files map to the same name)", name));
return 2;
}
mcpp::manifest::Target t;
t.name = name;
t.kind = mcpp::manifest::Target::TestBinary;
// Relative to the member/package root prepare_build will operate on.
t.main = std::filesystem::relative(f, testRoot).string();
for (auto const& tgf : testGlobFlags) {
if (!tgf.files.contains(f)) continue;
for (auto const& d : tgf.gf.defines) t.defines.push_back(d);
for (auto const& fl : tgf.gf.cflags) t.cflags.push_back(fl);
for (auto const& fl : tgf.gf.cxxflags) t.cxxflags.push_back(fl);
}
testTargets.push_back(std::move(t));
}

// --list: enumerate (filtered) tests and stop — no toolchain resolution,
// no build. Names/paths come straight from discovery, so this also works
// on tests that do not currently compile.
Expand Down
74 changes: 74 additions & 0 deletions src/build/test_targets.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// mcpp.build.test_targets — shared tests/**/*.cpp discovery for test and IDE.
export module mcpp.build.test_targets;

import std;
import mcpp.manifest;
import mcpp.modgraph.scanner;
import mcpp.project;

export namespace mcpp::build {

struct TestTargetDiscovery {
std::filesystem::path root;
std::vector<mcpp::manifest::Target> targets;
};

std::expected<TestTargetDiscovery, std::string>
discover_test_targets(const std::filesystem::path& manifestRoot,
std::string_view packageFilter = {}) {
auto rootManifest = mcpp::manifest::load(manifestRoot / "mcpp.toml");
if (!rootManifest) return std::unexpected(rootManifest.error().format());

auto member = mcpp::project::resolve_member_dir(
*rootManifest, manifestRoot, packageFilter);
if (!member) return std::unexpected(member.error());
const auto testRoot = member->empty() ? manifestRoot : *member;

auto manifest = member->empty()
? std::move(rootManifest)
: mcpp::manifest::load(testRoot / "mcpp.toml");
if (!manifest) return std::unexpected(manifest.error().format());

const auto testFiles = mcpp::modgraph::expand_glob(testRoot, "tests/**/*.cpp");
if (testFiles.empty()) return TestTargetDiscovery{.root = testRoot};

// `[build].flags` 对测试源同样生效,必须与 `mcpp test` 的编译参数一致。
struct TestGlobFlags {
mcpp::manifest::GlobFlags flags;
std::set<std::filesystem::path> files;
};
std::vector<TestGlobFlags> globFlags;
for (const auto& flags : manifest->buildConfig.globFlags) {
auto files = mcpp::modgraph::expand_glob(testRoot, flags.glob);
globFlags.push_back({flags, {files.begin(), files.end()}});
}

TestTargetDiscovery result{.root = testRoot};
std::set<std::string> seenNames;
for (const auto& file : testFiles) {
auto relative = std::filesystem::relative(file, testRoot / "tests");
auto name = relative.replace_extension("").generic_string();
if (!seenNames.insert(name).second) {
return std::unexpected(std::format(
"duplicate test name '{}' (two test files map to the same name)", name));
}

mcpp::manifest::Target target;
target.name = std::move(name);
target.kind = mcpp::manifest::Target::TestBinary;
target.main = std::filesystem::relative(file, testRoot).string();
for (const auto& matched : globFlags) {
if (!matched.files.contains(file)) continue;
target.defines.insert(target.defines.end(),
matched.flags.defines.begin(), matched.flags.defines.end());
target.cflags.insert(target.cflags.end(),
matched.flags.cflags.begin(), matched.flags.cflags.end());
target.cxxflags.insert(target.cxxflags.end(),
matched.flags.cxxflags.begin(), matched.flags.cxxflags.end());
}
result.targets.push_back(std::move(target));
}
return result;
}

} // namespace mcpp::build
4 changes: 3 additions & 1 deletion src/cli/cmd_ide.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,9 @@ export int cmd_ide_configure(const mcpplibs::cmdline::ParsedArgs& parsed) {
const auto operationId = mcpp::ide::new_operation_id();
std::println("{}", mcpp::ide::configure_started_event(operationId));
std::fflush(stdout);
const auto result = mcpp::ide::configure_project(request);
const auto result = mcpp::ide::run_configure_safely([&] {
return mcpp::ide::configure_project(request);
});
if (!result) {
for (const auto& line : mcpp::ide::configure_failure_events(
result.error(), operationId, 2))
Expand Down
Loading