Explorar o código

cxxmodules: add properties to control scanning

The `CXX_SCAN_FOR_MODULES` property may be used to control scanning for
targets and for source files rather than assuming "C++20 always needs to
be scanned".
Ben Boeckel %!s(int64=2) %!d(string=hai) anos
pai
achega
a02d792c6e

+ 2 - 0
Help/manual/cmake-properties.7.rst

@@ -195,6 +195,7 @@ Properties on Targets
    /prop_tgt/CXX_MODULE_SET
    /prop_tgt/CXX_MODULE_SET_NAME
    /prop_tgt/CXX_MODULE_SETS
+   /prop_tgt/CXX_SCAN_FOR_MODULES
    /prop_tgt/CXX_STANDARD
    /prop_tgt/CXX_STANDARD_REQUIRED
    /prop_tgt/DEBUG_POSTFIX
@@ -533,6 +534,7 @@ Properties on Source Files
    /prop_sf/COMPILE_DEFINITIONS
    /prop_sf/COMPILE_FLAGS
    /prop_sf/COMPILE_OPTIONS
+   /prop_sf/CXX_SCAN_FOR_MODULES
    /prop_sf/EXTERNAL_OBJECT
    /prop_sf/Fortran_FORMAT
    /prop_sf/Fortran_PREPROCESS

+ 1 - 0
Help/manual/cmake-variables.7.rst

@@ -417,6 +417,7 @@ Variables that Control the Build
    /variable/CMAKE_CUDA_RESOLVE_DEVICE_SYMBOLS
    /variable/CMAKE_CUDA_RUNTIME_LIBRARY
    /variable/CMAKE_CUDA_SEPARABLE_COMPILATION
+   /variable/CMAKE_CXX_SCAN_FOR_MODULES
    /variable/CMAKE_DEBUG_POSTFIX
    /variable/CMAKE_DEFAULT_BUILD_TYPE
    /variable/CMAKE_DEFAULT_CONFIGS

+ 19 - 0
Help/prop_sf/CXX_SCAN_FOR_MODULES.rst

@@ -0,0 +1,19 @@
+CXX_SCAN_FOR_MODULES
+--------------------
+
+.. versionadded:: 3.26
+
+``CXX_SCAN_FOR_MODULES`` is a boolean specifying whether CMake will scan the
+source for C++ module dependencies.  See also the
+:prop_tgt:`CXX_SCAN_FOR_MODULES` for target-wide settings.
+
+When this property is set ``ON``, CMake will scan the source at build time and
+add module dependency information to the compile line as necessary.  When this
+property is set ``OFF``, CMake will not scan the source at build time.  When
+this property is unset, the :prop_tgt:`CXX_SCAN_FOR_MODULES` property is
+consulted.
+
+Note that scanning is only performed if C++20 or higher is enabled for the
+target and the source uses the ``CXX`` language.  Scanning for modules in
+sources belonging to file sets of type ``CXX_MODULES`` and
+``CXX_MODULES_HEADER_UNITS`` is always performed.

+ 22 - 0
Help/prop_tgt/CXX_SCAN_FOR_MODULES.rst

@@ -0,0 +1,22 @@
+CXX_SCAN_FOR_MODULES
+--------------------
+
+.. versionadded:: 3.26
+
+``CXX_SCAN_FOR_MODULES`` is a boolean specifying whether CMake will scan C++
+sources in the target for module dependencies.  See also the
+:prop_sf:`CXX_SCAN_FOR_MODULES` for per-source settings which, if set,
+overrides the target-wide settings.
+
+This property is initialized by the value of the
+:variable:`CMAKE_CXX_SCAN_FOR_MODULES` variable if it is set when a target is
+created.
+
+When this property is set ``ON`` or unset, CMake will scan the target's
+``CXX`` sources at build time and add module dependency information to the
+compile line as necessary.  When this property is set ``OFF``, CMake will not
+scan the target's ``CXX`` sources at build time.
+
+Note that scanning is only performed if C++20 or higher is enabled for the
+target.  Scanning for modules in the target's sources belonging to file sets
+of type ``CXX_MODULES`` and ``CXX_MODULES_HEADER_UNITS`` is always performed.

+ 5 - 0
Help/release/dev/cxx-scanning-properties.rst

@@ -0,0 +1,5 @@
+cxx-scanning-properties
+-----------------------
+
+* The :prop_tgt:`CXX_SCAN_FOR_MODULES` target and source file properties may
+  be used to enable or disable scanning for C++ module dependencies.

+ 10 - 0
Help/variable/CMAKE_CXX_SCAN_FOR_MODULES.rst

@@ -0,0 +1,10 @@
+CMAKE_CXX_SCAN_FOR_MODULES
+--------------------------
+
+.. versionadded:: 3.26
+
+Whether to scan C++ source files for module dependencies.
+
+This variable is used to initialize the :prop_tgt:`CXX_SCAN_FOR_MODULES`
+property on all the targets.  See that target property for additional
+information.

+ 48 - 8
Source/cmNinjaTargetGenerator.cxx

@@ -109,12 +109,13 @@ cmGlobalNinjaGenerator* cmNinjaTargetGenerator::GetGlobalGenerator() const
 }
 
 std::string cmNinjaTargetGenerator::LanguageCompilerRule(
-  const std::string& lang, const std::string& config) const
+  const std::string& lang, const std::string& config,
+  WithScanning withScanning) const
 {
   return cmStrCat(
     lang, "_COMPILER__",
     cmGlobalNinjaGenerator::EncodeRuleName(this->GeneratorTarget->GetName()),
-    '_', config);
+    withScanning == WithScanning::Yes ? "_scanned_" : "_unscanned_", config);
 }
 
 std::string cmNinjaTargetGenerator::LanguagePreprocessAndScanRule(
@@ -231,6 +232,32 @@ cmFileSet const* cmNinjaTargetGenerator::GetFileSetForSource(
   return fsit->second;
 }
 
+bool cmNinjaTargetGenerator::NeedDyndepForSource(std::string const& lang,
+                                                 std::string const& config,
+                                                 cmSourceFile const* sf)
+{
+  bool const needDyndep = this->NeedDyndep(lang, config);
+  if (!needDyndep) {
+    return false;
+  }
+  auto const* fs = this->GetFileSetForSource(config, sf);
+  if (fs &&
+      (fs->GetType() == "CXX_MODULES"_s ||
+       fs->GetType() == "CXX_MODULE_HEADER_UNITS"_s)) {
+    return true;
+  }
+  auto const sfProp = sf->GetProperty("CXX_SCAN_FOR_MODULES");
+  if (sfProp.IsSet()) {
+    return sfProp.IsOn();
+  }
+  auto const tgtProp =
+    this->GeneratorTarget->GetProperty("CXX_SCAN_FOR_MODULES");
+  if (tgtProp.IsSet()) {
+    return tgtProp.IsOn();
+  }
+  return true;
+}
+
 std::string cmNinjaTargetGenerator::OrderDependsTargetForTarget(
   const std::string& config)
 {
@@ -670,6 +697,19 @@ cmNinjaRule GetScanRule(
 
 void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
                                               const std::string& config)
+{
+  // For some cases we scan to dynamically discover dependencies.
+  bool const needDyndep = this->NeedDyndep(lang, config);
+
+  if (needDyndep) {
+    this->WriteCompileRule(lang, config, WithScanning::Yes);
+  }
+  this->WriteCompileRule(lang, config, WithScanning::No);
+}
+
+void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
+                                              const std::string& config,
+                                              WithScanning withScanning)
 {
   cmRulePlaceholderExpander::RuleVariables vars;
   vars.CMTargetName = this->GetGeneratorTarget()->GetName().c_str();
@@ -690,7 +730,6 @@ void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
   cmMakefile* mf = this->GetMakefile();
 
   // For some cases we scan to dynamically discover dependencies.
-  bool const needDyndep = this->NeedDyndep(lang, config);
   bool const compilationPreprocesses = !this->NeedExplicitPreprocessing(lang);
 
   std::string flags = "$FLAGS";
@@ -728,7 +767,7 @@ void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
     this->GetLocalGenerator()->ConvertToOutputFormat(
       cmSystemTools::GetCMakeCommand(), cmLocalGenerator::SHELL);
 
-  if (needDyndep) {
+  if (withScanning == WithScanning::Yes) {
     const auto& scanDepType = this->GetMakefile()->GetSafeDefinition(
       cmStrCat("CMAKE_EXPERIMENTAL_", lang, "_SCANDEP_DEPFILE_FORMAT"));
 
@@ -834,7 +873,7 @@ void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
     this->GetGlobalGenerator()->AddRule(rule);
   }
 
-  cmNinjaRule rule(this->LanguageCompilerRule(lang, config));
+  cmNinjaRule rule(this->LanguageCompilerRule(lang, config, withScanning));
   // If using a response file, move defines, includes, and flags into it.
   if (!responseFlag.empty()) {
     rule.RspFile = "$RSP_FILE";
@@ -888,7 +927,7 @@ void cmNinjaTargetGenerator::WriteCompileRule(const std::string& lang,
     }
   }
 
-  if (needDyndep && !modmapFormat.empty()) {
+  if (withScanning == WithScanning::Yes && !modmapFormat.empty()) {
     std::string modmapFlags = mf->GetRequiredDefinition(
       cmStrCat("CMAKE_EXPERIMENTAL_", lang, "_MODULE_MAP_FLAG"));
     cmSystemTools::ReplaceString(modmapFlags, "<MODULE_MAP_FILE>",
@@ -1348,8 +1387,10 @@ void cmNinjaTargetGenerator::WriteObjectBuildStatement(
     !(language == "RC" || (language == "CUDA" && !flag));
   int const commandLineLengthLimit =
     ((lang_supports_response && this->ForceResponseFile())) ? -1 : 0;
+  bool const needDyndep = this->NeedDyndepForSource(language, config, source);
 
-  cmNinjaBuild objBuild(this->LanguageCompilerRule(language, config));
+  cmNinjaBuild objBuild(this->LanguageCompilerRule(
+    language, config, needDyndep ? WithScanning::Yes : WithScanning::No));
   cmNinjaVars& vars = objBuild.Variables;
   vars["FLAGS"] = this->ComputeFlagsForObject(source, language, config);
   vars["DEFINES"] = this->ComputeDefines(source, language, config);
@@ -1458,7 +1499,6 @@ void cmNinjaTargetGenerator::WriteObjectBuildStatement(
   }
 
   // For some cases we scan to dynamically discover dependencies.
-  bool const needDyndep = this->NeedDyndep(language, config);
   bool const compilationPreprocesses =
     !this->NeedExplicitPreprocessing(language);
 

+ 12 - 1
Source/cmNinjaTargetGenerator.h

@@ -19,6 +19,7 @@
 #include "cmOSXBundleGenerator.h"
 
 class cmCustomCommand;
+class cmFileSet;
 class cmGeneratedFileStream;
 class cmGeneratorTarget;
 class cmLocalNinjaGenerator;
@@ -67,8 +68,14 @@ protected:
   cmFileSet const* GetFileSetForSource(std::string const& config,
                                        cmSourceFile const* sf);
 
+  enum class WithScanning
+  {
+    No,
+    Yes,
+  };
   std::string LanguageCompilerRule(const std::string& lang,
-                                   const std::string& config) const;
+                                   const std::string& config,
+                                   WithScanning withScanning) const;
   std::string LanguagePreprocessAndScanRule(std::string const& lang,
                                             const std::string& config) const;
   std::string LanguageScanRule(std::string const& lang,
@@ -76,6 +83,8 @@ protected:
   std::string LanguageDyndepRule(std::string const& lang,
                                  const std::string& config) const;
   bool NeedDyndep(std::string const& lang, std::string const& config) const;
+  bool NeedDyndepForSource(std::string const& lang, std::string const& config,
+                           cmSourceFile const* sf);
   bool NeedExplicitPreprocessing(std::string const& lang) const;
   bool CompileWithDefines(std::string const& lang) const;
   bool NeedCxxModuleSupport(std::string const& lang,
@@ -154,6 +163,8 @@ protected:
                           const std::string& config);
   void WriteCompileRule(const std::string& language,
                         const std::string& config);
+  void WriteCompileRule(const std::string& language, const std::string& config,
+                        WithScanning withScanning);
   void WriteObjectBuildStatements(const std::string& config,
                                   const std::string& fileConfig,
                                   bool firstForConfig);

+ 1 - 0
Source/cmTarget.cxx

@@ -526,6 +526,7 @@ cmTarget::cmTarget(std::string const& name, cmStateEnums::TargetType type,
     initProp("ANDROID_ANT_ADDITIONAL_OPTIONS");
     initProp("BUILD_RPATH");
     initProp("BUILD_RPATH_USE_ORIGIN");
+    initProp("CXX_SCAN_FOR_MODULES");
     initProp("INSTALL_NAME_DIR");
     initProp("INSTALL_REMOVE_ENVIRONMENT_RPATH");
     initPropValue("INSTALL_RPATH", "");

+ 1 - 0
Tests/RunCMake/CXXModules/RunCMakeTest.cmake

@@ -138,6 +138,7 @@ if ("named" IN_LIST CMake_TEST_MODULE_COMPILATION)
   set(RunCMake_CXXModules_NO_TEST 1)
   run_cxx_module_test(circular)
   unset(RunCMake_CXXModules_NO_TEST)
+  run_cxx_module_test(scan_properties)
 endif ()
 
 # Tests which use named modules in shared libraries.

+ 9 - 0
Tests/RunCMake/CXXModules/examples/scan_properties-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) at CMakeLists.txt:20 \(target_sources\):
+  CMake's C\+\+ module support is experimental.  It is meant only for
+  experimentation and feedback to CMake developers.
+This warning is for project developers.  Use -Wno-dev to suppress it.
+
+CMake Warning \(dev\):
+  C\+\+20 modules support via CMAKE_EXPERIMENTAL_CXX_MODULE_DYNDEP is
+  experimental.  It is meant only for compiler developers to try.
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 54 - 0
Tests/RunCMake/CXXModules/examples/scan_properties/CMakeLists.txt

@@ -0,0 +1,54 @@
+cmake_minimum_required(VERSION 3.24)
+project(scan_properties CXX)
+
+include("${CMAKE_SOURCE_DIR}/../cxx-modules-rules.cmake")
+
+# To detect that not-to-be scanned sources are not scanned, add a `-D` to the
+# scan flags so that the files can detect whether scanning happened and error
+# if not.
+string(APPEND CMAKE_EXPERIMENTAL_CXX_MODULE_MAP_FLAG
+  " -DCMAKE_SCANNED_THIS_SOURCE")
+string(APPEND CMAKE_EXPERIMENTAL_CXX_SCANDEP_SOURCE
+  " -DCMAKE_SCANNED_THIS_SOURCE")
+
+set_property(SOURCE always_scan.cxx
+  PROPERTY CXX_SCAN_FOR_MODULES 1)
+set_property(SOURCE never_scan.cxx
+  PROPERTY CXX_SCAN_FOR_MODULES 0)
+
+add_executable(scans_everything)
+target_sources(scans_everything
+  PRIVATE
+    main.cxx
+    join.cxx
+    always_scan.cxx
+    never_scan.cxx
+  PRIVATE
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        module.cxx)
+target_compile_features(scans_everything PRIVATE cxx_std_20)
+target_compile_definitions(scans_everything PRIVATE SCAN_AT_TARGET_LEVEL=1)
+
+set(CMAKE_CXX_SCAN_FOR_MODULES 0)
+
+add_executable(no_scan_everything)
+target_sources(no_scan_everything
+  PRIVATE
+    main.cxx
+    join.cxx
+    always_scan.cxx
+    never_scan.cxx
+  PRIVATE
+    FILE_SET CXX_MODULES
+      BASE_DIRS
+        "${CMAKE_CURRENT_SOURCE_DIR}"
+      FILES
+        module.cxx)
+target_compile_features(no_scan_everything PRIVATE cxx_std_20)
+target_compile_definitions(no_scan_everything PRIVATE SCAN_AT_TARGET_LEVEL=0)
+
+add_test(NAME scanned COMMAND scans_everything)
+add_test(NAME unscanned COMMAND no_scan_everything)

+ 10 - 0
Tests/RunCMake/CXXModules/examples/scan_properties/always_scan.cxx

@@ -0,0 +1,10 @@
+#ifndef CMAKE_SCANNED_THIS_SOURCE
+#  error "This file should have been scanned"
+#endif
+
+import M;
+
+int scanned_file()
+{
+  return from_module();
+}

+ 17 - 0
Tests/RunCMake/CXXModules/examples/scan_properties/join.cxx

@@ -0,0 +1,17 @@
+#if SCAN_AT_TARGET_LEVEL
+#  ifndef CMAKE_SCANNED_THIS_SOURCE
+#    error "This file should have been scanned"
+#  endif
+#else
+#  ifdef CMAKE_SCANNED_THIS_SOURCE
+#    error "This file should not have been scanned"
+#  endif
+#endif
+
+int scanned_file();
+int never_scan();
+
+int join()
+{
+  return scanned_file() + never_scan();
+}

+ 16 - 0
Tests/RunCMake/CXXModules/examples/scan_properties/main.cxx

@@ -0,0 +1,16 @@
+#if SCAN_AT_TARGET_LEVEL
+#  ifndef CMAKE_SCANNED_THIS_SOURCE
+#    error "This file should have been scanned"
+#  endif
+#else
+#  ifdef CMAKE_SCANNED_THIS_SOURCE
+#    error "This file should not have been scanned"
+#  endif
+#endif
+
+int join();
+
+int main(int argc, char** argv)
+{
+  return join();
+}

+ 10 - 0
Tests/RunCMake/CXXModules/examples/scan_properties/module.cxx

@@ -0,0 +1,10 @@
+#ifndef CMAKE_SCANNED_THIS_SOURCE
+#  error "This file should have been scanned"
+#endif
+
+export module M;
+
+export int from_module()
+{
+  return 0;
+}

+ 8 - 0
Tests/RunCMake/CXXModules/examples/scan_properties/never_scan.cxx

@@ -0,0 +1,8 @@
+#ifdef CMAKE_SCANNED_THIS_SOURCE
+#  error "This file should not have been scanned"
+#endif
+
+int never_scan()
+{
+  return 0;
+}