Skip to content

Commit 3dbee70

Browse files
committed
fix: config.toml is written as given, and a value is set before either line ending; Windows had appended a second [toolchain]
1 parent b2d159a commit 3dbee70

5 files changed

Lines changed: 58 additions & 15 deletions

File tree

‎.agents/docs/2026-11-14-ci-and-release-time-cost-design.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ I 使从缓存恢复的 emsdk 在 mcpp 2026.11.13.1 起自动重建链接;K
184184
| 失败 | 原因 | 处理 |
185185
|---|---|---|
186186
| unit RuntimePayload/APayloadStartsThroughItsHome | 用例在每个宿主上期望 `elf`;Windows 上 `runtime_payload_kind` 按设计为空(I:PE 不指名加载器,不钉运行库) | 期望随宿主,Windows 上为空;`starts_through_its_home` 的判定在各宿主上相同 |
187-
| unit Config/ConfigValueIsReplacedWhole(本分支五次完成的 Windows 运行中四次失败) | 写者以重命名替换文件期间,Windows 对读者的打开与对该名字的查询都给出拒绝(被替换的副本处于待删除状态)。`read_whole` 以 `exists` 的失败判定文件不存在,在重命名期间放弃,读者得到空内容。5.1 记录的写者一侧(两秒内未替换成功)已由写者的重试解决,剩下的是读者一侧 | `read_whole` 以打开本身的错误判断:Windows 上除“不存在”以外的拒绝在两秒内重试;其他系统的重命名立即替换名字,读取失败即为最终结果 |
187+
| unit Config/ConfigValueIsReplacedWhole(本分支五次完成的 Windows 运行中四次失败;下列原因经下一行更正) | 写者以重命名替换文件期间,Windows 对读者的打开与对该名字的查询都给出拒绝(被替换的副本处于待删除状态)。`read_whole` 以 `exists` 的失败判定文件不存在,在重命名期间放弃,读者得到空内容。5.1 记录的写者一侧(两秒内未替换成功)已由写者的重试解决,剩下的是读者一侧 | `read_whole` 以打开本身的错误判断:Windows 上除“不存在”以外的拒绝在两秒内重试;其他系统的重命名立即替换名字,读取失败即为最终结果 |
188188
| e2e run/contract 1024 | 用例断言快速路径重放,却不持有 machine;并行程序向共享 home 安装、改变运行环境描述时,完整路径按设计重新规划(E 的约定)。其上方的注释称其写 config.toml,与内容不符 | 移入 contract-machine 套件,注释改为其持有 machine 的原因 |
189189
| e2e openkal/sets 964 B(Linux 分片 2) | 本 PR 引入(5.2 B)。`RunBuilt` 不再供给宿主工具,归档的运行因此既找不到平台链接用的 `okld`,也找不到 runner `okrun`;此前的通过依赖共享 home 中别处留下的程序。本机可稳定复现:`runner 'okrun' for 'x86_64-openkal' was not found` | 两处按意图回答。不链接的准备(`compiles()` 为假或只描述)不要求链接与封装程序:`linksSomething` 一处计算,平台工具缺失与 ld.lld 缺失两个拒绝共用。平台声明为工具、本次准备未定位的 runner,取平台为本机架构构建的 runner 程序(`pack::runner_program`),与其他架构的 runner 同一机制。交给 runner 的文件清单与闭包记录写在归档的运行目录中、所描述的程序旁,不写入检出目录的输出树(用例 B 断言该树中没有测试的产物) |
190190

@@ -213,3 +213,10 @@ openkal-macos 仅在其为空时提供自己的存根,旧版 mcpp 不设该变
213213
**第四次运行(43576630955,含 bench)。** 另有两处与本 PR 改动无关、由负载暴露的用例缺陷:modules/extensions 218 断言无改动的构建被重放,
214214
却不持有 machine(与 1024 同类,移入 extensions-machine 套件);DetachLifetime 的辅助脚本在杀死控制进程后固定等待 0.15 s 再要求其锁,
215215
负载下内核结束该进程晚于此,改为在时限内等到锁被释放。
216+
217+
**ConfigValueIsReplacedWhole 的真实原因。** 上表的推断(读者把替换期间的拒绝当作不存在)在修正后仍失败;用例改为报告首个读错的读者
218+
所见,Windows 上为 `65605 bytes, at [toolchain]: [toolchain]\r\ndefault = "gcc@16.1.0"\r\n`。`write_replacing` 以文本模式写出,
219+
Windows 把每个 `\n` 写成 `\r\n`;`write_config_value` 只认 `\n` 结尾的 `[toolchain]`,再次设值时找不到该表而追加第二个,解析器拒绝
220+
重复的表。这是用户可见的缺陷(Windows 上第二次 `mcpp toolchain default` 之后 config.toml 无效),此前读者偶尔未在写入期间读取时
221+
用例通过。处理:`write_replacing` 按给定字节写出;`write_config_value` 在两种行尾之前匹配表头与键,新增的行沿用文件自身的行尾
222+
(Config/ConfigValueKeepsTheFilesLineEnding)。读者对拒绝的重试保留,它对应 Windows 替换期间真实存在的拒绝。

‎CHANGELOG.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,14 @@
161161
'='`). On Windows, which refuses a rename over a file a reader holds open,
162162
`mcpp toolchain default` and the other writers no longer fail with `cannot
163163
write config.toml`: the writer closes its own reader first and waits up to
164-
two seconds for the others; a reader whose open Windows refuses while the
165-
file is being replaced waits for the replacement as well, rather than
166-
reading the file as absent. A `config.toml` that is a symbolic link is
167-
written through, and the file keeps its permissions.
164+
two seconds for the others, and a reader whose open is refused while the
165+
file is being replaced waits for the replacement as well. The file is
166+
written as given: on Windows a text-mode write had ended each line in
167+
`\r\n`, after which a value set again found no `[toolchain]` and appended a
168+
second one; a `config.toml` an editor wrote with `\r\n` has its value
169+
replaced where it stands, and an added line ends as the file's others do.
170+
A `config.toml` that is a symbolic link is written through, and the file
171+
keeps its permissions.
168172
- `mcpp test` runs a case that holds the machine (P7.1, resource `machine`)
169173
with no other test beside it: the listed cases of a file that hold it are a
170174
part of their own (`<program>/<file> (machine)` beside the file's other

‎src/config.cppm‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,29 +1472,35 @@ write_config_value(const GlobalConfig& cfg, std::string_view section, std::strin
14721472
std::format("cannot open '{}'", cfg.configFile.string())});
14731473
std::string text = std::move(*read);
14741474

1475-
std::string line = std::format("{} = {}\n", key, literal);
1475+
// The file's own line ending: a config.toml written on Windows by an
1476+
// editor ends its lines with `\r\n`, and a line this writer adds ends as
1477+
// the others do. A header or a key is matched before either ending.
1478+
const std::string_view eol = text.find("\r\n") != std::string::npos ? "\r\n" : "\n";
1479+
std::string line = std::format("{} = {}{}", key, literal, eol);
14761480
const auto header = std::format("[{}]", section);
1481+
auto ends_line = [&](std::size_t at) {
1482+
return at == text.size() || text[at] == '\n' || (text[at] == '\r' && at + 1 < text.size() && text[at + 1] == '\n');
1483+
};
14771484

14781485
// The header at the start of a line, so `[toolchain]` is not found inside
14791486
// `[toolchain.x]` or a comment.
14801487
auto sectionPos = std::string::npos;
14811488
for (auto p = text.find(header); p != std::string::npos; p = text.find(header, p + 1))
1482-
if ((p == 0 || text[p - 1] == '\n')
1483-
&& (p + header.size() == text.size() || text[p + header.size()] == '\n')) {
1489+
if ((p == 0 || text[p - 1] == '\n') && ends_line(p + header.size())) {
14841490
sectionPos = p;
14851491
break;
14861492
}
14871493
if (sectionPos == std::string::npos) {
14881494
// Append the section's block at end.
1489-
if (!text.empty() && text.back() != '\n') text += '\n';
1490-
text += std::format("\n{}\n{}", header, line);
1495+
if (!text.empty() && text.back() != '\n') text += eol;
1496+
text += std::format("{}{}{}{}", eol, header, eol, line);
14911497
} else {
14921498
// Locate an existing `<key> = ...` line within the section. Match at
14931499
// line starts with a word boundary after the key, so `default` never
14941500
// matches the `default_target` line (and vice versa).
1495-
auto eol = text.find('\n', sectionPos);
1496-
if (eol == std::string::npos) eol = text.size();
1497-
auto bodyStart = (eol == text.size()) ? text.size() : eol + 1;
1501+
auto headerEnd = text.find('\n', sectionPos);
1502+
if (headerEnd == std::string::npos) headerEnd = text.size();
1503+
auto bodyStart = (headerEnd == text.size()) ? text.size() : headerEnd + 1;
14981504
auto nextSec = text.find("\n[", bodyStart);
14991505
auto bodyEnd = (nextSec == std::string::npos) ? text.size() : nextSec;
15001506
auto body = std::string_view(text).substr(bodyStart, bodyEnd - bodyStart);

‎src/home/files.cpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ bool write_replacing(const std::filesystem::path& path, std::string_view text) {
151151
auto tmp = file;
152152
tmp += std::format(".{:x}.tmp", std::random_device{}());
153153
{
154-
std::ofstream os(tmp);
154+
// The bytes as given: a text-mode stream on Windows writes each `\n`
155+
// as `\r\n`, and a reader that looks for `\n` (write_config_value)
156+
// no longer finds the line it wrote.
157+
std::ofstream os(tmp, std::ios::binary);
155158
if (!os) return false;
156159
os << text;
157160
if (!os.flush()) {

‎tests/unit/test_config.cpp‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,29 @@ UTest xlings_json_leaves_a_user_artifact_base_alone{"ConfigIndexMigration/Xlings
338338
std::filesystem::remove_all(dir);
339339
}};
340340

341+
// A config.toml whose lines end in `\r\n` (an editor on Windows) has its value replaced where it
342+
// stands, and a line the writer adds ends as the file's others do. A writer that matched only
343+
// `\n` found no `[toolchain]` and appended a second one, which the parser refuses.
344+
UTest config_value_keeps_the_files_line_ending{"Config/ConfigValueKeepsTheFilesLineEnding", [] {
345+
const auto home = make_tempdir("mcpp-config-crlf");
346+
mcpp::config::GlobalConfig cfg;
347+
cfg.configFile = home / "config.toml";
348+
std::ofstream(cfg.configFile, std::ios::binary) << "[xlings]\r\nbinary = \"bundled\"\r\n\r\n[toolchain]\r\ndefault = \"gcc@16.1.0\"\r\n";
349+
for (auto value : {"\"llvm@23.1.3\"", "\"gcc@16.1.0\""})
350+
expect(fatal(mcpp::config::write_config_value(cfg, "toolchain", "default", value).has_value()));
351+
expect(fatal(mcpp::config::write_config_value(cfg, "build", "default_jobs", "4").has_value()));
352+
const auto text = mcpp::home::read_whole(cfg.configFile).value_or("");
353+
std::size_t headers = 0;
354+
for (auto p = text.find("[toolchain]"); p != std::string::npos; p = text.find("[toolchain]", p + 1)) ++headers;
355+
expect(eq(headers, std::size_t{1})) << text;
356+
expect(text.contains("[toolchain]\r\ndefault = \"gcc@16.1.0\"\r\n")) << text;
357+
expect(text.contains("\r\n[build]\r\ndefault_jobs = 4\r\n")) << text;
358+
bool bare = false;
359+
for (std::size_t i = 0; i < text.size(); ++i) bare = bare || (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'));
360+
expect(!bare) << "a line ends in a bare \\n: " << text;
361+
std::filesystem::remove_all(home);
362+
}};
363+
341364
// A value written while another reader reads the file: the reader sees the previous copy or the
342365
// new one, whole, and the writer leaves no file of its own beside it. Programs started at once on
343366
// a fresh home each write config.toml; a rewrite in place (truncate, then write) let one read a
@@ -348,7 +371,7 @@ UTest config_value_is_replaced_whole{"Config/ConfigValueIsReplacedWhole", [] {
348371
cfg.configFile = home / "config.toml";
349372
const std::string padding(64 * 1024, '#');
350373
{
351-
std::ofstream(cfg.configFile) << "[xlings]\nbinary = \"bundled\"\n" << padding << "\n[toolchain]\ndefault = \"gcc@16.1.0\"\n";
374+
std::ofstream(cfg.configFile, std::ios::binary) << "[xlings]\nbinary = \"bundled\"\n" << padding << "\n[toolchain]\ndefault = \"gcc@16.1.0\"\n";
352375
}
353376
std::atomic<int> partial{0};
354377
// What the first reader that missed the value saw: the failure should say

0 commit comments

Comments
 (0)