From ca13bab696bbadc28d753f236831251d96a5a38d Mon Sep 17 00:00:00 2001
From: Aleix Pol <aleixpol@kde.org>
Date: Tue, 3 Mar 2015 02:58:35 +0100
Subject: [PATCH] cmake: Add option to generate target metadata for IDEs

Create a CMAKE_EXPORT_PROJECT_METADATA option that can be used to enable
generation of a 'ProjectTargets.json' file.  Write into the file a JSON
description of the build targets that an IDE might load.
---
 Help/variable/CMAKE_EXPORT_PROJECT_METADATA.rst    | 14 ++++
 Modules/CMakeGenericSystem.cmake                   |  5 ++
 Source/cmGlobalGenerator.cxx                       | 93 ++++++++++++++++++++++
 Source/cmGlobalGenerator.h                         |  1 +
 .../CMAKE_EXPORT_PROJECT_METADATA/CMakeLists.txt   |  3 +
 .../CMAKE_EXPORT_PROJECT_METADATA/OneTarget.cmake  |  3 +
 .../RunCMakeTest.cmake                             | 21 +++++
 .../CMAKE_EXPORT_PROJECT_METADATA/empty.cpp        |  7 ++
 Tests/RunCMake/CMakeLists.txt                      |  2 +
 9 files changed, 149 insertions(+)
 create mode 100644 Help/variable/CMAKE_EXPORT_PROJECT_METADATA.rst
 create mode 100644 Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/CMakeLists.txt
 create mode 100644 Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/OneTarget.cmake
 create mode 100644 Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/RunCMakeTest.cmake
 create mode 100644 Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/empty.cpp

diff --git a/Help/variable/CMAKE_EXPORT_PROJECT_METADATA.rst b/Help/variable/CMAKE_EXPORT_PROJECT_METADATA.rst
new file mode 100644
index 0000000..35bbbba
--- /dev/null
+++ b/Help/variable/CMAKE_EXPORT_PROJECT_METADATA.rst
@@ -0,0 +1,14 @@
+CMAKE_EXPORT_PROJECT_METADATA
+-----------------------------
+
+Exports the metadata of the metadata of the project.
+
+This variable will be used to specify whether a ProjectTargets.json will be
+when generating the build directory. To do so, the variable will be initialized
+with the version number of the file to generate.
+
+This file can be used by 3rd party software such as an IDE or a code browser to
+figure out how the project will be built.
+
+Currently the only supported value is "1.0". Any other value will result with
+the file not being generated.
diff --git a/Modules/CMakeGenericSystem.cmake b/Modules/CMakeGenericSystem.cmake
index 8a14aea..0e7f48a 100644
--- a/Modules/CMakeGenericSystem.cmake
+++ b/Modules/CMakeGenericSystem.cmake
@@ -67,6 +67,11 @@ if(CMAKE_GENERATOR MATCHES "Ninja")
   mark_as_advanced(CMAKE_EXPORT_COMPILE_COMMANDS)
 endif()
 
+set(CMAKE_EXPORT_PROJECT_METADATA "" CACHE STRING
+    "Enables the output of a ProjectTargets.json file during generation when a version is provided."
+)
+mark_as_advanced(CMAKE_EXPORT_PROJECT_METADATA)
+
 # GetDefaultWindowsPrefixBase
 #
 # Compute the base directory for CMAKE_INSTALL_PREFIX based on:
diff --git a/Source/cmGlobalGenerator.cxx b/Source/cmGlobalGenerator.cxx
index 36395aa..2b34d5f 100644
--- a/Source/cmGlobalGenerator.cxx
+++ b/Source/cmGlobalGenerator.cxx
@@ -2870,6 +2870,99 @@ void cmGlobalGenerator::WriteSummary()
     this->WriteSummary(ti->second);
     fout << ti->second->GetSupportDirectory() << "\n";
     }
+
+  const char* version = mf->GetDefinition("CMAKE_EXPORT_PROJECT_METADATA");
+  if(version && *version)
+    {
+      if (std::string(version) == "1.0")
+        WriteProjectTargetsJson();
+      else
+        cmSystemTools::Error(
+          "Generator implementation error, "
+          "Unknown export metadata version.");
+    }
+}
+
+void printBacktraceJson(const cmListFileBacktrace& bt, Json::Value& val)
+{
+  for(cmListFileBacktrace::const_iterator it = bt.begin(); it!=bt.end(); ++it)
+    {
+      const cmListFileContext& ctx = *it;
+      std::string path = ctx.FilePath;
+      path += ':';
+      path += std::to_string(ctx.Line);
+
+      val.append( path );
+    }
+}
+
+void cmGlobalGenerator::WriteProjectTargetsJson()
+{
+  cmMakefile* mf = this->LocalGenerators[0]->GetMakefile();
+  std::string projectTargetsPath = mf->GetHomeOutputDirectory();
+  projectTargetsPath += "/ProjectTargets.json";
+
+  Json::Value pf_root(Json::objectValue);
+
+  pf_root["version"] = cmVersion::GetCMakeVersion();
+  Json::Value& pf_dependencies = pf_root["dependencies"] = Json::arrayValue;
+
+  for(std::vector<cmLocalGenerator*>::const_iterator i =
+        this->LocalGenerators.begin(); i != this->LocalGenerators.end(); ++i)
+    {
+    std::vector<std::string> const& lf = (*i)->GetMakefile()->GetListFiles();
+    for(std::vector<std::string>::const_iterator fi = lf.begin();
+        fi != lf.end(); ++fi)
+      {
+      pf_dependencies.append(*fi);
+      }
+    }
+
+  // Generate summary information files for each target.
+  Json::Value& pf_targets = pf_root["targets"] = Json::arrayValue;
+  for(TargetMap::const_iterator ti =
+        this->TotalTargets.begin(); ti != this->TotalTargets.end(); ++ti)
+    {
+    if ((ti->second)->GetType() == cmTarget::INTERFACE_LIBRARY)
+      {
+      continue;
+      }
+
+    cmTarget* t = ti->second;
+    Json::Value & pf_targetValue = pf_targets.append(Json::objectValue);
+    pf_targetValue["name"] = ti->first;
+    pf_targetValue["type"] = cmTarget::GetTargetTypeName(t->GetType());
+
+    std::vector<std::string> configs;
+    t->GetMakefile()->GetConfigurations(configs);
+
+    Json::Value& configsArray = pf_targetValue["configs"] = Json::arrayValue;
+    for (unsigned int i = 0; i<configs.size(); ++i)
+      {
+      std::vector<cmSourceFile*> sources;
+      t->GetSourceFiles(sources, configs[i]);
+      Json::Value& targetConfig = configsArray.append(Json::objectValue);
+      targetConfig ["name"] = configs[i];
+      Json::Value& configSources = targetConfig["sources"] = Json::arrayValue;
+      for (unsigned int j = 0; j<sources.size(); ++j)
+        {
+        configSources.append(sources[j]->GetFullPath());
+        }
+      }
+
+    if(t->GetType() != cmTarget::UTILITY)
+      {
+      pf_targetValue["directory"] = t->GetMakefile()->GetCurrentOutputDirectory();
+      pf_targetValue["location"] = t->GetLocationForBuild();
+      Json::Value& targetBacktrace = pf_targetValue["backtrace"] = Json::arrayValue;
+
+      printBacktraceJson(t->GetBacktrace(), targetBacktrace);
+      }
+    pf_targetValue["installed"] = (t->GetHaveInstallRule() ? "true" : "false");
+    }
+
+  cmGeneratedFileStream aout(projectTargetsPath.c_str());
+  aout << pf_root;
 }
 
 //----------------------------------------------------------------------------
diff --git a/Source/cmGlobalGenerator.h b/Source/cmGlobalGenerator.h
index 5b9ddee..5cca031 100644
--- a/Source/cmGlobalGenerator.h
+++ b/Source/cmGlobalGenerator.h
@@ -458,6 +458,7 @@ private:
 
   void WriteSummary();
   void WriteSummary(cmTarget* target);
+  void WriteProjectTargetsJson();
   void FinalizeTargetCompileInfo();
 
   virtual void ForceLinkerLanguages();
diff --git a/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/CMakeLists.txt b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/CMakeLists.txt
new file mode 100644
index 0000000..12cd3c7
--- /dev/null
+++ b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/CMakeLists.txt
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 2.8.4)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/OneTarget.cmake b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/OneTarget.cmake
new file mode 100644
index 0000000..0bc72ac
--- /dev/null
+++ b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/OneTarget.cmake
@@ -0,0 +1,3 @@
+project(OneTarget)
+
+add_executable(MyTest empty.cpp)
diff --git a/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/RunCMakeTest.cmake b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/RunCMakeTest.cmake
new file mode 100644
index 0000000..c9d2ea1
--- /dev/null
+++ b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/RunCMakeTest.cmake
@@ -0,0 +1,21 @@
+include(RunCMake)
+
+# Run with ctest -R RunCMake.CMAKE_EXPORT_PROJECT_METADATA -V
+
+set(RunCMake_TEST_OPTIONS "-DCMAKE_EXPORT_PROJECT_METADATA=1.0")
+run_cmake(OneTarget)
+
+set(test "OneTarget-ProjectsFile")
+set(PROJECTSFILE "${CMAKE_CURRENT_LIST_DIR}/OneTarget-build/ProjectTargets.json")
+if (EXISTS "${PROJECTSFILE}")
+    file(READ ${PROJECTSFILE} PROJECTSFILE_CONTENT)
+
+    if (NOT PROJECTSFILE_CONTENT MATCHES "\"name\" : \"MyTest\"")
+        message(SEND_ERROR "${test} - FAILED: No target name.")
+    endif()
+    if (NOT PROJECTSFILE_CONTENT MATCHES "\"type\" : \"EXECUTABLE\"")
+        message(SEND_ERROR "${test} - FAILED: wrong target type.")
+    endif()
+else()
+    message(SEND_ERROR "${test} - FAILED: Couldn't find ${PROJECTSFILE}")
+endif ()
diff --git a/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/empty.cpp b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/empty.cpp
new file mode 100644
index 0000000..bfbbdde
--- /dev/null
+++ b/Tests/RunCMake/CMAKE_EXPORT_PROJECT_METADATA/empty.cpp
@@ -0,0 +1,7 @@
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+int empty()
+{
+  return 0;
+}
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index eb42057..2e4b379 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -208,3 +208,5 @@ add_RunCMake_test(IfacePaths_SOURCES TEST_DIR IfacePaths)
 if(RPMBUILD_EXECUTABLE)
   add_RunCMake_test(CPackRPM)
 endif()
+
+add_RunCMake_test(CMAKE_EXPORT_PROJECT_METADATA)
-- 
2.3.1

