Browse Source

try_compile: Add SOURCE_FROM_{ARG,VAR}

Add ability to "feed" try_compile (and try_run) sources more directly,
either from literal content, or from a CMake variable which contains
literal content. This saves the user from needing a separate step to
write the content to a file, and allows for the sources to only exist in
the scratch directory.
Matthew Woehlke 3 years ago
parent
commit
cb14ae2b87

+ 31 - 5
Help/command/try_compile.rst

@@ -55,7 +55,10 @@ Try Compiling Source Files
 
 
 .. code-block:: cmake
 .. code-block:: cmake
 
 
-  try_compile(<resultVar> SOURCES <srcfile...>
+  try_compile(<resultVar>
+              <SOURCES <srcfile...>]             |
+               SOURCE_FROM_ARG <name> <content>] |
+               SOURCE_FROM_VAR <name> <var>]     >...
               [CMAKE_FLAGS <flags>...]
               [CMAKE_FLAGS <flags>...]
               [COMPILE_DEFINITIONS <defs>...]
               [COMPILE_DEFINITIONS <defs>...]
               [LINK_OPTIONS <options>...]
               [LINK_OPTIONS <options>...]
@@ -74,10 +77,12 @@ Try building an executable or static library from one or more source files
 variable).  The success or failure of the ``try_compile``, i.e. ``TRUE`` or
 variable).  The success or failure of the ``try_compile``, i.e. ``TRUE`` or
 ``FALSE`` respectively, is returned in ``<resultVar>``.
 ``FALSE`` respectively, is returned in ``<resultVar>``.
 
 
-In this form, one or more source files must be provided.  If
-:variable:`CMAKE_TRY_COMPILE_TARGET_TYPE` is unset or is set to ``EXECUTABLE``,
-the sources must include a definition for ``main`` and CMake will create a
-``CMakeLists.txt`` file to build the source(s) as an executable.
+In this form, one or more source files must be provided. Additionally, one of
+``SOURCES`` and/or ``SOURCE_FROM_*`` must precede other keywords.
+
+If :variable:`CMAKE_TRY_COMPILE_TARGET_TYPE` is unset or is set to
+``EXECUTABLE``, the sources must include a definition for ``main`` and CMake
+will create a ``CMakeLists.txt`` file to build the source(s) as an executable.
 If :variable:`CMAKE_TRY_COMPILE_TARGET_TYPE` is set to ``STATIC_LIBRARY``,
 If :variable:`CMAKE_TRY_COMPILE_TARGET_TYPE` is set to ``STATIC_LIBRARY``,
 a static library will be built instead and no definition for ``main`` is
 a static library will be built instead and no definition for ``main`` is
 required.  For an executable, the generated ``CMakeLists.txt`` file would
 required.  For an executable, the generated ``CMakeLists.txt`` file would
@@ -163,6 +168,27 @@ The options are:
 ``OUTPUT_VARIABLE <var>``
 ``OUTPUT_VARIABLE <var>``
   Store the output from the build process in the given variable.
   Store the output from the build process in the given variable.
 
 
+``SOURCE_FROM_ARG <name> <content>``
+  .. versionadded:: 3.25
+
+  Write ``<content>`` to a file named ``<name>`` in the operation directory.
+  This can be used to bypass the need to separately write a source file when
+  the contents of the file are dynamically specified. The specified ``<name>``
+  is not allowed to contain path components.
+
+  ``SOURCE_FROM_ARG`` may be specified multiple times.
+
+``SOURCE_FROM_VAR <name> <content>``
+  .. versionadded:: 3.25
+
+  Write the contents of ``<var>`` to a file named ``<name>`` in the operation
+  directory. This is the same as ``SOURCE_FROM_ARG``, but takes the contents
+  from the specified CMake variable, rather than directly, which may be useful
+  when passing arguments through a function which wraps ``try_compile``. The
+  specified ``<name>`` is not allowed to contain path components.
+
+  ``SOURCE_FROM_VAR`` may be specified multiple times.
+
 ``<LANG>_STANDARD <std>``
 ``<LANG>_STANDARD <std>``
   .. versionadded:: 3.8
   .. versionadded:: 3.8
 
 

+ 7 - 1
Help/command/try_run.rst

@@ -12,7 +12,10 @@ Try Compiling and Running Source Files
 
 
 .. code-block:: cmake
 .. code-block:: cmake
 
 
-  try_run(<runResultVar> <compileResultVar> SOURCES <srcfile...>
+  try_run(<runResultVar> <compileResultVar>
+          <SOURCES <srcfile...>]             |
+           SOURCE_FROM_ARG <name> <content>] |
+           SOURCE_FROM_VAR <name> <var>]     >...
           [CMAKE_FLAGS <flags>...]
           [CMAKE_FLAGS <flags>...]
           [COMPILE_DEFINITIONS <defs>...]
           [COMPILE_DEFINITIONS <defs>...]
           [LINK_OPTIONS <options>...]
           [LINK_OPTIONS <options>...]
@@ -40,6 +43,9 @@ set to ``FAILED_TO_RUN``.  See the :command:`try_compile` command for
 documentation of options common to both commands, and for information on how
 documentation of options common to both commands, and for information on how
 the test project is constructed to build the source file.
 the test project is constructed to build the source file.
 
 
+One or more source files must be provided. Additionally, one of ``SOURCES``
+and/or ``SOURCE_FROM_*`` must precede other keywords.
+
 This command also supports an alternate signature
 This command also supports an alternate signature
 which was present in older versions of CMake:
 which was present in older versions of CMake:
 
 

+ 84 - 8
Source/cmCoreTryCompile.cxx

@@ -153,7 +153,7 @@ auto const TryCompileBaseArgParser =
     .Bind("__CMAKE_INTERNAL"_s, &Arguments::CMakeInternal)
     .Bind("__CMAKE_INTERNAL"_s, &Arguments::CMakeInternal)
   /* keep semicolon on own line */;
   /* keep semicolon on own line */;
 
 
-auto const TryCompileBaseNonProjectArgParser =
+auto const TryCompileBaseSourcesArgParser =
   cmArgumentParser<Arguments>{ TryCompileBaseArgParser }
   cmArgumentParser<Arguments>{ TryCompileBaseArgParser }
     .Bind("SOURCES"_s, &Arguments::Sources)
     .Bind("SOURCES"_s, &Arguments::Sources)
     .Bind("COMPILE_DEFINITIONS"_s, TryCompileCompileDefs,
     .Bind("COMPILE_DEFINITIONS"_s, TryCompileCompileDefs,
@@ -170,6 +170,12 @@ auto const TryCompileBaseNonProjectArgParser =
     .BIND_LANG_PROPS(OBJCXX)
     .BIND_LANG_PROPS(OBJCXX)
   /* keep semicolon on own line */;
   /* keep semicolon on own line */;
 
 
+auto const TryCompileBaseNewSourcesArgParser =
+  cmArgumentParser<Arguments>{ TryCompileBaseSourcesArgParser }
+    .Bind("SOURCE_FROM_ARG"_s, &Arguments::SourceFromArg)
+    .Bind("SOURCE_FROM_VAR"_s, &Arguments::SourceFromVar)
+  /* keep semicolon on own line */;
+
 auto const TryCompileBaseProjectArgParser =
 auto const TryCompileBaseProjectArgParser =
   cmArgumentParser<Arguments>{ TryCompileBaseArgParser }
   cmArgumentParser<Arguments>{ TryCompileBaseArgParser }
     .Bind("PROJECT"_s, &Arguments::ProjectName)
     .Bind("PROJECT"_s, &Arguments::ProjectName)
@@ -182,10 +188,10 @@ auto const TryCompileProjectArgParser =
   makeTryCompileParser(TryCompileBaseProjectArgParser);
   makeTryCompileParser(TryCompileBaseProjectArgParser);
 
 
 auto const TryCompileSourcesArgParser =
 auto const TryCompileSourcesArgParser =
-  makeTryCompileParser(TryCompileBaseNonProjectArgParser);
+  makeTryCompileParser(TryCompileBaseNewSourcesArgParser);
 
 
 auto const TryCompileOldArgParser =
 auto const TryCompileOldArgParser =
-  makeTryCompileParser(TryCompileBaseNonProjectArgParser)
+  makeTryCompileParser(TryCompileBaseSourcesArgParser)
     .Bind(1, &Arguments::BinaryDirectory)
     .Bind(1, &Arguments::BinaryDirectory)
     .Bind(2, &Arguments::SourceDirectoryOrFile)
     .Bind(2, &Arguments::SourceDirectoryOrFile)
     .Bind(3, &Arguments::ProjectName)
     .Bind(3, &Arguments::ProjectName)
@@ -196,7 +202,7 @@ auto const TryRunProjectArgParser =
   makeTryRunParser(TryCompileBaseProjectArgParser);
   makeTryRunParser(TryCompileBaseProjectArgParser);
 
 
 auto const TryRunSourcesArgParser =
 auto const TryRunSourcesArgParser =
-  makeTryRunParser(TryCompileBaseNonProjectArgParser);
+  makeTryRunParser(TryCompileBaseNewSourcesArgParser);
 
 
 auto const TryRunOldArgParser = makeTryRunParser(TryCompileOldArgParser);
 auto const TryRunOldArgParser = makeTryRunParser(TryCompileOldArgParser);
 
 
@@ -397,8 +403,21 @@ bool cmCoreTryCompile::TryCompileCode(Arguments& arguments,
     return false;
     return false;
   }
   }
 
 
-  // only valid for srcfile signatures
-  if (!this->SrcFileSignature) {
+  if (this->SrcFileSignature) {
+    if (arguments.SourceFromArg && arguments.SourceFromArg->size() % 2) {
+      this->Makefile->IssueMessage(
+        MessageType::FATAL_ERROR,
+        "SOURCE_FROM_ARG requires exactly two arguments");
+      return false;
+    }
+    if (arguments.SourceFromVar && arguments.SourceFromVar->size() % 2) {
+      this->Makefile->IssueMessage(
+        MessageType::FATAL_ERROR,
+        "SOURCE_FROM_VAR requires exactly two arguments");
+      return false;
+    }
+  } else {
+    // only valid for srcfile signatures
     if (!arguments.LangProps.empty()) {
     if (!arguments.LangProps.empty()) {
       this->Makefile->IssueMessage(
       this->Makefile->IssueMessage(
         MessageType::FATAL_ERROR,
         MessageType::FATAL_ERROR,
@@ -419,6 +438,7 @@ bool cmCoreTryCompile::TryCompileCode(Arguments& arguments,
       return false;
       return false;
     }
     }
   }
   }
+
   // make sure the binary directory exists
   // make sure the binary directory exists
   if (useUniqueBinaryDirectory) {
   if (useUniqueBinaryDirectory) {
     this->BinaryDirectory =
     this->BinaryDirectory =
@@ -449,10 +469,35 @@ bool cmCoreTryCompile::TryCompileCode(Arguments& arguments,
     std::vector<std::string> sources;
     std::vector<std::string> sources;
     if (arguments.Sources) {
     if (arguments.Sources) {
       sources = std::move(*arguments.Sources);
       sources = std::move(*arguments.Sources);
-    } else {
-      // TODO: ensure SourceDirectoryOrFile has a value
+    } else if (arguments.SourceDirectoryOrFile) {
       sources.emplace_back(*arguments.SourceDirectoryOrFile);
       sources.emplace_back(*arguments.SourceDirectoryOrFile);
     }
     }
+    if (arguments.SourceFromArg) {
+      auto const k = arguments.SourceFromArg->size();
+      for (auto i = decltype(k){ 0 }; i < k; i += 2) {
+        const auto& name = (*arguments.SourceFromArg)[i + 0];
+        const auto& content = (*arguments.SourceFromArg)[i + 1];
+        auto out = this->WriteSource(name, content, "SOURCES_FROM_ARG");
+        if (out.empty()) {
+          return false;
+        }
+        sources.emplace_back(std::move(out));
+      }
+    }
+    if (arguments.SourceFromVar) {
+      auto const k = arguments.SourceFromVar->size();
+      for (auto i = decltype(k){ 0 }; i < k; i += 2) {
+        const auto& name = (*arguments.SourceFromVar)[i + 0];
+        const auto& var = (*arguments.SourceFromVar)[i + 1];
+        const auto& content = this->Makefile->GetDefinition(var);
+        auto out = this->WriteSource(name, content, "SOURCES_FROM_VAR");
+        if (out.empty()) {
+          return false;
+        }
+        sources.emplace_back(std::move(out));
+      }
+    }
+    // TODO: ensure sources is not empty
 
 
     // Detect languages to enable.
     // Detect languages to enable.
     cmGlobalGenerator* gg = this->Makefile->GetGlobalGenerator();
     cmGlobalGenerator* gg = this->Makefile->GetGlobalGenerator();
@@ -1132,3 +1177,34 @@ void cmCoreTryCompile::FindOutputFile(const std::string& targetName)
 
 
   this->OutputFile = cmSystemTools::CollapseFullPath(outputFileLocation);
   this->OutputFile = cmSystemTools::CollapseFullPath(outputFileLocation);
 }
 }
+
+std::string cmCoreTryCompile::WriteSource(std::string const& filename,
+                                          std::string const& content,
+                                          char const* command) const
+{
+  if (!cmSystemTools::GetFilenamePath(filename).empty()) {
+    const auto& msg =
+      cmStrCat(command, " given invalid filename \"", filename, "\"");
+    this->Makefile->IssueMessage(MessageType::FATAL_ERROR, msg);
+    return {};
+  }
+
+  auto filepath = cmStrCat(this->BinaryDirectory, "/", filename);
+  cmsys::ofstream file{ filepath.c_str(), std::ios::out };
+  if (!file) {
+    const auto& msg =
+      cmStrCat(command, " failed to open \"", filename, "\" for writing");
+    this->Makefile->IssueMessage(MessageType::FATAL_ERROR, msg);
+    return {};
+  }
+
+  file << content;
+  if (!file) {
+    const auto& msg = cmStrCat(command, " failed to write \"", filename, "\"");
+    this->Makefile->IssueMessage(MessageType::FATAL_ERROR, msg);
+    return {};
+  }
+
+  file.close();
+  return filepath;
+}

+ 7 - 0
Source/cmCoreTryCompile.h

@@ -40,6 +40,10 @@ public:
     cm::optional<std::string> ProjectName;
     cm::optional<std::string> ProjectName;
     cm::optional<std::string> TargetName;
     cm::optional<std::string> TargetName;
     cm::optional<ArgumentParser::NonEmpty<std::vector<std::string>>> Sources;
     cm::optional<ArgumentParser::NonEmpty<std::vector<std::string>>> Sources;
+    cm::optional<ArgumentParser::NonEmpty<std::vector<std::string>>>
+      SourceFromArg;
+    cm::optional<ArgumentParser::NonEmpty<std::vector<std::string>>>
+      SourceFromVar;
     ArgumentParser::MaybeEmpty<std::vector<std::string>> CMakeFlags{
     ArgumentParser::MaybeEmpty<std::vector<std::string>> CMakeFlags{
       1, "CMAKE_FLAGS"
       1, "CMAKE_FLAGS"
     }; // fake argv[0]
     }; // fake argv[0]
@@ -103,6 +107,9 @@ public:
   cmMakefile* Makefile;
   cmMakefile* Makefile;
 
 
 private:
 private:
+  std::string WriteSource(std::string const& name, std::string const& content,
+                          char const* command) const;
+
   Arguments ParseArgs(
   Arguments ParseArgs(
     const cmRange<std::vector<std::string>::const_iterator>& args,
     const cmRange<std::vector<std::string>::const_iterator>& args,
     const cmArgumentParser<Arguments>& parser,
     const cmArgumentParser<Arguments>& parser,

+ 4 - 0
Tests/RunCMake/try_compile/RunCMakeTest.cmake

@@ -20,6 +20,10 @@ set(RunCMake_TEST_OPTIONS -Dtry_compile_DEFS=new_signature.cmake)
 include(${RunCMake_SOURCE_DIR}/old_and_new_signature_tests.cmake)
 include(${RunCMake_SOURCE_DIR}/old_and_new_signature_tests.cmake)
 unset(RunCMake_TEST_OPTIONS)
 unset(RunCMake_TEST_OPTIONS)
 
 
+run_cmake(SourceFromOneArg)
+run_cmake(SourceFromThreeArgs)
+run_cmake(SourceFromBadName)
+
 run_cmake(ProjectCopyFile)
 run_cmake(ProjectCopyFile)
 run_cmake(NonSourceCopyFile)
 run_cmake(NonSourceCopyFile)
 run_cmake(NonSourceCompileDefinitions)
 run_cmake(NonSourceCompileDefinitions)

+ 1 - 0
Tests/RunCMake/try_compile/SourceFromBadName-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/try_compile/SourceFromBadName-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at SourceFromBadName.cmake:[0-9]+ \(try_compile\):
+  SOURCES_FROM_ARG given invalid filename "bad/name.c"
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)

+ 1 - 0
Tests/RunCMake/try_compile/SourceFromBadName.cmake

@@ -0,0 +1 @@
+try_compile(RESULT SOURCE_FROM_ARG bad/name.c "int main();")

+ 1 - 0
Tests/RunCMake/try_compile/SourceFromOneArg-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/try_compile/SourceFromOneArg-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at SourceFromOneArg.cmake:[0-9]+ \(try_compile\):
+  SOURCE_FROM_ARG requires exactly two arguments
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)

+ 1 - 0
Tests/RunCMake/try_compile/SourceFromOneArg.cmake

@@ -0,0 +1 @@
+try_compile(RESULT SOURCE_FROM_ARG test.c)

+ 1 - 0
Tests/RunCMake/try_compile/SourceFromThreeArgs-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/try_compile/SourceFromThreeArgs-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at SourceFromThreeArgs.cmake:[0-9]+ \(try_compile\):
+  SOURCE_FROM_ARG requires exactly two arguments
+Call Stack \(most recent call first\):
+  CMakeLists.txt:3 \(include\)

+ 1 - 0
Tests/RunCMake/try_compile/SourceFromThreeArgs.cmake

@@ -0,0 +1 @@
+try_compile(RESULT SOURCE_FROM_ARG test.c "int" "main();")

+ 42 - 0
Tests/TryCompile/CMakeLists.txt

@@ -64,6 +64,48 @@ set(try_compile_compile_output_var COMPILE_OUT)
 set(try_compile_run_output_var RUN_OUTPUT)
 set(try_compile_run_output_var RUN_OUTPUT)
 include(old_and_new_signature_tests.cmake)
 include(old_and_new_signature_tests.cmake)
 
 
+# try to compile an empty source specified directly
+try_compile(SHOULD_FAIL_DUE_TO_EMPTY_SOURCE
+  SOURCE_FROM_ARG empty.c "")
+if(SHOULD_FAIL_DUE_TO_EMPTY_SOURCE)
+  message(SEND_ERROR "Trying to compile an empty source succeeded?")
+endif()
+
+try_compile(SHOULD_FAIL_DUE_TO_EMPTY_SOURCE
+  SOURCE_FROM_VAR empty.c NAME_OF_A_VAR_THAT_IS_NOT_SET)
+if(SHOULD_FAIL_DUE_TO_EMPTY_SOURCE)
+  message(SEND_ERROR "Trying to compile an empty source succeeded?")
+endif()
+
+# try to run a source specified directly
+set(TRY_RUN_MAIN_CODE
+  "extern int answer(); \n"
+  "int main() { return answer(); }\n")
+set(TRY_RUN_EXT_CODE
+  "int answer() { return 42; }\n")
+
+try_run(SHOULD_EXIT_WITH_ERROR SHOULD_COMPILE
+  SOURCE_FROM_ARG main.c "${TRY_RUN_MAIN_CODE}"
+  SOURCE_FROM_ARG answer.c "${TRY_RUN_EXT_CODE}"
+  COMPILE_OUTPUT_VARIABLE COMPILE_OUTPUT)
+if(NOT SHOULD_COMPILE)
+  message(SEND_ERROR " SOURCE_FROM_ARG failed compiling: ${COMPILE_OUTPUT}")
+endif()
+if(NOT SHOULD_EXIT_WITH_ERROR EQUAL 42)
+  message(SEND_ERROR " SOURCE_FROM_ARG gave unexpected run result: ${SHOULD_EXIT_WITH_ERROR}")
+endif()
+
+try_run(SHOULD_EXIT_WITH_ERROR SHOULD_COMPILE
+  SOURCE_FROM_VAR main.c TRY_RUN_MAIN_CODE
+  SOURCE_FROM_VAR answer.c TRY_RUN_EXT_CODE
+  COMPILE_OUTPUT_VARIABLE COMPILE_OUTPUT)
+if(NOT SHOULD_COMPILE)
+  message(SEND_ERROR " SOURCE_FROM_VAR failed compiling: ${COMPILE_OUTPUT}")
+endif()
+if(NOT SHOULD_EXIT_WITH_ERROR EQUAL 42)
+  message(SEND_ERROR " SOURCE_FROM_VAR gave unexpected run result: ${SHOULD_EXIT_WITH_ERROR}")
+endif()
+
 # try to compile a project (old signature)
 # try to compile a project (old signature)
 message("Testing try_compile project mode (old signature)")
 message("Testing try_compile project mode (old signature)")
 try_compile(TEST_INNER
 try_compile(TEST_INNER