diff --git a/CMakeLists.txt b/CMakeLists.txt index c8bd063dd..2ec8b5722 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -702,6 +702,18 @@ endif() # setup some Testing support (a macro defined in this file) CMAKE_SETUP_TESTING() +# Check whether to build server mode or not: +set(CMake_HAVE_SERVER_MODE 0) +if(NOT CMake_TEST_EXTERNAL_CMAKE AND NOT CMAKE_BOOTSTRAP AND CMAKE_USE_LIBUV) + list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_auto_type CMake_HAVE_CXX_AUTO_TYPE) + list(FIND CMAKE_CXX_COMPILE_FEATURES cxx_range_for CMake_HAVE_CXX_RANGE_FOR) + if(CMake_HAVE_CXX_AUTO_TYPE AND CMake_HAVE_CXX_RANGE_FOR) + if(CMake_HAVE_CXX_MAKE_UNIQUE) + set(CMake_HAVE_SERVER_MODE 1) + endif() + endif() +endif() + if(NOT CMake_TEST_EXTERNAL_CMAKE) if(NOT CMake_VERSION_IS_RELEASE) if(CMAKE_C_COMPILER_ID STREQUAL "GNU" AND diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 39773e15b..a2dead644 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -786,6 +786,17 @@ add_executable(cmake cmakemain.cxx cmcmd.cxx cmcmd.h ${MANIFEST_FILE}) list(APPEND _tools cmake) target_link_libraries(cmake CMakeLib) +if(CMake_HAVE_SERVER_MODE) + add_library(CMakeServerLib + cmServer.cxx cmServer.h + cmServerProtocol.cxx cmServerProtocol.h + ) + target_link_libraries(CMakeServerLib CMakeLib) + set_property(SOURCE cmcmd.cxx APPEND PROPERTY COMPILE_DEFINITIONS HAVE_SERVER_MODE=1) + + target_link_libraries(cmake CMakeServerLib) +endif() + # Build CTest executable add_executable(ctest ctest.cxx ${MANIFEST_FILE}) list(APPEND _tools ctest) diff --git a/Source/cmServer.cxx b/Source/cmServer.cxx new file mode 100644 index 000000000..7643b4783 --- /dev/null +++ b/Source/cmServer.cxx @@ -0,0 +1,352 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2015 Stephen Kelly + Copyright 2016 Tobias Hunger + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmServer.h" + +#include "cmServerProtocol.h" +#include "cmVersionMacros.h" +#include "cmake.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_reader.h" +#include "cm_jsoncpp_value.h" +#endif + +const char kTYPE_KEY[] = "type"; +const char kCOOKIE_KEY[] = "cookie"; +const char REPLY_TO_KEY[] = "inReplyTo"; +const char ERROR_MESSAGE_KEY[] = "errorMessage"; + +const char ERROR_TYPE[] = "error"; +const char REPLY_TYPE[] = "reply"; +const char PROGRESS_TYPE[] = "progress"; + +const char START_MAGIC[] = "[== CMake Server ==["; +const char END_MAGIC[] = "]== CMake Server ==]"; + +typedef struct +{ + uv_write_t req; + uv_buf_t buf; +} write_req_t; + +void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) +{ + (void)handle; + *buf = uv_buf_init(static_cast(malloc(suggested_size)), + static_cast(suggested_size)); +} + +void free_write_req(uv_write_t* req) +{ + write_req_t* wr = reinterpret_cast(req); + free(wr->buf.base); + free(wr); +} + +void on_stdout_write(uv_write_t* req, int status) +{ + (void)status; + auto server = reinterpret_cast(req->data); + free_write_req(req); + server->PopOne(); +} + +void write_data(uv_stream_t* dest, std::string content, uv_write_cb cb) +{ + write_req_t* req = static_cast(malloc(sizeof(write_req_t))); + req->req.data = dest->data; + req->buf = uv_buf_init(static_cast(malloc(content.size())), + static_cast(content.size())); + memcpy(req->buf.base, content.c_str(), content.size()); + uv_write(reinterpret_cast(req), static_cast(dest), + &req->buf, 1, cb); +} + +void read_stdin(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) +{ + if (nread > 0) { + auto server = reinterpret_cast(stream->data); + std::string result = std::string(buf->base, buf->base + nread); + server->handleData(result); + } + + if (buf->base) + free(buf->base); +} + +cmServer::cmServer() +{ +} + +cmServer::~cmServer() +{ + if (!this->Protocol) // Daemon was never fully started! + return; + + uv_close(reinterpret_cast(this->InputStream), NULL); + uv_close(reinterpret_cast(this->OutputStream), NULL); + uv_loop_close(this->Loop); + + for (cmServerProtocol* p : this->SupportedProtocols) { + delete p; + } +} + +void cmServer::PopOne() +{ + this->Writing = false; + if (this->Queue.empty()) { + return; + } + + Json::Reader reader; + Json::Value value; + const std::string input = this->Queue.front(); + this->Queue.erase(this->Queue.begin()); + + if (!reader.parse(input, value)) { + this->WriteParseError("Failed to parse JSON input."); + return; + } + + const cmServerRequest request(this, value[kTYPE_KEY].asString(), + value[kCOOKIE_KEY].asString(), value); + + if (request.Type == "") { + cmServerResponse response(request); + response.SetError("No type given in request."); + this->WriteResponse(response); + return; + } + + this->WriteResponse(this->Protocol ? this->Protocol->Process(request) + : this->SetProtocolVersion(request)); +} + +void cmServer::handleData(const std::string& data) +{ + this->DataBuffer += data; + + for (;;) { + auto needle = this->DataBuffer.find('\n'); + + if (needle == std::string::npos) { + return; + } + std::string line = this->DataBuffer.substr(0, needle); + const auto ls = line.size(); + if (ls > 1 && line.at(ls - 1) == '\r') + line.erase(ls - 1, 1); + this->DataBuffer.erase(this->DataBuffer.begin(), + this->DataBuffer.begin() + needle + 1); + if (line == START_MAGIC) { + this->JsonData.clear(); + continue; + } + if (line == END_MAGIC) { + this->Queue.push_back(this->JsonData); + this->JsonData.clear(); + if (!this->Writing) { + this->PopOne(); + } + } else { + this->JsonData += line; + this->JsonData += "\n"; + } + } +} + +void cmServer::RegisterProtocol(cmServerProtocol* protocol) +{ + auto version = protocol->ProtocolVersion(); + assert(version.first >= 0); + assert(version.second >= 0); + auto it = std::find_if(this->SupportedProtocols.begin(), + this->SupportedProtocols.end(), + [version](cmServerProtocol* p) { + return p->ProtocolVersion() == version; + }); + if (it == this->SupportedProtocols.end()) + this->SupportedProtocols.push_back(protocol); +} + +void cmServer::PrintHello() const +{ + Json::Value hello = Json::objectValue; + hello[kTYPE_KEY] = "hello"; + + Json::Value& protocolVersions = hello["supportedProtocolVersions"] = + Json::arrayValue; + + for (auto const& proto : this->SupportedProtocols) { + auto version = proto->ProtocolVersion(); + Json::Value tmp = Json::objectValue; + tmp["major"] = version.first; + tmp["minor"] = version.second; + protocolVersions.append(tmp); + } + + this->WriteJsonObject(hello); +} + +cmServerResponse cmServer::SetProtocolVersion(const cmServerRequest& request) +{ + if (request.Type != "handshake") + return request.ReportError("Waiting for type \"handshake\"."); + + Json::Value requestedProtocolVersion = request.Data["protocolVersion"]; + if (requestedProtocolVersion.isNull()) + return request.ReportError( + "\"protocolVersion\" is required for \"handshake\"."); + + if (!requestedProtocolVersion.isObject()) + return request.ReportError("\"protocolVersion\" must be a JSON object."); + + Json::Value majorValue = requestedProtocolVersion["major"]; + if (!majorValue.isInt()) + return request.ReportError("\"major\" must be set and an integer."); + + Json::Value minorValue = requestedProtocolVersion["minor"]; + if (!minorValue.isNull() && !minorValue.isInt()) + return request.ReportError("\"minor\" must be unset or an integer."); + + const int major = majorValue.asInt(); + const int minor = minorValue.isNull() ? -1 : minorValue.asInt(); + if (major < 0) + return request.ReportError("\"major\" must be >= 0."); + if (!minorValue.isNull() && minor < 0) + return request.ReportError("\"minor\" must be >= 0 when set."); + + this->Protocol = + this->FindMatchingProtocol(this->SupportedProtocols, major, minor); + if (!this->Protocol) { + return request.ReportError("Protocol version not supported."); + } + + std::string errorMessage; + if (!this->Protocol->Activate(request, &errorMessage)) { + this->Protocol = CM_NULLPTR; + return request.ReportError("Failed to activate protocol version: " + + errorMessage); + } + return request.Reply(Json::objectValue); +} + +void cmServer::Serve() +{ + assert(!this->Protocol); + + this->Loop = uv_default_loop(); + + if (uv_guess_handle(1) == UV_TTY) { + uv_tty_init(this->Loop, &this->Input.tty, 0, 1); + uv_tty_set_mode(&this->Input.tty, UV_TTY_MODE_NORMAL); + this->Input.tty.data = this; + InputStream = reinterpret_cast(&this->Input.tty); + + uv_tty_init(this->Loop, &this->Output.tty, 1, 0); + uv_tty_set_mode(&this->Output.tty, UV_TTY_MODE_NORMAL); + this->Output.tty.data = this; + OutputStream = reinterpret_cast(&this->Output.tty); + } else { + uv_pipe_init(this->Loop, &this->Input.pipe, 0); + uv_pipe_open(&this->Input.pipe, 0); + this->Input.pipe.data = this; + InputStream = reinterpret_cast(&this->Input.pipe); + + uv_pipe_init(this->Loop, &this->Output.pipe, 0); + uv_pipe_open(&this->Output.pipe, 1); + this->Output.pipe.data = this; + OutputStream = reinterpret_cast(&this->Output.pipe); + } + + this->PrintHello(); + + uv_read_start(this->InputStream, alloc_buffer, read_stdin); + + uv_run(this->Loop, UV_RUN_DEFAULT); +} + +void cmServer::WriteJsonObject(const Json::Value& jsonValue) const +{ + Json::FastWriter writer; + + std::string result = std::string("\n") + std::string(START_MAGIC) + + std::string("\n") + writer.write(jsonValue) + std::string(END_MAGIC) + + std::string("\n"); + + this->Writing = true; + write_data(this->OutputStream, result, on_stdout_write); +} + +cmServerProtocol* cmServer::FindMatchingProtocol( + const std::vector& protocols, int major, int minor) +{ + cmServerProtocol* bestMatch = nullptr; + for (auto protocol : protocols) { + auto version = protocol->ProtocolVersion(); + if (major != version.first) + continue; + if (minor == version.second) + return protocol; + if (!bestMatch || bestMatch->ProtocolVersion().second < version.second) + bestMatch = protocol; + } + return minor < 0 ? bestMatch : nullptr; +} + +void cmServer::WriteProgress(const cmServerRequest& request, int min, + int current, int max, + const std::string& message) const +{ + assert(min <= current && current <= max); + assert(message.length() != 0); + + Json::Value obj = Json::objectValue; + obj[kTYPE_KEY] = PROGRESS_TYPE; + obj[REPLY_TO_KEY] = request.Type; + obj[kCOOKIE_KEY] = request.Cookie; + obj["progressMessage"] = message; + obj["progressMinimum"] = min; + obj["progressMaximum"] = max; + obj["progressCurrent"] = current; + + this->WriteJsonObject(obj); +} + +void cmServer::WriteParseError(const std::string& message) const +{ + Json::Value obj = Json::objectValue; + obj[kTYPE_KEY] = ERROR_TYPE; + obj[ERROR_MESSAGE_KEY] = message; + obj[REPLY_TO_KEY] = ""; + obj[kCOOKIE_KEY] = ""; + + this->WriteJsonObject(obj); +} + +void cmServer::WriteResponse(const cmServerResponse& response) const +{ + assert(response.IsComplete()); + + Json::Value obj = response.Data(); + obj[kCOOKIE_KEY] = response.Cookie; + obj[kTYPE_KEY] = response.IsError() ? ERROR_TYPE : REPLY_TYPE; + obj[REPLY_TO_KEY] = response.Type; + if (response.IsError()) { + obj[ERROR_MESSAGE_KEY] = response.ErrorMessage(); + } + + this->WriteJsonObject(obj); +} diff --git a/Source/cmServer.h b/Source/cmServer.h new file mode 100644 index 000000000..0ef1e17e5 --- /dev/null +++ b/Source/cmServer.h @@ -0,0 +1,85 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2015 Stephen Kelly + Copyright 2016 Tobias Hunger + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#pragma once + +#include "cmListFileCache.h" +#include "cmState.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_value.h" +#include "cm_uv.h" +#endif + +#include +#include + +class cmServerProtocol; +class cmServerRequest; +class cmServerResponse; + +class cmServer +{ +public: + cmServer(); + ~cmServer(); + + void Serve(); + + // for callbacks: + void PopOne(); + void handleData(std::string const& data); + +private: + void RegisterProtocol(cmServerProtocol* protocol); + + // Handle requests: + cmServerResponse SetProtocolVersion(const cmServerRequest& request); + + void PrintHello() const; + + // Write responses: + void WriteProgress(const cmServerRequest& request, int min, int current, + int max, const std::string& message) const; + void WriteResponse(const cmServerResponse& response) const; + void WriteParseError(const std::string& message) const; + + void WriteJsonObject(Json::Value const& jsonValue) const; + + static cmServerProtocol* FindMatchingProtocol( + const std::vector& protocols, int major, int minor); + + cmServerProtocol* Protocol = nullptr; + std::vector SupportedProtocols; + std::vector Queue; + + std::string DataBuffer; + std::string JsonData; + + uv_loop_t* Loop = nullptr; + + typedef union + { + uv_tty_t tty; + uv_pipe_t pipe; + } InOutUnion; + + InOutUnion Input; + InOutUnion Output; + uv_stream_t* InputStream = nullptr; + uv_stream_t* OutputStream = nullptr; + + mutable bool Writing = false; + + friend class cmServerRequest; +}; diff --git a/Source/cmServerProtocol.cxx b/Source/cmServerProtocol.cxx new file mode 100644 index 000000000..659aa0f00 --- /dev/null +++ b/Source/cmServerProtocol.cxx @@ -0,0 +1,129 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2016 Tobias Hunger + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#include "cmServerProtocol.h" + +#include "cmExternalMakefileProjectGenerator.h" +#include "cmServer.h" +#include "cmake.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_reader.h" +#include "cm_jsoncpp_value.h" +#endif + +namespace { +// Vocabulary: + +const std::string kCOOKIE_KEY = "cookie"; +const std::string kTYPE_KEY = "type"; + +} // namespace + +cmServerRequest::cmServerRequest(cmServer* server, const std::string& t, + const std::string& c, const Json::Value& d) + : Type(t) + , Cookie(c) + , Data(d) + , m_Server(server) +{ +} + +void cmServerRequest::ReportProgress(int min, int current, int max, + const std::string& message) const +{ + this->m_Server->WriteProgress(*this, min, current, max, message); +} + +cmServerResponse cmServerRequest::Reply(const Json::Value& data) const +{ + cmServerResponse response(*this); + response.SetData(data); + return response; +} + +cmServerResponse cmServerRequest::ReportError(const std::string& message) const +{ + cmServerResponse response(*this); + response.SetError(message); + return response; +} + +cmServerResponse::cmServerResponse(const cmServerRequest& request) + : Type(request.Type) + , Cookie(request.Cookie) +{ +} + +void cmServerResponse::SetData(const Json::Value& data) +{ + assert(this->m_Payload == PAYLOAD_UNKNOWN); + if (!data[kCOOKIE_KEY].isNull() || !data[kTYPE_KEY].isNull()) { + this->SetError("Response contains cookie or type field."); + return; + } + this->m_Payload = PAYLOAD_DATA; + this->m_Data = data; +} + +void cmServerResponse::SetError(const std::string& message) +{ + assert(this->m_Payload == PAYLOAD_UNKNOWN); + this->m_Payload = PAYLOAD_ERROR; + this->m_ErrorMessage = message; +} + +bool cmServerResponse::IsComplete() const +{ + return this->m_Payload != PAYLOAD_UNKNOWN; +} + +bool cmServerResponse::IsError() const +{ + assert(this->m_Payload != PAYLOAD_UNKNOWN); + return this->m_Payload == PAYLOAD_ERROR; +} + +std::string cmServerResponse::ErrorMessage() const +{ + if (this->m_Payload == PAYLOAD_ERROR) + return this->m_ErrorMessage; + else + return std::string(); +} + +Json::Value cmServerResponse::Data() const +{ + assert(this->m_Payload != PAYLOAD_UNKNOWN); + return this->m_Data; +} + +bool cmServerProtocol::Activate(const cmServerRequest& request, + std::string* errorMessage) +{ + this->m_CMakeInstance = std::make_unique(); + const bool result = this->DoActivate(request, errorMessage); + if (!result) + this->m_CMakeInstance = CM_NULLPTR; + return result; +} + +cmake* cmServerProtocol::CMakeInstance() const +{ + return this->m_CMakeInstance.get(); +} + +bool cmServerProtocol::DoActivate(const cmServerRequest& /*request*/, + std::string* /*errorMessage*/) +{ + return true; +} diff --git a/Source/cmServerProtocol.h b/Source/cmServerProtocol.h new file mode 100644 index 000000000..e086f7252 --- /dev/null +++ b/Source/cmServerProtocol.h @@ -0,0 +1,97 @@ +/*============================================================================ + CMake - Cross Platform Makefile Generator + Copyright 2016 Tobias Hunger + + Distributed under the OSI-approved BSD License (the "License"); + see accompanying file Copyright.txt for details. + + This software is distributed WITHOUT ANY WARRANTY; without even the + implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the License for more information. +============================================================================*/ + +#pragma once + +#include "cmListFileCache.h" + +#if defined(CMAKE_BUILD_WITH_CMAKE) +#include "cm_jsoncpp_writer.h" +#endif + +#include +#include + +class cmake; +class cmServer; + +class cmServerRequest; + +class cmServerResponse +{ +public: + explicit cmServerResponse(const cmServerRequest& request); + + void SetData(const Json::Value& data); + void SetError(const std::string& message); + + bool IsComplete() const; + bool IsError() const; + std::string ErrorMessage() const; + Json::Value Data() const; + + const std::string Type; + const std::string Cookie; + +private: + enum PayLoad + { + PAYLOAD_UNKNOWN, + PAYLOAD_ERROR, + PAYLOAD_DATA + }; + PayLoad m_Payload = PAYLOAD_UNKNOWN; + std::string m_ErrorMessage; + Json::Value m_Data; +}; + +class cmServerRequest +{ +public: + void ReportProgress(int min, int current, int max, + const std::string& message) const; + + cmServerResponse Reply(const Json::Value& data) const; + cmServerResponse ReportError(const std::string& message) const; + + const std::string Type; + const std::string Cookie; + const Json::Value Data; + +private: + cmServerRequest(cmServer* server, const std::string& t, const std::string& c, + const Json::Value& d); + + cmServer* m_Server; + + friend class cmServer; +}; + +class cmServerProtocol +{ +public: + virtual ~cmServerProtocol() {} + + virtual std::pair ProtocolVersion() const = 0; + virtual const cmServerResponse Process(const cmServerRequest& request) = 0; + + bool Activate(const cmServerRequest& request, std::string* errorMessage); + +protected: + cmake* CMakeInstance() const; + // Implement protocol specific activation tasks here. Called from Activate(). + virtual bool DoActivate(const cmServerRequest& request, + std::string* errorMessage); + +private: + std::unique_ptr m_CMakeInstance; +}; diff --git a/Source/cmcmd.cxx b/Source/cmcmd.cxx index 3b385ab2e..c09ea8bb9 100644 --- a/Source/cmcmd.cxx +++ b/Source/cmcmd.cxx @@ -23,6 +23,10 @@ #include "cm_auto_ptr.hxx" #include "cmake.h" +#if defined(HAVE_SERVER_MODE) && HAVE_SERVER_MODE +#include "cmServer.h" +#endif + #if defined(CMAKE_BUILD_WITH_CMAKE) #include "cmDependsFortran.h" // For -E cmake_copy_f90_mod callback. #endif @@ -91,6 +95,7 @@ void CMakeCommandUsage(const char* program) << " remove_directory dir - remove a directory and its contents\n" << " rename oldname newname - rename a file or directory " "(on one volume)\n" + << " server - start cmake in server mode\n" << " sleep ... - sleep for given number of seconds\n" << " tar [cxt][vf][zjJ] file.tar [file/dir1 file/dir2 ...]\n" << " - create or extract a tar or zip archive\n" @@ -907,6 +912,19 @@ int cmcmd::ExecuteCMakeCommand(std::vector& args) #endif } return 0; + } else if (args[1] == "server") { + if (args.size() > 2) { + cmSystemTools::Error("Too many arguments to start server mode"); + return 1; + } +#if defined(HAVE_SERVER_MODE) && HAVE_SERVER_MODE + cmServer server; + server.Serve(); + return 0; +#else + cmSystemTools::Error("CMake was not built with server mode enabled"); + return 1; +#endif } #if defined(CMAKE_BUILD_WITH_CMAKE) diff --git a/Tests/RunCMake/CommandLine/E_server-arg-result.txt b/Tests/RunCMake/CommandLine/E_server-arg-result.txt new file mode 100644 index 000000000..d00491fd7 --- /dev/null +++ b/Tests/RunCMake/CommandLine/E_server-arg-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/CommandLine/E_server-arg-stderr.txt b/Tests/RunCMake/CommandLine/E_server-arg-stderr.txt new file mode 100644 index 000000000..7877c01c3 --- /dev/null +++ b/Tests/RunCMake/CommandLine/E_server-arg-stderr.txt @@ -0,0 +1 @@ +^CMake Error: Too many arguments to start server mode$ diff --git a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake index 6ae47a8f1..9f76ad910 100644 --- a/Tests/RunCMake/CommandLine/RunCMakeTest.cmake +++ b/Tests/RunCMake/CommandLine/RunCMakeTest.cmake @@ -12,6 +12,7 @@ run_cmake_command(E_capabilities ${CMAKE_COMMAND} -E capabilities) run_cmake_command(E_capabilities-arg ${CMAKE_COMMAND} -E capabilities --extra-arg) run_cmake_command(E_echo_append ${CMAKE_COMMAND} -E echo_append) run_cmake_command(E_rename-no-arg ${CMAKE_COMMAND} -E rename) +run_cmake_command(E_server-arg ${CMAKE_COMMAND} -E server --extra-arg) run_cmake_command(E_touch_nocreate-no-arg ${CMAKE_COMMAND} -E touch_nocreate) run_cmake_command(E_time ${CMAKE_COMMAND} -E time ${CMAKE_COMMAND} -E echo "hello world")