summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Help/index.rst1
-rw-r--r--Help/manual/cmake-file-api.7.rst220
-rw-r--r--Source/CMakeLists.txt2
-rw-r--r--Source/cmFileAPI.cxx299
-rw-r--r--Source/cmFileAPI.h109
-rw-r--r--Source/cmake.cxx10
-rw-r--r--Source/cmake.h3
-rw-r--r--Tests/RunCMake/CMakeLists.txt4
-rw-r--r--Tests/RunCMake/FileAPI/CMakeLists.txt3
-rw-r--r--Tests/RunCMake/FileAPI/Empty-check.cmake8
-rw-r--r--Tests/RunCMake/FileAPI/Empty-check.py15
-rw-r--r--Tests/RunCMake/FileAPI/Empty-prep.cmake1
-rw-r--r--Tests/RunCMake/FileAPI/Empty.cmake0
-rw-r--r--Tests/RunCMake/FileAPI/Nothing-check.cmake1
-rw-r--r--Tests/RunCMake/FileAPI/Nothing-prep.cmake1
-rw-r--r--Tests/RunCMake/FileAPI/Nothing.cmake0
-rw-r--r--Tests/RunCMake/FileAPI/RunCMakeTest.cmake40
-rw-r--r--Tests/RunCMake/FileAPI/SharedStateless-check.cmake15
-rw-r--r--Tests/RunCMake/FileAPI/SharedStateless-check.py22
-rw-r--r--Tests/RunCMake/FileAPI/SharedStateless-prep.cmake6
-rw-r--r--Tests/RunCMake/FileAPI/SharedStateless.cmake0
-rw-r--r--Tests/RunCMake/FileAPI/Stale-check.cmake4
-rw-r--r--Tests/RunCMake/FileAPI/Stale-prep.cmake1
-rw-r--r--Tests/RunCMake/FileAPI/Stale.cmake0
-rw-r--r--Tests/RunCMake/FileAPI/check_index.py88
25 files changed, 853 insertions, 0 deletions
diff --git a/Help/index.rst b/Help/index.rst
index fe1b73c..a948939 100644
--- a/Help/index.rst
+++ b/Help/index.rst
@@ -30,6 +30,7 @@ Reference Manuals
/manual/cmake-compile-features.7
/manual/cmake-developer.7
/manual/cmake-env-variables.7
+ /manual/cmake-file-api.7
/manual/cmake-generator-expressions.7
/manual/cmake-generators.7
/manual/cmake-language.7
diff --git a/Help/manual/cmake-file-api.7.rst b/Help/manual/cmake-file-api.7.rst
new file mode 100644
index 0000000..dd2ef83
--- /dev/null
+++ b/Help/manual/cmake-file-api.7.rst
@@ -0,0 +1,220 @@
+.. cmake-manual-description: CMake File-Based API
+
+cmake-file-api(7)
+*****************
+
+.. only:: html
+
+ .. contents::
+
+Introduction
+============
+
+CMake provides a file-based API that clients may use to get semantic
+information about the buildsystems CMake generates. Clients may use
+the API by writing query files to a specific location in a build tree
+to request zero or more `Object Kinds`_. When CMake generates the
+buildsystem in that build tree it will read the query files and write
+reply files for the client to read.
+
+The file-based API uses a ``<build>/.cmake/api/`` directory at the top
+of a build tree. The API is versioned to support changes to the layout
+of files within the API directory. API file layout versioning is
+orthogonal to the versioning of `Object Kinds`_ used in replies.
+This version of CMake supports only one API version, `API v1`_.
+
+API v1
+======
+
+API v1 is housed in the ``<build>/.cmake/api/v1/`` directory.
+It has the following subdirectories:
+
+``query/``
+ Holds query files written by clients.
+ These may be `v1 Shared Stateless Query Files`_.
+
+``reply/``
+ Holds reply files written by CMake whenever it runs to generate a build
+ system. These are indexed by a `v1 Reply Index File`_ file that may
+ reference additional `v1 Reply Files`_. CMake owns all reply files.
+ Clients must never remove them.
+
+ Clients may look for and read a reply index file at any time.
+ Clients may optionally create the ``reply/`` directory at any time
+ and monitor it for the appearance of a new reply index file.
+
+v1 Shared Stateless Query Files
+-------------------------------
+
+Shared stateless query files allow clients to share requests for
+major versions of the `Object Kinds`_ and get all requested versions
+recognized by the CMake that runs.
+
+Clients may create shared requests by creating empty files in the
+``v1/query/`` directory. The form is::
+
+ <build>/.cmake/api/v1/query/<kind>-v<major>
+
+where ``<kind>`` is one of the `Object Kinds`_, ``-v`` is literal,
+and ``<major>`` is the major version number.
+
+Files of this form are stateless shared queries not owned by any specific
+client. Once created they should not be removed without external client
+coordination or human intervention.
+
+v1 Reply Index File
+-------------------
+
+CMake writes an ``index-*.json`` file to the ``v1/reply/`` directory
+whenever it runs to generate a build system. Clients must read the
+reply index file first and may read other `v1 Reply Files`_ only by
+following references. The form of the reply index file name is::
+
+ <build>/.cmake/api/v1/reply/index-<unspecified>.json
+
+where ``index-`` is literal and ``<unspecified>`` is an unspecified
+name selected by CMake. Whenever a new index file is generated it
+is given a new name and any old one is deleted. During the short
+time between these steps there may be multiple index files present;
+the one with the largest name in lexicographic order is the current
+index file.
+
+The reply index file contains a JSON object:
+
+.. code-block:: json
+
+ {
+ "cmake": {
+ "version": {
+ "major": 3, "minor": 14, "patch": 0, "suffix": "",
+ "string": "3.14.0", "isDirty": false
+ },
+ "paths": {
+ "cmake": "/prefix/bin/cmake",
+ "ctest": "/prefix/bin/ctest",
+ "cpack": "/prefix/bin/cpack",
+ "root": "/prefix/share/cmake-3.14"
+ }
+ },
+ "objects": [
+ { "kind": "<kind>",
+ "version": { "major": 1, "minor": 0 },
+ "jsonFile": "<file>" },
+ { "...": "..." }
+ ],
+ "reply": {
+ "<kind>-v<major>": { "kind": "<kind>",
+ "version": { "major": 1, "minor": 0 },
+ "jsonFile": "<file>" },
+ "<unknown>": { "error": "unknown query file" },
+ "...": {}
+ }
+ }
+
+The members are:
+
+``cmake``
+ A JSON object containing information about the instance of CMake that
+ generated the reply. It contains members:
+
+ ``version``
+ A JSON object specifying the version of CMake with members:
+
+ ``major``, ``minor``, ``patch``
+ Integer values specifying the major, minor, and patch version components.
+ ``suffix``
+ A string specifying the version suffix, if any, e.g. ``g0abc3``.
+ ``string``
+ A string specifying the full version in the format
+ ``<major>.<minor>.<patch>[-<suffix>]``.
+ ``isDirty``
+ A boolean indicating whether the version was built from a version
+ controlled source tree with local modifications.
+
+ ``paths``
+ A JSON object specifying paths to things that come with CMake.
+ It has members for ``cmake``, ``ctest``, and ``cpack`` whose values
+ are JSON strings specifying the absolute path to each tool,
+ represented with forward slashes. It also has a ``root`` member for
+ the absolute path to the directory containing CMake resources like the
+ ``Modules/`` directory (see :variable:`CMAKE_ROOT`).
+
+``objects``
+ A JSON array listing all versions of all `Object Kinds`_ generated
+ as part of the reply. Each array entry is a
+ `v1 Reply File Reference`_.
+
+``reply``
+ A JSON object mirroring the content of the ``query/`` directory
+ that CMake loaded to produce the reply. The members are of the form
+
+ ``<kind>-v<major>``
+ A member of this form appears for each of the
+ `v1 Shared Stateless Query Files`_ that CMake recognized as a
+ request for object kind ``<kind>`` with major version ``<major>``.
+ The value is a `v1 Reply File Reference`_ to the corresponding
+ reply file for that object kind and version.
+
+ ``<unknown>``
+ A member of this form appears for each of the
+ `v1 Shared Stateless Query Files`_ that CMake did not recognize.
+ The value is a JSON object with a single ``error`` member
+ containing a string with an error message indicating that the
+ query file is unknown.
+
+After reading the reply index file, clients may read the other
+`v1 Reply Files`_ it references.
+
+v1 Reply File Reference
+^^^^^^^^^^^^^^^^^^^^^^^
+
+The reply index file represents each reference to another reply file
+using a JSON object with members:
+
+``kind``
+ A string specifying one of the `Object Kinds`_.
+``version``
+ A JSON object with members ``major`` and ``minor`` specifying
+ integer version components of the object kind.
+``jsonFile``
+ A JSON string specifying a path relative to the reply index file
+ to another JSON file containing the object.
+
+v1 Reply Files
+--------------
+
+Reply files containing specific `Object Kinds`_ are written by CMake.
+The names of these files are unspecified and must not be interpreted
+by clients. Clients must first read the `v1 Reply Index File`_ and
+and follow references to the names of the desired response objects.
+
+Reply files (including the index file) will never be replaced by
+files of the same name but different content. This allows a client
+to read the files concurrently with a running CMake that may generate
+a new reply. However, after generating a new reply CMake will attempt
+to remove reply files from previous runs that it did not just write.
+If a client attempts to read a reply file referenced by the index but
+finds the file missing, that means a concurrent CMake has generated
+a new reply. The client may simply start again by reading the new
+reply index file.
+
+Object Kinds
+============
+
+The CMake file-based API reports semantic information about the build
+system using the following kinds of JSON objects. Each kind of object
+is versioned independently using semantic versioning with major and
+minor components. Every kind of object has the form:
+
+.. code-block:: json
+
+ {
+ "kind": "<kind>",
+ "version": { "major": 1, "minor": 0 },
+ "...": {}
+ }
+
+The ``kind`` member is a string specifying the object kind name.
+The ``version`` member is a JSON object with ``major`` and ``minor``
+members specifying integer components of the object kind's version.
+Additional top-level members are specific to each object kind.
diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt
index 9aebfa7..ec71fe0 100644
--- a/Source/CMakeLists.txt
+++ b/Source/CMakeLists.txt
@@ -207,6 +207,8 @@ set(SRCS
cmExtraKateGenerator.h
cmExtraSublimeTextGenerator.cxx
cmExtraSublimeTextGenerator.h
+ cmFileAPI.cxx
+ cmFileAPI.h
cmFileLock.cxx
cmFileLock.h
cmFileLockPool.cxx
diff --git a/Source/cmFileAPI.cxx b/Source/cmFileAPI.cxx
new file mode 100644
index 0000000..23e0ced
--- /dev/null
+++ b/Source/cmFileAPI.cxx
@@ -0,0 +1,299 @@
+/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
+ file Copyright.txt or https://cmake.org/licensing for details. */
+#include "cmFileAPI.h"
+
+#include "cmCryptoHash.h"
+#include "cmSystemTools.h"
+#include "cmTimestamp.h"
+#include "cmake.h"
+#include "cmsys/Directory.hxx"
+#include "cmsys/FStream.hxx"
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <ctime>
+#include <iomanip>
+#include <sstream>
+#include <utility>
+
+cmFileAPI::cmFileAPI(cmake* cm)
+ : CMakeInstance(cm)
+{
+ this->APIv1 =
+ this->CMakeInstance->GetHomeOutputDirectory() + "/.cmake/api/v1";
+
+ Json::StreamWriterBuilder wbuilder;
+ wbuilder["indentation"] = "\t";
+ this->JsonWriter =
+ std::unique_ptr<Json::StreamWriter>(wbuilder.newStreamWriter());
+}
+
+void cmFileAPI::ReadQueries()
+{
+ std::string const query_dir = this->APIv1 + "/query";
+ this->QueryExists = cmSystemTools::FileIsDirectory(query_dir);
+ if (!this->QueryExists) {
+ return;
+ }
+
+ // Load queries at the top level.
+ std::vector<std::string> queries = cmFileAPI::LoadDir(query_dir);
+
+ // Read the queries and save for later.
+ for (std::string& query : queries) {
+ if (!cmFileAPI::ReadQuery(query, this->TopQuery.Known)) {
+ this->TopQuery.Unknown.push_back(std::move(query));
+ }
+ }
+}
+
+void cmFileAPI::WriteReplies()
+{
+ if (this->QueryExists) {
+ cmSystemTools::MakeDirectory(this->APIv1 + "/reply");
+ this->WriteJsonFile(this->BuildReplyIndex(), "index", ComputeSuffixTime);
+ }
+
+ this->RemoveOldReplyFiles();
+}
+
+std::vector<std::string> cmFileAPI::LoadDir(std::string const& dir)
+{
+ std::vector<std::string> files;
+ cmsys::Directory d;
+ d.Load(dir);
+ for (unsigned long i = 0; i < d.GetNumberOfFiles(); ++i) {
+ std::string f = d.GetFile(i);
+ if (f != "." && f != "..") {
+ files.push_back(std::move(f));
+ }
+ }
+ std::sort(files.begin(), files.end());
+ return files;
+}
+
+void cmFileAPI::RemoveOldReplyFiles()
+{
+ std::string const reply_dir = this->APIv1 + "/reply";
+ std::vector<std::string> files = this->LoadDir(reply_dir);
+ for (std::string const& f : files) {
+ if (this->ReplyFiles.find(f) == this->ReplyFiles.end()) {
+ std::string file = reply_dir + "/" + f;
+ cmSystemTools::RemoveFile(file);
+ }
+ }
+}
+
+std::string cmFileAPI::WriteJsonFile(
+ Json::Value const& value, std::string const& prefix,
+ std::string (*computeSuffix)(std::string const&))
+{
+ std::string fileName;
+
+ // Write the json file with a temporary name.
+ std::string const& tmpFile = this->APIv1 + "/tmp.json";
+ cmsys::ofstream ftmp(tmpFile.c_str());
+ this->JsonWriter->write(value, &ftmp);
+ ftmp << "\n";
+ ftmp.close();
+ if (!ftmp) {
+ cmSystemTools::RemoveFile(tmpFile);
+ return fileName;
+ }
+
+ // Compute the final name for the file.
+ fileName = prefix + "-" + computeSuffix(tmpFile) + ".json";
+
+ // Create the destination.
+ std::string file = this->APIv1 + "/reply";
+ cmSystemTools::MakeDirectory(file);
+ file += "/";
+ file += fileName;
+
+ // If the final name already exists then assume it has proper content.
+ // Otherwise, atomically place the reply file at its final name
+ if (cmSystemTools::FileExists(file, true) ||
+ !cmSystemTools::RenameFile(tmpFile.c_str(), file.c_str())) {
+ cmSystemTools::RemoveFile(tmpFile);
+ }
+
+ // Record this among files we have just written.
+ this->ReplyFiles.insert(fileName);
+
+ return fileName;
+}
+
+std::string cmFileAPI::ComputeSuffixHash(std::string const& file)
+{
+ cmCryptoHash hasher(cmCryptoHash::AlgoSHA3_256);
+ std::string hash = hasher.HashFile(file);
+ hash.resize(20, '0');
+ return hash;
+}
+
+std::string cmFileAPI::ComputeSuffixTime(std::string const&)
+{
+ std::chrono::milliseconds ms =
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::system_clock::now().time_since_epoch());
+ std::chrono::seconds s =
+ std::chrono::duration_cast<std::chrono::seconds>(ms);
+
+ std::time_t ts = s.count();
+ std::size_t tms = ms.count() % 1000;
+
+ cmTimestamp cmts;
+ std::ostringstream ss;
+ ss << cmts.CreateTimestampFromTimeT(ts, "%Y-%m-%dT%H-%M-%S", true) << '-'
+ << std::setfill('0') << std::setw(4) << tms;
+ return ss.str();
+}
+
+bool cmFileAPI::ReadQuery(std::string const& query,
+ std::vector<Object>& objects)
+{
+ // Parse the "<kind>-" syntax.
+ std::string::size_type sep_pos = query.find('-');
+ if (sep_pos == std::string::npos) {
+ return false;
+ }
+ std::string kindName = query.substr(0, sep_pos);
+ std::string verStr = query.substr(sep_pos + 1);
+ if (kindName == ObjectKindName(ObjectKind::InternalTest)) {
+ Object o;
+ o.Kind = ObjectKind::InternalTest;
+ if (verStr == "v1") {
+ o.Version = 1;
+ } else if (verStr == "v2") {
+ o.Version = 2;
+ } else {
+ return false;
+ }
+ objects.push_back(o);
+ return true;
+ }
+ return false;
+}
+
+Json::Value cmFileAPI::BuildReplyIndex()
+{
+ Json::Value index(Json::objectValue);
+
+ // Report information about this version of CMake.
+ index["cmake"] = this->BuildCMake();
+
+ // Reply to all queries that we loaded.
+ index["reply"] = this->BuildReply(this->TopQuery);
+
+ // Move our index of generated objects into its field.
+ Json::Value& objects = index["objects"] = Json::arrayValue;
+ for (auto& entry : this->ReplyIndexObjects) {
+ objects.append(std::move(entry.second)); // NOLINT(*)
+ }
+
+ return index;
+}
+
+Json::Value cmFileAPI::BuildCMake()
+{
+ Json::Value cmake = Json::objectValue;
+ cmake["version"] = this->CMakeInstance->ReportVersionJson();
+ Json::Value& cmake_paths = cmake["paths"] = Json::objectValue;
+ cmake_paths["cmake"] = cmSystemTools::GetCMakeCommand();
+ cmake_paths["ctest"] = cmSystemTools::GetCTestCommand();
+ cmake_paths["cpack"] = cmSystemTools::GetCPackCommand();
+ cmake_paths["root"] = cmSystemTools::GetCMakeRoot();
+ return cmake;
+}
+
+Json::Value cmFileAPI::BuildReply(Query const& q)
+{
+ Json::Value reply = Json::objectValue;
+ for (Object const& o : q.Known) {
+ std::string const& name = ObjectName(o);
+ reply[name] = this->AddReplyIndexObject(o);
+ }
+
+ for (std::string const& name : q.Unknown) {
+ reply[name] = cmFileAPI::BuildReplyError("unknown query file");
+ }
+ return reply;
+}
+
+Json::Value cmFileAPI::BuildReplyError(std::string const& error)
+{
+ Json::Value e = Json::objectValue;
+ e["error"] = error;
+ return e;
+}
+
+Json::Value const& cmFileAPI::AddReplyIndexObject(Object const& o)
+{
+ Json::Value& indexEntry = this->ReplyIndexObjects[o];
+ if (!indexEntry.isNull()) {
+ // The reply object has already been generated.
+ return indexEntry;
+ }
+
+ // Generate this reply object.
+ Json::Value const& object = this->BuildObject(o);
+ assert(object.isObject());
+
+ // Populate this index entry.
+ indexEntry = Json::objectValue;
+ indexEntry["kind"] = object["kind"];
+ indexEntry["version"] = object["version"];
+ indexEntry["jsonFile"] = this->WriteJsonFile(object, ObjectName(o));
+ return indexEntry;
+}
+
+const char* cmFileAPI::ObjectKindName(ObjectKind kind)
+{
+ // Keep in sync with ObjectKind enum.
+ static const char* objectKindNames[] = {
+ "__test" //
+ };
+ return objectKindNames[size_t(kind)];
+}
+
+std::string cmFileAPI::ObjectName(Object const& o)
+{
+ std::string name = ObjectKindName(o.Kind);
+ name += "-v";
+ name += std::to_string(o.Version);
+ return name;
+}
+
+Json::Value cmFileAPI::BuildObject(Object const& object)
+{
+ Json::Value value;
+
+ switch (object.Kind) {
+ case ObjectKind::InternalTest:
+ value = this->BuildInternalTest(object);
+ break;
+ }
+
+ return value;
+}
+
+// The "__test" object kind is for internal testing of CMake.
+
+static unsigned int const InternalTestV1Minor = 3;
+static unsigned int const InternalTestV2Minor = 0;
+
+Json::Value cmFileAPI::BuildInternalTest(Object const& object)
+{
+ Json::Value test = Json::objectValue;
+ test["kind"] = this->ObjectKindName(object.Kind);
+ Json::Value& version = test["version"] = Json::objectValue;
+ if (object.Version == 2) {
+ version["major"] = 2;
+ version["minor"] = InternalTestV2Minor;
+ } else {
+ version["major"] = 1;
+ version["minor"] = InternalTestV1Minor;
+ }
+ return test;
+}
diff --git a/Source/cmFileAPI.h b/Source/cmFileAPI.h
new file mode 100644
index 0000000..39b054d
--- /dev/null
+++ b/Source/cmFileAPI.h
@@ -0,0 +1,109 @@
+/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
+ file Copyright.txt or https://cmake.org/licensing for details. */
+#ifndef cmFileAPI_h
+#define cmFileAPI_h
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include "cm_jsoncpp_value.h"
+#include "cm_jsoncpp_writer.h"
+
+#include <map>
+#include <memory> // IWYU pragma: keep
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+class cmake;
+
+class cmFileAPI
+{
+public:
+ cmFileAPI(cmake* cm);
+
+ /** Read fileapi queries from disk. */
+ void ReadQueries();
+
+ /** Write fileapi replies to disk. */
+ void WriteReplies();
+
+ /** Get the "cmake" instance with which this was constructed. */
+ cmake* GetCMakeInstance() const { return this->CMakeInstance; }
+
+private:
+ cmake* CMakeInstance;
+
+ /** The api/v1 directory location. */
+ std::string APIv1;
+
+ /** The set of files we have just written to the reply directory. */
+ std::unordered_set<std::string> ReplyFiles;
+
+ static std::vector<std::string> LoadDir(std::string const& dir);
+ void RemoveOldReplyFiles();
+
+ // Keep in sync with ObjectKindName.
+ enum class ObjectKind
+ {
+ InternalTest
+ };
+
+ /** Identify one object kind and major version. */
+ struct Object
+ {
+ ObjectKind Kind;
+ unsigned long Version = 0;
+ friend bool operator<(Object const& l, Object const& r)
+ {
+ if (l.Kind != r.Kind) {
+ return l.Kind < r.Kind;
+ }
+ return l.Version < r.Version;
+ }
+ };
+
+ /** Represent content of a query directory. */
+ struct Query
+ {
+ /** Known object kind-version pairs. */
+ std::vector<Object> Known;
+ /** Unknown object kind names. */
+ std::vector<std::string> Unknown;
+ };
+
+ /** Whether the top-level query directory exists at all. */
+ bool QueryExists = false;
+
+ /** The content of the top-level query directory. */
+ Query TopQuery;
+
+ /** Reply index object generated for object kind/version.
+ This populates the "objects" field of the reply index. */
+ std::map<Object, Json::Value> ReplyIndexObjects;
+
+ std::unique_ptr<Json::StreamWriter> JsonWriter;
+
+ std::string WriteJsonFile(
+ Json::Value const& value, std::string const& prefix,
+ std::string (*computeSuffix)(std::string const&) = ComputeSuffixHash);
+ static std::string ComputeSuffixHash(std::string const&);
+ static std::string ComputeSuffixTime(std::string const&);
+
+ static bool ReadQuery(std::string const& query,
+ std::vector<Object>& objects);
+
+ Json::Value BuildReplyIndex();
+ Json::Value BuildCMake();
+ Json::Value BuildReply(Query const& q);
+ static Json::Value BuildReplyError(std::string const& error);
+ Json::Value const& AddReplyIndexObject(Object const& o);
+
+ static const char* ObjectKindName(ObjectKind kind);
+ static std::string ObjectName(Object const& o);
+
+ Json::Value BuildObject(Object const& object);
+
+ Json::Value BuildInternalTest(Object const& object);
+};
+
+#endif
diff --git a/Source/cmake.cxx b/Source/cmake.cxx
index 2ac7f4d..e81d14b 100644
--- a/Source/cmake.cxx
+++ b/Source/cmake.cxx
@@ -30,6 +30,7 @@
#if defined(CMAKE_BUILD_WITH_CMAKE)
# include "cm_jsoncpp_writer.h"
+# include "cmFileAPI.h"
# include "cmGraphVizWriter.h"
# include "cmVariableWatch.h"
# include <unordered_map>
@@ -1443,6 +1444,11 @@ int cmake::ActualConfigure()
this->TruncateOutputLog("CMakeError.log");
}
+#if defined(CMAKE_BUILD_WITH_CMAKE)
+ this->FileAPI = cm::make_unique<cmFileAPI>(this);
+ this->FileAPI->ReadQueries();
+#endif
+
// actually do the configure
this->GlobalGenerator->Configure();
// Before saving the cache
@@ -1682,6 +1688,10 @@ int cmake::Generate()
// for the Visual Studio and Xcode generators.)
this->SaveCache(this->GetHomeOutputDirectory());
+#if defined(CMAKE_BUILD_WITH_CMAKE)
+ this->FileAPI->WriteReplies();
+#endif
+
return 0;
}
diff --git a/Source/cmake.h b/Source/cmake.h
index d3d0e80..d00acc7 100644
--- a/Source/cmake.h
+++ b/Source/cmake.h
@@ -6,6 +6,7 @@
#include "cmConfigure.h" // IWYU pragma: keep
#include <map>
+#include <memory> // IWYU pragma: keep
#include <set>
#include <string>
#include <unordered_set>
@@ -21,6 +22,7 @@
#endif
class cmExternalMakefileProjectGeneratorFactory;
+class cmFileAPI;
class cmFileTimeComparison;
class cmGlobalGenerator;
class cmGlobalGeneratorFactory;
@@ -528,6 +530,7 @@ private:
#if defined(CMAKE_BUILD_WITH_CMAKE)
cmVariableWatch* VariableWatch;
+ std::unique_ptr<cmFileAPI> FileAPI;
#endif
cmState* State;
diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt
index a4d829b..d5aa0c3 100644
--- a/Tests/RunCMake/CMakeLists.txt
+++ b/Tests/RunCMake/CMakeLists.txt
@@ -66,6 +66,9 @@ function(add_RunCMake_test_group test types)
endforeach()
endfunction()
+# Some tests use python for extra checks.
+find_package(PythonInterp QUIET)
+
if(XCODE_VERSION AND "${XCODE_VERSION}" VERSION_LESS 6.1)
set(Swift_ARGS -DXCODE_BELOW_6_1=1)
endif()
@@ -155,6 +158,7 @@ add_RunCMake_test(DisallowedCommands)
add_RunCMake_test(ExternalData)
add_RunCMake_test(FeatureSummary)
add_RunCMake_test(FPHSA)
+add_RunCMake_test(FileAPI -DPYTHON_EXECUTABLE=${PYTHON_EXECUTABLE})
add_RunCMake_test(FindBoost)
add_RunCMake_test(FindLua)
add_RunCMake_test(FindOpenGL)
diff --git a/Tests/RunCMake/FileAPI/CMakeLists.txt b/Tests/RunCMake/FileAPI/CMakeLists.txt
new file mode 100644
index 0000000..44025d3
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/CMakeLists.txt
@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.12)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)
diff --git a/Tests/RunCMake/FileAPI/Empty-check.cmake b/Tests/RunCMake/FileAPI/Empty-check.cmake
new file mode 100644
index 0000000..2764b42
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Empty-check.cmake
@@ -0,0 +1,8 @@
+set(expect
+ query
+ reply
+ reply/index-[0-9.T-]+.json
+ )
+check_api("^${expect}$")
+
+check_python(Empty)
diff --git a/Tests/RunCMake/FileAPI/Empty-check.py b/Tests/RunCMake/FileAPI/Empty-check.py
new file mode 100644
index 0000000..75bf096
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Empty-check.py
@@ -0,0 +1,15 @@
+from check_index import *
+
+def check_reply(r):
+ assert is_dict(r)
+ assert sorted(r.keys()) == []
+
+def check_objects(o):
+ assert is_list(o)
+ assert len(o) == 0
+
+assert is_dict(index)
+assert sorted(index.keys()) == ["cmake", "objects", "reply"]
+check_cmake(index["cmake"])
+check_reply(index["reply"])
+check_objects(index["objects"])
diff --git a/Tests/RunCMake/FileAPI/Empty-prep.cmake b/Tests/RunCMake/FileAPI/Empty-prep.cmake
new file mode 100644
index 0000000..1d1f69e
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Empty-prep.cmake
@@ -0,0 +1 @@
+file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query")
diff --git a/Tests/RunCMake/FileAPI/Empty.cmake b/Tests/RunCMake/FileAPI/Empty.cmake
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Empty.cmake
diff --git a/Tests/RunCMake/FileAPI/Nothing-check.cmake b/Tests/RunCMake/FileAPI/Nothing-check.cmake
new file mode 100644
index 0000000..cd4f42e
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Nothing-check.cmake
@@ -0,0 +1 @@
+check_api("^$")
diff --git a/Tests/RunCMake/FileAPI/Nothing-prep.cmake b/Tests/RunCMake/FileAPI/Nothing-prep.cmake
new file mode 100644
index 0000000..b850d47
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Nothing-prep.cmake
@@ -0,0 +1 @@
+file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1")
diff --git a/Tests/RunCMake/FileAPI/Nothing.cmake b/Tests/RunCMake/FileAPI/Nothing.cmake
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Nothing.cmake
diff --git a/Tests/RunCMake/FileAPI/RunCMakeTest.cmake b/Tests/RunCMake/FileAPI/RunCMakeTest.cmake
new file mode 100644
index 0000000..bb016ca
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/RunCMakeTest.cmake
@@ -0,0 +1,40 @@
+include(RunCMake)
+
+# Function called in *-check.cmake scripts to check api files.
+function(check_api expect)
+ file(GLOB_RECURSE actual
+ LIST_DIRECTORIES TRUE
+ RELATIVE ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1
+ ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/*
+ )
+ if(NOT "${actual}" MATCHES "${expect}")
+ set(RunCMake_TEST_FAILED "API files:
+ ${actual}
+do not match what we expected:
+ ${expect}
+in directory:
+ ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1" PARENT_SCOPE)
+ endif()
+endfunction()
+
+function(check_python case)
+ if(RunCMake_TEST_FAILED OR NOT PYTHON_EXECUTABLE)
+ return()
+ endif()
+ file(GLOB index ${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/index-*.json)
+ execute_process(
+ COMMAND ${PYTHON_EXECUTABLE} "${RunCMake_SOURCE_DIR}/${case}-check.py" "${index}"
+ RESULT_VARIABLE result
+ OUTPUT_VARIABLE output
+ ERROR_VARIABLE output
+ )
+ if(NOT result EQUAL 0)
+ string(REPLACE "\n" "\n " output " ${output}")
+ set(RunCMake_TEST_FAILED "Unexpected index:\n${output}" PARENT_SCOPE)
+ endif()
+endfunction()
+
+run_cmake(Nothing)
+run_cmake(Empty)
+run_cmake(Stale)
+run_cmake(SharedStateless)
diff --git a/Tests/RunCMake/FileAPI/SharedStateless-check.cmake b/Tests/RunCMake/FileAPI/SharedStateless-check.cmake
new file mode 100644
index 0000000..7f3bb23
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/SharedStateless-check.cmake
@@ -0,0 +1,15 @@
+set(expect
+ query
+ query/__test-v1
+ query/__test-v2
+ query/__test-v3
+ query/query.json
+ query/unknown
+ reply
+ reply/__test-v1-[0-9a-f]+.json
+ reply/__test-v2-[0-9a-f]+.json
+ reply/index-[0-9.T-]+.json
+ )
+check_api("^${expect}$")
+
+check_python(SharedStateless)
diff --git a/Tests/RunCMake/FileAPI/SharedStateless-check.py b/Tests/RunCMake/FileAPI/SharedStateless-check.py
new file mode 100644
index 0000000..79f52d7
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/SharedStateless-check.py
@@ -0,0 +1,22 @@
+from check_index import *
+
+def check_reply(r):
+ assert is_dict(r)
+ assert sorted(r.keys()) == ["__test-v1", "__test-v2", "__test-v3", "query.json", "unknown"]
+ check_index__test(r["__test-v1"], 1, 3)
+ check_index__test(r["__test-v2"], 2, 0)
+ check_error(r["__test-v3"], "unknown query file")
+ check_error(r["query.json"], "unknown query file")
+ check_error(r["unknown"], "unknown query file")
+
+def check_objects(o):
+ assert is_list(o)
+ assert len(o) == 2
+ check_index__test(o[0], 1, 3)
+ check_index__test(o[1], 2, 0)
+
+assert is_dict(index)
+assert sorted(index.keys()) == ["cmake", "objects", "reply"]
+check_cmake(index["cmake"])
+check_reply(index["reply"])
+check_objects(index["objects"])
diff --git a/Tests/RunCMake/FileAPI/SharedStateless-prep.cmake b/Tests/RunCMake/FileAPI/SharedStateless-prep.cmake
new file mode 100644
index 0000000..b280414
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/SharedStateless-prep.cmake
@@ -0,0 +1,6 @@
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/__test-v1" "")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/__test-v2" "")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/__test-v3" "")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/query.json" "")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/unknown" "")
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "")
diff --git a/Tests/RunCMake/FileAPI/SharedStateless.cmake b/Tests/RunCMake/FileAPI/SharedStateless.cmake
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/SharedStateless.cmake
diff --git a/Tests/RunCMake/FileAPI/Stale-check.cmake b/Tests/RunCMake/FileAPI/Stale-check.cmake
new file mode 100644
index 0000000..7ee2c9e
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Stale-check.cmake
@@ -0,0 +1,4 @@
+set(expect
+ reply
+ )
+check_api("^${expect}$")
diff --git a/Tests/RunCMake/FileAPI/Stale-prep.cmake b/Tests/RunCMake/FileAPI/Stale-prep.cmake
new file mode 100644
index 0000000..e920925
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Stale-prep.cmake
@@ -0,0 +1 @@
+file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "")
diff --git a/Tests/RunCMake/FileAPI/Stale.cmake b/Tests/RunCMake/FileAPI/Stale.cmake
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/Stale.cmake
diff --git a/Tests/RunCMake/FileAPI/check_index.py b/Tests/RunCMake/FileAPI/check_index.py
new file mode 100644
index 0000000..6cc16a5
--- /dev/null
+++ b/Tests/RunCMake/FileAPI/check_index.py
@@ -0,0 +1,88 @@
+import sys
+import os
+import json
+import re
+
+if sys.version_info[0] >= 3:
+ unicode = str
+
+def is_bool(x):
+ return isinstance(x, bool)
+
+def is_dict(x):
+ return isinstance(x, dict)
+
+def is_list(x):
+ return isinstance(x, list)
+
+def is_int(x):
+ return isinstance(x, int) or isinstance(x, long)
+
+def is_string(x):
+ return isinstance(x, str) or isinstance(x, unicode)
+
+def check_cmake(cmake):
+ assert is_dict(cmake)
+ assert sorted(cmake.keys()) == ["paths", "version"]
+ check_cmake_version(cmake["version"])
+ check_cmake_paths(cmake["paths"])
+
+def check_cmake_version(v):
+ assert is_dict(v)
+ assert sorted(v.keys()) == ["isDirty", "major", "minor", "patch", "string", "suffix"]
+ assert is_string(v["string"])
+ assert is_int(v["major"])
+ assert is_int(v["minor"])
+ assert is_int(v["patch"])
+ assert is_string(v["suffix"])
+ assert is_bool(v["isDirty"])
+
+def check_cmake_paths(v):
+ assert is_dict(v)
+ assert sorted(v.keys()) == ["cmake", "cpack", "ctest", "root"]
+ assert is_string(v["cmake"])
+ assert is_string(v["cpack"])
+ assert is_string(v["ctest"])
+ assert is_string(v["root"])
+
+def check_index_object(indexEntry, kind, major, minor, check):
+ assert is_dict(indexEntry)
+ assert sorted(indexEntry.keys()) == ["jsonFile", "kind", "version"]
+ assert is_string(indexEntry["kind"])
+ assert indexEntry["kind"] == kind
+ assert is_dict(indexEntry["version"])
+ assert sorted(indexEntry["version"].keys()) == ["major", "minor"]
+ assert indexEntry["version"]["major"] == major
+ assert indexEntry["version"]["minor"] == minor
+ assert is_string(indexEntry["jsonFile"])
+ filepath = os.path.join(reply_dir, indexEntry["jsonFile"])
+ with open(filepath) as f:
+ object = json.load(f)
+ assert is_dict(object)
+ assert "kind" in object
+ assert is_string(object["kind"])
+ assert object["kind"] == kind
+ assert "version" in object
+ assert is_dict(object["version"])
+ assert sorted(object["version"].keys()) == ["major", "minor"]
+ assert object["version"]["major"] == major
+ assert object["version"]["minor"] == minor
+ if check:
+ check(object)
+
+def check_index__test(indexEntry, major, minor):
+ def check(object):
+ assert sorted(object.keys()) == ["kind", "version"]
+ check_index_object(indexEntry, "__test", major, minor, check)
+
+def check_error(value, error):
+ assert is_dict(value)
+ assert sorted(value.keys()) == ["error"]
+ assert is_string(value["error"])
+ assert value["error"] == error
+
+reply_index = sys.argv[1]
+reply_dir = os.path.dirname(reply_index)
+
+with open(reply_index) as f:
+ index = json.load(f)