[libclang] Add clang_ModuleCache_prune (#190067)

This allows a build system to direct Clang to prune a module cache
directory using the same method Clang does internally.

This also changes `clang::maybePruneImpl` to clean up files directly in
the directory, not just subdirectories.
This commit is contained in:
Michael Spencer 2026-04-02 14:26:27 -07:00 committed by GitHub
parent bf50e847fb
commit b1ef47f459
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 132 additions and 26 deletions

View File

@ -18,6 +18,7 @@
#include "clang-c/CXString.h"
#include "clang-c/ExternC.h"
#include "clang-c/Platform.h"
#include <time.h>
LLVM_CLANG_C_EXTERN_C_BEGIN
@ -143,6 +144,23 @@ clang_ModuleMapDescriptor_writeToBuffer(CXModuleMapDescriptor, unsigned options,
*/
CINDEX_LINKAGE void clang_ModuleMapDescriptor_dispose(CXModuleMapDescriptor);
/**
* Prune module files in the module cache directory that haven't been accessed
* in a long time.
*
* \param Path the path to the module cache directory.
*
* \param PruneInterval the minimum time in seconds between two prune
* operations. If the timestamp file is newer than this, pruning is skipped.
*
* \param PruneAfter the time in seconds after which unused module files are
* removed.
*
*/
CINDEX_LINKAGE void clang_ModuleCache_prune(const char *Path,
time_t PruneInterval,
time_t PruneAfter);
/**
* @}
*/

View File

@ -60,38 +60,48 @@ void clang::maybePruneImpl(StringRef Path, time_t PruneInterval,
// Walk the entire module cache, looking for unused module files and module
// indices.
std::error_code EC;
auto TryPruneFile = [&](StringRef FilePath) {
// We only care about module and global module index files.
StringRef Filename = llvm::sys::path::filename(FilePath);
StringRef Extension = llvm::sys::path::extension(FilePath);
if (Extension != ".pcm" && Extension != ".timestamp" &&
Filename != "modules.idx")
return;
// Don't prune the pruning timestamp file.
if (Filename == "modules.timestamp")
return;
// Look at this file. If we can't stat it, there's nothing interesting
// there.
if (llvm::sys::fs::status(FilePath, StatBuf))
return;
// If the file has been used recently enough, leave it there.
time_t FileAccessTime = llvm::sys::toTimeT(StatBuf.getLastAccessedTime());
if (CurrentTime - FileAccessTime <= PruneAfter)
return;
// Remove the file.
llvm::sys::fs::remove(FilePath);
// Remove the timestamp file created by implicit module builds.
std::string TimestampFilename = FilePath.str() + ".timestamp";
llvm::sys::fs::remove(TimestampFilename);
};
for (llvm::sys::fs::directory_iterator Dir(Path, EC), DirEnd;
Dir != DirEnd && !EC; Dir.increment(EC)) {
// If we don't have a directory, there's nothing to look into.
if (!llvm::sys::fs::is_directory(Dir->path()))
// If we don't have a directory, try to prune it as a file in the root.
if (!llvm::sys::fs::is_directory(Dir->path())) {
TryPruneFile(Dir->path());
continue;
}
// Walk all the files within this directory.
for (llvm::sys::fs::directory_iterator File(Dir->path(), EC), FileEnd;
File != FileEnd && !EC; File.increment(EC)) {
// We only care about module and global module index files.
StringRef Extension = llvm::sys::path::extension(File->path());
if (Extension != ".pcm" && Extension != ".timestamp" &&
llvm::sys::path::filename(File->path()) != "modules.idx")
continue;
// Look at this file. If we can't stat it, there's nothing interesting
// there.
if (llvm::sys::fs::status(File->path(), StatBuf))
continue;
// If the file has been used recently enough, leave it there.
time_t FileAccessTime = llvm::sys::toTimeT(StatBuf.getLastAccessedTime());
if (CurrentTime - FileAccessTime <= PruneAfter)
continue;
// Remove the file.
llvm::sys::fs::remove(File->path());
// Remove the timestamp file.
std::string TimpestampFilename = File->path() + ".timestamp";
llvm::sys::fs::remove(TimpestampFilename);
}
File != FileEnd && !EC; File.increment(EC))
TryPruneFile(File->path());
// If we removed all the files in the directory, remove the directory
// itself.

View File

@ -12,6 +12,7 @@
#include "clang-c/BuildSystem.h"
#include "CXString.h"
#include "clang/Serialization/ModuleCache.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/Support/CBindingWrapping.h"
#include "llvm/Support/Chrono.h"
@ -150,3 +151,9 @@ clang_ModuleMapDescriptor_writeToBuffer(CXModuleMapDescriptor MMD, unsigned,
void clang_ModuleMapDescriptor_dispose(CXModuleMapDescriptor MMD) {
delete MMD;
}
void clang_ModuleCache_prune(const char *Path, time_t PruneInterval,
time_t PruneAfter) {
if (Path)
clang::maybePruneImpl(Path, PruneInterval, PruneAfter);
}

View File

@ -457,6 +457,11 @@ LLVM_21 {
clang_Cursor_isGCCAssemblyVolatile;
};
LLVM_23 {
global:
clang_ModuleCache_prune;
};
# Example of how to add a new symbol version entry. If you do add a new symbol
# version, please update the example to depend on the version you added.
# LLVM_X {

View File

@ -7,9 +7,11 @@
//===----------------------------------------------------------------------===//
#include "TestUtils.h"
#include "clang-c/BuildSystem.h"
#include "clang-c/Index.h"
#include "clang-c/Rewrite.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Chrono.h"
#include "llvm/Support/Debug.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
@ -356,6 +358,70 @@ TEST(libclang, ModuleMapDescriptor) {
clang_ModuleMapDescriptor_dispose(MMD);
}
TEST(libclang, ModuleCache_prune) {
llvm::SmallString<256> CacheDir;
ASSERT_FALSE(
llvm::sys::fs::createUniqueDirectory("libclang-module-cache", CacheDir));
auto writeFile = [](const llvm::Twine &Path, llvm::StringRef Content = "") {
std::error_code EC;
llvm::raw_fd_ostream OS(Path.str(), EC);
ASSERT_FALSE(EC);
OS << Content;
};
auto setOldAccessTime = [](const llvm::Twine &Path) {
int FD;
std::error_code EC = llvm::sys::fs::openFileForReadWrite(
Path, FD, llvm::sys::fs::CD_OpenExisting, llvm::sys::fs::OF_None);
ASSERT_FALSE(EC);
// Set the access time to be old enough to get pruned.
time_t OldTime = time(nullptr) - 86400;
EC = llvm::sys::fs::setLastAccessAndModificationTime(
FD, llvm::sys::toTimePoint(OldTime));
ASSERT_FALSE(EC);
::close(FD);
};
// Create a subdirectory with a .pcm file and a .timestamp file.
llvm::SmallString<256> SubDir(CacheDir);
llvm::sys::path::append(SubDir, "ABCDEF");
ASSERT_FALSE(llvm::sys::fs::create_directory(SubDir));
llvm::SmallString<256> PCMFile(SubDir);
llvm::sys::path::append(PCMFile, "Foo.pcm");
writeFile(PCMFile, "fake pcm");
// Also create a .pcm file in the root of the cache directory.
llvm::SmallString<256> RootPCMFile(CacheDir);
llvm::sys::path::append(RootPCMFile, "Bar.pcm");
writeFile(RootPCMFile, "fake pcm");
setOldAccessTime(PCMFile);
setOldAccessTime(RootPCMFile);
// Create the modules.timestamp file with an old modification time so the
// prune interval check passes.
llvm::SmallString<256> ModulesTimestamp(CacheDir);
llvm::sys::path::append(ModulesTimestamp, "modules.timestamp");
writeFile(ModulesTimestamp);
setOldAccessTime(ModulesTimestamp);
// Verify the files exist before pruning.
EXPECT_TRUE(llvm::sys::fs::exists(PCMFile));
EXPECT_TRUE(llvm::sys::fs::exists(RootPCMFile));
clang_ModuleCache_prune(CacheDir.c_str(), /*PruneInterval=*/1,
/*PruneAfter=*/1);
// The old .pcm files should have been removed from both the subdirectory
// and the root.
EXPECT_FALSE(llvm::sys::fs::exists(PCMFile));
EXPECT_FALSE(llvm::sys::fs::exists(RootPCMFile));
llvm::sys::fs::remove_directories(CacheDir);
}
TEST_F(LibclangParseTest, GlobalOptions) {
EXPECT_EQ(clang_CXIndex_getGlobalOptions(Index), CXGlobalOpt_None);
}