From 8fce59848b52f71ae310fcd64fcf943fb2c42bf6 Mon Sep 17 00:00:00 2001 From: Brad King Date: Thu, 13 Sep 2018 08:48:29 -0400 Subject: fileapi: Add protocol v1 support for client-specific query files Add support for client-owned stateless query files. These allow clients to *own* requests for major object versions and get all those recognized by CMake. Issue: #18398 --- Help/manual/cmake-file-api.7.rst | 53 +++++++++++++++++++++- Source/cmFileAPI.cxx | 27 ++++++++++- Source/cmFileAPI.h | 4 ++ Tests/RunCMake/FileAPI/ClientStateless-check.cmake | 15 ++++++ Tests/RunCMake/FileAPI/ClientStateless-check.py | 26 +++++++++++ Tests/RunCMake/FileAPI/ClientStateless-prep.cmake | 5 ++ Tests/RunCMake/FileAPI/ClientStateless.cmake | 0 .../FileAPI/DuplicateStateless-check.cmake | 20 ++++++++ Tests/RunCMake/FileAPI/DuplicateStateless-check.py | 31 +++++++++++++ .../RunCMake/FileAPI/DuplicateStateless-prep.cmake | 10 ++++ Tests/RunCMake/FileAPI/DuplicateStateless.cmake | 0 Tests/RunCMake/FileAPI/EmptyClient-check.cmake | 9 ++++ Tests/RunCMake/FileAPI/EmptyClient-check.py | 20 ++++++++ Tests/RunCMake/FileAPI/EmptyClient-prep.cmake | 2 + Tests/RunCMake/FileAPI/EmptyClient.cmake | 0 Tests/RunCMake/FileAPI/MixedStateless-check.cmake | 16 +++++++ Tests/RunCMake/FileAPI/MixedStateless-check.py | 27 +++++++++++ Tests/RunCMake/FileAPI/MixedStateless-prep.cmake | 6 +++ Tests/RunCMake/FileAPI/MixedStateless.cmake | 0 Tests/RunCMake/FileAPI/RunCMakeTest.cmake | 4 ++ 20 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 Tests/RunCMake/FileAPI/ClientStateless-check.cmake create mode 100644 Tests/RunCMake/FileAPI/ClientStateless-check.py create mode 100644 Tests/RunCMake/FileAPI/ClientStateless-prep.cmake create mode 100644 Tests/RunCMake/FileAPI/ClientStateless.cmake create mode 100644 Tests/RunCMake/FileAPI/DuplicateStateless-check.cmake create mode 100644 Tests/RunCMake/FileAPI/DuplicateStateless-check.py create mode 100644 Tests/RunCMake/FileAPI/DuplicateStateless-prep.cmake create mode 100644 Tests/RunCMake/FileAPI/DuplicateStateless.cmake create mode 100644 Tests/RunCMake/FileAPI/EmptyClient-check.cmake create mode 100644 Tests/RunCMake/FileAPI/EmptyClient-check.py create mode 100644 Tests/RunCMake/FileAPI/EmptyClient-prep.cmake create mode 100644 Tests/RunCMake/FileAPI/EmptyClient.cmake create mode 100644 Tests/RunCMake/FileAPI/MixedStateless-check.cmake create mode 100644 Tests/RunCMake/FileAPI/MixedStateless-check.py create mode 100644 Tests/RunCMake/FileAPI/MixedStateless-prep.cmake create mode 100644 Tests/RunCMake/FileAPI/MixedStateless.cmake diff --git a/Help/manual/cmake-file-api.7.rst b/Help/manual/cmake-file-api.7.rst index dd2ef83..51655b2 100644 --- a/Help/manual/cmake-file-api.7.rst +++ b/Help/manual/cmake-file-api.7.rst @@ -31,7 +31,8 @@ It has the following subdirectories: ``query/`` Holds query files written by clients. - These may be `v1 Shared Stateless Query Files`_. + These may be `v1 Shared Stateless Query Files`_ or + `v1 Client Stateless Query Files`_. ``reply/`` Holds reply files written by CMake whenever it runs to generate a build @@ -62,6 +63,27 @@ 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 Client Stateless Query Files +------------------------------- + +Client stateless query files allow clients to create owned requests for +major versions of the `Object Kinds`_ and get all requested versions +recognized by the CMake that runs. + +Clients may create owned requests by creating empty files in +client-specific query subdirectories. The form is:: + + /.cmake/api/v1/query/client-/-v + +where ``client-`` is literal, ```` is a string uniquely +identifying the client, ```` is one of the `Object Kinds`_, +``-v`` is literal, and ```` is the major version number. +Each client must choose a unique ```` identifier via its +own means. + +Files of this form are stateless queries owned by the client ````. +The owning client may remove them at any time. + v1 Reply Index File ------------------- @@ -107,7 +129,14 @@ The reply index file contains a JSON object: "version": { "major": 1, "minor": 0 }, "jsonFile": "" }, "": { "error": "unknown query file" }, - "...": {} + "...": {}, + "client-": { + "-v": { "kind": "", + "version": { "major": 1, "minor": 0 }, + "jsonFile": "" }, + "": { "error": "unknown query file" }, + "...": {} + } } } @@ -162,6 +191,26 @@ The members are: containing a string with an error message indicating that the query file is unknown. + ``client-`` + A member of this form appears for each client-owned directory + holding `v1 Client Stateless Query Files`_. + The value is a JSON object mirroring the content of the + ``query/client-/`` directory. The members are of the form: + + ``-v`` + A member of this form appears for each of the + `v1 Client Stateless Query Files`_ that CMake recognized as a + request for object kind ```` with major version ````. + The value is a `v1 Reply File Reference`_ to the corresponding + reply file for that object kind and version. + + ```` + A member of this form appears for each of the + `v1 Client 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. diff --git a/Source/cmFileAPI.cxx b/Source/cmFileAPI.cxx index 23e0ced..e401b58 100644 --- a/Source/cmFileAPI.cxx +++ b/Source/cmFileAPI.cxx @@ -2,6 +2,7 @@ file Copyright.txt or https://cmake.org/licensing for details. */ #include "cmFileAPI.h" +#include "cmAlgorithms.h" #include "cmCryptoHash.h" #include "cmSystemTools.h" #include "cmTimestamp.h" @@ -42,7 +43,9 @@ void cmFileAPI::ReadQueries() // Read the queries and save for later. for (std::string& query : queries) { - if (!cmFileAPI::ReadQuery(query, this->TopQuery.Known)) { + if (cmHasLiteralPrefix(query, "client-")) { + this->ReadClient(query); + } else if (!cmFileAPI::ReadQuery(query, this->TopQuery.Known)) { this->TopQuery.Unknown.push_back(std::move(query)); } } @@ -176,6 +179,21 @@ bool cmFileAPI::ReadQuery(std::string const& query, return false; } +void cmFileAPI::ReadClient(std::string const& client) +{ + // Load queries for the client. + std::string clientDir = this->APIv1 + "/query/" + client; + std::vector queries = this->LoadDir(clientDir); + + // Read the queries and save for later. + Query& clientQuery = this->ClientQueries[client]; + for (std::string& query : queries) { + if (!this->ReadQuery(query, clientQuery.Known)) { + clientQuery.Unknown.push_back(std::move(query)); + } + } +} + Json::Value cmFileAPI::BuildReplyIndex() { Json::Value index(Json::objectValue); @@ -184,7 +202,12 @@ Json::Value cmFileAPI::BuildReplyIndex() index["cmake"] = this->BuildCMake(); // Reply to all queries that we loaded. - index["reply"] = this->BuildReply(this->TopQuery); + Json::Value& reply = index["reply"] = this->BuildReply(this->TopQuery); + for (auto const& client : this->ClientQueries) { + std::string const& clientName = client.first; + Query const& clientQuery = client.second; + reply[clientName] = this->BuildReply(clientQuery); + } // Move our index of generated objects into its field. Json::Value& objects = index["objects"] = Json::arrayValue; diff --git a/Source/cmFileAPI.h b/Source/cmFileAPI.h index 39b054d..589b837 100644 --- a/Source/cmFileAPI.h +++ b/Source/cmFileAPI.h @@ -77,6 +77,9 @@ private: /** The content of the top-level query directory. */ Query TopQuery; + /** The content of each "client-$client" query directory. */ + std::map ClientQueries; + /** Reply index object generated for object kind/version. This populates the "objects" field of the reply index. */ std::map ReplyIndexObjects; @@ -91,6 +94,7 @@ private: static bool ReadQuery(std::string const& query, std::vector& objects); + void ReadClient(std::string const& client); Json::Value BuildReplyIndex(); Json::Value BuildCMake(); diff --git a/Tests/RunCMake/FileAPI/ClientStateless-check.cmake b/Tests/RunCMake/FileAPI/ClientStateless-check.cmake new file mode 100644 index 0000000..955d9be --- /dev/null +++ b/Tests/RunCMake/FileAPI/ClientStateless-check.cmake @@ -0,0 +1,15 @@ +set(expect + query + query/client-foo + query/client-foo/__test-v1 + query/client-foo/__test-v2 + query/client-foo/__test-v3 + query/client-foo/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(ClientStateless) diff --git a/Tests/RunCMake/FileAPI/ClientStateless-check.py b/Tests/RunCMake/FileAPI/ClientStateless-check.py new file mode 100644 index 0000000..b7da314 --- /dev/null +++ b/Tests/RunCMake/FileAPI/ClientStateless-check.py @@ -0,0 +1,26 @@ +from check_index import * + +def check_reply(r): + assert is_dict(r) + assert sorted(r.keys()) == ["client-foo"] + check_reply_client_foo(r["client-foo"]) + +def check_reply_client_foo(r): + assert is_dict(r) + assert sorted(r.keys()) == ["__test-v1", "__test-v2", "__test-v3", "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["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/ClientStateless-prep.cmake b/Tests/RunCMake/FileAPI/ClientStateless-prep.cmake new file mode 100644 index 0000000..1b8d772 --- /dev/null +++ b/Tests/RunCMake/FileAPI/ClientStateless-prep.cmake @@ -0,0 +1,5 @@ +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/__test-v1" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/__test-v2" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/__test-v3" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/unknown" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "") diff --git a/Tests/RunCMake/FileAPI/ClientStateless.cmake b/Tests/RunCMake/FileAPI/ClientStateless.cmake new file mode 100644 index 0000000..e69de29 diff --git a/Tests/RunCMake/FileAPI/DuplicateStateless-check.cmake b/Tests/RunCMake/FileAPI/DuplicateStateless-check.cmake new file mode 100644 index 0000000..4959c1e --- /dev/null +++ b/Tests/RunCMake/FileAPI/DuplicateStateless-check.cmake @@ -0,0 +1,20 @@ +set(expect + query + query/__test-v1 + query/__test-v2 + query/__test-v3 + query/client-foo + query/client-foo/__test-v1 + query/client-foo/__test-v2 + query/client-foo/__test-v3 + query/client-foo/unknown + 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(DuplicateStateless) diff --git a/Tests/RunCMake/FileAPI/DuplicateStateless-check.py b/Tests/RunCMake/FileAPI/DuplicateStateless-check.py new file mode 100644 index 0000000..3335479 --- /dev/null +++ b/Tests/RunCMake/FileAPI/DuplicateStateless-check.py @@ -0,0 +1,31 @@ +from check_index import * + +def check_reply(r): + assert is_dict(r) + assert sorted(r.keys()) == ["__test-v1", "__test-v2", "__test-v3", "client-foo", "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_reply_client_foo(r["client-foo"]) + check_error(r["query.json"], "unknown query file") + check_error(r["unknown"], "unknown query file") + +def check_reply_client_foo(r): + assert is_dict(r) + assert sorted(r.keys()) == ["__test-v1", "__test-v2", "__test-v3", "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["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/DuplicateStateless-prep.cmake b/Tests/RunCMake/FileAPI/DuplicateStateless-prep.cmake new file mode 100644 index 0000000..51b9852 --- /dev/null +++ b/Tests/RunCMake/FileAPI/DuplicateStateless-prep.cmake @@ -0,0 +1,10 @@ +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/query/client-foo/__test-v1" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/__test-v2" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/__test-v3" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo/unknown" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "") diff --git a/Tests/RunCMake/FileAPI/DuplicateStateless.cmake b/Tests/RunCMake/FileAPI/DuplicateStateless.cmake new file mode 100644 index 0000000..e69de29 diff --git a/Tests/RunCMake/FileAPI/EmptyClient-check.cmake b/Tests/RunCMake/FileAPI/EmptyClient-check.cmake new file mode 100644 index 0000000..4e5745c --- /dev/null +++ b/Tests/RunCMake/FileAPI/EmptyClient-check.cmake @@ -0,0 +1,9 @@ +set(expect + query + query/client-foo + reply + reply/index-[0-9.T-]+.json + ) +check_api("^${expect}$") + +check_python(EmptyClient) diff --git a/Tests/RunCMake/FileAPI/EmptyClient-check.py b/Tests/RunCMake/FileAPI/EmptyClient-check.py new file mode 100644 index 0000000..f887908 --- /dev/null +++ b/Tests/RunCMake/FileAPI/EmptyClient-check.py @@ -0,0 +1,20 @@ +from check_index import * + +def check_reply(r): + assert is_dict(r) + assert sorted(r.keys()) == ["client-foo"] + check_reply_client_foo(r["client-foo"]) + +def check_reply_client_foo(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/EmptyClient-prep.cmake b/Tests/RunCMake/FileAPI/EmptyClient-prep.cmake new file mode 100644 index 0000000..31512fd --- /dev/null +++ b/Tests/RunCMake/FileAPI/EmptyClient-prep.cmake @@ -0,0 +1,2 @@ +file(MAKE_DIRECTORY "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/query/client-foo" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "") diff --git a/Tests/RunCMake/FileAPI/EmptyClient.cmake b/Tests/RunCMake/FileAPI/EmptyClient.cmake new file mode 100644 index 0000000..e69de29 diff --git a/Tests/RunCMake/FileAPI/MixedStateless-check.cmake b/Tests/RunCMake/FileAPI/MixedStateless-check.cmake new file mode 100644 index 0000000..5795614 --- /dev/null +++ b/Tests/RunCMake/FileAPI/MixedStateless-check.cmake @@ -0,0 +1,16 @@ +set(expect + query + query/__test-v1 + query/__test-v3 + query/client-foo + query/client-foo/__test-v2 + query/client-foo/unknown + query/query.json + 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(MixedStateless) diff --git a/Tests/RunCMake/FileAPI/MixedStateless-check.py b/Tests/RunCMake/FileAPI/MixedStateless-check.py new file mode 100644 index 0000000..be019ab --- /dev/null +++ b/Tests/RunCMake/FileAPI/MixedStateless-check.py @@ -0,0 +1,27 @@ +from check_index import * + +def check_reply(r): + assert is_dict(r) + assert sorted(r.keys()) == ["__test-v1", "__test-v3", "client-foo", "query.json"] + check_index__test(r["__test-v1"], 1, 3) + check_error(r["__test-v3"], "unknown query file") + check_reply_client_foo(r["client-foo"]) + check_error(r["query.json"], "unknown query file") + +def check_reply_client_foo(r): + assert is_dict(r) + assert sorted(r.keys()) == ["__test-v2", "unknown"] + check_index__test(r["__test-v2"], 2, 0) + 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/MixedStateless-prep.cmake b/Tests/RunCMake/FileAPI/MixedStateless-prep.cmake new file mode 100644 index 0000000..030baac --- /dev/null +++ b/Tests/RunCMake/FileAPI/MixedStateless-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/client-foo/__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/client-foo/unknown" "") +file(WRITE "${RunCMake_TEST_BINARY_DIR}/.cmake/api/v1/reply/object-to-be-deleted.json" "") diff --git a/Tests/RunCMake/FileAPI/MixedStateless.cmake b/Tests/RunCMake/FileAPI/MixedStateless.cmake new file mode 100644 index 0000000..e69de29 diff --git a/Tests/RunCMake/FileAPI/RunCMakeTest.cmake b/Tests/RunCMake/FileAPI/RunCMakeTest.cmake index bb016ca..a7351b3 100644 --- a/Tests/RunCMake/FileAPI/RunCMakeTest.cmake +++ b/Tests/RunCMake/FileAPI/RunCMakeTest.cmake @@ -36,5 +36,9 @@ endfunction() run_cmake(Nothing) run_cmake(Empty) +run_cmake(EmptyClient) run_cmake(Stale) run_cmake(SharedStateless) +run_cmake(ClientStateless) +run_cmake(MixedStateless) +run_cmake(DuplicateStateless) -- cgit v0.12