Merge topic 'AUTOMOC-cxxmodules-support'

6280cc3df9 Autogen: Add AUTOMOC support for C++ module units
266c6f0109 Autogen: Factor out the info-file source-entry reader

Acked-by: Kitware Robot <kwrobot@kitware.com>
Tested-by: buildbot <buildbot@kitware.com>
Reviewed-by: Vito Gamberini <vito.gamberini@kitware.com>
Reviewed-by: Ben Boeckel <ben.boeckel@kitware.com>
Merge-request: !12490
This commit is contained in:
Brad King
2026-09-11 09:16:10 -04:00
committed by Kitware Robot
30 changed files with 748 additions and 168 deletions
+23
View File
@@ -107,6 +107,29 @@ be generated in a different location than if it was not included. This is
described in the section `Output file location`_.
C++ module unit processing
^^^^^^^^^^^^^^^^^^^^^^^^^^
C++ module interface and partition units that are members of a
``FILE_SET`` of type ``CXX_MODULES`` (see :prop_tgt:`CXX_MODULE_SETS`)
are scanned by ``AUTOMOC`` for a Qt macro from
:prop_tgt:`AUTOMOC_MACRO_NAMES`, just like other source files. Running
``moc`` on them requires Qt 6.13 or newer, whose ``moc`` supports C++
module units. With older Qt, a Qt macro found in such a unit is
reported as an error, and a unit without one is left alone.
If a Qt macro is found, ``moc`` is run on the module unit and its
generated output is compiled as its own translation unit, as a module
implementation unit of the same module. This output participates in
the target's C++ module dependency scanning like any other source. It
is not added to the ``mocs_compilation.cpp`` file described in
`Output file location`_.
Module implementation units and private module fragments are not
supported: ``moc`` rejects a Qt macro found in either of these, and
``AUTOMOC`` does not work around that restriction.
Output file location
^^^^^^^^^^^^^^^^^^^^
+8
View File
@@ -0,0 +1,8 @@
automoc-cxx-modules
-------------------
* :prop_tgt:`AUTOMOC` now processes C++ module interface and partition
units that are members of a ``FILE_SET`` of type ``CXX_MODULES``.
``moc`` is run on such units and its generated output is compiled as a
module implementation unit of the same module. This requires Qt 6.13
or newer, whose ``moc`` supports C++ module units.
+119 -5
View File
@@ -33,10 +33,12 @@
#include "cmCustomCommandLines.h"
#include "cmDiagnostics.h"
#include "cmEvaluatedTargetProperty.h"
#include "cmFileSetMetadata.h"
#include "cmGenExContext.h"
#include "cmGeneratedFileStream.h"
#include "cmGeneratorExpression.h"
#include "cmGeneratorExpressionDAGChecker.h"
#include "cmGeneratorFileSet.h"
#include "cmGeneratorTarget.h"
#include "cmGlobalGenerator.h"
#include "cmLinkItem.h"
@@ -1009,6 +1011,17 @@ bool cmQtAutoGenInitializer::InitScanFiles()
this->AutogenTarget.Sources.emplace(muf->SF, std::move(muf));
};
auto addMUModuleUnit = [this](MUFileHandle&& muf) {
if (muf->SkipMoc) {
return;
}
// AUTOUIC is not wired up for module units (the uic eval pass ignores
// the ModuleUnits collection), so don't carry a UicIt flag nothing acts
// on.
muf->UicIt = false;
this->AutogenTarget.ModuleUnits.emplace(muf->SF, std::move(muf));
};
// Scan through target files
{
// Scan through target files
@@ -1020,7 +1033,15 @@ bool cmQtAutoGenInitializer::InitScanFiles()
// Register files that will be scanned by moc or uic
if (this->MocOrUicEnabled()) {
if (cm->IsAHeaderExtension(extLower)) {
// Query one config only: file-set membership can differ per
// config, but a per-config module unit kind is not modeled here.
cmGeneratorFileSet const* fileSet =
this->GenTarget->GetFileSetForSource(this->ConfigDefault,
acs.Source);
if (fileSet &&
fileSet->GetType() == cm::FileSetMetadata::CXX_MODULES) {
addMUModuleUnit(makeMUFile(acs.Source, fullPath, acs.Configs, true));
} else if (cm->IsAHeaderExtension(extLower)) {
addMUHeader(makeMUFile(acs.Source, fullPath, acs.Configs, true),
extLower);
} else if (cm->IsACLikeSourceExtension(extLower)) {
@@ -1406,6 +1427,68 @@ bool cmQtAutoGenInitializer::InitAutogenTarget()
} else {
autogenByproducts.push_back(this->Moc.CompilationFileGenex);
}
// Module-unit moc outputs are implementation units ("module M;") that
// are compiled individually rather than folded into
// mocs_compilation.cpp, and must be scanned so dyndep can order them
// after the module's BMI.
// Sort by path so GetMocBuildPath's dedup-suffix assignment and the
// AddSource order below do not depend on unordered_map hash order.
// moc can only process C++ module units since Qt 6.13. With older Qt
// there is no moc output to compile, and cmQtAutoMocUic reports any
// meta-object macro found in such a unit instead.
std::vector<MUFile*> moduleUnits;
if (this->QtVersion >= IntegerVersion(6, 13)) {
moduleUnits.reserve(this->AutogenTarget.ModuleUnits.size());
for (auto const& pair : this->AutogenTarget.ModuleUnits) {
moduleUnits.push_back(pair.second.get());
}
std::sort(moduleUnits.begin(), moduleUnits.end(),
[](MUFile const* a, MUFile const* b) {
return (a->FullPath < b->FullPath);
});
}
for (MUFile* mufPtr : moduleUnits) {
MUFile& muf = *mufPtr;
if (!muf.MocIt) {
continue;
}
std::string const& mocBuildPath = this->GetMocBuildPath(muf);
if (!this->MultiConfig || this->GlobalGen->IsXcode()) {
std::string const outPath =
cmStrCat(this->Dir.Include.Default, '/', mocBuildPath);
cmSourceFile* sf = this->RegisterGeneratedSource(outPath, true);
// A PCH force-include would inject declarations ahead of the
// module implementation unit's "module M;", which may only be
// preceded by comments and preprocessor directives.
sf->SetProperty("SKIP_PRECOMPILE_HEADERS", "ON");
this->GenTarget->AddSource(outPath);
// Declare as a byproduct so Ninja re-stats it after autogen reruns.
if (useDepfile) {
timestampByproducts.push_back(outPath);
} else {
autogenByproducts.push_back(outPath);
}
} else {
for (auto const& cfg : this->ConfigsList) {
std::string const outPath =
cmStrCat(this->Dir.Include.Config.at(cfg), '/', mocBuildPath);
cmSourceFile* sf = this->RegisterGeneratedSource(outPath, true);
// A PCH force-include would inject declarations ahead of the
// module implementation unit's "module M;", which may only be
// preceded by comments and preprocessor directives.
sf->SetProperty("SKIP_PRECOMPILE_HEADERS", "ON");
this->GenTarget->AddSource(
cmStrCat("$<$<CONFIG:"_s, cfg, ">:"_s, outPath, ">"_s));
// Declare as a byproduct so Ninja re-stats it after autogen reruns.
if (useDepfile) {
timestampByproducts.push_back(outPath);
} else {
autogenByproducts.push_back(outPath);
}
}
}
}
}
if (this->Uic.Enabled) {
@@ -1906,6 +1989,7 @@ bool cmQtAutoGenInitializer::SetupWriteAutogenInfo()
std::set<std::string> uic_skip;
std::vector<MUFile const*> headers;
std::vector<MUFile const*> sources;
std::vector<MUFile const*> moduleUnits;
// Filter headers
{
@@ -1955,6 +2039,21 @@ bool cmQtAutoGenInitializer::SetupWriteAutogenInfo()
});
}
// Filter module units
{
moduleUnits.reserve(this->AutogenTarget.ModuleUnits.size());
for (auto const& pair : this->AutogenTarget.ModuleUnits) {
MUFile const* const muf = pair.second.get();
if (muf->MocIt) {
moduleUnits.emplace_back(muf);
}
}
std::sort(moduleUnits.begin(), moduleUnits.end(),
[](MUFile const* a, MUFile const* b) {
return (a->FullPath < b->FullPath);
});
}
// Info writer
InfoWriter info;
@@ -2016,6 +2115,15 @@ bool cmQtAutoGenInitializer::SetupWriteAutogenInfo()
jval[1u] = cmStrCat(muf->MocIt ? 'M' : 'm', muf->UicIt ? 'U' : 'u');
jval[2u] = cfgArray(muf->Configs);
});
info.SetArrayArray("CXX_MODULE_UNITS", moduleUnits,
[this, &cfgArray](Json::Value& jval, MUFile const* muf) {
jval.resize(4u);
jval[0u] = muf->FullPath;
jval[1u] = cmStrCat(muf->MocIt ? 'M' : 'm',
muf->UicIt ? 'U' : 'u');
jval[2u] = cfgArray(muf->Configs);
jval[3u] = this->GetMocBuildPath(*muf);
});
// Write moc settings
if (this->Moc.Enabled) {
@@ -2170,7 +2278,7 @@ bool cmQtAutoGenInitializer::SetupWriteRccInfo()
}
cmSourceFile* cmQtAutoGenInitializer::RegisterGeneratedSource(
std::string const& filename)
std::string const& filename, bool scanForModules)
{
cmSourceFile* gFile = this->Makefile->GetOrCreateSource(filename, true);
gFile->SetSpecialSourceType(
@@ -2178,7 +2286,7 @@ cmSourceFile* cmQtAutoGenInitializer::RegisterGeneratedSource(
gFile->MarkAsGenerated();
gFile->SetProperty("SKIP_AUTOGEN", "1");
gFile->SetProperty("SKIP_LINTING", "ON");
gFile->SetProperty("CXX_SCAN_FOR_MODULES", "0");
gFile->SetProperty("CXX_SCAN_FOR_MODULES", scanForModules ? "1" : "0");
return gFile;
}
@@ -2457,9 +2565,15 @@ cmQtAutoGenInitializer::GetQtVersion(cmGeneratorTarget const* target,
return res;
}
std::string cmQtAutoGenInitializer::GetMocBuildPath(MUFile const& muf)
std::string const& cmQtAutoGenInitializer::GetMocBuildPath(MUFile const& muf)
{
std::string res;
// The de-duplication below is not idempotent: without memoizing, a second
// call for the same file would hand out a different path.
if (!muf.MocBuildPath.empty()) {
return muf.MocBuildPath;
}
std::string& res = muf.MocBuildPath;
if (!muf.MocIt) {
return res;
}
+7 -2
View File
@@ -63,6 +63,9 @@ public:
bool SkipUic = false;
bool MocIt = false;
bool UicIt = false;
// Memoized GetMocBuildPath() result. Mutable because the info file
// writer reaches this through const references.
mutable std::string MocBuildPath;
};
using MUFileHandle = std::unique_ptr<MUFile>;
@@ -119,7 +122,8 @@ private:
bool SetupWriteAutogenInfo();
bool SetupWriteRccInfo();
cmSourceFile* RegisterGeneratedSource(std::string const& filename);
cmSourceFile* RegisterGeneratedSource(std::string const& filename,
bool scanForModules = false);
cmSourceFile* AddGeneratedSource(std::string const& filename,
GenVarsT const& genVars,
bool prepend = false);
@@ -140,7 +144,7 @@ private:
std::string const& fileName);
void ConfigFileClean(ConfigString& configString);
std::string GetMocBuildPath(MUFile const& muf);
std::string const& GetMocBuildPath(MUFile const& muf);
bool GetQtExecutable(GenVarsT& genVars, std::string const& executable,
bool ignoreMissingTarget) const;
@@ -204,6 +208,7 @@ private:
// Sources to process
std::unordered_map<cmSourceFile*, MUFileHandle> Headers;
std::unordered_map<cmSourceFile*, MUFileHandle> Sources;
std::unordered_map<cmSourceFile*, MUFileHandle> ModuleUnits;
std::vector<MUFile*> FilesGenerated;
std::vector<cmSourceFile*> CMP0100HeadersWarn;
} AutogenTarget;
+248 -161
View File
@@ -125,6 +125,15 @@ public:
std::unordered_map<std::string, FileHandleT> Map_;
};
/** Kind of source file processed by moc. Headers and module units carry
a moc build path in the info file, sources do not. */
enum class SourceFileKind
{
Header,
Source,
ModuleUnit,
};
/** Source file data. */
class SourceFileT
{
@@ -138,7 +147,7 @@ public:
cmFileTime FileTime;
ParseCacheT::FileHandleT ParseData;
std::string BuildPath;
bool IsHeader = false;
SourceFileKind Kind = SourceFileKind::Source;
bool Moc = false;
bool Uic = false;
};
@@ -201,6 +210,7 @@ public:
// -- Sources
SourceFileMapT Headers;
SourceFileMapT Sources;
SourceFileMapT ModuleUnits;
};
/** Moc settings. */
@@ -249,6 +259,8 @@ public:
// -- Mappings
MappingMapT HeaderMappings;
MappingMapT SourceMappings;
// Module unit outputs are implementation units, kept out of CompFiles.
MappingMapT ModuleMappings;
MappingMapT Includes;
// -- Discovered files
SourceFileMapT HeadersDiscovered;
@@ -420,6 +432,7 @@ public:
{
void Process() override;
bool EvalHeader(SourceFileHandleT source);
bool EvalModuleUnit(SourceFileHandleT source);
bool EvalSource(SourceFileHandleT const& source);
bool FindIncludedHeader(SourceFileHandleT& headerHandle,
cm::string_view includerDir,
@@ -562,6 +575,9 @@ public:
private:
// -- Abstract processing interface
bool InitFromInfo(InfoT const& info) override;
// Read the source-entry array of the given kind from the info file into the
// matching source map.
bool InitSourceEntries(InfoT const& info, SourceFileKind kind);
void InitJobs();
bool Process() override;
// -- Settings file
@@ -1180,6 +1196,12 @@ void cmQtAutoMocUicT::JobEvalCacheMocT::Process()
return;
}
}
// Evaluate module units
for (auto const& pair : this->BaseEval().ModuleUnits) {
if (!this->EvalModuleUnit(pair.second)) {
return;
}
}
// Evaluate sources
for (auto const& pair : this->BaseEval().Sources) {
if (!this->EvalSource(pair.second)) {
@@ -1201,8 +1223,11 @@ bool cmQtAutoMocUicT::JobEvalCacheMocT::EvalHeader(SourceFileHandleT source)
MappingHandleT handle = std::make_shared<MappingT>();
handle->SourceFile = std::move(source);
// Absolute build path
if (this->BaseConst().MultiConfig) {
// A module unit's moc output is added as a target source at the include
// dir for every config, so it must be written there too, regardless of
// single- vs multi-config.
if (sourceFile.Kind == SourceFileKind::ModuleUnit ||
this->BaseConst().MultiConfig) {
handle->OutputFile =
this->Gen()->AbsoluteIncludePath(sourceFile.BuildPath);
} else {
@@ -1212,11 +1237,55 @@ bool cmQtAutoMocUicT::JobEvalCacheMocT::EvalHeader(SourceFileHandleT source)
// Register mapping in headers map
this->RegisterMapping(handle);
} else if (sourceFile.Kind == SourceFileKind::ModuleUnit) {
// A CXX_MODULES member without a meta-object macro still needs its
// registered moc output to exist; an empty TU is valid and scans clean.
std::string const outputFile =
this->Gen()->AbsoluteIncludePath(sourceFile.BuildPath);
std::string const placeholder =
"enum some_compilers { need_more_than_nothing };\n";
if (cmQtAutoGenerator::FileDiffers(outputFile, placeholder)) {
if (!cmQtAutoGenerator::FileWrite(outputFile, placeholder)) {
this->LogError(GenT::MOC,
cmStrCat("Writing MOC placeholder ",
this->MessagePath(outputFile), " failed."));
return false;
}
}
}
return true;
}
bool cmQtAutoMocUicT::JobEvalCacheMocT::EvalModuleUnit(
SourceFileHandleT source)
{
if (this->BaseConst().QtVersion >= IntegerVersion(6, 13)) {
return this->EvalHeader(std::move(source));
}
// Older moc cannot process C++ module units at all, so nothing can be
// generated for this one. Report a meta-object macro in it instead of
// leaving the user with a missing meta-object at link time.
SourceFileT const& sourceFile = *source;
auto const& parseData = sourceFile.ParseData->Moc;
if (!sourceFile.Moc || parseData.Macro.empty()) {
return true;
}
this->LogError(
GenT::MOC,
cmStrCat(this->MessagePath(sourceFile.FileName), "\ncontains a ",
Quoted(parseData.Macro),
" macro, but it is a C++ module unit and moc from Qt ",
this->BaseConst().QtVersion.Major, '.',
this->BaseConst().QtVersion.Minor,
" cannot process those.\nAUTOMOC handles meta-object macros in"
" C++ module units with Qt 6.13 or newer.\nConsider to\n"
" - move the affected class out of the module unit\n"
" - enable SKIP_AUTOMOC for this file"));
return false;
}
bool cmQtAutoMocUicT::JobEvalCacheMocT::EvalSource(
SourceFileHandleT const& source)
{
@@ -1464,7 +1533,7 @@ bool cmQtAutoMocUicT::JobEvalCacheMocT::FindIncludedHeader(
if (!handle) {
handle = std::make_shared<SourceFileT>(testPath);
handle->FileTime = fileTime;
handle->IsHeader = true;
handle->Kind = SourceFileKind::Header;
handle->Moc = true;
}
headerHandle = handle;
@@ -1542,11 +1611,23 @@ bool cmQtAutoMocUicT::JobEvalCacheMocT::RegisterIncluded(
void cmQtAutoMocUicT::JobEvalCacheMocT::RegisterMapping(
MappingHandleT mappingHandle) const
{
auto& regMap = mappingHandle->SourceFile->IsHeader
? this->MocEval().HeaderMappings
: this->MocEval().SourceMappings;
// Module units must never land in HeaderMappings: that map feeds
// CompFiles/mocs_compilation.cpp, and amalgamating multiple "module M;"
// implementation units into one TU is invalid.
MappingMapT* regMap = nullptr;
switch (mappingHandle->SourceFile->Kind) {
case SourceFileKind::Header:
regMap = &this->MocEval().HeaderMappings;
break;
case SourceFileKind::Source:
regMap = &this->MocEval().SourceMappings;
break;
case SourceFileKind::ModuleUnit:
regMap = &this->MocEval().ModuleMappings;
break;
}
// Check if source file already gets mapped
auto& regHandle = regMap[mappingHandle->SourceFile->FileName];
auto& regHandle = (*regMap)[mappingHandle->SourceFile->FileName];
if (!regHandle) {
// Yet unknown mapping
regHandle = std::move(mappingHandle);
@@ -1762,6 +1843,14 @@ void cmQtAutoMocUicT::JobProbeDepsMocT::Process()
return;
}
}
// Create moc module unit jobs. Never added to CompFiles: each output is
// a module implementation unit and must be compiled as its own TU.
for (auto const& pair : this->MocEval().ModuleMappings) {
if (!this->Generate(pair.second, false)) {
return;
}
}
}
bool cmQtAutoMocUicT::JobProbeDepsMocT::Generate(MappingHandleT const& mapping,
@@ -2056,8 +2145,10 @@ void cmQtAutoMocUicT::JobCompileMocT::Process()
cmd.emplace_back("--include");
cmd.push_back(this->MocConst().PredefsFileAbs);
}
// Add path prefix on demand
if (this->MocConst().PathPrefix && this->Mapping->SourceFile->IsHeader) {
// Add path prefix on demand. Module units are compiled directly, not
// included, so the prefix is meaningless for them.
if (this->MocConst().PathPrefix &&
this->Mapping->SourceFile->Kind == SourceFileKind::Header) {
for (std::string const& dir : this->MocConst().IncludePaths) {
cm::string_view prefix = sourceFile;
if (cmHasPrefix(prefix, dir)) {
@@ -2267,9 +2358,9 @@ std::vector<std::string>
cmQtAutoMocUicT::JobDepFilesMergeT::initialDependencies() const
{
std::vector<std::string> dependencies;
dependencies.reserve(this->BaseConst().ListFiles.size() +
this->BaseEval().Headers.size() +
this->BaseEval().Sources.size());
dependencies.reserve(
this->BaseConst().ListFiles.size() + this->BaseEval().Headers.size() +
this->BaseEval().Sources.size() + this->BaseEval().ModuleUnits.size());
cm::append(dependencies, this->BaseConst().ListFiles);
auto append_file_path =
[&dependencies](SourceFileMapT::value_type const& p) {
@@ -2279,6 +2370,9 @@ cmQtAutoMocUicT::JobDepFilesMergeT::initialDependencies() const
this->BaseEval().Headers.end(), append_file_path);
std::for_each(this->BaseEval().Sources.begin(),
this->BaseEval().Sources.end(), append_file_path);
// Module unit sources must also trigger autogen reruns.
std::for_each(this->BaseEval().ModuleUnits.begin(),
this->BaseEval().ModuleUnits.end(), append_file_path);
return dependencies;
}
@@ -2314,6 +2408,10 @@ void cmQtAutoMocUicT::JobDepFilesMergeT::Process()
this->MocEval().HeaderMappings.end(), processMappingEntry);
std::for_each(this->MocEval().SourceMappings.begin(),
this->MocEval().SourceMappings.end(), processMappingEntry);
// Module units also produce a moc ".d" file that must feed the
// merged depfile, even though they are excluded from CompFiles.
std::for_each(this->MocEval().ModuleMappings.begin(),
this->MocEval().ModuleMappings.end(), processMappingEntry);
// Remove SKIP_AUTOMOC files.
// Also remove AUTOUIC header files to avoid cyclic dependency.
@@ -2380,6 +2478,133 @@ cmQtAutoMocUicT::cmQtAutoMocUicT()
}
cmQtAutoMocUicT::~cmQtAutoMocUicT() = default;
bool cmQtAutoMocUicT::InitSourceEntries(InfoT const& info, SourceFileKind kind)
{
cm::string_view key;
cm::string_view fileNoun;
bool optional = false;
SourceFileMapT* map = nullptr;
// Sources are 3-tuples of name, flags and configs. Headers and module
// units add a moc build path: ahead of the configs for headers, behind
// them for module units. buildPathIndex is read only when hasBuildPath.
bool hasBuildPath = false;
Json::ArrayIndex entrySize = 3u;
Json::ArrayIndex configsIndex = 2u;
Json::ArrayIndex buildPathIndex = 0u;
switch (kind) {
case SourceFileKind::Header:
key = "HEADERS";
fileNoun = "header";
map = &this->BaseEval().Headers;
hasBuildPath = true;
entrySize = 4u;
configsIndex = 3u;
buildPathIndex = 2u;
break;
case SourceFileKind::Source:
key = "SOURCES";
fileNoun = "source";
map = &this->BaseEval().Sources;
break;
case SourceFileKind::ModuleUnit:
key = "CXX_MODULE_UNITS";
fileNoun = "module unit";
optional = true;
map = &this->BaseEval().ModuleUnits;
hasBuildPath = true;
entrySize = 4u;
configsIndex = 2u;
buildPathIndex = 3u;
break;
}
Json::Value const& entries = info.GetValue(std::string(key));
if (optional && entries.isNull()) {
return true;
}
if (!entries.isArray()) {
return info.LogError(cmStrCat(key, " JSON value is not an array."));
}
Json::ArrayIndex const arraySize = entries.size();
for (Json::ArrayIndex ii = 0; ii != arraySize; ++ii) {
auto testEntry = [&info, key, ii](bool test, cm::string_view msg) -> bool {
if (!test) {
info.LogError(cmStrCat(key, " entry ", ii, ": ", msg));
}
return !test;
};
Json::Value const& entry = entries[ii];
if (testEntry(entry.isArray(), "JSON value is not an array.") ||
testEntry(entry.size() == entrySize, "JSON array size invalid.")) {
return false;
}
Json::Value const& entryName = entry[0u];
Json::Value const& entryFlags = entry[1u];
Json::Value const& entryConfigs = entry[configsIndex];
if (testEntry(entryName.isString(),
"JSON value for name is not a string.") ||
testEntry(entryFlags.isString(),
"JSON value for flags is not a string.") ||
testEntry(entryConfigs.isNull() || entryConfigs.isArray(),
"JSON value for configs is not null or array.")) {
return false;
}
if (hasBuildPath &&
testEntry(entry[buildPathIndex].isString(),
"JSON value for build path is not a string.")) {
return false;
}
std::string name = entryName.asString();
std::string flags = entryFlags.asString();
if (testEntry(flags.size() == 2, "Invalid flags string size")) {
return false;
}
if (entryConfigs.isArray()) {
bool configFound = false;
Json::ArrayIndex const configArraySize = entryConfigs.size();
for (Json::ArrayIndex ci = 0; ci != configArraySize; ++ci) {
Json::Value const& config = entryConfigs[ci];
if (testEntry(config.isString(),
"JSON value in config array is not a string.")) {
return false;
}
configFound = configFound || config.asString() == this->InfoConfig();
}
if (!configFound) {
continue;
}
}
cmFileTime fileTime;
if (!fileTime.Load(name)) {
return info.LogError(cmStrCat("The ", fileNoun, " file ",
this->MessagePath(name),
" does not exist."));
}
SourceFileHandleT sourceHandle = std::make_shared<SourceFileT>(name);
sourceHandle->FileTime = fileTime;
sourceHandle->Kind = kind;
sourceHandle->Moc = (flags[0] == 'M');
sourceHandle->Uic = (flags[1] == 'U');
if (hasBuildPath && sourceHandle->Moc && this->MocConst().Enabled) {
std::string build = entry[buildPathIndex].asString();
if (build.empty()) {
return info.LogError(cmStrCat("The ", fileNoun, " file ",
this->MessagePath(name),
" has an empty build path."));
}
sourceHandle->BuildPath = std::move(build);
}
map->emplace(std::move(name), std::move(sourceHandle));
}
return true;
}
bool cmQtAutoMocUicT::InitFromInfo(InfoT const& info)
{
// -- Required settings
@@ -2616,158 +2841,18 @@ bool cmQtAutoMocUicT::InitFromInfo(InfoT const& info)
}
// -- Headers
{
Json::Value const& val = info.GetValue("HEADERS");
if (!val.isArray()) {
return info.LogError("HEADERS JSON value is not an array.");
}
Json::ArrayIndex const arraySize = val.size();
for (Json::ArrayIndex ii = 0; ii != arraySize; ++ii) {
// Test entry closure
auto testEntry = [&info, ii](bool test, cm::string_view msg) -> bool {
if (!test) {
info.LogError(cmStrCat("HEADERS entry ", ii, ": ", msg));
}
return !test;
};
if (!this->InitSourceEntries(info, SourceFileKind::Header)) {
return false;
}
Json::Value const& entry = val[ii];
if (testEntry(entry.isArray(), "JSON value is not an array.") ||
testEntry(entry.size() == 4, "JSON array size invalid.")) {
return false;
}
Json::Value const& entryName = entry[0u];
Json::Value const& entryFlags = entry[1u];
Json::Value const& entryBuild = entry[2u];
Json::Value const& entryConfigs = entry[3u];
if (testEntry(entryName.isString(),
"JSON value for name is not a string.") ||
testEntry(entryFlags.isString(),
"JSON value for flags is not a string.") ||
testEntry(entryConfigs.isNull() || entryConfigs.isArray(),
"JSON value for configs is not null or array.") ||
testEntry(entryBuild.isString(),
"JSON value for build path is not a string.")) {
return false;
}
std::string name = entryName.asString();
std::string flags = entryFlags.asString();
std::string build = entryBuild.asString();
if (testEntry(flags.size() == 2, "Invalid flags string size")) {
return false;
}
if (entryConfigs.isArray()) {
bool configFound = false;
Json::ArrayIndex const configArraySize = entryConfigs.size();
for (Json::ArrayIndex ci = 0; ci != configArraySize; ++ci) {
Json::Value const& config = entryConfigs[ci];
if (testEntry(config.isString(),
"JSON value in config array is not a string.")) {
return false;
}
configFound = configFound || config.asString() == this->InfoConfig();
}
if (!configFound) {
continue;
}
}
cmFileTime fileTime;
if (!fileTime.Load(name)) {
return info.LogError(cmStrCat(
"The header file ", this->MessagePath(name), " does not exist."));
}
SourceFileHandleT sourceHandle = std::make_shared<SourceFileT>(name);
sourceHandle->FileTime = fileTime;
sourceHandle->IsHeader = true;
sourceHandle->Moc = (flags[0] == 'M');
sourceHandle->Uic = (flags[1] == 'U');
if (sourceHandle->Moc && this->MocConst().Enabled) {
if (build.empty()) {
return info.LogError(
cmStrCat("Header file ", ii, " build path is empty"));
}
sourceHandle->BuildPath = std::move(build);
}
this->BaseEval().Headers.emplace(std::move(name),
std::move(sourceHandle));
}
// -- C++ module units
if (!this->InitSourceEntries(info, SourceFileKind::ModuleUnit)) {
return false;
}
// -- Sources
{
Json::Value const& val = info.GetValue("SOURCES");
if (!val.isArray()) {
return info.LogError("SOURCES JSON value is not an array.");
}
Json::ArrayIndex const arraySize = val.size();
for (Json::ArrayIndex ii = 0; ii != arraySize; ++ii) {
// Test entry closure
auto testEntry = [&info, ii](bool test, cm::string_view msg) -> bool {
if (!test) {
info.LogError(cmStrCat("SOURCES entry ", ii, ": ", msg));
}
return !test;
};
Json::Value const& entry = val[ii];
if (testEntry(entry.isArray(), "JSON value is not an array.") ||
testEntry(entry.size() == 3, "JSON array size invalid.")) {
return false;
}
Json::Value const& entryName = entry[0u];
Json::Value const& entryFlags = entry[1u];
Json::Value const& entryConfigs = entry[2u];
if (testEntry(entryName.isString(),
"JSON value for name is not a string.") ||
testEntry(entryFlags.isString(),
"JSON value for flags is not a string.") ||
testEntry(entryConfigs.isNull() || entryConfigs.isArray(),
"JSON value for configs is not null or array.")) {
return false;
}
std::string name = entryName.asString();
std::string flags = entryFlags.asString();
if (testEntry(flags.size() == 2, "Invalid flags string size")) {
return false;
}
if (entryConfigs.isArray()) {
bool configFound = false;
Json::ArrayIndex const configArraySize = entryConfigs.size();
for (Json::ArrayIndex ci = 0; ci != configArraySize; ++ci) {
Json::Value const& config = entryConfigs[ci];
if (testEntry(config.isString(),
"JSON value in config array is not a string.")) {
return false;
}
configFound = configFound || config.asString() == this->InfoConfig();
}
if (!configFound) {
continue;
}
}
cmFileTime fileTime;
if (!fileTime.Load(name)) {
return info.LogError(cmStrCat(
"The source file ", this->MessagePath(name), " does not exist."));
}
SourceFileHandleT sourceHandle = std::make_shared<SourceFileT>(name);
sourceHandle->FileTime = fileTime;
sourceHandle->IsHeader = false;
sourceHandle->Moc = (flags[0] == 'M');
sourceHandle->Uic = (flags[1] == 'U');
this->BaseEval().Sources.emplace(std::move(name),
std::move(sourceHandle));
}
if (!this->InitSourceEntries(info, SourceFileKind::Source)) {
return false;
}
// -- Init derived information
@@ -2840,6 +2925,8 @@ void cmQtAutoMocUicT::InitJobs()
// Add header parse jobs
this->CreateParseJobs<JobParseHeaderT>(this->BaseEval().Headers);
// Add module unit parse jobs (header-style: macro scan, no moc_/.moc scan)
this->CreateParseJobs<JobParseHeaderT>(this->BaseEval().ModuleUnits);
// Add source parse jobs
this->CreateParseJobs<JobParseSourceT>(this->BaseEval().Sources);
@@ -0,0 +1 @@
# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules/.
@@ -0,0 +1 @@
1
@@ -0,0 +1 @@
is not supported in a module implementation unit
@@ -0,0 +1 @@
# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules_impl_unit/.
@@ -0,0 +1 @@
# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules_no_macro/.
@@ -0,0 +1 @@
contains a "Q_OBJECT" macro, but it is a C\+\+ module unit and moc from Qt [0-9]+\.[0-9]+ cannot process those\.
@@ -0,0 +1 @@
# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules/.
+22
View File
@@ -0,0 +1,22 @@
enable_language(CXX)
set(info "")
foreach(var
CMAKE_CXX_COMPILE_FEATURES
CMAKE_MAKE_PROGRAM
)
if(DEFINED ${var})
string(APPEND info "set(${var} \"${${var}}\")\n")
endif()
endforeach()
# A compiler can only take part in C++ module dependency scanning if it has a
# scandep rule. Report it as a flag: the rule itself contains characters that
# would not survive a round trip through this file.
if(CMAKE_CXX_SCANDEP_SOURCE)
string(APPEND info "set(have_cxx_scandep 1)\n")
else()
string(APPEND info "set(have_cxx_scandep 0)\n")
endif()
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/info.cmake" "${info}")
+128
View File
@@ -10,4 +10,132 @@ if (DEFINED with_qt_version)
if (RunCMake_GENERATOR MATCHES "(Ninja|Makefiles|Visual Studio)")
run_cmake(AutoMocIncludeDirectoriesShort)
endif ()
# Detect information from the toolchain:
# - CMAKE_CXX_COMPILE_FEATURES
# - CMAKE_MAKE_PROGRAM
run_cmake(Inspect)
include("${RunCMake_BINARY_DIR}/Inspect-build/info.cmake")
# Building C++ module units at all needs a toolchain that can scan C++
# module dependencies and a generator that can use the result. The scandep
# rule covers the compiler side: it is absent both for compilers that cannot
# scan at all, such as AppleClang, and for versions that are too old.
set(cxx_modules_supported 0)
if (have_cxx_scandep AND "cxx_std_20" IN_LIST CMAKE_CXX_COMPILE_FEATURES)
if (RunCMake_GENERATOR MATCHES "Ninja")
execute_process(
COMMAND "${CMAKE_MAKE_PROGRAM}" --version
RESULT_VARIABLE _res
OUTPUT_VARIABLE _ninja_version
ERROR_QUIET
OUTPUT_STRIP_TRAILING_WHITESPACE)
if (NOT _res AND _ninja_version VERSION_GREATER_EQUAL "1.11")
set(cxx_modules_supported 1)
endif ()
elseif (RunCMake_GENERATOR MATCHES "Visual Studio")
set(cxx_modules_supported 1)
endif ()
endif ()
# AUTOMOC processes C++ module units only with Qt 6.13 or newer, whose moc
# supports them.
set(automoc_modules_supported 0)
if (cxx_modules_supported AND QtCore_VERSION VERSION_GREATER_EQUAL "6.13")
set(automoc_modules_supported 1)
endif ()
# A successful build IS the assertion: main.cpp uses the module classes, so
# if AUTOMOC did not moc the module units into their own module-attached
# translation units, the meta-object symbols would be undefined at link time.
# This exercises moc's compiler-predefines (--include) plus module-declaration
# handling, which only the C++-module-capable Clang/MSVC path reaches (GCC is
# skipped in the project due to QTBUG-142513).
if (automoc_modules_supported)
block()
set(RunCMake_TEST_SOURCE_DIR "${RunCMake_SOURCE_DIR}/cxx_modules")
set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/CxxModules-build")
run_cmake_with_options(CxxModules ${RunCMake_TEST_OPTIONS})
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(CxxModules-build ${CMAKE_COMMAND} --build . --config Debug)
endblock()
endif ()
# Toggling a module unit's macro must not require a reconfigure: autogen's
# depfile lists module-unit sources so ninja rescans them on rebuild. Use
# a writable copy of the sources so the edit never touches the tracked
# test files (mirrors how Autogen_6's incremental test avoids that).
if (automoc_modules_supported)
block()
set(incremental_src_dir "${RunCMake_BINARY_DIR}/CxxModulesIncremental-src")
file(REMOVE_RECURSE "${incremental_src_dir}")
file(COPY "${RunCMake_SOURCE_DIR}/cxx_modules/" DESTINATION "${incremental_src_dir}")
set(RunCMake_TEST_SOURCE_DIR "${incremental_src_dir}")
set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/CxxModulesIncremental-build")
run_cmake_with_options(CxxModulesIncremental ${RunCMake_TEST_OPTIONS})
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(CxxModulesIncremental-build1 ${CMAKE_COMMAND} --build . --config Debug)
file(WRITE "${incremental_src_dir}/mod-empty.cppm" [[
module;
#include <QObject>
export module Mod:Empty;
export class EmptyObject : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
signals:
void emptySignal(int value);
};
]])
run_cmake_command(CxxModulesIncremental-build2 ${CMAKE_COMMAND} --build . --config Debug)
endblock()
endif ()
# moc rejects a meta-object macro in a module implementation unit, and
# AUTOMOC does not work around that. Such a unit is not a member of the
# CXX_MODULES file set, so it goes through the ordinary source handling and
# its ".moc" include is honored; moc then fails.
if (automoc_modules_supported)
block()
set(RunCMake_TEST_SOURCE_DIR "${RunCMake_SOURCE_DIR}/cxx_modules_impl_unit")
set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/CxxModulesImplUnit-build")
run_cmake_with_options(CxxModulesImplUnit ${RunCMake_TEST_OPTIONS})
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(CxxModulesImplUnit-build ${CMAKE_COMMAND} --build . --config Debug)
endblock()
endif ()
# A C++ module unit without a meta-object macro must not disturb AUTOMOC,
# regardless of the Qt version: nothing is generated for the module unit,
# while the ordinary header of the target is still moc'd. The module unit
# pulls in no Qt, so this also covers compilers that cannot yet compile
# QObject into a module unit.
if (cxx_modules_supported)
block()
set(RunCMake_TEST_SOURCE_DIR "${RunCMake_SOURCE_DIR}/cxx_modules_no_macro")
set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/CxxModulesNoMacro-build")
run_cmake_with_options(CxxModulesNoMacro ${RunCMake_TEST_OPTIONS})
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(CxxModulesNoMacro-build ${CMAKE_COMMAND} --build . --config Debug)
endblock()
endif ()
# With older Qt, moc cannot process C++ module units at all. A meta-object
# macro in one must be reported as such, rather than left to fail later in
# the compiler or the linker. The project still configures; only the build
# is expected to fail.
if (cxx_modules_supported AND QtCore_VERSION VERSION_LESS "6.13")
block()
set(RunCMake_TEST_SOURCE_DIR "${RunCMake_SOURCE_DIR}/cxx_modules")
set(RunCMake_TEST_BINARY_DIR "${RunCMake_BINARY_DIR}/CxxModulesUnsupportedQt-build")
run_cmake_with_options(CxxModulesUnsupportedQt ${RunCMake_TEST_OPTIONS})
set(RunCMake_TEST_NO_CLEAN 1)
run_cmake_command(CxxModulesUnsupportedQt-build ${CMAKE_COMMAND} --build . --config Debug)
endblock()
endif ()
endif()
@@ -0,0 +1,28 @@
cmake_minimum_required(VERSION 3.28)
project(cxx_modules_automoc CXX)
find_package(Qt${with_qt_version} REQUIRED COMPONENTS Core)
# QObject pulled into a module unit triggers exposure violations under GCC
# (QTBUG-142513); skip cleanly there, matching qtbase's own module tests.
# With older Qt no module unit is ever compiled here, because the build is
# expected to fail in AUTOMOC first, so the exclusion does not apply.
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
Qt${with_qt_version}Core_VERSION VERSION_GREATER_EQUAL "6.13")
return()
endif()
add_executable(app main.cpp mod-impl.cpp)
target_sources(app
PRIVATE FILE_SET CXX_MODULES FILES
mod.cppm
mod-part.cppm
mod-internal.cppm
mod-empty.cppm)
set_target_properties(app PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
AUTOMOC ON)
target_link_libraries(app PRIVATE Qt${with_qt_version}::Core)
@@ -0,0 +1,8 @@
import Mod;
int main()
{
PrimaryObject primary;
PartObject part;
return 0;
}
@@ -0,0 +1,3 @@
export module Mod:Empty;
export int emptyValue() { return 0; }
@@ -0,0 +1,14 @@
module;
#include <QObject>
module Mod;
// The implementation partition is imported here rather than from mod.cppm,
// where Clang diagnoses it with
// -Wimport-implementation-partition-unit-in-interface-unit. Referencing the
// meta-object makes the link fail if AUTOMOC did not moc mod-internal.cppm.
import :Internal;
int internalMethodCount()
{
return InternalObject::staticMetaObject.methodCount();
}
@@ -0,0 +1,12 @@
module;
#include <QObject>
module Mod:Internal;
class InternalObject : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
signals:
void internalSignal(int value);
};
@@ -0,0 +1,12 @@
module;
#include <QObject>
export module Mod:Part;
export class PartObject : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
signals:
void partSignal(int value);
};
@@ -0,0 +1,13 @@
module;
#include <QObject>
export module Mod;
export import :Part;
export class PrimaryObject : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
signals:
void primarySignal(int value);
};
@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.28)
project(cxx_modules_impl_unit_automoc CXX)
find_package(Qt${with_qt_version} REQUIRED COMPONENTS Core)
# moc rejects a meta-object macro in a module implementation unit. The build
# is expected to fail in moc, so no compiler ever sees the module unit and no
# compiler-specific exclusion is needed here.
add_executable(app main.cpp mod-impl.cpp)
target_sources(app
PRIVATE FILE_SET CXX_MODULES FILES
mod.cppm)
set_target_properties(app PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
AUTOMOC ON)
target_link_libraries(app PRIVATE Qt${with_qt_version}::Core)
@@ -0,0 +1,6 @@
import Mod;
int main()
{
return implObjectWorks() ? 0 : 1;
}
@@ -0,0 +1,22 @@
module;
#include <QObject>
module Mod;
// A module implementation unit is not a member of the CXX_MODULES file set, so
// AUTOMOC treats it like any other source and honors the ".moc" include below.
// moc then rejects the macro.
class ImplObject : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
signals:
void implSignal();
};
bool implObjectWorks()
{
return ImplObject::staticMetaObject.methodCount() > 0;
}
#include "mod-impl.moc"
@@ -0,0 +1,3 @@
export module Mod;
export bool implObjectWorks();
@@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.28)
project(cxx_modules_no_macro_automoc CXX)
find_package(Qt${with_qt_version} REQUIRED COMPONENTS Core)
# The module unit pulls in no Qt at all, so this builds with any compiler that
# can scan C++ modules, and with any Qt version: nothing is generated for a
# module unit without a meta-object macro, while object.h is still moc'd.
add_executable(app main.cpp object.h)
target_sources(app
PRIVATE FILE_SET CXX_MODULES FILES
mod.cppm)
set_target_properties(app PROPERTIES
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
AUTOMOC ON)
target_link_libraries(app PRIVATE Qt${with_qt_version}::Core)
@@ -0,0 +1,9 @@
#include "object.h"
import Mod;
int main()
{
Object object;
return object.metaObject()->methodCount() > 0 && modValue() == 42 ? 0 : 1;
}
@@ -0,0 +1,6 @@
export module Mod;
export int modValue()
{
return 42;
}
@@ -0,0 +1,11 @@
#pragma once
#include <QObject>
class Object : public QObject
{
Q_OBJECT
public:
using QObject::QObject;
signals:
void objectSignal(int value);
};