diff --git a/Help/prop_tgt/AUTOMOC.rst b/Help/prop_tgt/AUTOMOC.rst index 1ec36f85a5..8890458d6a 100644 --- a/Help/prop_tgt/AUTOMOC.rst +++ b/Help/prop_tgt/AUTOMOC.rst @@ -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 ^^^^^^^^^^^^^^^^^^^^ diff --git a/Help/release/dev/automoc-cxx-modules.rst b/Help/release/dev/automoc-cxx-modules.rst new file mode 100644 index 0000000000..400b376fec --- /dev/null +++ b/Help/release/dev/automoc-cxx-modules.rst @@ -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. diff --git a/Source/cmQtAutoGenInitializer.cxx b/Source/cmQtAutoGenInitializer.cxx index f02914d3f3..e24bd38735 100644 --- a/Source/cmQtAutoGenInitializer.cxx +++ b/Source/cmQtAutoGenInitializer.cxx @@ -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 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("$<$:"_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 uic_skip; std::vector headers; std::vector sources; + std::vector 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; } diff --git a/Source/cmQtAutoGenInitializer.h b/Source/cmQtAutoGenInitializer.h index 874996107a..879d98d4d4 100644 --- a/Source/cmQtAutoGenInitializer.h +++ b/Source/cmQtAutoGenInitializer.h @@ -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; @@ -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 Headers; std::unordered_map Sources; + std::unordered_map ModuleUnits; std::vector FilesGenerated; std::vector CMP0100HeadersWarn; } AutogenTarget; diff --git a/Source/cmQtAutoMocUic.cxx b/Source/cmQtAutoMocUic.cxx index e8ec88135c..bd3701dffa 100644 --- a/Source/cmQtAutoMocUic.cxx +++ b/Source/cmQtAutoMocUic.cxx @@ -125,6 +125,15 @@ public: std::unordered_map 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, @@ -560,19 +573,11 @@ public: void CreateParseJobs(SourceFileMapT const& sourceMap); private: - // Kind of source-entry array in the info file. Headers are 4-tuples carrying - // a moc build path; sources are 3-tuples without one. - enum class SourceEntryKind - { - Header, - Source, - }; - // -- 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, SourceEntryKind kind); + bool InitSourceEntries(InfoT const& info, SourceFileKind kind); void InitJobs(); bool Process() override; // -- Settings file @@ -1191,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)) { @@ -1212,8 +1223,11 @@ bool cmQtAutoMocUicT::JobEvalCacheMocT::EvalHeader(SourceFileHandleT source) MappingHandleT handle = std::make_shared(); 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 { @@ -1223,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) { @@ -1475,7 +1533,7 @@ bool cmQtAutoMocUicT::JobEvalCacheMocT::FindIncludedHeader( if (!handle) { handle = std::make_shared(testPath); handle->FileTime = fileTime; - handle->IsHeader = true; + handle->Kind = SourceFileKind::Header; handle->Moc = true; } headerHandle = handle; @@ -1553,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); @@ -1773,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, @@ -2067,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)) { @@ -2278,9 +2358,9 @@ std::vector cmQtAutoMocUicT::JobDepFilesMergeT::initialDependencies() const { std::vector 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) { @@ -2290,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; } @@ -2325,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. @@ -2391,33 +2478,53 @@ cmQtAutoMocUicT::cmQtAutoMocUicT() } cmQtAutoMocUicT::~cmQtAutoMocUicT() = default; -bool cmQtAutoMocUicT::InitSourceEntries(InfoT const& info, - SourceEntryKind kind) +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 SourceEntryKind::Header: + case SourceFileKind::Header: key = "HEADERS"; fileNoun = "header"; map = &this->BaseEval().Headers; + hasBuildPath = true; + entrySize = 4u; + configsIndex = 3u; + buildPathIndex = 2u; break; - case SourceEntryKind::Source: + 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; } - // Sources are 3-tuples; headers add a moc build path. - bool const isHeader = kind != SourceEntryKind::Source; 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 entrySize = isHeader ? 4u : 3u; - Json::ArrayIndex const configsIndex = isHeader ? 3u : 2u; 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 { @@ -2444,8 +2551,8 @@ bool cmQtAutoMocUicT::InitSourceEntries(InfoT const& info, "JSON value for configs is not null or array.")) { return false; } - if (isHeader && - testEntry(entry[2u].isString(), + if (hasBuildPath && + testEntry(entry[buildPathIndex].isString(), "JSON value for build path is not a string.")) { return false; } @@ -2481,11 +2588,11 @@ bool cmQtAutoMocUicT::InitSourceEntries(InfoT const& info, SourceFileHandleT sourceHandle = std::make_shared(name); sourceHandle->FileTime = fileTime; - sourceHandle->IsHeader = isHeader; + sourceHandle->Kind = kind; sourceHandle->Moc = (flags[0] == 'M'); sourceHandle->Uic = (flags[1] == 'U'); - if (isHeader && sourceHandle->Moc && this->MocConst().Enabled) { - std::string build = entry[2u].asString(); + 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), @@ -2734,12 +2841,17 @@ bool cmQtAutoMocUicT::InitFromInfo(InfoT const& info) } // -- Headers - if (!this->InitSourceEntries(info, SourceEntryKind::Header)) { + if (!this->InitSourceEntries(info, SourceFileKind::Header)) { + return false; + } + + // -- C++ module units + if (!this->InitSourceEntries(info, SourceFileKind::ModuleUnit)) { return false; } // -- Sources - if (!this->InitSourceEntries(info, SourceEntryKind::Source)) { + if (!this->InitSourceEntries(info, SourceFileKind::Source)) { return false; } @@ -2813,6 +2925,8 @@ void cmQtAutoMocUicT::InitJobs() // Add header parse jobs this->CreateParseJobs(this->BaseEval().Headers); + // Add module unit parse jobs (header-style: macro scan, no moc_/.moc scan) + this->CreateParseJobs(this->BaseEval().ModuleUnits); // Add source parse jobs this->CreateParseJobs(this->BaseEval().Sources); diff --git a/Tests/RunCMake/Autogen_7/CxxModules.cmake b/Tests/RunCMake/Autogen_7/CxxModules.cmake new file mode 100644 index 0000000000..03a5beefcb --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModules.cmake @@ -0,0 +1 @@ +# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules/. diff --git a/Tests/RunCMake/Autogen_7/CxxModulesImplUnit-build-result.txt b/Tests/RunCMake/Autogen_7/CxxModulesImplUnit-build-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesImplUnit-build-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/Autogen_7/CxxModulesImplUnit-build-stdout.txt b/Tests/RunCMake/Autogen_7/CxxModulesImplUnit-build-stdout.txt new file mode 100644 index 0000000000..a0f4c709e4 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesImplUnit-build-stdout.txt @@ -0,0 +1 @@ +is not supported in a module implementation unit diff --git a/Tests/RunCMake/Autogen_7/CxxModulesImplUnit.cmake b/Tests/RunCMake/Autogen_7/CxxModulesImplUnit.cmake new file mode 100644 index 0000000000..986fba421b --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesImplUnit.cmake @@ -0,0 +1 @@ +# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules_impl_unit/. diff --git a/Tests/RunCMake/Autogen_7/CxxModulesNoMacro.cmake b/Tests/RunCMake/Autogen_7/CxxModulesNoMacro.cmake new file mode 100644 index 0000000000..d6eef9ec72 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesNoMacro.cmake @@ -0,0 +1 @@ +# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules_no_macro/. diff --git a/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt-build-result.txt b/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt-build-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt-build-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt-build-stdout.txt b/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt-build-stdout.txt new file mode 100644 index 0000000000..78d8d71eb8 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt-build-stdout.txt @@ -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\. diff --git a/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt.cmake b/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt.cmake new file mode 100644 index 0000000000..03a5beefcb --- /dev/null +++ b/Tests/RunCMake/Autogen_7/CxxModulesUnsupportedQt.cmake @@ -0,0 +1 @@ +# Driven via RunCMake_TEST_SOURCE_DIR pointing at cxx_modules/. diff --git a/Tests/RunCMake/Autogen_7/Inspect.cmake b/Tests/RunCMake/Autogen_7/Inspect.cmake new file mode 100644 index 0000000000..06d98e980a --- /dev/null +++ b/Tests/RunCMake/Autogen_7/Inspect.cmake @@ -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}") diff --git a/Tests/RunCMake/Autogen_7/RunCMakeTest.cmake b/Tests/RunCMake/Autogen_7/RunCMakeTest.cmake index a669452249..03f2e8468a 100644 --- a/Tests/RunCMake/Autogen_7/RunCMakeTest.cmake +++ b/Tests/RunCMake/Autogen_7/RunCMakeTest.cmake @@ -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 +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() diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/CMakeLists.txt b/Tests/RunCMake/Autogen_7/cxx_modules/CMakeLists.txt new file mode 100644 index 0000000000..2a0f410ecb --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/CMakeLists.txt @@ -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) diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/main.cpp b/Tests/RunCMake/Autogen_7/cxx_modules/main.cpp new file mode 100644 index 0000000000..431feb3847 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/main.cpp @@ -0,0 +1,8 @@ +import Mod; + +int main() +{ + PrimaryObject primary; + PartObject part; + return 0; +} diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/mod-empty.cppm b/Tests/RunCMake/Autogen_7/cxx_modules/mod-empty.cppm new file mode 100644 index 0000000000..810b8f375c --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/mod-empty.cppm @@ -0,0 +1,3 @@ +export module Mod:Empty; + +export int emptyValue() { return 0; } diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/mod-impl.cpp b/Tests/RunCMake/Autogen_7/cxx_modules/mod-impl.cpp new file mode 100644 index 0000000000..12ae4bc0d9 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/mod-impl.cpp @@ -0,0 +1,14 @@ +module; +#include +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(); +} diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/mod-internal.cppm b/Tests/RunCMake/Autogen_7/cxx_modules/mod-internal.cppm new file mode 100644 index 0000000000..aace2ea931 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/mod-internal.cppm @@ -0,0 +1,12 @@ +module; +#include +module Mod:Internal; + +class InternalObject : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; +signals: + void internalSignal(int value); +}; diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/mod-part.cppm b/Tests/RunCMake/Autogen_7/cxx_modules/mod-part.cppm new file mode 100644 index 0000000000..d76be552bc --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/mod-part.cppm @@ -0,0 +1,12 @@ +module; +#include +export module Mod:Part; + +export class PartObject : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; +signals: + void partSignal(int value); +}; diff --git a/Tests/RunCMake/Autogen_7/cxx_modules/mod.cppm b/Tests/RunCMake/Autogen_7/cxx_modules/mod.cppm new file mode 100644 index 0000000000..474757e1a1 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules/mod.cppm @@ -0,0 +1,13 @@ +module; +#include +export module Mod; +export import :Part; + +export class PrimaryObject : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; +signals: + void primarySignal(int value); +}; diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/CMakeLists.txt b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/CMakeLists.txt new file mode 100644 index 0000000000..c4d8eec07f --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/CMakeLists.txt @@ -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) diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/main.cpp b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/main.cpp new file mode 100644 index 0000000000..79b7d97d1a --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/main.cpp @@ -0,0 +1,6 @@ +import Mod; + +int main() +{ + return implObjectWorks() ? 0 : 1; +} diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/mod-impl.cpp b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/mod-impl.cpp new file mode 100644 index 0000000000..6e5798cf5c --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/mod-impl.cpp @@ -0,0 +1,22 @@ +module; +#include +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" diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/mod.cppm b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/mod.cppm new file mode 100644 index 0000000000..072cf7ecf1 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_impl_unit/mod.cppm @@ -0,0 +1,3 @@ +export module Mod; + +export bool implObjectWorks(); diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/CMakeLists.txt b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/CMakeLists.txt new file mode 100644 index 0000000000..7e96a38b63 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/CMakeLists.txt @@ -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) diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/main.cpp b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/main.cpp new file mode 100644 index 0000000000..200e7d5746 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/main.cpp @@ -0,0 +1,9 @@ +#include "object.h" + +import Mod; + +int main() +{ + Object object; + return object.metaObject()->methodCount() > 0 && modValue() == 42 ? 0 : 1; +} diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/mod.cppm b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/mod.cppm new file mode 100644 index 0000000000..dd9ada98c6 --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/mod.cppm @@ -0,0 +1,6 @@ +export module Mod; + +export int modValue() +{ + return 42; +} diff --git a/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/object.h b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/object.h new file mode 100644 index 0000000000..fd9f875eed --- /dev/null +++ b/Tests/RunCMake/Autogen_7/cxx_modules_no_macro/object.h @@ -0,0 +1,11 @@ +#pragma once +#include + +class Object : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; +signals: + void objectSignal(int value); +};