Skip to content

Commit 35dcd1e

Browse files
committed
Merge branch 'feat/eight-t4' into feat/eight-reports
2 parents 6ef09b5 + a114160 commit 35dcd1e

16 files changed

Lines changed: 753 additions & 64 deletions

‎docs/04-mcpp-toml.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1471,9 +1471,11 @@ is a table of exactly two strings. `from` is relative to the declaring package's
14711471
root, and `to` is relative to the executable's directory, where `"."` means that
14721472
directory itself. Both are separated by `/` on every host, and neither may be
14731473
absolute, name a drive, or contain an empty, `.` or `..` component; an entry
1474-
that does is refused, and the refusal names its index. Two sources for one
1475-
destination are refused naming the destination, while one file name in two
1476-
directories is not a collision. `deploy` is a key of its own rather than a table
1474+
that does is refused, and the refusal names its index. Two or more sources for
1475+
one destination merge into a single copy, placed once every source is
1476+
byte-identical (mcpp#723); they are refused at build time, naming every source
1477+
and the destination, when they are not. One file name in two directories is
1478+
not a collision. `deploy` is a key of its own rather than a table
14771479
form of `deploy_files`, because a descriptor reader that predates it meets `{`
14781480
inside `deploy_files` and does not terminate, whereas it skips a `runtime` key it
14791481
does not know. `mcpp pack` stages the files of both keys at the same relative

‎docs/specs/build-plugins.md‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,19 @@
140140
从不作为 `-L`)、`mcpp run` 的加载路径、`mcpp pack` 的闭包搜索与运行时校验,并把依赖包的
141141
声明传到消费方的可执行文件。(**已实现**,mcpp#702)
142142
- **R4.2** 一个在配置时已知的文件需要位于程序旁的某个相对位置时(Qt 的平台插件、Vulkan 的
143-
ICD 清单),插件用 `deploy`。(**已实现**,协议 11)
143+
ICD 清单),插件用 `deploy`。(**已实现**,协议 11)两个或更多来源为同一目的地各自声明
144+
`deploy` 时,规划期不再把它当作错误拒绝:被声明的来源此时可能尚未生成,其内容无法比较。
145+
引擎把它们合并为一条施工边,将每个来源都列为该边的输入;施工时(`mcpp stage`)逐字节核对
146+
这些来源,字节相同则放置,不同则该边失败,消息点名每一个来源与该目的地。(**已实现**,
147+
mcpp#723)
144148
- **R4.3** Windows 的可执行文件没有运行路径。`mcpp run` 通过 `PATH` 使用运行时搜索目录,
145149
`mcpp pack` 把闭包需要的 DLL 放到程序旁(**已实现**)。链接之后,引擎把程序直接或间接导入的、
146150
位于其运行时搜索目录中的非系统 DLL 放到程序旁,使从构建目录直接启动的程序同样能找到它们;
147151
闭包的求解与 `mcpp pack` 相同,DLL 在其目录中被替换后下一次构建再次放置。(**已实现**,
148-
mcpp#702)
152+
mcpp#702)一个目的地只有一个写者:本条的放置以 R4.2 与工具链耦合运行时 DLL(`toolchain-
153+
coupled`)合并而成的部署清单为唯一权威,**禁止**写入该清单已经放置的名字。遇到清单已放置
154+
的名字时,本条只比较该名字现有文件与运行时搜索目录中同名文件的字节,相同则不作声张,不同
155+
则以警告点名这一差异,**禁止**覆盖清单已放置的文件。(**已实现**,mcpp#723)
149156
- **R4.4** 插件**禁止**在 `link_flag` 中写运行路径(`-Wl,-rpath,...`),**必须**使用 R4.1。
150157
(作者义务)
151158

‎docs/zh/04-mcpp-toml.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,9 +1401,10 @@ Link intent 把各个发现阶段分开处理:
14011401
两个字符串组成的表。`from` 相对声明它的包根目录,`to` 相对可执行
14021402
文件所在目录,`"."` 意味着那个目录本身。两者在每个宿主上都以 `/`
14031403
分隔,都不能是绝对路径、不能命名一个驱动器、不能含有空、`.` 或 `..`
1404-
组成部分;不满足的条目会被拒绝,拒绝信息点名它的索引。两个来源指向
1405-
同一个目的地会被拒绝并点名那个目的地,而一个文件名出现在两个不同
1406-
目录下不算冲突。`deploy` 是一个独立的键,而不是 `deploy_files` 的
1404+
组成部分;不满足的条目会被拒绝,拒绝信息点名它的索引。两个或更多
1405+
来源指向同一个目的地会合并为一次拷贝,字节相同则放置(mcpp#723);
1406+
不同则在施工时失败,点名每一个来源与该目的地。一个文件名出现在两个
1407+
不同目录下不算冲突。`deploy` 是一个独立的键,而不是 `deploy_files` 的
14071408
表格形式,因为一个早于它出现的描述符读取器,遇到 `deploy_files` 里的
14081409
`{` 会无法终止,而它会跳过一个不认识的 `runtime` 键。`mcpp pack` 把
14091410
这两个键指向的文件,以打包出的可执行文件为参照,拷贝到同样的相对

‎src/build/flags.cppm‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,16 +1547,19 @@ CompileFlags compute_flags(const BuildPlan& plan) {
15471547
auto clash = std::ranges::find_if(plan.runtimeDeployFiles,
15481548
[&](auto const& d) { return d.dest == dest; });
15491549
if (clash != plan.runtimeDeployFiles.end()) {
1550-
if (clash->source.lexically_normal()
1551-
!= src.lexically_normal())
1550+
if (std::ranges::none_of(clash->sources,
1551+
[&](auto const& s) {
1552+
return s.lexically_normal()
1553+
== src.lexically_normal();
1554+
}))
15521555
f.diagnostics.push_back(std::format(
15531556
"toolchain-coupled would stage '{}' beside the "
15541557
"artifact, but this project already deploys "
15551558
"'{}' there; keeping the project's file",
1556-
src.string(), clash->source.string()));
1559+
src.string(), clash->sources.front().string()));
15571560
continue;
15581561
}
1559-
f.toolchainRuntimeDeploy.push_back({src, dest});
1562+
f.toolchainRuntimeDeploy.push_back({{src}, dest});
15601563
}
15611564
}
15621565
}

‎src/build/ninja_backend.cppm‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1935,7 +1935,16 @@ std::string emit_ninja_string(const BuildPlan& plan) {
19351935
for (auto const& d : plan.linkIntent.runtimeSearchDirs)
19361936
dirs += " " + ninja_command_word(d.string());
19371937
append("rule place_dlls\n");
1938-
append(" command = $mcpp place-dlls --output $out --depfile $out.d $in" + dirs + "\n");
1938+
// `$placed` (SPEC-007 R4.2/R4.3, #723 self-review: one destination,
1939+
// one writer) names, per edge, the DLLs the merged deploy list
1940+
// already places directly beside THIS program. `place-dlls` skips
1941+
// them — that list is the authority for its own destinations, and
1942+
// this mechanism only compares and warns instead of racing it. It is
1943+
// always exactly one shell word, comma-joining the names (never
1944+
// empty in the ninja_command_word sense: `''`/`""` when there is
1945+
// nothing to say) so it can never absorb `$in` or the directories
1946+
// that follow it, whatever it lists.
1947+
append(" command = $mcpp place-dlls --output $out --depfile $out.d $in $placed" + dirs + "\n");
19391948
append(" depfile = $out.d\n");
19401949
append(" deps = gcc\n");
19411950
append(" description = DLLS $in\n\n");
@@ -2958,6 +2967,18 @@ std::string emit_ninja_string(const BuildPlan& plan) {
29582967
append("build " + exe + ".dlls: place_dlls " + exe
29592968
+ (prepareStamps.empty() ? std::string{} : " |" + prepareStamps)
29602969
+ "\n");
2970+
// One destination, one writer (SPEC-007 R4.2/R4.3, #723 self-
2971+
// review): the names the merged deploy list already places in
2972+
// THIS program's own directory. `place-dlls` must not place a
2973+
// second, competing copy of one of these — see the `$placed`
2974+
// comment above, and `place_runtime_dlls` in mcpp.pack.
2975+
std::string placedHere;
2976+
for (auto const& d : deployFiles) {
2977+
if (d.dest.parent_path() != lu.output.parent_path()) continue;
2978+
if (!placedHere.empty()) placedHere += ',';
2979+
placedHere += d.dest.filename().string();
2980+
}
2981+
append(" placed = " + ninja_command_word(placedHere) + "\n");
29612982
append("default " + exe + ".dlls\n\n");
29622983
}
29632984

@@ -2975,10 +2996,20 @@ std::string emit_ninja_string(const BuildPlan& plan) {
29752996
// previous `mcpp run` gets the skip-if-equivalent treatment instead of a
29762997
// hard "cannot copy" failure.
29772998
// Inert on RPATH platforms where the merged deploy list is empty.
2999+
//
3000+
// SPEC-007 R4.2 (#723): a destination with more than one source (two
3001+
// packages of this graph each generated the same file) becomes ONE edge
3002+
// with every source as an input, not one edge per source. `mcpp stage`
3003+
// is where they are checked against each other's bytes — planning cannot,
3004+
// because a generated source may not exist yet. A destination with
3005+
// exactly one source (every project before this feature, and most
3006+
// packages after it) emits the exact same line as always: the loop below
3007+
// reduces to the one-word case with no change in spelling.
29783008
for (auto const& d : deployFiles) {
2979-
append(std::format("build {} : stage_file {}\n",
2980-
escape_ninja_path(d.dest),
2981-
escape_ninja_path(d.source)));
3009+
std::string ins;
3010+
for (auto const& s : d.sources) ins += " " + escape_ninja_path(s);
3011+
append(std::format("build {} : stage_file{}\n",
3012+
escape_ninja_path(d.dest), ins));
29823013
}
29833014
if (!deployFiles.empty())
29843015
append("\n");

‎src/build/plan.cppm‎

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -437,7 +437,15 @@ struct BuildPlan {
437437
// byte-for-byte unchanged; only a Windows prebuilt-DLL package (or a test
438438
// that ships a .dll) populates it. dest is relative to outputDir.
439439
struct DeployFile {
440-
std::filesystem::path source; // absolute source DLL
440+
// Absolute source paths. Usually one; more than one means two or more
441+
// packages of this graph each generate a file for this destination
442+
// (SPEC-007 R4.2) — an `artifacts` dependency and its consumer asking
443+
// one plugin for the same translation catalog is the case #723 was
444+
// filed for. Planning no longer refuses this: at planning time a
445+
// generated source may not exist yet, so its content cannot be
446+
// compared. `mcpp stage` (mcpp.build.stage) is where the sources are
447+
// actually checked, at build time, against each other's bytes.
448+
std::vector<std::filesystem::path> sources;
441449
std::filesystem::path dest; // relative to outputDir, e.g. bin/libopenblas.dll
442450
};
443451
std::vector<DeployFile> runtimeDeployFiles;
@@ -1430,38 +1438,41 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14301438

14311439
// `toDir` is a `runtime.deploy` destination, relative to the executable's
14321440
// directory; empty and "." both mean that directory itself, which is where
1433-
// every `deploy_files` entry goes. The collision check keys on the full
1434-
// relative destination, so two files of one name in two directories do not
1435-
// collide, and two sources for one destination still do.
1441+
// every `deploy_files` entry goes. The check keys on the full relative
1442+
// destination, so two files of one name in two directories do not
1443+
// collide.
1444+
//
1445+
// Two sources for one destination are no longer refused HERE (SPEC-007
1446+
// R4.2, #723): at planning time a generated source may not exist yet, so
1447+
// its content cannot be compared. Both stay as inputs of the one
1448+
// `stage_file` edge this destination becomes (ninja_backend.cppm), and
1449+
// `mcpp stage` (mcpp.build.stage) is where the invariant — one
1450+
// destination, one content — is actually checked, once the sources exist.
1451+
// A source already listed for this destination (the ordinary case: the
1452+
// same file reached through two graph edges) is not duplicated.
14361453
auto add_deploy = [&](const std::filesystem::path& source,
1437-
std::string_view toDir = {})
1438-
-> std::optional<std::string> {
1454+
std::string_view toDir = {}) {
14391455
const auto normalized = source.lexically_normal();
14401456
auto destDir = std::filesystem::path("bin");
14411457
if (!toDir.empty() && toDir != ".") destDir /= std::filesystem::path(toDir);
14421458
const auto dest = destDir / source.filename();
14431459
auto existing = std::ranges::find_if(plan.runtimeDeployFiles,
14441460
[&](auto const& value) { return value.dest == dest; });
14451461
if (existing != plan.runtimeDeployFiles.end()) {
1446-
if (existing->source.lexically_normal() != normalized) {
1447-
return std::format(
1448-
"runtime deploy collision: '{}' and '{}' both target '{}'",
1449-
existing->source.string(), normalized.string(), dest.string());
1450-
}
1451-
return std::nullopt;
1462+
if (std::ranges::find(existing->sources, normalized)
1463+
== existing->sources.end())
1464+
existing->sources.push_back(normalized);
1465+
return;
14521466
}
1453-
plan.runtimeDeployFiles.push_back({normalized, dest});
1454-
return std::nullopt;
1467+
plan.runtimeDeployFiles.push_back({{normalized}, dest});
14551468
};
14561469
// Structured deploy files are explicit and platform-neutral. Legacy
14571470
// library_dirs keeps its one-train DLL discovery behavior below.
14581471
for (auto const& source : plan.linkIntent.deployFiles) {
1459-
if (auto collision = add_deploy(source))
1460-
return std::unexpected(std::move(*collision));
1472+
add_deploy(source);
14611473
}
14621474
for (auto const& entry : plan.linkIntent.deploy) {
1463-
if (auto collision = add_deploy(entry.from, entry.to))
1464-
return std::unexpected(std::move(*collision));
1475+
add_deploy(entry.from, entry.to);
14651476
}
14661477
for (auto const& dir : plan.linkIntent.runtimeSearchDirs) {
14671478
std::error_code dirEc;
@@ -1472,8 +1483,7 @@ make_plan(const mcpp::manifest::Manifest& manifest,
14721483
std::ranges::transform(ext, ext.begin(),
14731484
[](unsigned char c){ return std::tolower(c); });
14741485
if (ext != ".dll") continue;
1475-
if (auto collision = add_deploy(entry.path()))
1476-
return std::unexpected(std::move(*collision));
1486+
add_deploy(entry.path());
14771487
}
14781488
}
14791489
// The same private runtime directories embedded as executable RUNPATH are

‎src/build/stage.cppm‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ std::expected<StageOutcome, StageError> stage_file(const std::filesystem::path&
7575
const std::filesystem::path& dst,
7676
const StageOptions& opts = {});
7777

78+
// Publish one destination that may have more than one source (SPEC-007 R4.2,
79+
// mcpp#723): two or more packages of one graph can each generate a
80+
// byte-identical file and deploy it under the same name, and the plan no
81+
// longer refuses that at planning time — a generated source may not exist yet
82+
// when the plan is built, so its content cannot be compared there. This is
83+
// where the invariant is actually checked, because by build time every source
84+
// exists: every source is compared against the first by content
85+
// (`same_content`, never masking the way `bmi_equivalent` does — a deploy
86+
// target is an ordinary file, not a BMI with a compiler-embedded clock), and
87+
// the first is staged when they all agree. Disagreement is refused, naming
88+
// every source and the destination, so the message points at every producer
89+
// instead of an arbitrary pair.
90+
//
91+
// `srcs.size() == 1` is not a special case bolted on top: it takes the same
92+
// path as every other count, with the comparison loop simply empty, and ends
93+
// up calling `stage_file` on that one source exactly as before this function
94+
// existed. This is what keeps a project with one source per destination —
95+
// every project before this feature — emitting the same `mcpp stage`
96+
// invocation it always has.
97+
std::expected<StageOutcome, StageError> stage_files(
98+
const std::vector<std::filesystem::path>& srcs,
99+
const std::filesystem::path& dst,
100+
const StageOptions& opts = {});
101+
78102
// Byte-for-byte comparison (exported for tests). False when either file is
79103
// unreadable or the sizes differ.
80104
bool same_content(const std::filesystem::path& a, const std::filesystem::path& b);
@@ -337,4 +361,27 @@ std::expected<StageOutcome, StageError> stage_file(const std::filesystem::path&
337361
return std::unexpected(StageError{failure_message(src, dst, last)});
338362
}
339363

364+
std::expected<StageOutcome, StageError> stage_files(
365+
const std::vector<std::filesystem::path>& srcs,
366+
const std::filesystem::path& dst,
367+
const StageOptions& opts)
368+
{
369+
if (srcs.empty()) {
370+
return std::unexpected(StageError{
371+
std::format("staging '{}' requires at least one source", dst.string())});
372+
}
373+
for (std::size_t i = 1; i < srcs.size(); ++i) {
374+
if (!same_content(srcs.front(), srcs[i])) {
375+
std::string list;
376+
for (auto const& s : srcs) list += std::format("\n {}", s.string());
377+
return std::unexpected(StageError{std::format(
378+
"two or more sources disagree for one destination\n"
379+
" destination: {}\n"
380+
" sources:{}",
381+
dst.string(), list)});
382+
}
383+
}
384+
return stage_file(srcs.front(), dst, opts);
385+
}
386+
340387
} // namespace mcpp::build::stage

‎src/cli/cmd_build.cppm‎

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -914,19 +914,25 @@ export int cmd_dyndep(const mcpplibs::cmdline::ParsedArgs& parsed) {
914914
}
915915

916916
// Invoked by ninja during build (stage_file rule):
917-
// mcpp stage --output <dst> <src>
917+
// mcpp stage --output <dst> <src>...
918918
//
919919
// Publishes a cache-owned artifact (std BMI, std.o, runtime DLL) into the
920920
// build directory. See mcpp.build.stage for the semantics — in particular why
921921
// an already-equivalent destination is left untouched (#311).
922+
//
923+
// More than one source (SPEC-007 R4.2, mcpp#723) means two or more packages
924+
// of this graph deploy the same destination; `stage_files` places it when
925+
// every source is byte-identical and otherwise fails, naming every source
926+
// and the destination. One source — every invocation before this feature —
927+
// takes the exact path it always has.
922928
export int cmd_stage(const mcpplibs::cmdline::ParsedArgs& parsed) {
923929
std::filesystem::path outPath = parsed.option_or_empty("output").value();
924930
if (outPath.empty()) {
925931
std::println(stderr, "error: --output <path> required");
926932
return 2;
927933
}
928-
if (parsed.positional_count() != 1) {
929-
std::println(stderr, "error: stage requires exactly one source path");
934+
if (parsed.positional_count() < 1) {
935+
std::println(stderr, "error: stage requires at least one source path");
930936
return 2;
931937
}
932938

@@ -939,9 +945,13 @@ export int cmd_stage(const mcpplibs::cmdline::ParsedArgs& parsed) {
939945
if (!verify.empty())
940946
opts.verify = mcpp::build::stage::parse_verify(verify);
941947

942-
auto r = mcpp::build::stage::stage_file(
943-
mcpp::platform::fs::extended_length(std::filesystem::path{parsed.positional(0)}),
944-
mcpp::platform::fs::extended_length(outPath), opts);
948+
std::vector<std::filesystem::path> sources;
949+
for (std::size_t i = 0; i < parsed.positional_count(); ++i)
950+
sources.push_back(mcpp::platform::fs::extended_length(
951+
std::filesystem::path{parsed.positional(i)}));
952+
953+
auto r = mcpp::build::stage::stage_files(
954+
sources, mcpp::platform::fs::extended_length(outPath), opts);
945955
if (!r) {
946956
std::println(stderr, "error: {}", r.error().message);
947957
return 1;

0 commit comments

Comments
 (0)