Selaa lähdekoodia

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 vuotta sitten
vanhempi
sitoutus
cb14ae2b87

+ 31 - 5
Help/command/try_compile.rst

@@ -55,7 +55,10 @@ Try Compiling Source Files
 
 .. 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>...]
               [COMPILE_DEFINITIONS <defs>...]
               [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
 ``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``,
 a static library will be built instead and no definition for ``main`` is
 required.  For an executable, the generated ``CMakeLists.txt`` file would
@@ -163,6 +168,27 @@ The options are:
 ``OUTPUT_VARIABLE <var>``
   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>``
   .. versionadded:: 3.8
 

+ 7 - 1
Help/command/try_run.rst

@@ -12,7 +12,10 @@ Try Compiling and Running Source Files
 
 .. 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>...]
           [COMPILE_DEFINITIONS <defs>...]
           [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
 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
 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)
   /* keep semicolon on own line */;
 
-auto const TryCompileBaseNonProjectArgParser =
+auto const TryCompileBaseSourcesArgParser =
   cmArgumentParser<Arguments>{ TryCompileBaseArgParser }
     .Bind("SOURCES"_s, &Arguments::Sources)
     .Bind("COMPILE_DEFINITIONS"_s, TryCompileCompileDefs,
@@ -170,6 +170,12 @@ auto const TryCompileBaseNonProjectArgParser =
     .BIND_LANG_PROPS(OBJCXX)
   /* 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 =
   cmArgumentParser<Arguments>{ TryCompileBaseArgParser }
     .Bind("PROJECT"_s, &Arguments::ProjectName)
@@ -182,10 +188,10 @@ auto const TryCompileProjectArgParser =
   makeTryCompileParser(TryCompileBaseProjectArgParser);
 
 auto const TryCompileSourcesArgParser =
-  makeTryCompileParser(TryCompileBaseNonProjectArgParser);
+  makeTryCompileParser(TryCompileBaseNewSourcesArgParser);
 
 auto const TryCompileOldArgParser =
-  makeTryCompileParser(TryCompileBaseNonProjectArgParser)
+  makeTryCompileParser(TryCompileBaseSourcesArgParser)
     .Bind(1, &Arguments::BinaryDirectory)
     .Bind(2, &Arguments::SourceDirectoryOrFile)
     .Bind(3, &Arguments::ProjectName)
@@ -196,7 +202,7 @@ auto const TryRunProjectArgParser =
   makeTryRunParser(TryCompileBaseProjectArgParser);
 
 auto const TryRunSourcesArgParser =
-  makeTryRunParser(TryCompileBaseNonProjectArgParser);
+  makeTryRunParser(TryCompileBaseNewSourcesArgParser);
 
 auto const TryRunOldArgParser = makeTryRunParser(TryCompileOldArgParser);
 
@@ -397,8 +403,21 @@ bool cmCoreTryCompile::TryCompileCode(Arguments& arguments,
     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()) {
       this->Makefile->IssueMessage(
         MessageType::FATAL_ERROR,
@@ -419,6 +438,7 @@ bool cmCoreTryCompile::TryCompileCode(Arguments& arguments,
       return false;
     }
   }
+
   // make sure the binary directory exists
   if (useUniqueBinaryDirectory) {
     this->BinaryDirectory =
@@ -449,10 +469,35 @@ bool cmCoreTryCompile::TryCompileCode(Arguments& arguments,
     std::vector<std::string> sources;
     if (arguments.Sources) {
       sources = std::move(*arguments.Sources);
-    } else {
-      // TODO: ensure SourceDirectoryOrFile has a value
+    } else if (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.
     cmGlobalGenerator* gg = this->Makefile->GetGlobalGenerator();
@@ -1132,3 +1177,34 @@ void cmCoreTryCompile::FindOutputFile(const std::string& targetName)
 
   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> TargetName;
     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{
       1, "CMAKE_FLAGS"
     }; // fake argv[0]
@@ -103,6 +107,9 @@ public:
   cmMakefile* Makefile;
 
 private:
+  std::string WriteSource(std::string const& name, std::string const& content,
+                          char const* command) const;
+
   Arguments ParseArgs(
     const cmRange<std::vector<std::string>::const_iterator>& args,
     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)
 unset(RunCMake_TEST_OPTIONS)
 
+run_cmake(SourceFromOneArg)
+run_cmake(SourceFromThreeArgs)
+run_cmake(SourceFromBadName)
+
 run_cmake(ProjectCopyFile)
 run_cmake(NonSourceCopyFile)
 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)
 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)
 message("Testing try_compile project mode (old signature)")
 try_compile(TEST_INNER