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
feat(ide): publish compile database before build
  • Loading branch information
wellwei committed Aug 7, 2026
commit 54faf3e8fb29c1891df1b8283e1414111a9e2ae4
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ 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,
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
26 changes: 18 additions & 8 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,9 @@ export struct BuildOverrides {
bool strict = false; // --strict: schema warnings become errors
std::string capabilities; // --cap blas=openblas,lapack=mkl (provider pins)
std::string cache_mode; // --cache global|local|off ("" = unset)
// IDE configure 只需要准确的编译参数和预期 BMI 路径;实际 std BMI
// 由 ide prepare/build 物化,避免 CDB 发布等待一次编译。
bool materialize_std_modules = true;
};

// ── git dependency helpers ──────────────────────────────────────────────────
Expand Down Expand Up @@ -4743,10 +4746,14 @@ prepare_build(bool print_fingerprint,
if (f.enabled && !f.flags.empty()) dst += std::format(" ({})", f.flags);
if (!f.enabled) dst += std::format(" ({})", f.reason);
}
std::println("c++fly on {}: {}; enabled: {}; skipped: {}",
tc->label(), stdFlagAndDialect,
enabled.empty() ? "(none)" : enabled,
skipped.empty() ? "(none)" : skipped);
// IDE configure 的 stdout 是 NDJSON;普通 build 仍保留这条
// 人类可读摘要,但 quiet 模式必须完全静默,避免污染协议。
if (!mcpp::ui::is_quiet()) {
std::println("c++fly on {}: {}; enabled: {}; skipped: {}",
tc->label(), stdFlagAndDialect,
enabled.empty() ? "(none)" : enabled,
skipped.empty() ? "(none)" : skipped);
}
}
for (auto& f : mcpp::toolchain::cppfly::effective_dialect_flags(
*tc, m->cppStandard.experimental,
Expand Down Expand Up @@ -4850,10 +4857,13 @@ prepare_build(bool print_fingerprint,
// a std BMI built without it structurally lacks std::meta). Both
// pieces were already in the fingerprint; this fixes the COMMAND
// construction the fingerprint promised (stdFlagAndDialect above).
auto sm = mcpp::toolchain::ensure_built(
*tc, m->package.standard, stdFlagAndDialect,
mcpp::platform::macos::deployment_target(
m->buildConfig.macosDeploymentTarget));
const auto deploymentTarget = mcpp::platform::macos::deployment_target(
m->buildConfig.macosDeploymentTarget);
auto sm = overrides.materialize_std_modules
? mcpp::toolchain::ensure_built(
*tc, m->package.standard, stdFlagAndDialect, deploymentTarget)
: mcpp::toolchain::describe_std_module(
*tc, m->package.standard, stdFlagAndDialect, deploymentTarget);
if (!sm) return std::unexpected(sm.error().message);
stdBmiPath = sm->bmiPath;
stdObjectPath = sm->objectPath;
Expand Down
23 changes: 22 additions & 1 deletion src/cli.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ void print_usage() {
std::println(" mcpp emit xpkg [-V VER] [-o FILE] Generate xpkg Lua entry");
std::println(" mcpp xpkg parse <file.lua> [--json] Validate an xpkg descriptor (resolver grammar)");
std::println(" mcpp ide snapshot [selectors] Inspect workspace state for IDE clients");
std::println(" mcpp ide configure [selectors] Resolve and publish a fresh IDE CDB");
std::println("");
std::println("Resource management:");
std::println(" mcpp toolchain install|list|default Manage mcpp's private toolchains");
Expand Down Expand Up @@ -407,8 +408,28 @@ int run(int argc, char** argv) {
.help("Echo dev-dependency inclusion"))
.option(cl::Option("format").takes_value().value_name("FORMAT")
.help("Output format (json)")))
.subcommand(cl::App("configure")
.description("Resolve the project and publish a fresh compile database")
.option(cl::Option("package").short_name('p').takes_value().value_name("MEMBER")
.help("Select one workspace member"))
.option(cl::Option("workspace").help("Reserved for workspace fan-out"))
.option(cl::Option("profile").takes_value().value_name("NAME")
.help("Build profile"))
.option(cl::Option("target").takes_value().value_name("TRIPLE")
.help("Target triple"))
.option(cl::Option("features").takes_value().value_name("LIST")
.help("Comma-separated features"))
.option(cl::Option("cap").takes_value().value_name("LIST")
.help("Comma-separated capability pins"))
.option(cl::Option("include-dev-dependencies")
.help("Include development dependencies"))
.option(cl::Option("format").takes_value().value_name("FORMAT")
.help("Output format (ndjson)")))
.action(wrap_rc([&dispatch_sub](const cl::ParsedArgs& p) {
return dispatch_sub("ide", p, {{"snapshot", cmd_ide_snapshot}});
return dispatch_sub("ide", p, {
{"snapshot", cmd_ide_snapshot},
{"configure", cmd_ide_configure},
});
})))

// ─── resource management ───────────────────────────────────────
Expand Down
30 changes: 30 additions & 0 deletions src/cli/cmd_ide.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export module mcpp.cli.cmd_ide;

import std;
import mcpplibs.cmdline;
import mcpp.ide.configure;
import mcpp.ide.inspect;
import mcpp.ide.model;
import mcpp.ide.snapshot;
Expand Down Expand Up @@ -75,4 +76,33 @@ export int cmd_ide_snapshot(const mcpplibs::cmdline::ParsedArgs& parsed) {
return inspection.state == mcpp::ide::SnapshotState::Unavailable ? 3 : 0;
}

export int cmd_ide_configure(const mcpplibs::cmdline::ParsedArgs& parsed) {
const auto format = parsed.value("format").value_or("ndjson");
if (format != "ndjson") {
// IDE 客户端依赖逐行 JSON;拒绝未知格式,避免先执行 prepare
// 再发现无法解析,或在错误请求下意外发布兼容 CDB。
std::println(stderr, "error: unsupported IDE configure format '{}'; expected ndjson",
format);
return 2;
}
std::error_code ec;
auto start = std::filesystem::current_path(ec);
if (ec) start = ".";
auto absolute = std::filesystem::absolute(start, ec);
if (!ec) start = std::move(absolute);

mcpp::ide::ConfigureRequest request{
.start = start.lexically_normal(),
.selectors = selectors_from(parsed),
};
const auto result = mcpp::ide::configure_project(request);
if (!result) {
std::println("{}", mcpp::ide::configure_error_event(result.error()));
return 3;
}
for (const auto& line : mcpp::ide::configure_events(*result))
std::println("{}", line);
return 0;
}

} // namespace mcpp::cli
189 changes: 189 additions & 0 deletions src/ide/configure.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// mcpp.ide.configure — resolve a build plan and publish its CDB before Ninja.
// 该模块只负责 IDE 配置阶段,不驱动普通源文件编译或最终链接。
module;
#include <cstdio>
export module mcpp.ide.configure;
import std;
import mcpp.build.compile_commands;
import mcpp.build.flags;
import mcpp.build.prepare;
import mcpp.ide.model;
import mcpp.libs.json;
import mcpp.project;
import mcpp.toolchain.fingerprint;
import mcpp.ui;

namespace mcpp::ide {
namespace {
using Json = nlohmann::ordered_json;

// configure 需要暂时屏蔽 prepare_build 的人类状态输出;用 RAII 保证
// 解析失败或异常返回时也恢复调用者的 quiet 状态。
struct QuietGuard {
const bool previous = mcpp::ui::is_quiet();

QuietGuard() { mcpp::ui::set_quiet(true); }
~QuietGuard() { mcpp::ui::set_quiet(previous); }
};

std::string join(const std::vector<std::string>& values) {
std::string result;
for (const auto& value : values) {
if (!result.empty()) result.push_back(',');
result += value;
}
return result;
}

std::string toolchain_fingerprint(const mcpp::toolchain::Fingerprint& fingerprint) {
// Build fingerprint 还包含 flags、依赖和 BMI 状态;这些变化应只使
// snapshotId 变化,不能让同一个工具链配置在 IDE 中产生新配置项。
std::string identity;
for (std::size_t i = 0; i < 5; ++i) {
if (i != 0) identity.push_back('\x1f');
identity += fingerprint.parts[i];
}
return "toolchain-fnv1a64:" + mcpp::toolchain::hash_string(identity);
}

ConfigurationSelectors configuration_selectors(const Selectors& selectors) {
return {
.package = selectors.package, .workspace = selectors.workspace,
.profile = selectors.profile.value_or(""), .target = selectors.target.value_or(""),
.features = selectors.features, .capabilities = selectors.capabilities,
.includeDevDependencies = selectors.includeDevDependencies,
};
}

Json event(std::uint64_t seq, std::string_view type) {
Json value = Json::object();
value["schemaVersion"] = 1; value["seq"] = seq; value["type"] = type;
return value;
}
} // namespace

export struct ConfigureRequest {
std::filesystem::path start;
Selectors selectors;
};

export struct ConfigureResult {
std::string projectId;
std::string configurationId;
std::string snapshotId;
std::filesystem::path projectRoot;
std::filesystem::path compileCommands;
std::filesystem::path compatibilityCompileCommands;
std::string toolchain;
std::string toolchainFingerprint;
std::size_t compileCommandCount = 0;
std::filesystem::path stdModule;
std::string stdModuleState;
};

export std::expected<ConfigureResult, std::string>
configure_project(const ConfigureRequest& request) {
auto start = request.start.empty() ? std::filesystem::current_path() : request.start;
auto root = mcpp::project::find_manifest_root(start);
if (!root) return std::unexpected("no mcpp.toml found in current directory or any parent");
if (request.selectors.workspace) {
return std::unexpected(
"workspace configure fan-out is not available yet; select one member with --package");
}

mcpp::build::BuildOverrides overrides;
overrides.project_root = *root;
if (request.selectors.package) overrides.package_filter = *request.selectors.package;
if (request.selectors.profile) overrides.profile = *request.selectors.profile;
if (request.selectors.target) overrides.target_triple = *request.selectors.target;
overrides.features = join(request.selectors.features);
overrides.capabilities = join(request.selectors.capabilities);
overrides.materialize_std_modules = false;

// configure 的结构化 stdout 不能混入普通进度;mcpp 自身写入的项目文件
// 仍保持原有 prepare_build 语义,但状态行转为诊断/事件。
QuietGuard quiet;
auto context = mcpp::build::prepare_build(
/*print_fingerprint=*/false, request.selectors.includeDevDependencies, {},
std::move(overrides));
if (!context) return std::unexpected(context.error());

const auto configSelectors = configuration_selectors(request.selectors);
auto resolvedSelectors = configSelectors;
resolvedSelectors.profile = context->profile;
resolvedSelectors.target = context->tc.targetTriple;
resolvedSelectors.cacheMode = std::string(
mcpp::build::cache_mode_name(context->cacheMode));
resolvedSelectors.cppStandard = context->manifest.package.standard;
const auto toolchainFingerprint = toolchain_fingerprint(context->fp);
const auto configId = configuration_id(
context->projectRoot, resolvedSelectors, toolchainFingerprint);
const auto projectId = std::format("project-fnv1a64:{}",
mcpp::toolchain::hash_string(context->projectRoot.generic_string()));
auto flags = mcpp::build::compute_flags(context->plan);
const auto content = mcpp::build::emit_compile_commands(context->plan, flags);
auto parsed = nlohmann::json::parse(content, nullptr, /*allow_exceptions=*/false);
if (parsed.is_discarded() || !parsed.is_array())
return std::unexpected("mcpp produced an invalid compile database");

const auto ideRoot = context->projectRoot / ".mcpp" / "ide" / "replies";
const auto snapshotCdb = ideRoot / std::format("compile_commands-{}.json", configId);
if (auto written = mcpp::build::write_fresh_compile_commands(snapshotCdb, content); !written)
return std::unexpected(written.error());
const auto compatibilityCdb = context->projectRoot / "compile_commands.json";
if (auto written = mcpp::build::write_fresh_compile_commands(compatibilityCdb, content); !written)
return std::unexpected(written.error());

const auto snapshotId = std::format("snapshot-fnv1a64:{}",
mcpp::toolchain::hash_string(configId + "\x1f" + content));
return ConfigureResult{
.projectId = projectId, .configurationId = configId, .snapshotId = snapshotId,
.projectRoot = context->projectRoot, .compileCommands = snapshotCdb,
.compatibilityCompileCommands = compatibilityCdb, .toolchain = context->tc.label(),
.toolchainFingerprint = toolchainFingerprint, .compileCommandCount = parsed.size(),
.stdModule = context->plan.stdBmiPath,
.stdModuleState = context->plan.stdBmiPath.empty()
? "not-required"
: (std::filesystem::is_regular_file(context->plan.stdBmiPath) ? "ready" : "pending"),
};
}

export std::vector<std::string> configure_events(const ConfigureResult& result) {
std::vector<std::string> lines;
auto started = event(1, "operation-started");
started["operation"] = "configure"; started["configurationId"] = result.configurationId;
lines.push_back(started.dump());
auto published = event(2, "snapshot-published");
published["phase"] = "configured"; published["state"] = "configured";
published["projectId"] = result.projectId; published["configurationId"] = result.configurationId;
published["snapshotId"] = result.snapshotId;
published["compileCommands"] = result.compileCommands.generic_string();
published["compatibilityCompileCommands"] = result.compatibilityCompileCommands.generic_string();
published["compileCommandCount"] = result.compileCommandCount;
published["toolchain"] = result.toolchain;
published["toolchainFingerprint"] = result.toolchainFingerprint;
if (!result.stdModule.empty()) {
auto stdModule = Json::object();
stdModule["kind"] = "std-module";
stdModule["path"] = result.stdModule.generic_string();
stdModule["state"] = result.stdModuleState;
published["stdModule"] = std::move(stdModule);
}
lines.push_back(published.dump());
auto finished = event(3, "operation-finished");
finished["operation"] = "configure"; finished["status"] = "success";
finished["phase"] = "configured"; finished["configurationId"] = result.configurationId;
lines.push_back(finished.dump());
return lines;
}

export std::string configure_error_event(std::string_view message) {
auto value = event(1, "diagnostic");
value["diagnostic"] = Json::object({
{"code", "MCPP_IDE_CONFIGURE_FAILED"}, {"severity", "error"},
{"source", "mcpp"}, {"message", message},
});
return value.dump();
}

} // namespace mcpp::ide
4 changes: 4 additions & 0 deletions src/ide/model.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ struct ConfigurationSelectors {
std::vector<std::string> features;
std::vector<std::string> capabilities;
bool includeDevDependencies = false;
std::string cacheMode = "global";
std::string cppStandard = "c++23";
};

std::string configuration_id(const std::filesystem::path& workspaceRoot,
Expand Down Expand Up @@ -130,6 +132,8 @@ std::string configuration_id(const std::filesystem::path& workspaceRoot,
for (const auto& feature : selectors.features) append("feature", feature);
for (const auto& capability : selectors.capabilities) append("capability", capability);
append("dev", selectors.includeDevDependencies ? "1" : "0");
append("cache", selectors.cacheMode);
append("standard", selectors.cppStandard);
append("toolchain", toolchainFingerprint);

std::uint64_t hash = 14695981039346656037ull;
Expand Down
Loading