Skip to content

Commit 076b145

Browse files
committed
fix(build): preserve CDB links during atomic publication
1 parent 4b0ef8b commit 076b145

4 files changed

Lines changed: 111 additions & 17 deletions

File tree

.agents/docs/2026-08-08-configure-only-cdb-design.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ CDB 发布前,并保留旧 CDB。项目自身模块或未缓存依赖模块保
132132

133133
- POSIX 使用同文件系统 `rename`
134134
- Windows 使用 `MoveFileExW(..., MOVEFILE_REPLACE_EXISTING)`
135+
- Windows 遇到短暂 sharing violation 时有限退避重试;
135136
- 不先删除 destination;
136137
- 函数为 `noexcept` 风格,通过 `error_code` 报错。
137138

@@ -140,9 +141,10 @@ CDB 写入流程:
140141
1. 生成并解析 JSON,要求顶层为数组。
141142
2. 与现有有效条目合并并删除已不存在源文件的旧条目。
142143
3. 内容未变化时不写文件,避免无意义触发 clangd 重索引。
143-
4. 在同目录完成唯一临时文件写入和 flush。
144-
5. 通过 `replace_file` 原子替换目标。
145-
6. 替换失败时清理临时文件,返回错误,旧文件保持不变。
144+
4. 若根 CDB 是文件符号链接,解析并原子更新其目标,保留链接本身。
145+
5. 在同目录完成带跨进程随机量的临时文件写入和 flush。
146+
6. 通过 `replace_file` 原子替换目标。
147+
7. 替换失败时清理临时文件,返回错误,旧文件保持不变。
146148

147149
`write_compile_commands()` 改为返回结构化成功或错误。为保持普通构建兼容性:
148150

src/build/compile_commands.cppm

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,46 @@ publish_compile_commands(
253253
"fresh compile database for '{}' is not a JSON array", path.string())));
254254
}
255255

256+
std::filesystem::path publishPath = path;
257+
std::error_code statusEc;
258+
const bool isLink = std::filesystem::is_symlink(path, statusEc);
259+
if (statusEc) {
260+
return std::unexpected(write_error(std::format(
261+
"cannot inspect compile database '{}': {}", path.string(),
262+
statusEc.message())));
263+
}
264+
if (isLink) {
265+
auto target = std::filesystem::read_symlink(path, statusEc);
266+
if (statusEc) {
267+
return std::unexpected(write_error(std::format(
268+
"cannot resolve compile database link '{}': {}", path.string(),
269+
statusEc.message())));
270+
}
271+
publishPath = target.is_absolute() ? target : path.parent_path() / target;
272+
}
273+
256274
std::optional<std::string> existing;
257-
if (std::ifstream is(path, std::ios::binary); is) {
275+
std::ifstream input(publishPath, std::ios::binary);
276+
if (input) {
258277
std::stringstream ss;
259-
ss << is.rdbuf();
260-
if (is.bad()) {
278+
ss << input.rdbuf();
279+
if (input.bad()) {
261280
return std::unexpected(write_error(std::format(
262-
"cannot read existing compile database '{}'", path.string())));
281+
"cannot read existing compile database '{}'", publishPath.string())));
263282
}
264283
existing = ss.str();
284+
} else {
285+
std::error_code existsEc;
286+
auto exists = std::filesystem::exists(publishPath, existsEc);
287+
if (existsEc) {
288+
return std::unexpected(write_error(std::format(
289+
"cannot inspect existing compile database '{}': {}",
290+
publishPath.string(), existsEc.message())));
291+
}
292+
if (exists) {
293+
return std::unexpected(write_error(std::format(
294+
"cannot read existing compile database '{}'", publishPath.string())));
295+
}
265296
}
266297

267298
// 完全相同的有效输入不重写文件,避免 clangd 因 mtime 变化重复索引。
@@ -288,9 +319,12 @@ publish_compile_commands(
288319
}
289320

290321
static std::atomic<std::uint64_t> sequence{0};
291-
auto temp = path.parent_path()
292-
/ std::format(".{}.tmp.{}.{}", path.filename().string(),
322+
const auto nonce = std::random_device{}();
323+
// 临时文件和链接目标同目录,避免 rename 跨文件系统;随机量降低跨进程碰撞概率。
324+
auto temp = publishPath.parent_path()
325+
/ std::format(".{}.tmp.{}.{}.{}", publishPath.filename().string(),
293326
std::chrono::steady_clock::now().time_since_epoch().count(),
327+
static_cast<unsigned long long>(nonce),
294328
sequence.fetch_add(1, std::memory_order_relaxed));
295329
auto cleanup_temp = [&] {
296330
std::error_code cleanupEc;
@@ -318,10 +352,10 @@ publish_compile_commands(
318352
}
319353

320354
std::error_code ec;
321-
if (!replaceFile(temp, path, ec)) {
355+
if (!replaceFile(temp, publishPath, ec)) {
322356
cleanup_temp();
323357
return std::unexpected(write_error(std::format(
324-
"cannot replace '{}': {}", path.string(), ec.message())));
358+
"cannot replace '{}': {}", publishPath.string(), ec.message())));
325359
}
326360

327361
return CompileCommandsWriteResult{true, finalJson.size()};

src/platform/fs.cppm

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,23 @@ bool replace_file(const std::filesystem::path& source,
145145
const std::filesystem::path& destination,
146146
std::error_code& ec) {
147147
#if defined(_WIN32)
148-
// 直接替换已有文件,不能先删除 last-known-good CDB。
149-
if (MoveFileExW(source.wstring().c_str(), destination.wstring().c_str(),
150-
MOVEFILE_REPLACE_EXISTING)) {
151-
ec.clear();
152-
return true;
148+
// 直接替换已有文件,不能先删除 last-known-good CDB。编辑器或杀毒软件
149+
// 可能短暂占用目标文件,sharing violation 仅做有限退避后再报告失败。
150+
auto delay = std::chrono::milliseconds{50};
151+
for (int attempt = 0; attempt < 4; ++attempt) {
152+
if (MoveFileExW(source.wstring().c_str(), destination.wstring().c_str(),
153+
MOVEFILE_REPLACE_EXISTING)) {
154+
ec.clear();
155+
return true;
156+
}
157+
const auto error = GetLastError();
158+
if (error != ERROR_SHARING_VIOLATION || attempt == 3) {
159+
ec = std::error_code(static_cast<int>(error), std::system_category());
160+
return false;
161+
}
162+
std::this_thread::sleep_for(delay);
163+
delay *= 3;
153164
}
154-
ec = std::error_code(static_cast<int>(GetLastError()), std::system_category());
155165
return false;
156166
#else
157167
// 临时文件与目标文件位于同一文件系统时,rename 提供原子替换。

tests/unit/test_compile_commands.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,51 @@ TEST(CompileCommandsWriter, ReplacementFailurePreservesOldDatabase) {
166166
EXPECT_EQ(std::distance(std::filesystem::directory_iterator(temp.path),
167167
std::filesystem::directory_iterator{}), 1);
168168
}
169+
170+
TEST(CompileCommandsWriter, ReplacesSymlinkTargetWithoutRemovingLink) {
171+
TempDir temp;
172+
auto target = temp.path / "build" / "compile_commands.json";
173+
auto link = temp.path / "compile_commands.json";
174+
std::filesystem::create_directories(target.parent_path());
175+
auto oldContent = cdb({entry((temp.path / "old.cpp").string(), "-DOLD")});
176+
auto newContent = cdb({entry((temp.path / "new.cpp").string(), "-DNEW")});
177+
std::ofstream(target) << oldContent;
178+
std::error_code symlinkEc;
179+
std::filesystem::create_symlink(target, link, symlinkEc);
180+
if (symlinkEc) GTEST_SKIP() << "symlink unavailable: " << symlinkEc.message();
181+
182+
auto result = publish_compile_commands(
183+
link, newContent, [](const std::filesystem::path&) { return false; });
184+
185+
ASSERT_TRUE(result.has_value()) << result.error().message;
186+
EXPECT_TRUE(std::filesystem::is_symlink(link));
187+
auto published = read_file(target);
188+
EXPECT_NE(published.find("-DNEW"), std::string::npos);
189+
EXPECT_EQ(published.find("-DOLD"), std::string::npos);
190+
}
191+
192+
TEST(CompileCommandsWriter, ExistingUnreadableDatabaseIsNotOverwritten) {
193+
TempDir temp;
194+
auto path = temp.path / "compile_commands.json";
195+
std::ofstream(path) << "last-known-good";
196+
std::error_code permissionEc;
197+
std::filesystem::permissions(
198+
path, std::filesystem::perms::owner_read,
199+
std::filesystem::perm_options::remove, permissionEc);
200+
if (permissionEc) GTEST_SKIP() << "cannot change permissions: " << permissionEc.message();
201+
std::ifstream probe(path);
202+
if (probe) {
203+
std::filesystem::permissions(path, std::filesystem::perms::owner_all,
204+
std::filesystem::perm_options::replace, permissionEc);
205+
GTEST_SKIP() << "test user can still read restricted file";
206+
}
207+
208+
auto result = publish_compile_commands(
209+
path, cdb({entry((temp.path / "new.cpp").string(), "-DNEW")}),
210+
[](const std::filesystem::path&) { return true; });
211+
212+
ASSERT_FALSE(result.has_value());
213+
EXPECT_NE(result.error().message.find("read existing"), std::string::npos);
214+
std::filesystem::permissions(path, std::filesystem::perms::owner_all,
215+
std::filesystem::perm_options::replace, permissionEc);
216+
}

0 commit comments

Comments
 (0)