Skip to content

Commit a90449f

Browse files
committed
[libclang] Add API to generate a reproducer for the explicitly-built modules. (#10577)
Capturing a single `-cc1` command to reproduce a bug with the explicitly-built modules isn't sufficient, we need to know what .pcm files were built and how. rdar://59743925
1 parent 46ca354 commit a90449f

File tree

5 files changed

+270
-0
lines changed

5 files changed

+270
-0
lines changed

clang/include/clang-c/Dependencies.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,56 @@ CXCStringArray
590590
clang_experimental_DependencyScannerService_getInvalidNegStatCachedPaths(
591591
CXDependencyScannerService);
592592

593+
/**
594+
* Options used to generate a reproducer.
595+
*/
596+
typedef struct CXOpaqueDependencyScannerReproducerOptions
597+
*CXDependencyScannerReproducerOptions;
598+
599+
/**
600+
* Creates a set of settings for
601+
* \c clang_experimental_DependencyScanner_generateReproducer action.
602+
* Must be disposed with
603+
* \c clang_experimental_DependencyScannerReproducerOptions_dispose.
604+
*
605+
* \param argc the number of compiler invocation arguments (including argv[0]).
606+
* \param argv the compiler driver invocation arguments (including argv[0]).
607+
* \param ModuleName If non-NULL, reproduce building the named module and all
608+
* the intermediate modules. Otherwise, reproduce building
609+
* the whole translation unit.
610+
* \param WorkingDirectory the directory in which the invocation runs.
611+
*/
612+
CINDEX_LINKAGE CXDependencyScannerReproducerOptions
613+
clang_experimental_DependencyScannerReproducerOptions_create(
614+
int argc, const char *const *argv, const char *ModuleName,
615+
const char *WorkingDirectory);
616+
617+
CINDEX_LINKAGE void
618+
clang_experimental_DependencyScannerReproducerOptions_dispose(
619+
CXDependencyScannerReproducerOptions);
620+
621+
/**
622+
* Generates a reproducer to compile a requested file with required modules.
623+
*
624+
* Here the reproducer means the required input data with the commands to
625+
* compile intermediate modules and a requested file. Required intermediate
626+
* modules and the order of their compilation are determined by the function
627+
* and don't need to be provided.
628+
*
629+
* \param CXOptions object created via
630+
* \c clang_experimental_DependencyScannerReproducerOptions_create.
631+
* \param [out] MessageOut A pointer to store the human-readable message
632+
* describing the result of the operation. If non-NULL,
633+
* owned and should be disposed by the caller.
634+
*
635+
* \returns \c CXError_Success on success; otherwise a non-zero \c CXErrorCode
636+
* indicating the kind of error. \p MessageOut is guaranteed to be populated
637+
* for a success case but is allowed to be empty when encountered an error.
638+
*/
639+
CINDEX_LINKAGE enum CXErrorCode
640+
clang_experimental_DependencyScanner_generateReproducer(
641+
CXDependencyScannerReproducerOptions CXOptions, CXString *MessageOut);
642+
593643
/**
594644
* @}
595645
*/
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Test generating a reproducer for a modular build where required modules are
2+
// built explicitly as separate steps.
3+
4+
// RUN: rm -rf %t
5+
// RUN: split-file %s %t
6+
//
7+
// RUN: c-index-test core -gen-deps-reproducer -working-dir %t \
8+
// RUN: -- clang-executable -c %t/reproducer.c -o %t/reproducer.o \
9+
// RUN: -fmodules -fmodules-cache-path=%t | FileCheck %t/reproducer.c
10+
11+
// Test a failed attempt at generating a reproducer.
12+
// RUN: not c-index-test core -gen-deps-reproducer -working-dir %t \
13+
// RUN: -- clang-executable -c %t/failed-reproducer.c -o %t/reproducer.o \
14+
// RUN: -fmodules -fmodules-cache-path=%t 2>&1 | FileCheck %t/failed-reproducer.c
15+
16+
//--- modular-header.h
17+
void fn_in_modular_header(void);
18+
19+
//--- module.modulemap
20+
module Test { header "modular-header.h" export * }
21+
22+
//--- reproducer.c
23+
// CHECK: Sources and associated run script(s) are located at:
24+
#include "modular-header.h"
25+
26+
void test(void) {
27+
fn_in_modular_header();
28+
}
29+
30+
//--- failed-reproducer.c
31+
// CHECK: fatal error: 'non-existing-header.h' file not found
32+
#include "non-existing-header.h"

clang/tools/c-index-test/core_main.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ enum class ActionType {
5959
AggregateAsJSON,
6060
ScanDeps,
6161
ScanDepsByModuleName,
62+
GenerateDepsReproducer,
6263
UploadCachedJob,
6364
MaterializeCachedJob,
6465
ReplayCachedJob,
@@ -87,6 +88,8 @@ Action(cl::desc("Action:"), cl::init(ActionType::None),
8788
"Get file dependencies"),
8889
clEnumValN(ActionType::ScanDepsByModuleName, "scan-deps-by-mod-name",
8990
"Get file dependencies by module name alone"),
91+
clEnumValN(ActionType::GenerateDepsReproducer, "gen-deps-reproducer",
92+
"Generate a reproducer for the file"),
9093
clEnumValN(ActionType::UploadCachedJob, "upload-cached-job",
9194
"Upload cached compilation data to upstream CAS"),
9295
clEnumValN(ActionType::MaterializeCachedJob, "materialize-cached-job",
@@ -923,6 +926,30 @@ static int scanDeps(ArrayRef<const char *> Args, std::string WorkingDirectory,
923926
return 1;
924927
}
925928

929+
static int generateDepsReproducer(ArrayRef<const char *> Args,
930+
std::string WorkingDirectory) {
931+
CXDependencyScannerReproducerOptions Opts =
932+
clang_experimental_DependencyScannerReproducerOptions_create(
933+
Args.size(), Args.data(), /*ModuleName=*/nullptr,
934+
WorkingDirectory.c_str());
935+
auto DisposeOpts = llvm::make_scope_exit([&] {
936+
clang_experimental_DependencyScannerReproducerOptions_dispose(Opts);
937+
});
938+
CXString MessageString;
939+
auto DisposeMessageString = llvm::make_scope_exit([&]() {
940+
clang_disposeString(MessageString);
941+
});
942+
CXErrorCode ExitCode =
943+
clang_experimental_DependencyScanner_generateReproducer(Opts,
944+
&MessageString);
945+
if (ExitCode == CXError_Success) {
946+
llvm::outs() << clang_getCString(MessageString) << "\n";
947+
} else {
948+
llvm::errs() << "error: " << clang_getCString(MessageString) << "\n";
949+
}
950+
return (ExitCode == CXError_Success) ? 0 : 1;
951+
}
952+
926953
static int uploadCachedJob(std::string CacheKey, CXCASDatabases DBs) {
927954
CXError Err = nullptr;
928955
CXCASCachedCompilation CComp = clang_experimental_cas_getCachedCompilation(
@@ -1548,6 +1575,14 @@ int indextest_core_main(int argc, const char **argv) {
15481575
options::OutputDir, DBs, options::ModuleName);
15491576
}
15501577

1578+
if (options::Action == ActionType::GenerateDepsReproducer) {
1579+
if (options::WorkingDir.empty()) {
1580+
errs() << "error: missing -working-dir\n";
1581+
return 1;
1582+
}
1583+
return generateDepsReproducer(CompArgs, options::WorkingDir);
1584+
}
1585+
15511586
if (options::Action == ActionType::UploadCachedJob) {
15521587
if (options::InputFiles.empty()) {
15531588
errs() << "error: missing cache key\n";

clang/tools/libclang/CDependencies.cpp

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,153 @@ std::string OutputLookup::lookupModuleOutput(const ModuleDeps &MD,
640640
PCMPath.first->second = ::lookupModuleOutput(MD, MOK, MLOContext, MLO);
641641
return PCMPath.first->second;
642642
}
643+
644+
namespace {
645+
struct DependencyScannerReproducerOptions {
646+
int argc;
647+
const char *const *argv;
648+
const char *ModuleName;
649+
const char *WorkingDirectory;
650+
};
651+
652+
// Helper class to capture a returnable error code and to return a formatted
653+
// message in a provided CXString pointer.
654+
class MessageEmitter {
655+
const CXErrorCode ErrorCode;
656+
CXString *OutputString;
657+
std::string Buffer;
658+
llvm::raw_string_ostream Stream;
659+
660+
public:
661+
MessageEmitter(CXErrorCode Code, CXString *Output)
662+
: ErrorCode(Code), OutputString(Output), Stream(Buffer) {}
663+
~MessageEmitter() {
664+
if (OutputString)
665+
*OutputString = clang::cxstring::createDup(Buffer.c_str());
666+
}
667+
668+
operator CXErrorCode() const { return ErrorCode; }
669+
670+
template <typename T> MessageEmitter &operator<<(const T &t) {
671+
Stream << t;
672+
return *this;
673+
}
674+
};
675+
} // end anonymous namespace
676+
677+
DEFINE_SIMPLE_CONVERSION_FUNCTIONS(DependencyScannerReproducerOptions,
678+
CXDependencyScannerReproducerOptions)
679+
680+
CXDependencyScannerReproducerOptions
681+
clang_experimental_DependencyScannerReproducerOptions_create(
682+
int argc, const char *const *argv, const char *ModuleName,
683+
const char *WorkingDirectory) {
684+
return wrap(new DependencyScannerReproducerOptions{argc, argv, ModuleName,
685+
WorkingDirectory});
686+
}
687+
688+
void clang_experimental_DependencyScannerReproducerOptions_dispose(
689+
CXDependencyScannerReproducerOptions Options) {
690+
delete unwrap(Options);
691+
}
692+
693+
enum CXErrorCode clang_experimental_DependencyScanner_generateReproducer(
694+
CXDependencyScannerReproducerOptions CXOptions, CXString *MessageOut) {
695+
auto Report = [MessageOut](CXErrorCode ErrorCode) -> MessageEmitter {
696+
return MessageEmitter(ErrorCode, MessageOut);
697+
};
698+
auto ReportFailure = [&Report]() -> MessageEmitter {
699+
return Report(CXError_Failure);
700+
};
701+
702+
DependencyScannerReproducerOptions &Opts = *unwrap(CXOptions);
703+
int argc = Opts.argc;
704+
const char *const *argv = Opts.argv;
705+
if (argc < 2 || !argv)
706+
return Report(CXError_InvalidArguments) << "missing compilation command";
707+
if (!Opts.WorkingDirectory)
708+
return Report(CXError_InvalidArguments) << "missing working directory";
709+
710+
CASOptions CASOpts;
711+
IntrusiveRefCntPtr<llvm::cas::CachingOnDiskFileSystem> FS;
712+
DependencyScanningService DepsService(
713+
ScanningMode::DependencyDirectivesScan, ScanningOutputFormat::Full,
714+
CASOpts, /*CAS=*/nullptr, /*ActionCache=*/nullptr, FS);
715+
DependencyScanningTool DepsTool(DepsService);
716+
717+
llvm::SmallString<128> ReproScriptPath;
718+
int ScriptFD;
719+
if (auto EC = llvm::sys::fs::createTemporaryFile("reproducer", "sh", ScriptFD,
720+
ReproScriptPath)) {
721+
return ReportFailure() << "failed to create a reproducer script file";
722+
}
723+
SmallString<128> FileCachePath = ReproScriptPath;
724+
llvm::sys::path::replace_extension(FileCachePath, ".cache");
725+
726+
std::string FileCacheName = llvm::sys::path::filename(FileCachePath).str();
727+
auto LookupOutput = [&FileCacheName](const ModuleDeps &MD,
728+
ModuleOutputKind MOK) -> std::string {
729+
if (MOK != ModuleOutputKind::ModuleFile)
730+
return "";
731+
return FileCacheName + "/explicitly-built-modules/" +
732+
MD.ID.ModuleName + "-" + MD.ID.ContextHash + ".pcm";
733+
};
734+
735+
std::vector<std::string> Compilation{argv, argv + argc};
736+
llvm::DenseSet<ModuleID> AlreadySeen;
737+
auto TUDepsOrErr = DepsTool.getTranslationUnitDependencies(
738+
Compilation, Opts.WorkingDirectory, AlreadySeen, std::move(LookupOutput));
739+
if (!TUDepsOrErr)
740+
return ReportFailure() << "failed to generate a reproducer\n"
741+
<< toString(TUDepsOrErr.takeError());
742+
743+
TranslationUnitDeps TU = *TUDepsOrErr;
744+
llvm::raw_fd_ostream ScriptOS(ScriptFD, /*shouldClose=*/true);
745+
ScriptOS << "# Original command:\n#";
746+
for (StringRef Arg : Compilation)
747+
ScriptOS << ' ' << Arg;
748+
ScriptOS << "\n\n";
749+
750+
ScriptOS << "# Dependencies:\n";
751+
std::string ReproExecutable = std::string(argv[0]);
752+
auto PrintArguments = [&ReproExecutable,
753+
&FileCacheName](llvm::raw_fd_ostream &OS,
754+
ArrayRef<std::string> Arguments) {
755+
OS << ReproExecutable;
756+
for (int I = 0, E = Arguments.size(); I < E; ++I)
757+
OS << ' ' << Arguments[I];
758+
OS << " -ivfsoverlay \"" << FileCacheName << "/vfs/vfs.yaml\"";
759+
OS << '\n';
760+
};
761+
for (ModuleDeps &Dep : TU.ModuleGraph)
762+
PrintArguments(ScriptOS, Dep.getBuildArguments());
763+
ScriptOS << "\n# Translation unit:\n";
764+
for (const Command &BuildCommand : TU.Commands)
765+
PrintArguments(ScriptOS, BuildCommand.Arguments);
766+
767+
SmallString<128> VFSCachePath = FileCachePath;
768+
llvm::sys::path::append(VFSCachePath, "vfs");
769+
std::string VFSCachePathStr = VFSCachePath.str().str();
770+
llvm::FileCollector FileCollector(VFSCachePathStr,
771+
/*OverlayRoot=*/VFSCachePathStr);
772+
for (const auto &FileDep : TU.FileDeps) {
773+
FileCollector.addFile(FileDep);
774+
}
775+
for (ModuleDeps &ModuleDep : TU.ModuleGraph) {
776+
ModuleDep.forEachFileDep([&FileCollector](StringRef FileDep) {
777+
FileCollector.addFile(FileDep);
778+
});
779+
}
780+
if (FileCollector.copyFiles(/*StopOnError=*/true))
781+
return ReportFailure()
782+
<< "failed to copy the files used for the compilation";
783+
SmallString<128> VFSOverlayPath = VFSCachePath;
784+
llvm::sys::path::append(VFSOverlayPath, "vfs.yaml");
785+
if (FileCollector.writeMapping(VFSOverlayPath))
786+
return ReportFailure() << "failed to write a VFS overlay mapping";
787+
788+
return Report(CXError_Success)
789+
<< "Created a reproducer. Sources and associated run script(s) are "
790+
"located at:\n "
791+
<< FileCachePath << "\n " << ReproScriptPath;
792+
}

clang/tools/libclang/libclang.map

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,9 @@ LLVM_21 {
578578
clang_experimental_DepGraphModule_isInStableDirs;
579579
clang_getFullyQualifiedName;
580580
clang_experimental_DependencyScannerService_getInvalidNegStatCachedPaths;
581+
clang_experimental_DependencyScannerReproducerOptions_create;
582+
clang_experimental_DependencyScannerReproducerOptions_dispose;
583+
clang_experimental_DependencyScanner_generateReproducer;
581584
};
582585

583586
# Example of how to add a new symbol version entry. If you do add a new symbol

0 commit comments

Comments
 (0)