Skip to content

Commit 5867295

Browse files
committed
feat(emit): baseline and local arguments, private, and config-files
The S1 SHOULD fields the lsp-mcpp validator reads, with values the plan determines: - `baseline-arguments` and `local-arguments`. A unit's arguments are its driver, the set's baseline, its local arguments and its own trailing `-c <source> -o <object>`, whose operands name the unit's source and object from its work directory. The baseline is the longest prefix every unit of the set shares; a prefix keeps argument order, which decides include search and macro definitions. - `private` is false: every module is visible to every set. - `config-files` lists what the driver reads without being named: the clang `<driver>.cfg` of resolve_clang_driver unless the units pass `--no-default-config`, and GCC's `lib/gcc/<triple>/<version>/specs` (or the major version) beside the driver. e2e 688 gains J (the decomposition, private, config-files) and K (the discovered test's imports inside a comment and a raw string are not `requires`); its F mapping compares paths with one separator, and the fixture's main.cpp is well-formed. SPEC-005 R3.2a, R3.6, R3.8 and R3.9a state the rules. Refs #636
1 parent 36124cb commit 5867295

4 files changed

Lines changed: 168 additions & 24 deletions

File tree

‎.agents/docs/2026-09-14-636-build-database-and-the-latest-xlings.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,18 +305,31 @@ guarantee covers mcpp's writes. A build program that writes outside
305305

306306
| S1 | from | note |
307307
|---|---|---|
308-
| toolchain id | `<compiler_name>-<version>-<targetTriple>` | |
308+
| toolchain id | `<compiler_family>-<version>-<triple>`, the triple as the compiler spells it | opaque to a consumer; `llvm`, as in `llvm@22.1.8` |
309309
| `family` | `Toolchain::compiler_name()` (`modules/toolchain-model/src/model.cppm:261-268`): `gcc`, `clang`, `msvc` | not `compiler_family()`, which answers `llvm` |
310310
| `driver`, `version`, `target`, `sysroot` | `binaryPath`, `version`, `targetTriple`, `sysroot` | |
311311
| `stdlib` | `name` and `version` from `stdlibId` and `stdlibVersion`; no `module-metadata` | std resolves through units (next row) |
312+
| `config-files` | clang: the `<driver>.cfg` of `resolve_clang_driver`, unless the units pass `--no-default-config`; GCC: `lib/gcc/<targetTriple>/<version or major>/specs` beside the driver | read from the layout the driver searches; no driver is run |
312313
| set `mcpp:std` | units for `stdModuleSource` and `stdCompatSource`, carrying the commands of 5.3 (3) | One rule for GCC's `bits/std.cc`, libc++'s `std.cppm`, MSVC's `std.ixx` and a package's own `std.cppm`; S1 §6.1 lets units outrank a manifest. `mcpp:` is the engine's reserved namespace (SPEC-002), so the name cannot collide with a package. |
313314
| sets | one per package; test targets' sources in `<package>:test` | `family-name` is the package; `ide.configuration` is `BuildContext::profile`; `ide.kind` comes from the package's declared targets |
314315
| `visible-sets` | every other set | The engine resolves imports over one flat graph per invocation (`scanner.cppm:1289-1319`). A narrower closure would describe a rule the build does not enforce; if the engine later refuses undeclared imports, the database inherits it. |
315316
| units | every `CompileUnit` except NASM units, as in the CDB (`compile_commands.cppm:225-228`) | rendered from the record of 5.3 (4) |
317+
| `baseline-arguments`, `local-arguments` | a unit's arguments are its driver, the set's baseline, its local arguments and its own `-c <source> -o <object>`; the baseline is the longest prefix every unit of the set shares | a prefix keeps argument order, which decides include search and macro definitions; a common subset would not |
318+
| `private` | `false` | every module is visible to every set, as the `visible-sets` row states |
316319
| `provides` | `providesModule` mapped to `""` | S1-8-6 permits an empty path for a producer that performs no build |
317320
| `requires` | `imports` | partitions are already written in full |
318321
| `ide.role` | the declaration form of 5.3 (5) | `unknown` for `scan_overrides` units |
319322

323+
A target's entry source that no `sources` glob matched (a discovered test, a
324+
`main` outside the globs) had its imports read from line-leading `import` alone,
325+
so an import in a comment or a raw string was planned as one. Validating the
326+
lsp-mcpp repository's own database against S1 found it: its scanner test
327+
required three modules no source provides. The entry is now read by the
328+
scanner (`scan_entry_file`); a file the scanner refuses, which this path never
329+
refused, keeps its line-leading imports with the role `unknown`. The standard
330+
library check before planning reads entries the same way, so the two cannot
331+
disagree about `import std`.
332+
320333
### 5.5 `watch` and `inputs-fingerprint`
321334

322335
- `watch` lists what the resolution read and a user edits: the manifests of the

‎docs/specs/build-database.md‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
| 标题 | mcpp 输出的构建数据库:内容、取值规则与不写工程目录的保证 |
77
| 状态 | 评审中 v1.0 |
88
| 版本 | 1.0 |
9-
| 最后修改 | 2026-09-14 |
9+
| 最后修改 | 2026-09-15 |
1010
| 对应实现 | mcpp >= 2026.9.15.1 |
1111
| 相关设计文档 | `.agents/docs/2026-09-14-636-build-database-and-the-latest-xlings.md` |
1212
| 相关 issue | #636 |
@@ -71,6 +71,10 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
7171
`target` 为编译器自身拼写的目标三元组;构建使用 sysroot 时给出 `sysroot`;`stdlib`
7272
给出 `name`(`libstdc++`、`libc++`、`msvc-stl` 或 `other`)与 `version`,不给出
7373
`module-metadata`,标准库模块经 §3.4 的单元解析。**已实现**
74+
- **R3.2a** `config-files` 列出驱动在命令行之外读取的配置文件,空数组表示没有:clang
75+
驱动旁的 `<驱动名>.cfg`,单元带 `--no-default-config` 时不列出;GCC 驱动库目录中
76+
`lib/gcc/<targetTriple>/<版本>/specs`,版本目录也可以只写主版本号。取值来自驱动
77+
搜索的目录布局,命令不运行驱动。**已实现**
7478

7579
### 3.2 集合
7680

@@ -82,7 +86,11 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
8286
- **R3.5** `family-name` 为包名,`mcpp:std` 集合的为 `mcpp:std`;`ide.configuration`
8387
为 profile 名;`ide.kind` 在测试集合为 `test`,在根包集合按其目标为 `library`、
8488
`executable` 或 `other`,在依赖包集合与 `mcpp:std` 为 `library`。**已实现**
85-
- **R3.6** 不输出 `baseline-arguments` 与 `local-arguments`。**已实现**
89+
- **R3.6** 单元的 `arguments` 依次是驱动、集合的 `baseline-arguments`、单元的
90+
`local-arguments`,以及单元自己结尾的 `-c <source> -o <object>`(若有;两个操作数
91+
相对 `work-directory` 指向 `source` 与 `object`)。`baseline-arguments` 是集合中每个
92+
单元去掉驱动与该结尾后的最长公共前缀。取前缀而不取公共子集,因为参数顺序决定
93+
头文件搜索与宏定义。**已实现**
8694

8795
### 3.3 翻译单元
8896

@@ -91,7 +99,7 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
9199
`file`、`directory`、`arguments`、`output` 取自同一条记录,因而逐字相同。
92100
**已实现**
93101
- **R3.8** `provides` 把单元提供的模块名映射到空字符串,命令不执行构建(S1-8-6);
94-
`requires` 为单元导入的模块名,分区写全名 `M:P`。不输出 `private`,即 `false`。
102+
`requires` 为单元导入的模块名,分区写全名 `M:P`。`private` 为 `false`,理由同 R3.4。
95103
**已实现**
96104
- **R3.9** `ide.role` 取自扫描器读到的模块声明形式:
97105

@@ -105,6 +113,11 @@ mcpp 输出的 S1 文档满足 S1 等级 2,不输出 `ide.options`。等级 3
105113
| `scan_overrides` 声明的单元;P1689 扫描中无法区分实现单元与导入者的单元 | `unknown` |
106114

107115
**已实现**
116+
- **R3.9a** 没有被 `sources` glob 匹配的目标入口源文件(发现的测试、glob 之外的
117+
`main`)与包源文件由同一扫描器读取,注释与原始字符串中的 `import` 不是导入。扫描器
118+
拒绝的入口文件(`#if` 块中的 `import`、头文件单元)从未在这条路径上被拒绝,现在也
119+
不被拒绝:`requires` 为其代码中行首的 `import`,`ide.role` 为 `unknown`。入口声明
120+
自身提供模块时,`ide.role` 为 `unknown`,因为构建不为该单元产出 BMI。**已实现**
108121

109122
### 3.4 标准库模块
110123

‎src/build/build_database.cppm‎

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import mcpp.modgraph.graph;
3535
import mcpp.modgraph.scanner;
3636
import mcpp.platform;
3737
import mcpp.toolchain.fingerprint;
38+
import mcpp.toolchain.linkmodel;
3839
import mcpp.toolchain.model;
3940
import mcpp.toolchain.stdmod;
4041

@@ -151,23 +152,95 @@ Redirect redirect_kind(std::string_view w) {
151152
return j == w.size() ? Redirect::Detached : Redirect::Attached;
152153
}
153154

154-
bool names_source(std::string_view word, const std::filesystem::path& source,
155+
bool names_path(std::string_view word, const std::filesystem::path& path,
155156
const std::filesystem::path& cwd) {
156157
std::filesystem::path w{std::string(word)};
157-
const auto want = source.lexically_normal();
158+
const auto want = path.lexically_normal();
158159
if (w.lexically_normal() == want) return true;
159160
if (!w.is_absolute() && !cwd.empty() && (cwd / w).lexically_normal() == want)
160161
return true;
161162
return false;
162163
}
163164

165+
// S1 `config-files`: the configuration files the driver reads without being
166+
// named on a command line. A clang driver reads the `.cfg` beside it unless a
167+
// unit passes `--no-default-config`, which mcpp does whenever that file exists
168+
// (mcpp.toolchain.linkmodel::resolve_clang_driver). A GCC driver reads the
169+
// `specs` file in its library directory for its own machine and version, which
170+
// a distribution may spell with the major version only. Found from the layout
171+
// the driver searches; no driver is run.
172+
nlohmann::json config_files(const mcpp::toolchain::Toolchain& tc,
173+
const std::vector<mcpp::build::UnitInvocation>& invocations) {
174+
nlohmann::json out = nlohmann::json::array();
175+
using C = mcpp::toolchain::CompilerId;
176+
std::error_code ec;
177+
if (tc.compiler == C::Clang) {
178+
const auto dm = mcpp::toolchain::resolve_clang_driver(tc);
179+
const bool read = std::ranges::any_of(invocations, [](auto const& inv) {
180+
return std::ranges::find(inv.arguments, "--no-default-config") == inv.arguments.end();
181+
});
182+
if (dm.hasCfg && read) out.push_back(native_string(dm.cfgPath));
183+
} else if (tc.compiler == C::GCC && !tc.targetTriple.empty()) {
184+
const auto machine = tc.binaryPath.parent_path().parent_path()
185+
/ "lib" / "gcc" / tc.targetTriple;
186+
const auto major = tc.version.substr(0, tc.version.find('.'));
187+
for (auto const& version : {tc.version, major}) {
188+
const auto specs = machine / version / "specs";
189+
if (!version.empty() && std::filesystem::is_regular_file(specs, ec)) {
190+
out.push_back(native_string(specs));
191+
break;
192+
}
193+
}
194+
}
195+
return out;
196+
}
197+
198+
// S1 `baseline-arguments` and `local-arguments`. A unit's arguments are its
199+
// driver, then the set's baseline, then its local arguments, then its own
200+
// trailing `-c <source> -o <object>` when it has one: the baseline is the longest
201+
// prefix that every unit of the set shares after the driver and without that
202+
// tail. A prefix keeps the order of the arguments, which decides include search
203+
// and macro definitions; a common subset would not.
204+
void split_baseline(nlohmann::json& set) {
205+
std::vector<std::vector<std::string>> semantic;
206+
for (auto const& u : set["translation-units"]) {
207+
auto args = u["arguments"].get<std::vector<std::string>>();
208+
std::vector<std::string> rest(args.size() > 1 ? args.begin() + 1 : args.end(), args.end());
209+
const std::filesystem::path cwd{u["work-directory"].get<std::string>()};
210+
const auto n = rest.size();
211+
if (n >= 4 && rest[n - 4] == "-c" && rest[n - 2] == "-o"
212+
&& names_path(rest[n - 3], u["source"].get<std::string>(), cwd)
213+
&& names_path(rest[n - 1], u["object"].get<std::string>(), cwd))
214+
rest.resize(n - 4);
215+
semantic.push_back(std::move(rest));
216+
}
217+
std::vector<std::string> baseline;
218+
if (!semantic.empty()) {
219+
baseline = semantic.front();
220+
for (auto const& unit : semantic) {
221+
std::size_t k = 0;
222+
while (k < baseline.size() && k < unit.size() && baseline[k] == unit[k]) ++k;
223+
baseline.resize(k);
224+
}
225+
}
226+
std::size_t i = 0;
227+
for (auto& u : set["translation-units"]) {
228+
const auto& unit = semantic[i++];
229+
u["local-arguments"] = std::vector<std::string>(
230+
unit.begin() + static_cast<std::ptrdiff_t>(baseline.size()), unit.end());
231+
}
232+
set["baseline-arguments"] = std::move(baseline);
233+
}
234+
164235
nlohmann::json toolchain_json(const mcpp::toolchain::Toolchain& tc,
165-
std::string_view compilerTriple) {
236+
std::string_view compilerTriple,
237+
const std::vector<mcpp::build::UnitInvocation>& invocations) {
166238
nlohmann::json j{
167-
{"family", std::string(tc.compiler_name())},
168-
{"version", tc.version},
169-
{"driver", tc.binaryPath.string()},
170-
{"target", std::string(compilerTriple)},
239+
{"family", std::string(tc.compiler_name())},
240+
{"version", tc.version},
241+
{"driver", tc.binaryPath.string()},
242+
{"target", std::string(compilerTriple)},
243+
{"config-files", config_files(tc, invocations)},
171244
};
172245
if (!tc.sysroot.empty()) j["sysroot"] = native_string(tc.sysroot);
173246
if (!tc.stdlibId.empty()) {
@@ -347,7 +420,7 @@ std::optional<Invocation> recover_invocation(const std::vector<std::string>& com
347420
}
348421
}
349422
const bool named = std::ranges::any_of(argv, [&](const std::string& w) {
350-
return names_source(w, source, cwd.empty() ? defaultDirectory : cwd);
423+
return names_path(w, source, cwd.empty() ? defaultDirectory : cwd);
351424
});
352425
if (!named) continue;
353426
return Invocation{cwd.empty() ? defaultDirectory : cwd, std::move(argv)};
@@ -398,8 +471,6 @@ Rendered render(std::span<const Member> members,
398471
const std::string compilerTriple = ctx.plan.targetSide.llvmTriple.empty()
399472
? ctx.tc.targetTriple : ctx.plan.targetSide.llvmTriple;
400473
const auto tcId = toolchain_id(ctx.tc, compilerTriple);
401-
if (!toolchains.contains(tcId))
402-
toolchains[tcId] = toolchain_json(ctx.tc, compilerTriple);
403474

404475
const auto rootName = qualified_name(ctx.manifest);
405476
std::set<std::filesystem::path> testSources;
@@ -424,7 +495,10 @@ Rendered render(std::span<const Member> members,
424495
};
425496

426497
const auto flags = mcpp::build::compute_flags(ctx.plan);
427-
for (auto& inv : mcpp::build::unit_invocations(ctx.plan, flags)) {
498+
auto invocations = mcpp::build::unit_invocations(ctx.plan, flags);
499+
if (!toolchains.contains(tcId))
500+
toolchains[tcId] = toolchain_json(ctx.tc, compilerTriple, invocations);
501+
for (auto& inv : invocations) {
428502
const auto& cu = *inv.unit;
429503
const bool isTest = testSources.contains(cu.source.lexically_normal());
430504
const std::string package = cu.packageName.empty() ? rootName : cu.packageName;
@@ -448,6 +522,7 @@ Rendered render(std::span<const Member> members,
448522
{"work-directory", std::move(inv.directory)},
449523
{"arguments", std::move(inv.arguments)},
450524
{"object", std::move(inv.output)},
525+
{"private", false},
451526
{"provides", std::move(provides)},
452527
{"requires", std::move(requires_)},
453528
{"ide", {{"role", std::string(role_name(cu.declaration))}}},
@@ -478,6 +553,7 @@ Rendered render(std::span<const Member> members,
478553
{"work-directory", native_string(inv->workDirectory)},
479554
{"arguments", std::move(inv->arguments)},
480555
{"object", native_string(object)},
556+
{"private", false},
481557
{"provides", {{std::string(module), ""}}},
482558
{"requires", std::move(requires_)},
483559
{"ide", {{"role", "module-interface"}}},
@@ -493,7 +569,7 @@ Rendered render(std::span<const Member> members,
493569
nlohmann::json visible = nlohmann::json::array();
494570
for (auto const& other : order)
495571
if (other != name) visible.push_back(other);
496-
sets.push_back(nlohmann::json{
572+
nlohmann::json setJson{
497573
{"name", name},
498574
{"family-name", set.familyName},
499575
{"visible-sets", std::move(visible)},
@@ -503,7 +579,9 @@ Rendered render(std::span<const Member> members,
503579
{"configuration", ctx.profile},
504580
{"kind", set.kind},
505581
}},
506-
});
582+
};
583+
split_baseline(setJson);
584+
sets.push_back(std::move(setJson));
507585
}
508586

509587
watch_file(ctx.projectRoot / "mcpp.lock");

0 commit comments

Comments
 (0)