Skip to content

Commit 85c9037

Browse files
committed
#712 An `[xlings]` address resolves the payload xlings selected. A new module, mcpp.xpkg_version, ports xlings' version grammar (N segments, alpha segments, written-prefix equality for three or more segments, prefix ranges for one or two, operator ranges), and xpkg_payload_at selects among installed directories with it. `libglvnd@1.7` finds 1.7.0.1. #716 A provisioning stamp counts only while every address still resolves to a payload. A removed payload is re-provisioned online and refused offline, naming it. The build cache records the payload directories a build read, so the fast path declines when one is gone. xlings reports what each request resolved to (install_targets, protocol 1.1); mcpp records that per address and answers from the record first. #705 A host tool's tree stamp skips directories that hold their own mcpp.toml, and hashes paths as UTF-8. #710 The requesting build decides a host tool's toolchain once -- `--toolchain`, else the tool package's own (its workspace's, for a member), else the requesting build's host toolchain -- passes it to the sub-build as an override, and records it in the store key. A member built as a tool inherits the workspace root's root-position keys. #713 A member inherits the workspace root's `[xlings.workspace]` entries, conditional rows included; its own declaration of a package wins. #714 An unresolved `x.workspace = true` is refused at every position, naming the entry. `[build] sources = []` infers no library target. A rooted workspace resolves its own `workspace = true` entries. #715 A rule reaches a package's synthesised build program only through a device source whose extension it declared. #704 A host build applies its own `[target.<host-triple>]` row (toolchain, linkage, cxx_runtime), as `--target <host>` did. #707 `emit build-database` builds no host tool; one missing from the store is deferred with the note MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED (SPEC-005 v1.4 R2.5). #708 `mcpp::action::env(name, value)` and `cwd(dir)` (protocol 13), applied by the engine's action wrapper in its named form `__action`. An action with neither keeps the `__action-stamp` command line byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MajuX4J8ewFeWvZJjzRJt5
1 parent 52549fb commit 85c9037

24 files changed

Lines changed: 1528 additions & 256 deletions

‎docs/50-machine-output.md‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -489,20 +489,21 @@ program's directives: the manifest's own configuration, the toolchain, the
489489
module graph and the standard-library units are described as usual, and one
490490
`error` diagnostic, `MCPP_BUILD_DATABASE_PROGRAM_FAILED`, names it, with
491491
`path` naming its `build.mcpp`. A later failure that follows from the missing
492-
directives fails the whole member instead, under the rule above. A host tool
493-
a package requested that fails to build is a warning instead,
494-
`MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`, naming the tool, its package and the
495-
first line of the failure; planning continues, and a build program that only
496-
names the tool configures as it would after a successful build. `mcpp build`
497-
is unaffected by either: a build program or a host tool that fails there
498-
still fails the build.
492+
directives fails the whole member instead, under the rule above. The command
493+
builds no host tool (mcpp 2026.9.27.1+): a requested tool already in the
494+
global tool store is used, and one that is not is deferred, reported as the
495+
note `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED` naming the tool and its package.
496+
The build program that requested it receives the path the tool will be
497+
published at, and configures as it would after the tool was built. `mcpp build`
498+
is unaffected: it builds the tool, and a build program or a host tool that
499+
fails there still fails the build.
499500

500501
| code | severity | |
501502
|---|---|---|
502503
| `MCPP_LOCK_WOULD_CHANGE` | warning | the resolution differs from the project's `mcpp.lock`, which the command does not write |
503504
| `MCPP_GENERATED_FILE_NOT_MATERIALIZED` | warning | a root `[build] generated_files` entry is missing or stale on disk, and the command does not write it |
504505
| `MCPP_BUILD_DATABASE_STD_UNIT_UNDESCRIBED` | warning | no standard-library build command names its module source, so that unit is not listed |
505-
| `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT` | warning | a requested host tool failed to build; the tool is still built and its `check` actions still run |
506+
| `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED` | note | a requested host tool is not in the tool store and is not built by the command; the plan names the path it will be published at (2026.9.27.1+; replaces the 2026.9.26.2 warning `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`) |
506507
| `MCPP_BUILD_DATABASE_PROGRAM_FAILED` | error | a build program failed; its package is described without its directives |
507508

508509
`--protocol-version` declares `init-mcpp-home`, `read-project`, `network`,

‎docs/specs/build-database.md‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
|---|---|
55
| 规范编号 | SPEC-005 |
66
| 标题 | mcpp 输出的构建数据库:内容、取值规则与不写工程目录的保证 |
7-
| 状态 | 评审中 v1.3 |
8-
| 版本 | 1.3 |
7+
| 状态 | 评审中 v1.4 |
8+
| 版本 | 1.4 |
99
| 最后修改 | 2026-09-26 |
10-
| 对应实现 | mcpp >= 2026.9.15.1;v1.3 修改的 R2.5、R3.7、R3.8、R4.1、R5.2 为 mcpp >= 2026.9.26.2 |
10+
| 对应实现 | mcpp >= 2026.9.15.1;v1.3 修改的 R2.5、R3.7、R3.8、R4.1、R5.2 为 mcpp >= 2026.9.26.2;v1.4 修改的 R2.5 为 mcpp >= 2026.9.27.1 |
1111
| 相关设计文档 | `.agents/docs/2026-09-14-636-build-database-and-the-latest-xlings.md`<br>`.agents/docs/2026-09-26-compile-database-and-issue-699-design.md` |
12-
| 相关 issue | #636, #648, #655, #699, #702 |
12+
| 相关 issue | #636, #648, #655, #699, #702, #707 |
1313
| 依据的外部规范 | S1「C++ Build Database: IDE Profile」profile 0.2.0 与 S2 0.2.0 §3.4,取自 https://github.com/Sunrisepeak/lsp-mcpp-private 提交 `b82859d`(schema 自提交 `28ecd6e` 起未变);S2 0.3.0 §3.4 的部分回答(S2-3.4-12、S2-3.4-13,Sunrisepeak/mcpp-language-server#25);JSON Compilation Database |
1414

1515
## 0. 适用范围
@@ -52,11 +52,13 @@ Database 定义,本规范不重复它们的字段定义,只规定 mcpp 作为生
5252
- **R2.4** 根包 `[build] generated_files` 中缺失或内容与声明不一致的文件不被写入,
5353
每个输出一条警告 `MCPP_GENERATED_FILE_NOT_MATERIALIZED`。**已实现**
5454
- **R2.5** 构建程序照常运行,工作目录为包根,与 `mcpp build` 相同;构建程序在
55-
`MCPP_OUT_DIR` 之外写入的内容不在本保证之内。依赖提供的宿主工具照常构建到全局
56-
工具库,它声明的 `check` 动作照常运行。构建失败的宿主工具在本命令下降级为
57-
警告 `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`,消息点名工具、其所属包与失败信息
58-
的第一行;规划继续,请求该工具的构建程序收到的是该工具本应发布到的路径。
59-
`mcpp build` 不受影响,宿主工具构建失败在其中仍使目标失败。**已实现**
55+
`MCPP_OUT_DIR` 之外写入的内容不在本保证之内。命令不构建依赖提供的宿主工具
56+
(R2.2):全局工具库中已有的工具照常使用;库中没有的工具被推迟,请求它的构建
57+
程序收到该工具将被发布到的路径,命令输出说明 `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED`,
58+
消息点名工具与其所属包。被推迟的工具的包不被规划,它声明的动作不运行。构建程序
59+
若在配置期执行该路径,遇到的情形与工具构建失败时相同(SPEC-007 R5.3)。
60+
`mcpp build` 不受影响:它构建宿主工具,构建失败仍使目标失败。**已实现**
61+
(mcpp >= 2026.9.27.1)
6062
- **R2.6** `mcpp --protocol-version` 为这条命令声明 `init-mcpp-home`、`read-project`、
6163
`network`、`write-global-cache` 与 `exec-build-script`,不声明 `write-project`。
6264
**已实现**
@@ -200,3 +202,4 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
200202
| 1.1 | 2026-09-16 | R5.2 增加离线诊断码 `MCPP_OFFLINE_DOWNLOAD_REQUIRED`;R5.3 的 `network` 按观测列出;新增 R5.4(子进程不继承调用方描述符,xlings 子进程有期限并随 mcpp 结束)(#648)。 |
201203
| 1.2 | 2026-09-17 | R3.7 陈述 `arguments` 的每一项是编译器收到的参数,单元 flag 按 SPEC-004 §8 的词列出(#655)。 |
202204
| 1.3 | 2026-09-26 | R2.5:`emit` 下构建失败的宿主工具是警告。R3.7:`work-directory` 是输出目录,模块接口单元的 `arguments` 带语言 flag。R3.8:标准库单元的 `provides` 指向 std 缓存中的 BMI,工具链带 `build-id`。R4.1:compile-commands 文档包含标准库单元(S1-12-1)。R5.2:成员各自规划,构建程序失败的包不带其指令地被描述(#699,#702)。 |
205+
| 1.4 | 2026-09-26 | R2.5:命令不构建宿主工具;工具库中没有的工具被推迟,输出说明 `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED`,取代 1.3 的警告 `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`(#707)。 |

‎docs/zh/50-machine-output.md‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -452,18 +452,19 @@ mcpp emit build-database [--spec s1|compile-commands] --format json
452452
构建程序失败的包(#699 第 2 项)会被描述为不含该程序产生的指令:清单自身
453453
的那部分配置、工具链、模块图与标准库单元仍照常描述,另附一条 `error` 诊断
454454
`MCPP_BUILD_DATABASE_PROGRAM_FAILED` 点名它,`path` 为它的 `build.mcpp`。若
455-
后续失败是由缺失的指令引起的,则按上面的规则使整个成员失败。包请求的宿主
456-
工具构建失败则降级为警告 `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`,点名工具、
457-
所属包与失败信息的第一行;规划继续进行,只点名该工具而不运行它的构建程序
458-
会像该工具构建成功时一样完成配置。这两者都不影响 `mcpp build`:构建程序或
459-
宿主工具在其中失败仍然会使构建失败。
455+
后续失败是由缺失的指令引起的,则按上面的规则使整个成员失败。这条命令
456+
不构建宿主工具(mcpp 2026.9.27.1+):全局工具库中已有的被请求工具照常使用,
457+
库中没有的被推迟,以说明 `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED` 报告,点名
458+
工具与所属包。请求它的构建程序收到该工具将被发布到的路径,并像工具已构建时
459+
一样完成配置。`mcpp build` 不受影响:它构建该工具,构建程序或宿主工具在其中
460+
失败仍然会使构建失败。
460461

461462
| 诊断码 | 严重级别 | |
462463
|---|---|---|
463464
| `MCPP_LOCK_WOULD_CHANGE` | 警告 | 解析结果与项目的 `mcpp.lock` 不一致,命令不写这个文件 |
464465
| `MCPP_GENERATED_FILE_NOT_MATERIALIZED` | 警告 | 根包 `[build] generated_files` 中的某个文件缺失或内容已过期,命令不写这个文件 |
465466
| `MCPP_BUILD_DATABASE_STD_UNIT_UNDESCRIBED` | 警告 | 没有任何标准库构建命令点名它的模块源文件,该单元因此不被列出 |
466-
| `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT` | 警告 | 被请求的宿主工具构建失败;该工具仍会被构建,它的 `check` 动作仍会运行 |
467+
| `MCPP_BUILD_DATABASE_HOST_TOOL_DEFERRED` | 说明 | 被请求的宿主工具不在工具库中,命令不构建它;计划给出它将被发布到的路径(2026.9.27.1+;取代 2026.9.26.2 的警告 `MCPP_BUILD_DATABASE_HOST_TOOL_UNBUILT`) |
467468
| `MCPP_BUILD_DATABASE_PROGRAM_FAILED` | 错误 | 构建程序失败;它所属的包被描述为不含它产生的指令 |
468469

469470
`--protocol-version` 为这条命令声明 `init-mcpp-home`、`read-project`、

‎modules/buildmcpp/src/directives.cppm‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,14 @@ std::optional<mcpp::manifest::BuildAction> decode_action(std::string_view payloa
11351135
// is identical either way, so a cache entry written before this field
11361136
// existed replays as one that never set it.
11371137
a.outputDir = j.value("output_dir", std::string{});
1138+
// mcpp#708, protocol 13. Omitted by an action that sets neither, so a
1139+
// payload written before the fields existed decodes as one that never
1140+
// set them. An `env` entry is `NAME=value` with a non-empty name.
1141+
arr("env", a.env);
1142+
for (auto const& e : a.env)
1143+
if (auto eq = e.find('='); eq == std::string::npos || eq == 0)
1144+
return std::nullopt;
1145+
a.cwd = j.value("cwd", std::string{});
11381146
if (a.command.empty() || a.outputs.empty()) return std::nullopt;
11391147
// Prepare only: `output_dir` is the whole point of the role (R3.2,
11401148
// R3.3) -- a `prepare` action with none declared would have a stamp
@@ -1268,6 +1276,23 @@ std::string action_error(const Directives& d) {
12681276
" payload: {}", *role, payload);
12691277
}
12701278
if (decode_action(payload)) continue;
1279+
// Named before the general message, like the role above: an `env`
1280+
// entry that is not `NAME=value` is the one thing wrong with an
1281+
// otherwise complete declaration.
1282+
try {
1283+
auto j = nlohmann::json::parse(payload);
1284+
if (auto it = j.find("env"); it != j.end() && it->is_array())
1285+
for (auto const& e : *it) {
1286+
const auto v = e.is_string() ? e.get<std::string>() : std::string{};
1287+
if (auto eq = v.find('='); eq == std::string::npos || eq == 0)
1288+
return std::format(
1289+
"build.mcpp declared an action whose environment entry "
1290+
"\"{}\" is not NAME=value.\n"
1291+
" mcpp::action::env(name, value) sets one variable "
1292+
"for the action's command; the name may not be empty.\n"
1293+
" payload: {}", v, payload);
1294+
}
1295+
} catch (...) {}
12711296
// A malformed action is a hard error, never a skip: an action that
12721297
// silently does not exist produces a build missing generated sources,
12731298
// and the user is left staring at a "no such file" three edges away.
@@ -1326,6 +1351,11 @@ void prepare_actions(std::vector<mcpp::manifest::BuildAction>& actions,
13261351
// package root it was written against.
13271352
if (!a.outputDir.empty() && a.outputDir.find("${mcpp.") == std::string::npos)
13281353
a.outputDir = abs_against(pkgRoot, a.outputDir);
1354+
// The command's directory (mcpp#708), anchored the same way: a
1355+
// relative spelling names a directory of the package that declared
1356+
// the action (SPEC-007 R2.2), never the build directory.
1357+
if (!a.cwd.empty() && a.cwd.find("${mcpp.") == std::string::npos)
1358+
a.cwd = abs_against(pkgRoot, a.cwd);
13291359
if (a.role != mcpp::manifest::BuildAction::Role::Source) continue;
13301360
for (auto const& o : a.outputs) {
13311361
if (o.find("${mcpp.") != std::string::npos) continue;

‎modules/buildmcpp/src/program_protocol.cppm‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,15 @@ export namespace mcpp::build::program_protocol {
9696
// same reason `deploy` and `runtime_search_dir` do -- a program that uses
9797
// them fails to COMPILE on an older engine, naming the missing symbol,
9898
// rather than reaching that engine as a string it misreads.
99-
inline constexpr int kProtocolVersion = 12;
99+
// v13 (mcpp#708): adds `mcpp::action::env(name, value)` and
100+
// `mcpp::action::cwd(dir)` -- the environment and the directory of an
101+
// action's command, which an argv with no shell cannot express. They reach
102+
// the engine as the `env` and `cwd` fields of the `action` payload and are
103+
// omitted when unset, so an action that uses neither serialises to the bytes
104+
// it did under v12 and no cached entry changes meaning. Same cost as v5's: a
105+
// package calling `env()` fails on an older engine at the build.mcpp COMPILE,
106+
// because that engine's bundled module has no such method.
107+
inline constexpr int kProtocolVersion = 13;
100108

101109
// ── Cache-format epoch ─────────────────────────────────────────────────────
102110
//

‎modules/buildmcpp/src/tool_store.cppm‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ nlohmann::json to_json(const Key& k);
117117
// and moves in both directions -- an edit and its reversal each produce a new
118118
// key, which is what the criterion demands. Build products, the version
119119
// control directory and the engine's own scratch are excluded, since they
120-
// change without the sources changing.
120+
// change without the sources changing. So is any directory below the root
121+
// that holds its own mcpp.toml: that is another package, keyed by its own
122+
// stamp when the tool depends on it and no input otherwise (#705).
121123
std::string tree_stamp(const std::filesystem::path& root);
122124

123125
// <cacheRoot>/tool/<index>/<pkg>@<ver>/<keyHex>/
@@ -213,14 +215,26 @@ std::string tree_stamp(const fs::path& root) {
213215
for (; it != fs::recursive_directory_iterator(); it.increment(ec)) {
214216
if (ec) break;
215217
const auto& p = it->path();
216-
const auto name = p.filename().string();
218+
const auto name = p.filename();
217219
if (it->is_directory(ec)) {
218-
if (name == "target" || name == ".git" || name == ".mcpp") it.disable_recursion_pending();
220+
// A directory holding its own mcpp.toml is ANOTHER PACKAGE (#705).
221+
// If the tool depends on it, that package is in the key through
222+
// `upstreamKeys` with its own stamp; if not, nothing in it is an
223+
// input of this tool. The case that forced this is a consumer
224+
// nested inside the tool's tree -- a fixture, an example, a
225+
// workspace member: every build of the consumer wrote under its
226+
// own directory and so rebuilt the tool it was building with.
227+
if (name == "target" || name == ".git" || name == ".mcpp"
228+
|| fs::is_regular_file(p / "mcpp.toml", ec))
229+
it.disable_recursion_pending();
219230
continue;
220231
}
221232
if (!it->is_regular_file(ec)) continue;
222233
if (name == "compile_commands.json") continue;
223-
const auto rel = p.lexically_relative(root).generic_string();
234+
// UTF-8, not the code page: this is a walk of a tree mcpp does not
235+
// control, and a stamp is an identity (check_narrow_conversions.sh).
236+
const auto rel8 = p.lexically_relative(root).generic_u8string();
237+
const std::string rel(rel8.begin(), rel8.end());
224238
const auto sz = fs::file_size(p, ec);
225239
const auto mt = fs::last_write_time(p, ec).time_since_epoch().count();
226240
rows.push_back(std::format("{}|{}|{}", rel, sz, mt));

‎modules/manifest/src/toml.cppm‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4270,6 +4270,18 @@ void apply_defaults_and_infer(Manifest& m, const std::filesystem::path& root) {
42704270
}
42714271
}
42724272
const bool hasModuleInterface = !moduleInterfaceExt.empty();
4273+
// `sources = []` states that the default build compiles nothing, so
4274+
// an interface under `src/` is not a library of that build (#714). It
4275+
// is the shape of a build-logic package: its module is reachable only
4276+
// through a feature that host-module consumers request. Inferring a
4277+
// library anyway made every build of the package -- a bare
4278+
// `mcpp build`, `emit build-database` -- link an archive with no
4279+
// inputs and refuse. A package whose features add a library's sources
4280+
// declares that target in `[targets]`. An accidentally empty glob
4281+
// (`sources = ["srcs/**"]`) is not this case and is still refused as an
4282+
// empty link (#533).
4283+
const bool declaredNothing =
4284+
m.buildConfig.sourcesDeclared && m.buildConfig.sources.empty();
42734285

42744286
if (hasMain) {
42754287
// #622 A3: inference stays `Binary`, deliberately. `app` is a
@@ -4286,7 +4298,7 @@ void apply_defaults_and_infer(Manifest& m, const std::filesystem::path& root) {
42864298
m.targetsInferred = true;
42874299
m.inferredNotes.push_back(
42884300
std::format("target {} (bin from src/main.cpp)", m.package.name));
4289-
} else if (hasModuleInterface) {
4301+
} else if (hasModuleInterface && !declaredNothing) {
42904302
Target t;
42914303
t.name = m.package.name;
42924304
t.kind = Target::Library;

‎modules/manifest/src/types.cppm‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,17 @@ struct BuildAction {
589589
// to still exist after a successful build and has itself just removed.
590590
std::string depfile;
591591
std::string description;
592+
// Environment variables set for the COMMAND, each `NAME=value`, in the
593+
// order declared (mcpp#708). Added to the environment the build already
594+
// passes on, never replacing it: `PATH`, `MCPP_OFFLINE` and the rest reach
595+
// the command as they reach every other edge (SPEC-007 R3.7). No name has
596+
// a meaning of its own here, `PATH` included.
597+
std::vector<std::string> env;
598+
// The directory the command runs in (mcpp#708). Empty means the build
599+
// directory, which is where every action ran before this field. Absolute
600+
// once `prepare_actions` has run; a relative spelling names a directory
601+
// of the declaring package (SPEC-007 R2.2).
602+
std::string cwd;
592603
};
593604

594605
// `[resources]` — metadata and assets compiled INTO the produced artifact

0 commit comments

Comments
 (0)