mirror of
https://github.com/Kitware/CMake.git
synced 2026-08-09 01:01:12 +00:00
fileapi: Fix dangling reply reference on case-insensitive filesystems
Reply file names embed the configuration name verbatim. Reconfiguring a build tree with a build type that differs from the previous one only in case (e.g. Debug -> debug) makes CMake write a reply whose name also differs only in case from the existing file. On a case-insensitive filesystem the write is skipped because the name already exists, but RemoveOldReplyFiles compared on-disk names to the just-written names textually and deleted the surviving file, leaving the reply index citing a target or directory reply that no longer exists on disk. Prune reply files by file identity via cmSystemTools::GetFileId instead of by name, so an on-disk entry that aliases a reply we just wrote is kept. An entry whose identity cannot be obtained is retained rather than deleted. Fixes: #28022
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <functional>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -155,12 +156,20 @@ std::vector<std::string> cmFileAPI::LoadDir(std::string const& dir)
|
|||||||
void cmFileAPI::RemoveOldReplyFiles()
|
void cmFileAPI::RemoveOldReplyFiles()
|
||||||
{
|
{
|
||||||
std::string const reply_dir = this->APIv1 + "/reply";
|
std::string const reply_dir = this->APIv1 + "/reply";
|
||||||
std::vector<std::string> files = this->LoadDir(reply_dir);
|
std::vector<std::string> const files = this->LoadDir(reply_dir);
|
||||||
for (std::string const& f : files) {
|
|
||||||
if (this->ReplyFiles.find(f) == this->ReplyFiles.end()) {
|
// Reply names embed the configuration verbatim, so on a case-insensitive
|
||||||
std::string file = cmStrCat(reply_dir, '/', f);
|
// filesystem a "debug" reply can alias a just-written "Debug" one; deleting
|
||||||
cmSystemTools::RemoveFile(file);
|
// by name would strip a file the index still cites. Decide by identity.
|
||||||
}
|
std::vector<std::string> const toRemove =
|
||||||
|
cmFileAPI::FilesToRemove<cmSystemTools::FileId>(
|
||||||
|
files, this->ReplyFiles,
|
||||||
|
[&reply_dir](std::string const& name,
|
||||||
|
cmSystemTools::FileId& id) -> bool {
|
||||||
|
return cmSystemTools::GetFileId(cmStrCat(reply_dir, '/', name), id);
|
||||||
|
});
|
||||||
|
for (std::string const& f : toRemove) {
|
||||||
|
cmSystemTools::RemoveFile(cmStrCat(reply_dir, '/', f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "cmConfigure.h" // IWYU pragma: keep
|
#include "cmConfigure.h" // IWYU pragma: keep
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -66,6 +67,49 @@ public:
|
|||||||
/** Build a JSON object with major and minor fields. */
|
/** Build a JSON object with major and minor fields. */
|
||||||
static Json::Value BuildVersion(unsigned int major, unsigned int minor);
|
static Json::Value BuildVersion(unsigned int major, unsigned int minor);
|
||||||
|
|
||||||
|
/** Return the subset of 'entries' to delete after a configure: those not
|
||||||
|
named in 'replyNames' whose identity ('getId') also matches no reply.
|
||||||
|
Deciding by identity rather than name keeps an entry that aliases a
|
||||||
|
just-written reply on a case-insensitive filesystem; an entry whose
|
||||||
|
identity cannot be obtained is retained. Static/templated for tests. */
|
||||||
|
template <typename FileIdT>
|
||||||
|
static std::vector<std::string> FilesToRemove(
|
||||||
|
std::vector<std::string> const& entries,
|
||||||
|
std::unordered_set<std::string> const& replyNames,
|
||||||
|
std::function<bool(std::string const&, FileIdT&)> const& getId)
|
||||||
|
{
|
||||||
|
std::vector<FileIdT> keptIds;
|
||||||
|
for (std::string const& name : replyNames) {
|
||||||
|
FileIdT id;
|
||||||
|
if (getId(name, id)) {
|
||||||
|
keptIds.push_back(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> toRemove;
|
||||||
|
for (std::string const& entry : entries) {
|
||||||
|
if (replyNames.find(entry) != replyNames.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
FileIdT id;
|
||||||
|
if (!getId(entry, id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool aliasesKept = false;
|
||||||
|
for (FileIdT const& keptId : keptIds) {
|
||||||
|
if (id == keptId) {
|
||||||
|
aliasesKept = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aliasesKept) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
toRemove.push_back(entry);
|
||||||
|
}
|
||||||
|
return toRemove;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
cmake* CMakeInstance;
|
cmake* CMakeInstance;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ set(CMakeLib_TESTS
|
|||||||
testDateTime.cxx
|
testDateTime.cxx
|
||||||
testDebug.cxx
|
testDebug.cxx
|
||||||
testDocumentationFormatter.cxx
|
testDocumentationFormatter.cxx
|
||||||
|
testCMFileAPI.cxx
|
||||||
testGccDepfileReader.cxx
|
testGccDepfileReader.cxx
|
||||||
testGeneratedFileStream.cxx
|
testGeneratedFileStream.cxx
|
||||||
testGenExBoundOperand.cxx
|
testGenExBoundOperand.cxx
|
||||||
|
|||||||
102
Tests/CMakeLib/testCMFileAPI.cxx
Normal file
102
Tests/CMakeLib/testCMFileAPI.cxx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||||
|
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||||||
|
#include <functional>
|
||||||
|
#include <iostream>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "cmFileAPI.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Identity oracle over a name->id map. A name absent from the map models a
|
||||||
|
// file whose identity cannot be obtained (GetFileId failure).
|
||||||
|
std::function<bool(std::string const&, int&)> makeOracle(
|
||||||
|
std::map<std::string, int> const& ids)
|
||||||
|
{
|
||||||
|
return [ids](std::string const& name, int& id) -> bool {
|
||||||
|
auto const it = ids.find(name);
|
||||||
|
if (it == ids.end()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
id = it->second;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkCase(char const* label, std::vector<std::string> const& entries,
|
||||||
|
std::unordered_set<std::string> const& replyNames,
|
||||||
|
std::map<std::string, int> const& ids,
|
||||||
|
std::set<std::string> const& expected)
|
||||||
|
{
|
||||||
|
std::vector<std::string> const removed =
|
||||||
|
cmFileAPI::FilesToRemove<int>(entries, replyNames, makeOracle(ids));
|
||||||
|
std::set<std::string> const actual(removed.begin(), removed.end());
|
||||||
|
if (actual != expected) {
|
||||||
|
std::cout << "FAILED: " << label << "\n expected removals:";
|
||||||
|
for (std::string const& e : expected) {
|
||||||
|
std::cout << ' ' << e;
|
||||||
|
}
|
||||||
|
std::cout << "\n actual removals: ";
|
||||||
|
for (std::string const& a : actual) {
|
||||||
|
std::cout << ' ' << a;
|
||||||
|
}
|
||||||
|
std::cout << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int testCMFileAPI(int /*unused*/, char* /*unused*/[])
|
||||||
|
{
|
||||||
|
bool ok = true;
|
||||||
|
|
||||||
|
// An on-disk entry that resolves to the same identity as a just-written
|
||||||
|
// reply (a case-only-different name aliasing one inode) is kept.
|
||||||
|
ok &= checkCase(
|
||||||
|
"case-variant alias kept", { "target-foo-Debug-H.json" },
|
||||||
|
{ "target-foo-debug-H.json" },
|
||||||
|
{ { "target-foo-debug-H.json", 1 }, { "target-foo-Debug-H.json", 1 } },
|
||||||
|
{});
|
||||||
|
|
||||||
|
// A genuinely stale entry with a distinct identity is removed.
|
||||||
|
ok &= checkCase("distinct stale removed", { "target-old-H.json" },
|
||||||
|
{ "target-new-H.json" },
|
||||||
|
{ { "target-new-H.json", 1 }, { "target-old-H.json", 2 } },
|
||||||
|
{ "target-old-H.json" });
|
||||||
|
|
||||||
|
// An entry whose exact name was just written is kept without consulting the
|
||||||
|
// identity oracle (its name is intentionally absent from the id map).
|
||||||
|
ok &= checkCase("exact-name match kept", { "index-x.json" },
|
||||||
|
{ "index-x.json" }, {}, {});
|
||||||
|
|
||||||
|
// Fail-safe: an entry whose identity cannot be obtained is retained.
|
||||||
|
ok &= checkCase("candidate id failure retained", { "unreadable.json" },
|
||||||
|
{ "target-new-H.json" }, { { "target-new-H.json", 1 } }, {});
|
||||||
|
|
||||||
|
// No kept-side fail-safe is needed: an alias shares its inode, so when a
|
||||||
|
// kept reply's identity is unknown the candidate's is too and the
|
||||||
|
// candidate-side fail-safe retains it. Both names are absent to model it.
|
||||||
|
ok &= checkCase("unknown kept id: correlated alias retained",
|
||||||
|
{ "alias-of-unidentified.json" },
|
||||||
|
{ "target-new-H.json", "unidentified-kept.json" },
|
||||||
|
{ { "target-new-H.json", 1 } }, {});
|
||||||
|
|
||||||
|
// Mixed: an aliasing entry is kept while an unrelated stale entry is
|
||||||
|
// removed.
|
||||||
|
ok &= checkCase("mixed alias and stale", { "a-alias.json", "stale.json" },
|
||||||
|
{ "a.json", "b.json" },
|
||||||
|
{ { "a.json", 1 },
|
||||||
|
{ "b.json", 2 },
|
||||||
|
{ "a-alias.json", 1 },
|
||||||
|
{ "stale.json", 3 } },
|
||||||
|
{ "stale.json" });
|
||||||
|
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
2
Tests/RunCMake/FileAPI/ConfigCaseReconfigure-prep.cmake
Normal file
2
Tests/RunCMake/FileAPI/ConfigCaseReconfigure-prep.cmake
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
file(REMOVE_RECURSE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query")
|
||||||
|
file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/codemodel-v2" "")
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
set(reply_dir "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply")
|
||||||
|
|
||||||
|
# Every reply file referenced from the index must still exist on disk after
|
||||||
|
# the reconfigure. A dangling "jsonFile" reference is the bug this guards; it
|
||||||
|
# manifests only on a case-insensitive filesystem (Windows, default macOS).
|
||||||
|
file(GLOB reply_files "${reply_dir}/*.json")
|
||||||
|
set(dangling "")
|
||||||
|
foreach(reply_file IN LISTS reply_files)
|
||||||
|
file(READ "${reply_file}" content)
|
||||||
|
string(REGEX MATCHALL "\"jsonFile\"[ \t]*:[ \t]*\"[^\"]+\"" refs "${content}")
|
||||||
|
foreach(ref IN LISTS refs)
|
||||||
|
string(REGEX REPLACE "\"jsonFile\"[ \t]*:[ \t]*\"([^\"]+)\"" "\\1" name "${ref}")
|
||||||
|
if(NOT EXISTS "${reply_dir}/${name}")
|
||||||
|
get_filename_component(from "${reply_file}" NAME)
|
||||||
|
string(APPEND dangling "\n '${name}' referenced by ${from} is missing")
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
|
endforeach()
|
||||||
|
if(dangling)
|
||||||
|
set(RunCMake_TEST_FAILED "Dangling File API reply references:${dangling}")
|
||||||
|
endif()
|
||||||
2
Tests/RunCMake/FileAPI/ConfigCaseReconfigure.cmake
Normal file
2
Tests/RunCMake/FileAPI/ConfigCaseReconfigure.cmake
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
enable_language(C)
|
||||||
|
add_library(foo STATIC empty.c)
|
||||||
@@ -125,6 +125,19 @@ run_cmake(ProjectQueryGood)
|
|||||||
run_cmake(ProjectQueryBad)
|
run_cmake(ProjectQueryBad)
|
||||||
run_cmake(FailConfigure)
|
run_cmake(FailConfigure)
|
||||||
|
|
||||||
|
# Reconfiguring with a case-only build-type change (Debug -> debug) must not
|
||||||
|
# leave the reply index citing a target file that cleanup deleted on a
|
||||||
|
# case-insensitive filesystem (Windows, default macOS).
|
||||||
|
function(run_config_case)
|
||||||
|
if(NOT RunCMake_GENERATOR_IS_MULTI_CONFIG)
|
||||||
|
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/ConfigCaseReconfigure-build)
|
||||||
|
run_cmake_with_options(ConfigCaseReconfigure -DCMAKE_BUILD_TYPE=Debug)
|
||||||
|
set(RunCMake_TEST_NO_CLEAN 1)
|
||||||
|
run_cmake_command(ConfigCaseReconfigure-recon ${CMAKE_COMMAND} . -DCMAKE_BUILD_TYPE=debug)
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
run_config_case()
|
||||||
|
|
||||||
function(run_object object)
|
function(run_object object)
|
||||||
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${object}-build)
|
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${object}-build)
|
||||||
list(APPEND RunCMake_TEST_OPTIONS ${ARGN} -DCMAKE_POLICY_DEFAULT_CMP0118=NEW)
|
list(APPEND RunCMake_TEST_OPTIONS ${ARGN} -DCMAKE_POLICY_DEFAULT_CMP0118=NEW)
|
||||||
|
|||||||
Reference in New Issue
Block a user