From 9f3a9830162f379d6f856c84668c3664df6d6477 Mon Sep 17 00:00:00 2001 From: Christian Kandeler Date: Mon, 18 Mar 2019 15:02:53 +0100 Subject: Introduce the session command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Offers a JSON-based API for interaction with other tools via stdin/ stdout. This allows for proper qbs support in IDEs that do not use Qt or even C++. Change-Id: Ib051a40b7ebe1c6e0c3147cca9bd96e7daec1fde Reviewed-by: Jörg Bornemann --- doc/appendix/json-api.qdoc | 817 +++++++++++++++++++++ doc/appendix/qbs-porting.qdoc | 2 +- doc/config/style/qt5-sidebar.html | 3 +- doc/doc.qbs | 1 + doc/qbs.qdoc | 7 +- doc/reference/cli/builtin/cli-session.qdoc | 54 ++ doc/reference/cli/cli-options.qdocinc | 1 + src/app/qbs/commandlinefrontend.cpp | 6 + src/app/qbs/parser/commandlineparser.cpp | 1 + src/app/qbs/parser/commandpool.cpp | 3 + src/app/qbs/parser/commandtype.h | 2 +- src/app/qbs/parser/parsercommand.cpp | 23 + src/app/qbs/parser/parsercommand.h | 14 + src/app/qbs/qbs.pro | 8 + src/app/qbs/qbs.qbs | 8 + src/app/qbs/session.cpp | 751 +++++++++++++++++++ src/app/qbs/session.h | 51 ++ src/app/qbs/sessionpacket.cpp | 113 +++ src/app/qbs/sessionpacket.h | 70 ++ src/app/qbs/sessionpacketreader.cpp | 85 +++ src/app/qbs/sessionpacketreader.h | 70 ++ src/app/qbs/stdinreader.cpp | 146 ++++ src/app/qbs/stdinreader.h | 66 ++ src/lib/corelib/api/project.cpp | 22 +- src/lib/corelib/api/project.h | 3 + src/lib/corelib/api/projectdata.cpp | 156 +++- src/lib/corelib/api/projectdata.h | 5 + src/lib/corelib/corelib.qbs | 1 + src/lib/corelib/tools/buildoptions.cpp | 55 ++ src/lib/corelib/tools/buildoptions.h | 3 + src/lib/corelib/tools/cleanoptions.cpp | 12 + src/lib/corelib/tools/cleanoptions.h | 6 + src/lib/corelib/tools/codelocation.cpp | 15 + src/lib/corelib/tools/codelocation.h | 2 + src/lib/corelib/tools/error.cpp | 24 +- src/lib/corelib/tools/error.h | 5 +- src/lib/corelib/tools/installoptions.cpp | 21 +- src/lib/corelib/tools/installoptions.h | 3 + src/lib/corelib/tools/jsonhelper.h | 89 +++ src/lib/corelib/tools/processresult.cpp | 30 + src/lib/corelib/tools/processresult.h | 3 + src/lib/corelib/tools/setupprojectparameters.cpp | 47 +- src/lib/corelib/tools/setupprojectparameters.h | 2 + src/lib/corelib/tools/stringconstants.h | 11 +- src/lib/corelib/tools/tools.pri | 1 + tests/auto/api/tst_api.cpp | 2 +- tests/auto/blackbox/testdata/qbs-session/file1.cpp | 1 + tests/auto/blackbox/testdata/qbs-session/file2.cpp | 1 + tests/auto/blackbox/testdata/qbs-session/lib.cpp | 1 + tests/auto/blackbox/testdata/qbs-session/lib.h | 1 + tests/auto/blackbox/testdata/qbs-session/main.cpp | 4 + .../qbs-session/modules/mymodule/mymodule.qbs | 5 + .../blackbox/testdata/qbs-session/qbs-session.qbs | 25 + tests/auto/blackbox/tst_blackbox.cpp | 644 ++++++++++++++++ tests/auto/blackbox/tst_blackbox.h | 1 + tests/auto/shared.h | 10 +- 56 files changed, 3477 insertions(+), 36 deletions(-) create mode 100644 doc/appendix/json-api.qdoc create mode 100644 doc/reference/cli/builtin/cli-session.qdoc create mode 100644 src/app/qbs/session.cpp create mode 100644 src/app/qbs/session.h create mode 100644 src/app/qbs/sessionpacket.cpp create mode 100644 src/app/qbs/sessionpacket.h create mode 100644 src/app/qbs/sessionpacketreader.cpp create mode 100644 src/app/qbs/sessionpacketreader.h create mode 100644 src/app/qbs/stdinreader.cpp create mode 100644 src/app/qbs/stdinreader.h create mode 100644 src/lib/corelib/tools/jsonhelper.h create mode 100644 tests/auto/blackbox/testdata/qbs-session/file1.cpp create mode 100644 tests/auto/blackbox/testdata/qbs-session/file2.cpp create mode 100644 tests/auto/blackbox/testdata/qbs-session/lib.cpp create mode 100644 tests/auto/blackbox/testdata/qbs-session/lib.h create mode 100644 tests/auto/blackbox/testdata/qbs-session/main.cpp create mode 100644 tests/auto/blackbox/testdata/qbs-session/modules/mymodule/mymodule.qbs create mode 100644 tests/auto/blackbox/testdata/qbs-session/qbs-session.qbs diff --git a/doc/appendix/json-api.qdoc b/doc/appendix/json-api.qdoc new file mode 100644 index 000000000..f8840de37 --- /dev/null +++ b/doc/appendix/json-api.qdoc @@ -0,0 +1,817 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \contentspage index.html + \previouspage porting-to-qbs.html + \nextpage attributions.html + \page json-api.html + + \title Appendix C: The JSON API + + This API is the recommended way to provide \QBS support to an IDE. + It is accessible via the \l{session} command. + + \section1 Packet Format + + All information is exchanged via \e packets, which have the following + structure: + \code + packet = "qbsmsg:" [] + \endcode + First comes a fixed string indentifying the start of a packet, followed + by the size of the actual data in bytes. After that, further meta data + might follow. There is none currently, but future extensions might add + some. A line feed character marks the end of the meta data section + and is followed immediately by the payload, which is a single JSON object + encoded in Base64 format. We call this object a \e message. + + \section1 Messages + + The message data is UTF8-encoded. + + Most messages are either \e requests or \e replies. Requests are messages + sent to \QBS via the session's standard input channel. Replies are messages + sent by \QBS via the session's standard output channel. A reply always + corresponds to one specific request. Every request (with the exception + of the \l{quit-message}{quit request}) expects exactly one reply. A reply implies + that the requested operation has finished. At the very least, it carries + information about whether the operation succeeded, and often contains + additional data specific to the respective request. + + Every message object has a \c type property, which is a string that uniquely + identifies the message type. + + All requests block the session for other requests, including those of the + same type. For instance, if client code wishes to restart building the + project with different parameters, it first has to send a + \l{cancel-message}{cancel} request, wait for the current build job's reply, + and only then can it request another build. The only other message beside + \l{cancel-message}{cancel} that can legally be sent while a request + is currently being handled is the \l{quit-message}{quit} message. + + A reply object may carry an \c error property, indicating that the respective + operation has failed. If this property is not present, the request was successful. + The format of the \c error property is described \l{ErrorInfo}{here}. + + In the remainder of this page, we describe the structure of all messages + that can be sent to and received from \QBS, respectively. The property + tables may have a column titled \e Mandatory, whose values indicate whether + the respective message property is always present. If this column is missing, + all properties of the respective message are mandatory, unless otherwise + noted. + + \section1 The \c hello Message + + This message is sent by \QBS exactly once, right after the session was started. + It is the only message from \QBS that is not a response to a request. + The value of the \c type property is \c "hello", the other properties are + as follows: + \table + \header \li Property \li Type + \row \li api-level \li int + \row \li api-compat-level \li int + \endtable + + The value of \c api-level is increased whenever the API is extended, for instance + by adding new messages or properties. + + The value of \c api-compat-level is increased whenever incompatible changes + are being done to this API. A tool written for API level \c n should refuse + to work with a \QBS version with an API compatibility level greater than \c n, + because it cannot guarantee proper behavior. This value will not change unless + it is absolutely necessary. + + The value of \c api-compat-level is always less than or equal to the + value of \c api-level. + + \section1 Resolving a Project + + To instruct \QBS to load a project from disk, a request of type + \c resolve-project is sent. The other properties are: + \table + \header \li Property \li Type \li Mandatory + \row \li build-root \li \l FilePath \li yes + \row \li configuration-name \li string \li no + \row \li data-mode \li \l DataMode \li no + \row \li dry-run \li bool \li no + \row \li environment \li \l Environment \li no + \row \li error-handling-mode \li string \li no + \row \li fallback-provider-enabled \li bool \li no + \row \li force-probe-execution \li bool \li no + \row \li log-time \li bool \li no + \row \li log-level \li \l LogLevel \li no + \row \li module-properties \li list of strings \li no + \row \li overridden-properties \li object \li no + \row \li project-file-path \li FilePath \li if resolving from scratch + \row \li restore-behavior \li string \li no + \row \li settings-directory \li string \li no + \row \li top-level-profile \li string \li no + \row \li wait-lock-build-graph \li bool \li no + \endtable + + The \c environment property defines the environment to be used for resolving + the project, as well as for all subsequent \QBS operations on this project. + + The \c error-handling-mode specifies how \QBS should deal with issues + in project files, such as assigning to an unknown property. The possible + values are \c "strict" and \c "relaxed". In strict mode, \QBS will + immediately abort and set the reply's \c error property accordingly. + In relaxed mode, \QBS will continue to resolve the project if possible. + A \l{warning-message}{warning message} will be emitted for every error that + was encountered, and the reply's \c error property will \e not be set. + The default error handling mode is \c "strict". + + If the \c log-time property is \c true, then \QBS will emit \l log-data messages + containing information about which part of the operation took how much time. + + The \c module-properties property lists the names of the module properties + which should be contained in the \l{ProductData}{product data} that + will be sent in the reply message. For instance, if the project to be resolved + is C++-based and the client code is interested in which C++ version the + code uses, then \c module-properties would contain \c{"cpp.cxxLanguageVersion"}. + + The \c overridden-properties property is used to override the values of + module, product or project properties. The possible ways to specify + keys are described \l{Overriding Property Values from the Command Line}{here}. + + The \c restore-behavior property specifies if and how to make use of + an existing build graph. The value \c "restore-only" indicates that + a build graph should be loaded from disk and used as-is. In this mode, + it is an error if the build graph file does not exist. + The value \c "resolve-only" indicates that the project should be resolved + from scratch and that an existing build graph should be ignored. In this mode, + it is an error if the \c "project-file-path" property is not present. + The default value is \c "restore-and-track-changes", which uses an + existing build graph if possible and re-resolves the project if no + build graph was found or if the parameters are different from the ones + used when the project was last resolved. + + The \c top-level-profile property specifies which \QBS profile to use + for resolving the project. It corresponds to the \c profile key when + using the \l resolve command. + + All other properties correspond to command line options of the \l resolve + command, and their semantics are described there. + + When the project has been resolved, \QBS will reply with a \c project-resolved + message. The possible properties are: + \table + \header \li Property \li Type \li Mandatory + \row \li error \li \l ErrorInfo \li no + \row \li project-data \li \l TopLevelProjectData \li no + \endtable + + The \c error-info property is present if and only if the operation + failed. The \c project-data property is present if and only if + the conditions stated by the request's \c data-mode property + are fulfilled. + + All other project-related requests need a resolved project to operate on. + If there is none, they will fail. + + There is at most one resolved project per session. If client code wants to + open several projects or one project in different configurations, it needs + to start additional sessions. + + \section1 Building a Project + + To build a project, a request of type \c build-project is sent. The other properties, + none of which are mandatory, are listed below: + \table + \header \li Property \li Type + \row \li active-file-tags \li string list + \row \li changed-files \li \l FilePath list + \row \li check-outputs \li bool + \row \li check-timestamps \li bool + \row \li clean-install-root \li bool + \row \li data-mode \li \l DataMode + \row \li dry-run \li bool + \row \li command-echo-mode \li string + \row \li enforce-project-job-limits \li bool + \row \li files-to-consider \li \l FilePath list + \row \li install \li bool + \row \li job-limits \li list of objects + \row \li keep-going \li bool + \row \li log-level \li \l LogLevel + \row \li log-time \li bool + \row \li max-job-count \li int + \row \li module-properties \li list of strings + \row \li products \li list of strings or \c "all" + \endtable + + All boolean properties except \c install default to \c false. + + The \c active-file-tags and \c files-to-consider are used to limit the + build to certain output tags and/or source files. + For instance, if only C/C++ object files should get built, then + \c active-file-tags would be set to \c "obj". + + The objects in a \c job-limits array consist of a string property \c pool + and an int property \c limit. + + If the \c log-time property is \c true, then \QBS will emit \l log-data messages + containing information about which part of the operation took how much time. + + If \c products is an array, the elements must correspond to the + \c full-display-name property of previously retrieved \l ProductData, + and only these products will get built. + If \c products is the string \c "all", then all products in the project will + get built. + If \c products is not present, then products whose + \l{Product::builtByDefault}{builtByDefault} property is \c false will + be skipped. + + The \c module-properties property has the same meaning as in the + \l{Resolving a Project}{resolve-project} request. + + All other properties correspond to options of the \l build command. + + When the build has finished, \QBS will reply with a \c project-built + message. The possible properties are: + \table + \header \li Property \li Type \li Mandatory + \row \li error \li \l ErrorInfo \li no + \row \li project-data \li \l TopLevelProjectData \li no + \endtable + + The \c error-info property is present if and only if the operation + failed. The \c project-data property is present if and only if + the conditions stated by the request's \c data-mode property + are fulfilled. + + Unless the \c command-echo-mode value is \c "silent", a message of type + \c command-description is emitted for every command to be executed. + It consists of two string properties \c highlight and \c message, + where \c message is the message to present to the user and \c highlight + is a hint on how to display the message. It corresponds to the + \l{Command and JavaScriptCommand}{Command} property of the same name. + + For finished process commands, a message of type \c process-result + might be emitted. The other properties are: + \table + \header \li Property \li Type + \row \li arguments \li list of strings + \row \li error \li string + \row \li executable-file-path \li \l FilePath + \row \li exit-code \li int + \row \li stderr \li list of strings + \row \li stdout \li list of strings + \row \li success \li bool + \row \li working-directory \li \l FilePath + \endtable + + The \c error string is one of \c "failed-to-start", \c "crashed", \c "timed-out", + \c "write-error", \c "read-error" and \c "unknown-error". + Its value is not meaningful unless \c success is \c false. + + The \c stdout and \c stderr properties describe the process's standard + output and standard error output, respectively, split into lines. + + The \c success property is \c true if the process finished without errors + and an exit code of zero. + + The other properties describe the exact command that was executed. + + This message is only emitted if the process failed or it has printed data + to one of the output channels. + + \section1 Cleaning a Project + + To remove a project's build artifacts, a request of type \c clean-project + is sent. The other properties are: + \table + \header \li Property \li Type + \row \li dry-run \li bool + \row \li keep-going \li bool + \row \li log-level \li \l LogLevel + \row \li log-time \li bool + \row \li products \li list of strings + \endtable + + The elements of the \c products array correspond to a \c full-display-name + of a \l ProductData. If this property is present, only the respective + products' artifacts are removed. + + If the \c log-time property is \c true, then \QBS will emit \l log-data messages + containing information about which part of the operation took how much time. + + All other properties correspond to options of the \l clean command. + + None of these properties are mandatory. + + After all artifacts have been removed, \QBS replies with a + \c project-cleaned message. If the operation was successful, this message + has no properties. Otherwise, a property \c error of type \l ErrorInfo + indicates what went wrong. + + \section1 Installing a Project + + Installing is normally part of the \l{Building a Project}{build} + process. To do it in a separate step, the \c install property + is set to \c false when building and a dedicated \c install-project + message is sent. The other properties are: + \table + \header \li Property \li Type + \row \li clean-install-root \li bool + \row \li dry-run \li bool + \row \li install-root \li \l FilePath + \row \li keep-going \li bool + \row \li log-level \li \l LogLevel + \row \li log-time \li bool + \row \li products \li list of strings + \row \li use-sysroot \li bool + \endtable + + The elements of the \c products array correspond to a \c full-display-name + of a \l ProductData. If this property is present, only the respective + products' artifacts are installed. + + If the \c log-time property is \c true, then \QBS will emit \l log-data messages + containing information about which part of the operation took how much time. + + If the \c use-sysroot property is \c true and \c install-root is not present, + then the install root will be \l{qbs::sysroot}{qbs.sysroot}. + + All other properties correspond to options of the \l install command. + + None of these properties are mandatory. + + \target cancel-message + \section1 Canceling an Operation + + Potentially long-running operations can be aborted using the \c cancel-job + request. This message does not have any properties. There is no dedicated + reply message; instead, the usual reply for the request associated with + the currently running operation will be sent, with the \c error property + set to indicate that it was canceled. + + If there is no operation in progress, this request will have no effect. + In particular, if it arrives after the operation that it was supposed to + cancel has already finished (i.e. there is a race condition), the reply + received by client code will not contain a cancellation-related error. + + \section1 Adding and Removing Source Files + + Source files can be added to and removed from \QBS project files with + the \c add-files and \c remove-files messages, respectively. These two + requests have the same set of properties: + \table + \header \li Property \li Type + \row \li files \li \l FilePath list + \row \li group \li string + \row \li product \li string + \endtable + + The \c files property specifies which files should be added or removed. + + The \c product property corresponds to the \c full-display-name of + a \l ProductData and specifies to which product to apply the operation. + + The \c group property corresponds to the \c name of a \l GroupData + and specifies to which group in the product to apply the operation. + + After the operation has finished, \QBS replies with a \c files-added + and \c files-removed message, respectively. Again, the properties are + the same: + \table + \header \li Property \li Type \li Mandatory + \row \li error \li \l ErrorInfo \li no + \row \li failed-files \li \l FilePath list \li no + \row \li project-data \li \l TopLevelProjectData \li no + \endtable + + If the \c error property is present, the operation has at least + partially failed and \c failed-files will list the files + that could not be added or removed. + + If the project data has changed as a result of the operation + (which it should unless the operation failed completely), then + the \c project-data property will contain the updated project data. + + \section1 The \c get-run-environment Message + + This request retrieves the full run environment for a specific + executable product, taking into account the + \l{Module::setupRunEnvironment}{setupRunEnvironment} scripts + of all modules pulled in by the product. The properties are as follows: + \table + \header \li Property \li Type \li Mandatory + \row \li base-environment \li \l Environment \li no + \row \li config \li list of strings \li no + \row \li product \li string \li yes + \endtable + + The \c base-environment property defines the environment into which + the \QBS-specific values should be merged. + + The \c config property corresponds to the \l{--setup-run-env-config} + option of the \l run command. + + The \c product property specifies the product whose environment to + retrieve. The value must correspond to the \c full-display-name + of some \l ProductData in the project. + + \QBS will reply with a \c run-environment message. In case of failure, + it will contain a property \c error of type \l ErrorInfo, otherwise + it will contain a property \c full-environment of type \l Environment. + + \section1 The \c get-generated-files-for-sources Message + + This request allows client code to retrieve information about + which artifacts are generated from a given source file. + Its sole property is a list \c products, whose elements are objects + with the two properties \c full-display-name and \c requests. + The first identifies the product to which the requests apply, and + it must match the property of the same name in a \l ProductData + in the project. + The latter is a list of objects with the following properties: + \table + \header \li Property \li Type \li Mandatory + \row \li source-file \li \l FilePath \li yes + \row \li tags \li list of strings \li no + \row \li recursive \li bool \li no + \endtable + + The \c source-file property specifies a source file in the respective + product. + + The \c tags property constrains the possible file tags of the generated + files to be matched. This is relevant if a source files serves as input + to more than one rule or the rule generates more than one type of output. + + If the \c recursive property is \c true, files indirectly generated + from the source file will also be returned. The default is \c false. + For instance, íf this property is enabled for a C++ source file, + the final link target (e.g. a library or an application executable) + will be returned in addition to the object file. + + \QBS will reply with a \c generated-files-for-sources message, whose + structure is similar to the request. It also has a single object list + property \c products, whose elements consist of a string property + \c full-display-name and an object list property \c results. + The properties of these objects are: + \table + \header \li Property \li Type + \row \li source-file \li \l FilePath + \row \li generated-files \li \l FilePath list + \endtable + + The \c source-file property corresponds to an entry of the same name + in the request, and the \c generated-files are the files which are + generated by \QBS rules that take the source file as an input, + taking the constraints specified in the request into account. + + Source files for which the list would be empty are not listed. + Similarly, products for which the \c results list would be empty + are also omitted. + + \note The results may be incomplete if the project has not been fully built. + + \section1 Closing a Project + + A project is closed with a \c release-project message. This request has + no properties. + + \QBS will reply with a \c project-released message. If no project was open, + the reply will contain an \c error property of type \l ErrorInfo. + + \target quit-message + \section1 Closing the Session + + To close the session, a \c quit message is sent. This request has no + properties. + + \QBS will cancel all currently running operations and then close itself. + No reply will be sent. + + \section1 Progress Messages + + While a request is being handled, \QBS may emit progress information in order + to enable client code to display a progress bar. + + \target task-started + \section2 The \c task-started Message + + This is always the first progress-related message for a specific request. + It appears at most once per request. + It consists of a string property \c description, whose value can be displayed + to users, and an integer property \c max-progress that indicates which + progress value corresponds to 100 per cent. + + \target task-progress + \section2 The \c task-progress Message + + This message updates the progress via an integer property \c progress. + + \target new-max-progress + \section2 The \c new-max-progress Message + + This message is emitted if the original estimated maximum progress has + to be corrected. Its integer property \c max-progress updates the + value from a preceding \l task-started message. + + \section1 Messages for Users + + There are two types of messages that purely contain information to be + presented to users. + + \target log-data + \section2 The \c log-data Message + + This object has a string property \c message, which is the text to be + shown to the user. + + \target warning-message + \section2 The \c warning Message + + This message has a single property \c warning of type \l ErrorInfo. + + \section1 The \c protocol-error Message + + \QBS sends this message as a reply to a request with an unknown \c type. + It contains an \c error property of type \l ErrorInfo. + + \section1 Project Data + + If a request can alter the build graph data, the associated reply may contain + a \c project-data property whose value is of type \l TopLevelProjectData. + + \section2 TopLevelProjectData + + This data type represents the entire project. It has the same properties + as \l PlainProjectData. If it is part of a \c project-resolved message, + these additional properties are also present: + \table + \header \li Property \li Type + \row \li build-directory \li \l FilePath + \row \li build-graph-file-path \li \l FilePath + \row \li build-system-files \li \l FilePath list + \row \li overridden-properties \li object + \row \li profile-data \li object + \endtable + + The value of \c build-directory is the top-level build directory. + + The \c build-graph-file-path value is the path to the build graph file. + + The \c build-system-files value contains all \QBS project files, including + modules and JavaScript helper files. + + The value of \c overridden-properties is the one that was passed in when + the project was last \l{Resolving a Project}{resolved}. + + The \c profile-data property maps the names of the profiles used in the project + to the respective property maps. Unless profile multiplexing is used, this + object will contain exactly one property. + + \section2 PlainProjectData + + This data type describes a \l Project item. The properties are as follows: + \table + \header \li Property \li Type + \row \li is-enabled \li bool + \row \li location \li \l FilePath + \row \li name \li string + \row \li products \li \l ProductData list + \row \li sub-projects \li \l PlainProjectData list + \endtable + + The \c is-enabled property corresponds to the project's + \l{Project::condition}{condition}. + + The \c location property is the exact position in a \QBS project file + where the corresponding \l Project item was defined. + + The \c products and \c sub-projects are what the project has pulled in via + its \l{Project::references}{references} property. + + \section2 ProductData + + This data type describes a \l Product item. The properties are as follows: + \table + \header \li Property \li Type + \row \li build-directory \li \l FilePath + \row \li dependencies \li list of strings + \row \li full-display-name \li string + \row \li generated-artifacts \li \l ArtifactData list + \row \li groups \li \l GroupData list + \row \li is-enabled \li bool + \row \li is-multiplexed \li bool + \row \li is-runnable \li bool + \row \li location \li \l Location + \row \li module-properties \li \l ModulePropertiesData + \row \li multiplex-configuration-id \li string + \row \li name \li string + \row \li properties \li object + \row \li target-executable \li \l FilePath + \row \li target-name \li string + \row \li type \li list of strings + \row \li version \li string + \endtable + + The \c dependencies are the names of products that occur in the (enabled) + \l Depends items of this product. + + The \c generated-artifacts are files that are created by the \l{Rule}{rules} + in this product. + + The \c groups list corresponds to the \l Group items in this product. + In addition, a "pseudo-group" is created for the \l{Product::files}{files} + property of the product itself. Its name is the same as the product's. + + The \c is-enabled property corresponds to the product's + \l{Product::condition}{condition}. A product may also get disabled + if it contains errors and \QBS was was instructed to operate in relaxed mode + when the project was \l{Resolving a Project}{resolved}. + + The \c is-multiplexed property is true if and only if the product is + \l{Multiplexing}{multiplexed} over one ore more properties. + + The \c is-runnable property indicates whether one of the product's + target artifacts is an executable file. + In that case, the file is available via the \c target-executable property. + + The \c location property is the exact position in a \QBS project file + where the corresponding \l Product item was defined. + + The \c module-properties object provides the values of the module properties + that were requested when the project was \l{Resolving a Project}{resolved}. + + The \c name property is the value given in the \l{Product::name}{Product item}, + whereas \c full-display-name is a name that uniquely identifies the + product in the entire project, even in the presence of multiplexing. + In the absence of multiplexing, it is the same as \c name. In either case, + it is suitable for being presented to users. + + See the \l Product item documentation for a description of the other + properties. + + \section2 GroupData + + This data type describes a \l Group item. The properties are: + \table + \header \li Property \li Type + \row \li is-enabled \li bool + \row \li location \li \l Location + \row \li module-properties \li \l ModulePropertiesData + \row \li name \li string + \row \li prefix \li string + \row \li source-artifacts \li \l ArtifactData list + \row \li source-artifacts-from-wildcards \li \l ArtifactData list + \endtable + + The \c is-enabled property corresponds to the groups's + \l{Group::condition}{condition}. However, if the group's product + is disabled, this property will always be \c false. + + The \c location property is the exact position in a \QBS project file + where the corresponding \l Group item occurs. + + The \c module-properties object provides the values of the module properties + that were requested when the project was \l{Resolving a Project}{resolved}. + If no module properties are set on the Group level and the value would therefore + be the same as in the group's product, then this property is omitted. + + The \c source-artifacts list corresponds the the files listed verbatim + in the group's \l{Group::files}{files} property. + + The \c source-artifacts-from-wildcards list represents the the files + expanded from wildcard entries in the group's \l{Group::files}{files} property. + + See the \l Group item documentation for a description of the other + properties. + + \section2 ArtifactData + + This data type represents files that occur in the project, either as sources + or as outputs of a rules. \QBS project files, on the other hand, are not + artifacts. The properties are: + \table + \header \li Property \li Type + \row \li file-path \li \l FilePath + \row \li file-tags \li list of strings + \row \li install-data \li object + \row \li is-executable \li bool + \row \li is-generated \li bool + \row \li is-target \li bool + \row \li module-properties \li \l ModulePropertiesData + \endtable + + The \c install-data property is an object whose \c is-installable property + indicates whether the artifact gets installed. If so, then the \l FilePath + properties \c install-file-path and \c install-root provide further + information. + + The \c is-target property is true if the artifact is a target artifact + of its product, that is, \c is-generated is true and \c file-tags + intersects with the \l{Product::type}{product type}. + + The \c module-properties object provides the values of the module properties + that were requested when the project was \l{Resolving a Project}{resolved}. + This property is only present for generated artifacts. For source artifacts, + the value can be retrieved from their \l{GroupData}{group}. + + The other properties should be self-explanatory. + + \section2 ModulePropertiesData + + This data type maps fully qualified module property names to their + respective values. + + \section1 Other Custom Data Types + + There are a number of custom data types that serve as building blocks in + various messages. They are described below. + + \section2 FilePath + + A \e FilePath is a string that describes a file or directory. FilePaths are + always absolute and use forward slashes for separators, regardless of + the host operating system. + + \section2 Location + + A \e Location is an object representing a file path and possibly also a position + within the respective file. It consists of the following properties: + \table + \header \li Property \li Type \li Mandatory + \row \li file-path \li \l FilePath \li yes + \row \li line \li int \li no + \row \li column \li int \li no + \endtable + + \section2 ErrorInfo + + An \e ErrorInfo is an object representing error information. Its sole property + \c items is an array of objects with the following structure: + \table + \header \li Property \li Type \li Mandatory + \row \li \c message \li string \li yes + \row \li location \li \l Location \li no + \endtable + + \section2 DataMode + + This is the type of the \c data-mode property in a + \l{Resolving a project}{resolve} or \l{Building a project}{build} + request. It is used to indicate under which circumstances + the reply message should include the project data. The possible + values have string type and are as follows: + \list + \li \c "never": Do not attach project data to the reply. + \li \c "always": Do attach project data to the reply. + \li \c "only-if-changed": Attach project data to the reply only + if it is different from the current + project data. + \endlist + The default value is \c "never". + + \section2 LogLevel + + This is the type of the \c log-level property that can occur + in various requests. It is used to indicate whether the client would like + to receive \l log-data and/or \l{warning-message}{warning} messages. + The possible values have string type and are as follows: + \list + \li "error": Do not log anything. + \li "warning": \QBS may emit \l{warning-message}{warnings}, but no + \l log-data messages. + \li "info": In addition to warnings, \QBS may emit informational + \l log-data messages. + \li "debug": \QBS may emit debug output. No messages will be generated; + instead, the standard error output channel will be used. + \endlist + The default value is \c "info". + + \section2 Environment + + This data type describes a set of environment variables. It is an object + whose keys are names of environment variables and whose values are + the values of these environment variables. + +*/ diff --git a/doc/appendix/qbs-porting.qdoc b/doc/appendix/qbs-porting.qdoc index e17d7b0ac..ba697d7be 100644 --- a/doc/appendix/qbs-porting.qdoc +++ b/doc/appendix/qbs-porting.qdoc @@ -29,7 +29,7 @@ \contentspage index.html \previouspage building-qbs.html \page porting-to-qbs.html - \nextpage attributions.html + \nextpage json-api.html \title Appendix B: Migrating from Other Build Systems diff --git a/doc/config/style/qt5-sidebar.html b/doc/config/style/qt5-sidebar.html index 99690bd01..3df5d1bbf 100644 --- a/doc/config/style/qt5-sidebar.html +++ b/doc/config/style/qt5-sidebar.html @@ -12,6 +12,7 @@
  • Reference
  • Appendix A: Building Qbs
  • Appendix B: Migrating from Other Build Systems
  • -
  • Appendix C: Code Attributions
  • +
  • Appendix C: The JSON API
  • +
  • Appendix D: Code Attributions
  • diff --git a/doc/doc.qbs b/doc/doc.qbs index 194d8e157..dd8377c6c 100644 --- a/doc/doc.qbs +++ b/doc/doc.qbs @@ -26,6 +26,7 @@ Project { "qbs-online.qdocconf", "config/*.qdocconf", "config/style/qt5-sidebar.html", + "appendix/**/*", "reference/**/*", "templates/**/*", "images/**", diff --git a/doc/qbs.qdoc b/doc/qbs.qdoc index 897f1a41b..08a901365 100644 --- a/doc/qbs.qdoc +++ b/doc/qbs.qdoc @@ -87,7 +87,8 @@ \li \l{Appendix A: Building Qbs} \li \l{Appendix B: Migrating from Other Build Systems} - \li \l{Appendix C: Licenses and Code Attributions} + \li \l{Appendix C: The JSON API} + \li \l{Appendix D: Licenses and Code Attributions} \endlist */ @@ -1966,10 +1967,10 @@ /*! \contentspage index.html - \previouspage porting-to-qbs.html + \previouspage json-api.html \page attributions.html - \title Appendix C: Licenses and Code Attributions + \title Appendix D: Licenses and Code Attributions \section1 Licenses diff --git a/doc/reference/cli/builtin/cli-session.qdoc b/doc/reference/cli/builtin/cli-session.qdoc new file mode 100644 index 000000000..62999a82e --- /dev/null +++ b/doc/reference/cli/builtin/cli-session.qdoc @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \contentspage cli.html + \page cli-session.html + \ingroup cli + + \title session + \brief Starts a session for interacting with an IDE + + \section1 Synopsis + + \code + qbs session + \endcode + + \section1 Description + + Starts a session, communicating via standard input and standard output. + + In this mode, \QBS takes commands from standard input and sends replies + to standard output, using a \l{Appendix C: The JSON API}{JSON-based API}. + + This is the recommended \QBS interface for IDEs. It can be used to retrieve + information about a project and interact with it in various ways, such + as building it, collecting the list of executables, adding new source files + and so on. + +*/ diff --git a/doc/reference/cli/cli-options.qdocinc b/doc/reference/cli/cli-options.qdocinc index f609b1964..b02ce68ea 100644 --- a/doc/reference/cli/cli-options.qdocinc +++ b/doc/reference/cli/cli-options.qdocinc @@ -442,6 +442,7 @@ //! [setup-run-env-config] + \target --setup-run-env-config \section2 \c --setup-run-env-config A comma-separated list of strings. They will show up in the \c config diff --git a/src/app/qbs/commandlinefrontend.cpp b/src/app/qbs/commandlinefrontend.cpp index 95c3c10bc..8be06f3af 100644 --- a/src/app/qbs/commandlinefrontend.cpp +++ b/src/app/qbs/commandlinefrontend.cpp @@ -40,6 +40,7 @@ #include "application.h" #include "consoleprogressobserver.h" +#include "session.h" #include "status.h" #include "parser/commandlineoption.h" #include "../shared/logging/consolelogger.h" @@ -129,6 +130,10 @@ void CommandLineFrontend::start() throw ErrorInfo(error); } break; + case SessionCommandType: { + startSession(); + return; + } default: break; } @@ -418,6 +423,7 @@ void CommandLineFrontend::handleProjectsResolved() break; case HelpCommandType: case VersionCommandType: + case SessionCommandType: Q_ASSERT_X(false, Q_FUNC_INFO, "Impossible."); } } diff --git a/src/app/qbs/parser/commandlineparser.cpp b/src/app/qbs/parser/commandlineparser.cpp index 3c25c51e2..052f6b92f 100644 --- a/src/app/qbs/parser/commandlineparser.cpp +++ b/src/app/qbs/parser/commandlineparser.cpp @@ -386,6 +386,7 @@ QList CommandLineParser::CommandLineParserPrivate::allCommands() cons commandPool.getCommand(DumpNodesTreeCommandType), commandPool.getCommand(ListProductsCommandType), commandPool.getCommand(VersionCommandType), + commandPool.getCommand(SessionCommandType), commandPool.getCommand(HelpCommandType)}; } diff --git a/src/app/qbs/parser/commandpool.cpp b/src/app/qbs/parser/commandpool.cpp index a49608c56..1362a563c 100644 --- a/src/app/qbs/parser/commandpool.cpp +++ b/src/app/qbs/parser/commandpool.cpp @@ -95,6 +95,9 @@ qbs::Command *CommandPool::getCommand(CommandType type) const case VersionCommandType: command = new VersionCommand(m_optionPool); break; + case SessionCommandType: + command = new SessionCommand(m_optionPool); + break; } } return command; diff --git a/src/app/qbs/parser/commandtype.h b/src/app/qbs/parser/commandtype.h index a8c618933..7d70356e7 100644 --- a/src/app/qbs/parser/commandtype.h +++ b/src/app/qbs/parser/commandtype.h @@ -45,7 +45,7 @@ enum CommandType { ResolveCommandType, BuildCommandType, CleanCommandType, RunCommandType, ShellCommandType, StatusCommandType, UpdateTimestampsCommandType, DumpNodesTreeCommandType, InstallCommandType, HelpCommandType, GenerateCommandType, ListProductsCommandType, - VersionCommandType, + VersionCommandType, SessionCommandType, }; } // namespace qbs diff --git a/src/app/qbs/parser/parsercommand.cpp b/src/app/qbs/parser/parsercommand.cpp index 9485b0878..c7185a725 100644 --- a/src/app/qbs/parser/parsercommand.cpp +++ b/src/app/qbs/parser/parsercommand.cpp @@ -593,4 +593,27 @@ void VersionCommand::parseNext(QStringList &input) throwError(Tr::tr("This command takes no arguments.")); } +QString SessionCommand::shortDescription() const +{ + return Tr::tr("Starts a session for an IDE."); +} + +QString SessionCommand::longDescription() const +{ + QString description = Tr::tr("qbs %1\n").arg(representation()); + return description += Tr::tr("Communicates on stdin and stdout via a JSON-based API.\n" + "Intended for use with other tools, such as IDEs.\n"); +} + +QString SessionCommand::representation() const +{ + return QLatin1String("session"); +} + +void SessionCommand::parseNext(QStringList &input) +{ + QBS_CHECK(!input.empty()); + throwError(Tr::tr("This command takes no arguments.")); +} + } // namespace qbs diff --git a/src/app/qbs/parser/parsercommand.h b/src/app/qbs/parser/parsercommand.h index 649563ba1..8998d38e6 100644 --- a/src/app/qbs/parser/parsercommand.h +++ b/src/app/qbs/parser/parsercommand.h @@ -261,6 +261,20 @@ private: void parseNext(QStringList &input) override; }; +class SessionCommand : public Command +{ +public: + SessionCommand(CommandLineOptionPool &optionPool) : Command(optionPool) {} + +private: + CommandType type() const override { return SessionCommandType; } + QString shortDescription() const override; + QString longDescription() const override; + QString representation() const override; + QList supportedOptions() const override { return {}; } + void parseNext(QStringList &input) override; +}; + } // namespace qbs #endif // QBS_PARSER_COMMAND_H diff --git a/src/app/qbs/qbs.pro b/src/app/qbs/qbs.pro index ac9d6f0ca..e9f0061c6 100644 --- a/src/app/qbs/qbs.pro +++ b/src/app/qbs/qbs.pro @@ -6,6 +6,10 @@ TARGET = qbs SOURCES += main.cpp \ ctrlchandler.cpp \ application.cpp \ + session.cpp \ + sessionpacket.cpp \ + sessionpacketreader.cpp \ + stdinreader.cpp \ status.cpp \ consoleprogressobserver.cpp \ commandlinefrontend.cpp \ @@ -14,6 +18,10 @@ SOURCES += main.cpp \ HEADERS += \ ctrlchandler.h \ application.h \ + session.h \ + sessionpacket.h \ + sessionpacketreader.h \ + stdinreader.h \ status.h \ consoleprogressobserver.h \ commandlinefrontend.h \ diff --git a/src/app/qbs/qbs.qbs b/src/app/qbs/qbs.qbs index f22fe5de5..91357445e 100644 --- a/src/app/qbs/qbs.qbs +++ b/src/app/qbs/qbs.qbs @@ -23,8 +23,16 @@ QbsApp { "main.cpp", "qbstool.cpp", "qbstool.h", + "session.cpp", + "session.h", + "sessionpacket.cpp", + "sessionpacket.h", + "sessionpacketreader.cpp", + "sessionpacketreader.h", "status.cpp", "status.h", + "stdinreader.cpp", + "stdinreader.h", ] Group { name: "parser" diff --git a/src/app/qbs/session.cpp b/src/app/qbs/session.cpp new file mode 100644 index 000000000..0b93aff49 --- /dev/null +++ b/src/app/qbs/session.cpp @@ -0,0 +1,751 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "session.h" + +#include "sessionpacket.h" +#include "sessionpacketreader.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace qbs { +namespace Internal { + +class SessionLogSink : public QObject, public ILogSink +{ + Q_OBJECT +signals: + void newMessage(const QJsonObject &msg); + +private: + void doPrintMessage(LoggerLevel, const QString &message, const QString &) override + { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("log-data")); + msg.insert(StringConstants::messageKey(), message); + emit newMessage(msg); + } + + void doPrintWarning(const ErrorInfo &warning) override + { + QJsonObject msg; + static const QString warningString(QLatin1String("warning")); + msg.insert(StringConstants::type(), warningString); + msg.insert(warningString, warning.toJson()); + emit newMessage(msg); + } +}; + +class Session : public QObject +{ + Q_OBJECT +public: + Session(); + +private: + enum class ProjectDataMode { Never, Always, OnlyIfChanged }; + ProjectDataMode dataModeFromRequest(const QJsonObject &request); + QStringList modulePropertiesFromRequest(const QJsonObject &request); + void insertProjectDataIfNecessary( + QJsonObject &reply, + ProjectDataMode dataMode, + const ProjectData &oldProjectData, + bool includeTopLevelData + ); + void setLogLevelFromRequest(const QJsonObject &request); + bool checkNormalRequestPrerequisites(const char *replyType); + + void sendPacket(const QJsonObject &message); + void setupProject(const QJsonObject &request); + void buildProject(const QJsonObject &request); + void cleanProject(const QJsonObject &request); + void installProject(const QJsonObject &request); + void addFiles(const QJsonObject &request); + void removeFiles(const QJsonObject &request); + void getRunEnvironment(const QJsonObject &request); + void getGeneratedFilesForSources(const QJsonObject &request); + void releaseProject(); + void cancelCurrentJob(); + void quitSession(); + + void sendErrorReply(const char *replyType, const QString &message); + void sendErrorReply(const char *replyType, const ErrorInfo &error); + void insertErrorInfoIfNecessary(QJsonObject &reply, const ErrorInfo &error); + void connectProgressSignals(AbstractJob *job); + QList getProductsByName(const QStringList &productNames) const; + ProductData getProductByName(const QString &productName) const; + + struct ProductSelection { + ProductSelection(Project::ProductSelection s) : selection(s) {} + ProductSelection(const QList &p) : products(p) {} + + Project::ProductSelection selection = Project::ProductSelectionDefaultOnly; + QList products; + }; + ProductSelection getProductSelection(const QJsonObject &request); + + struct FileUpdateData { + QJsonObject createErrorReply(const char *type, const QString &mainMessage) const; + + ProductData product; + GroupData group; + QStringList filePaths; + ErrorInfo error; + }; + FileUpdateData prepareFileUpdate(const QJsonObject &request); + + SessionPacketReader m_packetReader; + Project m_project; + ProjectData m_projectData; + SessionLogSink m_logSink; + std::unique_ptr m_settings; + QJsonObject m_resolveRequest; + QStringList m_moduleProperties; + AbstractJob *m_currentJob = nullptr; +}; + +void startSession() +{ + const auto session = new Session; + QObject::connect(qApp, &QCoreApplication::aboutToQuit, session, [session] { delete session; }); +} + +Session::Session() +{ + sendPacket(SessionPacket::helloMessage()); + connect(&m_logSink, &SessionLogSink::newMessage, this, &Session::sendPacket); + connect(&m_packetReader, &SessionPacketReader::errorOccurred, + this, [](const QString &msg) { + std::cerr << qPrintable(tr("Error: %1").arg(msg)); + qApp->exit(EXIT_FAILURE); + }); + connect(&m_packetReader, &SessionPacketReader::packetReceived, this, [this](const QJsonObject &packet) { + // qDebug() << "got packet:" << packet; // Uncomment for debugging. + const QString type = packet.value(StringConstants::type()).toString(); + if (type == QLatin1String("resolve-project")) + setupProject(packet); + else if (type == QLatin1String("build-project")) + buildProject(packet); + else if (type == QLatin1String("clean-project")) + cleanProject(packet); + else if (type == QLatin1String("install-project")) + installProject(packet); + else if (type == QLatin1String("add-files")) + addFiles(packet); + else if (type == QLatin1String("remove-files")) + removeFiles(packet); + else if (type == QLatin1String("get-run-environment")) + getRunEnvironment(packet); + else if (type == QLatin1String("get-generated-files-for-sources")) + getGeneratedFilesForSources(packet); + else if (type == QLatin1String("release-project")) + releaseProject(); + else if (type == QLatin1String("quit")) + quitSession(); + else if (type == QLatin1String("cancel-job")) + cancelCurrentJob(); + else + sendErrorReply("protocol-error", tr("Unknown request type '%1'.").arg(type)); + }); + m_packetReader.start(); +} + +Session::ProjectDataMode Session::dataModeFromRequest(const QJsonObject &request) +{ + const QString modeString = request.value(QLatin1String("data-mode")).toString(); + if (modeString == QLatin1String("only-if-changed")) + return ProjectDataMode::OnlyIfChanged; + if (modeString == QLatin1String("always")) + return ProjectDataMode::Always; + return ProjectDataMode::Never; +} + +void Session::sendPacket(const QJsonObject &message) +{ + std::cout << SessionPacket::createPacket(message).constData() << std::flush; +} + +void Session::setupProject(const QJsonObject &request) +{ + if (m_currentJob) { + if (qobject_cast(m_currentJob) + && m_currentJob->state() == AbstractJob::StateCanceling) { + m_resolveRequest = std::move(request); + return; + } + sendErrorReply("project-resolved", + tr("Cannot start resolving while another job is still running.")); + return; + } + m_moduleProperties = modulePropertiesFromRequest(request); + auto params = SetupProjectParameters::fromJson(request); + const ProjectDataMode dataMode = dataModeFromRequest(request); + m_settings.reset(new Settings(params.settingsDirectory())); + const Preferences prefs(m_settings.get()); + const QString appDir = QDir::cleanPath(QCoreApplication::applicationDirPath()); + params.setSearchPaths(prefs.searchPaths(appDir + QLatin1String( + "/" QBS_RELATIVE_SEARCH_PATH))); + params.setPluginPaths(prefs.pluginPaths(appDir + QLatin1String( + "/" QBS_RELATIVE_PLUGINS_PATH))); + params.setLibexecPath(appDir + QLatin1String("/" QBS_RELATIVE_LIBEXEC_PATH)); + params.setOverrideBuildGraphData(true); + setLogLevelFromRequest(request); + SetupProjectJob * const setupJob = m_project.setupProject(params, &m_logSink, this); + m_currentJob = setupJob; + connectProgressSignals(setupJob); + connect(setupJob, &AbstractJob::finished, this, + [this, setupJob, dataMode](bool success) { + if (!m_resolveRequest.isEmpty()) { // Canceled job was superseded. + const QJsonObject newRequest = std::move(m_resolveRequest); + m_resolveRequest = QJsonObject(); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + setupProject(newRequest); + return; + } + const ProjectData oldProjectData = m_projectData; + m_project = setupJob->project(); + m_projectData = m_project.projectData(); + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("project-resolved")); + if (success) + insertProjectDataIfNecessary(reply, dataMode, oldProjectData, true); + else + insertErrorInfoIfNecessary(reply, setupJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::buildProject(const QJsonObject &request) +{ + if (!checkNormalRequestPrerequisites("build-done")) + return; + const ProductSelection productSelection = getProductSelection(request); + setLogLevelFromRequest(request); + auto options = BuildOptions::fromJson(request); + options.setSettingsDirectory(m_settings->baseDirectory()); + BuildJob * const buildJob = productSelection.products.empty() + ? m_project.buildAllProducts(options, productSelection.selection, this) + : m_project.buildSomeProducts(productSelection.products, options, this); + m_currentJob = buildJob; + m_moduleProperties = modulePropertiesFromRequest(request); + const ProjectDataMode dataMode = dataModeFromRequest(request); + connectProgressSignals(buildJob); + connect(buildJob, &BuildJob::reportCommandDescription, this, + [this](const QString &highlight, const QString &message) { + QJsonObject descData; + descData.insert(StringConstants::type(), QLatin1String("command-description")); + descData.insert(QLatin1String("highlight"), highlight); + descData.insert(StringConstants::messageKey(), message); + sendPacket(descData); + }); + connect(buildJob, &BuildJob::reportProcessResult, this, [this](const ProcessResult &result) { + if (result.success() && result.stdOut().isEmpty() && result.stdErr().isEmpty()) + return; + QJsonObject resultData = result.toJson(); + resultData.insert(StringConstants::type(), QLatin1String("process-result")); + sendPacket(resultData); + }); + connect(buildJob, &BuildJob::finished, this, + [this, dataMode](bool success) { + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("project-built")); + const ProjectData oldProjectData = m_projectData; + m_projectData = m_project.projectData(); + if (success) + insertProjectDataIfNecessary(reply, dataMode, oldProjectData, false); + else + insertErrorInfoIfNecessary(reply, m_currentJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::cleanProject(const QJsonObject &request) +{ + if (!checkNormalRequestPrerequisites("project-cleaned")) + return; + setLogLevelFromRequest(request); + const ProductSelection productSelection = getProductSelection(request); + const auto options = CleanOptions::fromJson(request); + m_currentJob = productSelection.products.empty() + ? m_project.cleanAllProducts(options, this) + : m_project.cleanSomeProducts(productSelection.products, options, this); + connectProgressSignals(m_currentJob); + connect(m_currentJob, &AbstractJob::finished, this, [this](bool success) { + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("project-cleaned")); + if (!success) + insertErrorInfoIfNecessary(reply, m_currentJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::installProject(const QJsonObject &request) +{ + if (!checkNormalRequestPrerequisites("install-done")) + return; + setLogLevelFromRequest(request); + const ProductSelection productSelection = getProductSelection(request); + const auto options = InstallOptions::fromJson(request); + m_currentJob = productSelection.products.empty() + ? m_project.installAllProducts(options, productSelection.selection, this) + : m_project.installSomeProducts(productSelection.products, options, this); + connectProgressSignals(m_currentJob); + connect(m_currentJob, &AbstractJob::finished, this, [this](bool success) { + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("install-done")); + if (!success) + insertErrorInfoIfNecessary(reply, m_currentJob->error()); + sendPacket(reply); + m_currentJob->deleteLater(); + m_currentJob = nullptr; + }); +} + +void Session::addFiles(const QJsonObject &request) +{ + const FileUpdateData data = prepareFileUpdate(request); + if (data.error.hasError()) { + sendPacket(data.createErrorReply("files-added", tr("Failed to add files to project: %1") + .arg(data.error.toString()))); + return; + } + ErrorInfo error; + QStringList failedFiles; +#ifdef QBS_ENABLE_PROJECT_FILE_UPDATES + for (const QString &filePath : data.filePaths) { + const ErrorInfo e = m_project.addFiles(data.product, data.group, {filePath}); + if (e.hasError()) { + for (const ErrorItem &ei : e.items()) + error.append(ei); + failedFiles.push_back(filePath); + } + } +#endif + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("files-added")); + insertErrorInfoIfNecessary(reply, error); + + if (failedFiles.size() != data.filePaths.size()) { + // Note that Project::addFiles() directly changes the existing project data object, so + // there's no need to retrieve it from m_project. + insertProjectDataIfNecessary(reply, ProjectDataMode::Always, {}, false); + } + + if (!failedFiles.isEmpty()) + reply.insert(QLatin1String("failed-files"), QJsonArray::fromStringList(failedFiles)); + sendPacket(reply); +} + +void Session::removeFiles(const QJsonObject &request) +{ + const FileUpdateData data = prepareFileUpdate(request); + if (data.error.hasError()) { + sendPacket(data.createErrorReply("files-removed", + tr("Failed to remove files from project: %1") + .arg(data.error.toString()))); + return; + } + ErrorInfo error; + QStringList failedFiles; +#ifdef QBS_ENABLE_PROJECT_FILE_UPDATES + for (const QString &filePath : data.filePaths) { + const ErrorInfo e = m_project.removeFiles(data.product, data.group, {filePath}); + if (e.hasError()) { + for (const ErrorItem &ei : e.items()) + error.append(ei); + failedFiles.push_back(filePath); + } + } +#endif + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String("files-removed")); + insertErrorInfoIfNecessary(reply, error); + if (failedFiles.size() != data.filePaths.size()) + insertProjectDataIfNecessary(reply, ProjectDataMode::Always, {}, false); + if (!failedFiles.isEmpty()) + reply.insert(QLatin1String("failed-files"), QJsonArray::fromStringList(failedFiles)); + sendPacket(reply); +} + +void Session::connectProgressSignals(AbstractJob *job) +{ + static QString maxProgressString(QLatin1String("max-progress")); + connect(job, &AbstractJob::taskStarted, this, + [this](const QString &description, int maxProgress) { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("task-started")); + msg.insert(StringConstants::descriptionProperty(), description); + msg.insert(maxProgressString, maxProgress); + sendPacket(msg); + }); + connect(job, &AbstractJob::totalEffortChanged, this, [this](int maxProgress) { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("new-max-progress")); + msg.insert(maxProgressString, maxProgress); + sendPacket(msg); + }); + connect(job, &AbstractJob::taskProgress, this, [this](int progress) { + QJsonObject msg; + msg.insert(StringConstants::type(), QLatin1String("task-progress")); + msg.insert(QLatin1String("progress"), progress); + sendPacket(msg); + }); +} + +static QList getProductsByNameForProject(const ProjectData &project, + QStringList &productNames) +{ + QList products; + if (productNames.empty()) + return products; + for (const ProductData &p : project.products()) { + for (auto it = productNames.begin(); it != productNames.end(); ++it) { + if (*it == p.fullDisplayName()) { + products << p; + productNames.erase(it); + if (productNames.empty()) + return products; + break; + } + } + } + for (const ProjectData &p : project.subProjects()) { + products << getProductsByNameForProject(p, productNames); + if (productNames.empty()) + break; + } + return products; +} + +QList Session::getProductsByName(const QStringList &productNames) const +{ + QStringList remainingNames = productNames; + return getProductsByNameForProject(m_projectData, remainingNames); +} + +ProductData Session::getProductByName(const QString &productName) const +{ + const QList products = getProductsByName({productName}); + return products.empty() ? ProductData() : products.first(); +} + +void Session::getRunEnvironment(const QJsonObject &request) +{ + const char * const replyType = "run-environment"; + if (!checkNormalRequestPrerequisites(replyType)) + return; + const QString productName = request.value(QLatin1String("product")).toString(); + const ProductData product = getProductByName(productName); + if (!product.isValid()) { + sendErrorReply(replyType, tr("No such product '%1'.").arg(productName)); + return; + } + const auto inEnv = fromJson( + request.value(QLatin1String("base-environment"))); + const QStringList config = fromJson(request.value(QLatin1String("config"))); + const RunEnvironment runEnv = m_project.getRunEnvironment(product, InstallOptions(), inEnv, + config, m_settings.get()); + ErrorInfo error; + const QProcessEnvironment outEnv = runEnv.runEnvironment(&error); + if (error.hasError()) { + sendErrorReply(replyType, error); + return; + } + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + QJsonObject outEnvObj; + const QStringList keys = outEnv.keys(); + for (const QString &key : keys) + outEnvObj.insert(key, outEnv.value(key)); + reply.insert(QLatin1String("full-environment"), outEnvObj); + sendPacket(reply); +} + +void Session::getGeneratedFilesForSources(const QJsonObject &request) +{ + const char * const replyType = "generated-files-for-sources"; + if (!checkNormalRequestPrerequisites(replyType)) + return; + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + const QJsonArray specs = request.value(StringConstants::productsKey()).toArray(); + QJsonArray resultProducts; + for (const QJsonValue &p : specs) { + const QJsonObject productObject = p.toObject(); + const ProductData product = getProductByName( + productObject.value(StringConstants::fullDisplayNameKey()).toString()); + if (!product.isValid()) + continue; + QJsonObject resultProduct; + resultProduct.insert(StringConstants::fullDisplayNameKey(), product.fullDisplayName()); + QJsonArray results; + const QJsonArray requests = productObject.value(QLatin1String("requests")).toArray(); + for (const QJsonValue &r : requests) { + const QJsonObject request = r.toObject(); + const QString filePath = request.value(QLatin1String("source-file")).toString(); + const QStringList tags = fromJson(request.value(QLatin1String("tags"))); + const bool recursive = request.value(QLatin1String("recursive")).toBool(); + const QStringList generatedFiles = m_project.generatedFiles(product, filePath, + recursive, tags); + if (!generatedFiles.isEmpty()) { + QJsonObject result; + result.insert(QLatin1String("source-file"), filePath); + result.insert(QLatin1String("generated-files"), + QJsonArray::fromStringList(generatedFiles)); + results << result; + } + } + if (!results.isEmpty()) { + resultProduct.insert(QLatin1String("results"), results); + resultProducts << resultProduct; + } + } + reply.insert(StringConstants::productsKey(), resultProducts); + sendPacket(reply); +} + +void Session::releaseProject() +{ + const char * const replyType = "project-released"; + if (!m_project.isValid()) { + sendErrorReply(replyType, tr("No open project.")); + return; + } + if (m_currentJob) { + m_currentJob->disconnect(this); + m_currentJob->cancel(); + m_currentJob = nullptr; + } + m_project = Project(); + m_projectData = ProjectData(); + m_resolveRequest = QJsonObject(); + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + sendPacket(reply); +} + +void Session::cancelCurrentJob() +{ + if (m_currentJob) { + if (!m_resolveRequest.isEmpty()) + m_resolveRequest = QJsonObject(); + m_currentJob->cancel(); + } +} + +Session::ProductSelection Session::getProductSelection(const QJsonObject &request) +{ + const QJsonValue productSelection = request.value(StringConstants::productsKey()); + if (productSelection.isArray()) + return ProductSelection(getProductsByName(fromJson(productSelection))); + return ProductSelection(productSelection.toString() == QLatin1String("all") + ? Project::ProductSelectionWithNonDefault + : Project::ProductSelectionDefaultOnly); +} + +Session::FileUpdateData Session::prepareFileUpdate(const QJsonObject &request) +{ + FileUpdateData data; + const QString productName = request.value(QLatin1String("product")).toString(); + data.product = getProductByName(productName); + if (data.product.isValid()) { + const QString groupName = request.value(QLatin1String("group")).toString(); + for (const GroupData &g : data.product.groups()) { + if (g.name() == groupName) { + data.group = g; + break; + } + } + if (!data.group.isValid()) + data.error = tr("Group '%1' not found in product '%2'.").arg(groupName, productName); + } else { + data.error = tr("Product '%1' not found in project.").arg(productName); + } + const QJsonArray filesArray = request.value(QLatin1String("files")).toArray(); + for (const QJsonValue &v : filesArray) + data.filePaths << v.toString(); + if (m_currentJob) + data.error = tr("Cannot update the list of source files while a job is running."); + if (!m_project.isValid()) + data.error = tr("No valid project. You need to resolve first."); +#ifndef QBS_ENABLE_PROJECT_FILE_UPDATES + data.error = ErrorInfo(tr("Project file updates are not enabled in this build of qbs.")); +#endif + return data; +} + +void Session::insertProjectDataIfNecessary(QJsonObject &reply, ProjectDataMode dataMode, + const ProjectData &oldProjectData, bool includeTopLevelData) +{ + const bool sendProjectData = dataMode == ProjectDataMode::Always + || (dataMode == ProjectDataMode::OnlyIfChanged && m_projectData != oldProjectData); + if (!sendProjectData) + return; + QJsonObject projectData = m_projectData.toJson(m_moduleProperties); + if (includeTopLevelData) { + QJsonArray buildSystemFiles; + for (const QString &f : m_project.buildSystemFiles()) + buildSystemFiles.push_back(f); + projectData.insert(StringConstants::buildDirectoryKey(), m_projectData.buildDirectory()); + projectData.insert(QLatin1String("build-system-files"), buildSystemFiles); + const Project::BuildGraphInfo bgInfo = m_project.getBuildGraphInfo(); + projectData.insert(QLatin1String("build-graph-file-path"), bgInfo.bgFilePath); + projectData.insert(QLatin1String("profile-data"), + QJsonObject::fromVariantMap(bgInfo.profileData)); + projectData.insert(QLatin1String("overridden-properties"), + QJsonObject::fromVariantMap(bgInfo.overriddenProperties)); + } + reply.insert(QLatin1String("project-data"), projectData); +} + +void Session::setLogLevelFromRequest(const QJsonObject &request) +{ + const QString logLevelString = request.value(QLatin1String("log-level")).toString(); + if (logLevelString.isEmpty()) + return; + for (const LoggerLevel l : {LoggerError, LoggerWarning, LoggerInfo, LoggerDebug, LoggerTrace}) { + if (logLevelString == logLevelName(l)) { + m_logSink.setLogLevel(l); + return; + } + } +} + +bool Session::checkNormalRequestPrerequisites(const char *replyType) +{ + if (m_currentJob) { + sendErrorReply(replyType, tr("Another job is still running.")); + return false; + } + if (!m_project.isValid()) { + sendErrorReply(replyType, tr("No valid project. You need to resolve first.")); + return false; + } + return true; +} + +QStringList Session::modulePropertiesFromRequest(const QJsonObject &request) +{ + return fromJson(request.value(StringConstants::modulePropertiesKey())); +} + +void Session::sendErrorReply(const char *replyType, const ErrorInfo &error) +{ + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(replyType)); + insertErrorInfoIfNecessary(reply, error); + sendPacket(reply); +} + +void Session::sendErrorReply(const char *replyType, const QString &message) +{ + sendErrorReply(replyType, ErrorInfo(message)); +} + +void Session::insertErrorInfoIfNecessary(QJsonObject &reply, const ErrorInfo &error) +{ + if (error.hasError()) + reply.insert(QLatin1String("error"), error.toJson()); +} + +void Session::quitSession() +{ + m_logSink.disconnect(this); + m_packetReader.disconnect(this); + if (m_currentJob) { + m_currentJob->disconnect(this); + connect(m_currentJob, &AbstractJob::finished, qApp, QCoreApplication::quit); + m_currentJob->cancel(); + } else { + qApp->quit(); + } +} + +QJsonObject Session::FileUpdateData::createErrorReply(const char *type, + const QString &mainMessage) const +{ + QBS_ASSERT(error.hasError(), return QJsonObject()); + ErrorInfo error(mainMessage); + for (const ErrorItem &ei : error.items()) + error.append(ei); + QJsonObject reply; + reply.insert(StringConstants::type(), QLatin1String(type)); + reply.insert(QLatin1String("error"), error.toJson()); + reply.insert(QLatin1String("failed-files"), QJsonArray::fromStringList(filePaths)); + return reply; +} + +} // namespace Internal +} // namespace qbs + +#include diff --git a/src/app/qbs/session.h b/src/app/qbs/session.h new file mode 100644 index 000000000..ebbc93b1f --- /dev/null +++ b/src/app/qbs/session.h @@ -0,0 +1,51 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QBS_SESSION_H +#define QBS_SESSION_H + +namespace qbs { +namespace Internal { + +void startSession(); + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/app/qbs/sessionpacket.cpp b/src/app/qbs/sessionpacket.cpp new file mode 100644 index 000000000..ce9fdaf76 --- /dev/null +++ b/src/app/qbs/sessionpacket.cpp @@ -0,0 +1,113 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "sessionpacket.h" + +#include +#include +#include + +#include +#include +#include +#include + +namespace qbs { +namespace Internal { + +const QByteArray packetStart = "qbsmsg:"; + +SessionPacket::Status SessionPacket::parseInput(QByteArray &input) +{ + //qDebug() << m_expectedPayloadLength << m_payload << input; + if (m_expectedPayloadLength == -1) { + const int packetStartOffset = input.indexOf(packetStart); + if (packetStartOffset == -1) + return Status::Incomplete; + const int numberOffset = packetStartOffset + packetStart.length(); + const int newLineOffset = input.indexOf('\n', numberOffset); + if (newLineOffset == -1) + return Status::Incomplete; + const QByteArray sizeString = input.mid(numberOffset, newLineOffset - numberOffset); + bool isNumber; + const int payloadLen = sizeString.toInt(&isNumber); + if (!isNumber || payloadLen < 0) + return Status::Invalid; + m_expectedPayloadLength = payloadLen; + input.remove(0, newLineOffset + 1); + } + const int bytesToAdd = m_expectedPayloadLength - m_payload.length(); + QBS_ASSERT(bytesToAdd >= 0, return Status::Invalid); + m_payload += input.left(bytesToAdd); + input.remove(0, bytesToAdd); + return isComplete() ? Status::Complete : Status::Incomplete; +} + +QJsonObject SessionPacket::retrievePacket() +{ + QBS_ASSERT(isComplete(), return QJsonObject()); + const auto packet = QJsonDocument::fromJson(QByteArray::fromBase64(m_payload)).object(); + m_payload.clear(); + m_expectedPayloadLength = -1; + return packet; +} + +QByteArray SessionPacket::createPacket(const QJsonObject &packet) +{ + const QByteArray jsonData = QJsonDocument(packet).toJson(QJsonDocument::Compact).toBase64(); + return QByteArray(packetStart).append(QByteArray::number(jsonData.length())).append('\n') + .append(jsonData); +} + +QJsonObject SessionPacket::helloMessage() +{ + return QJsonObject{ + {StringConstants::type(), QLatin1String("hello")}, + {QLatin1String("api-level"), 1}, + {QLatin1String("api-compat-level"), 1} + }; +} + +bool SessionPacket::isComplete() const +{ + return m_payload.length() == m_expectedPayloadLength; +} + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/sessionpacket.h b/src/app/qbs/sessionpacket.h new file mode 100644 index 000000000..d919ff340 --- /dev/null +++ b/src/app/qbs/sessionpacket.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QBS_SESSIONPACKET_H +#define QBS_SESSIONPACKET_H + +#include +#include + +namespace qbs { +namespace Internal { + +class SessionPacket +{ +public: + enum class Status { Incomplete, Complete, Invalid }; + Status parseInput(QByteArray &input); + + QJsonObject retrievePacket(); + + static QByteArray createPacket(const QJsonObject &packet); + static QJsonObject helloMessage(); + +private: + bool isComplete() const; + + QByteArray m_payload; + int m_expectedPayloadLength = -1; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/app/qbs/sessionpacketreader.cpp b/src/app/qbs/sessionpacketreader.cpp new file mode 100644 index 000000000..fe4b73f69 --- /dev/null +++ b/src/app/qbs/sessionpacketreader.cpp @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "sessionpacketreader.h" + +#include "sessionpacket.h" +#include "stdinreader.h" + +namespace qbs { +namespace Internal { + +class SessionPacketReader::Private +{ +public: + QByteArray incomingData; + SessionPacket currentPacket; +}; + +SessionPacketReader::SessionPacketReader(QObject *parent) : QObject(parent), d(new Private) { } + +SessionPacketReader::~SessionPacketReader() +{ + delete d; +} + +void SessionPacketReader::start() +{ + StdinReader * const stdinReader = StdinReader::create(this); + connect(stdinReader, &StdinReader::errorOccurred, this, &SessionPacketReader::errorOccurred); + connect(stdinReader, &StdinReader::dataAvailable, this, [this](const QByteArray &data) { + d->incomingData += data; + while (!d->incomingData.isEmpty()) { + switch (d->currentPacket.parseInput(d->incomingData)) { + case SessionPacket::Status::Invalid: + emit errorOccurred(tr("Received invalid input.")); + return; + case SessionPacket::Status::Complete: + emit packetReceived(d->currentPacket.retrievePacket()); + break; + case SessionPacket::Status::Incomplete: + return; + } + } + }); + stdinReader->start(); +} + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/sessionpacketreader.h b/src/app/qbs/sessionpacketreader.h new file mode 100644 index 000000000..87d70cf39 --- /dev/null +++ b/src/app/qbs/sessionpacketreader.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QBS_SESSIONPACKETREADER_H +#define QBS_SESSIONPACKETREADER_H + +#include +#include + +namespace qbs { +namespace Internal { + +class SessionPacketReader : public QObject +{ + Q_OBJECT +public: + explicit SessionPacketReader(QObject *parent = nullptr); + ~SessionPacketReader(); + + void start(); + +signals: + void packetReceived(const QJsonObject &packet); + void errorOccurred(const QString &msg); + +private: + class Private; + Private * const d; +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/app/qbs/stdinreader.cpp b/src/app/qbs/stdinreader.cpp new file mode 100644 index 000000000..4f784505d --- /dev/null +++ b/src/app/qbs/stdinreader.cpp @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "stdinreader.h" + +#include + +#include +#include + +#include +#include + +#ifdef Q_OS_WIN32 +#include +#include +#else +#include +#endif + +namespace qbs { +namespace Internal { + +class UnixStdinReader : public StdinReader +{ +public: + UnixStdinReader(QObject *parent) : StdinReader(parent), m_notifier(0, QSocketNotifier::Read) {} + +private: + void start() override + { + if (!m_stdIn.open(stdin, QIODevice::ReadOnly)) { + emit errorOccurred(tr("Cannot read from standard input.")); + return; + } + const auto emitError = [this] { + emit errorOccurred(tr("Failed to make standard input non-blocking: %1") + .arg(QLatin1String(std::strerror(errno)))); + }; +#ifdef Q_OS_UNIX + const int flags = fcntl(0, F_GETFL, 0); + if (flags == -1) { + emitError(); + return; + } + if (fcntl(0, F_SETFL, flags | O_NONBLOCK)) { + emitError(); + return; + } +#endif + connect(&m_notifier, &QSocketNotifier::activated, this, [this] { + emit dataAvailable(m_stdIn.readAll()); + }); + } + + QFile m_stdIn; + QSocketNotifier m_notifier; +}; + +class WindowsStdinReader : public StdinReader +{ +public: + WindowsStdinReader(QObject *parent) : StdinReader(parent) {} + +private: + void start() override + { +#ifdef Q_OS_WIN32 + m_stdinHandle = GetStdHandle(STD_INPUT_HANDLE); + if (!m_stdinHandle) { + emit errorOccurred(tr("Failed to create handle for standard input.")); + return; + } + + // A timer seems slightly less awful than to block in a thread + // (how would we abort that one?), but ideally we'd like + // to have a signal-based approach like in the Unix variant. + const auto timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, [this] { + char buf[1024]; + DWORD bytesAvail; + PeekNamedPipe(m_stdinHandle, nullptr, 0, nullptr, &bytesAvail, nullptr); + while (bytesAvail > 0) { + DWORD bytesRead; + ReadFile(m_stdinHandle, buf, std::min(bytesAvail, sizeof buf), &bytesRead, + nullptr); + emit dataAvailable(QByteArray(buf, bytesRead)); + bytesAvail -= bytesRead; + } + }); + timer->start(10); +#endif + } + +#ifdef Q_OS_WIN32 + HANDLE m_stdinHandle; +#endif +}; + +StdinReader *StdinReader::create(QObject *parent) +{ + if (HostOsInfo::isWindowsHost()) + return new WindowsStdinReader(parent); + return new UnixStdinReader(parent); +} + +StdinReader::StdinReader(QObject *parent) : QObject(parent) { } + +} // namespace Internal +} // namespace qbs diff --git a/src/app/qbs/stdinreader.h b/src/app/qbs/stdinreader.h new file mode 100644 index 000000000..b3737e5ae --- /dev/null +++ b/src/app/qbs/stdinreader.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QBS_STDINREADER_H +#define QBS_STDINREADER_H + +#include + +namespace qbs { +namespace Internal { + +class StdinReader : public QObject +{ + Q_OBJECT +public: + static StdinReader *create(QObject *parent); + virtual void start() = 0; + +signals: + void errorOccurred(const QString &error); + void dataAvailable(const QByteArray &data); + +protected: + explicit StdinReader(QObject *parent); +}; + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/lib/corelib/api/project.cpp b/src/lib/corelib/api/project.cpp index 3ffd6b2e9..d0fe7296e 100644 --- a/src/lib/corelib/api/project.cpp +++ b/src/lib/corelib/api/project.cpp @@ -726,7 +726,7 @@ void ProjectPrivate::updateExternalCodeLocations(const ProjectData &project, void ProjectPrivate::prepareChangeToProject() { if (internalProject->locked) - throw ErrorInfo(Tr::tr("A job is currently in process.")); + throw ErrorInfo(Tr::tr("A job is currently in progress.")); if (!m_projectData.isValid()) retrieveProjectData(m_projectData, internalProject); } @@ -766,7 +766,7 @@ RuleCommandList ProjectPrivate::ruleCommands(const ProductData &product, const QString &inputFilePath, const QString &outputFileTag) { if (internalProject->locked) - throw ErrorInfo(Tr::tr("A job is currently in process.")); + throw ErrorInfo(Tr::tr("A job is currently in progress.")); const ResolvedProductConstPtr resolvedProduct = internalProduct(product); if (!resolvedProduct) throw ErrorInfo(Tr::tr("No such product '%1'.").arg(product.name())); @@ -896,7 +896,7 @@ void ProjectPrivate::retrieveProjectData(ProjectData &projectData, } for (const ResolvedProductPtr &resolvedDependentProduct : qAsConst(resolvedProduct->dependencies)) { - product.d->dependencies << resolvedDependentProduct->name; + product.d->dependencies << resolvedDependentProduct->name; // FIXME: Shouldn't this be a unique name? } std::sort(product.d->type.begin(), product.d->type.end()); std::sort(product.d->groups.begin(), product.d->groups.end()); @@ -1252,6 +1252,22 @@ Project::BuildGraphInfo Project::getBuildGraphInfo(const QString &bgFilePath, return info; } +Project::BuildGraphInfo Project::getBuildGraphInfo() const +{ + QBS_ASSERT(isValid(), return {}); + BuildGraphInfo info; + try { + if (d->internalProject->locked) + throw ErrorInfo(Tr::tr("A job is currently in progress.")); + info.bgFilePath = d->internalProject->buildGraphFilePath(); + info.overriddenProperties = d->internalProject->overriddenValues; + info.profileData = d->internalProject->profileConfigs; + } catch (const ErrorInfo &e) { + info.error = e; + } + return info; +} + #ifdef QBS_ENABLE_PROJECT_FILE_UPDATES /*! * \brief Adds a new empty group to the given product. diff --git a/src/lib/corelib/api/project.h b/src/lib/corelib/api/project.h index 05f08deee..9000d6548 100644 --- a/src/lib/corelib/api/project.h +++ b/src/lib/corelib/api/project.h @@ -155,6 +155,9 @@ public: static BuildGraphInfo getBuildGraphInfo(const QString &bgFilePath, const QStringList &requestedProperties); + // Use with loaded project. Does not set requestedProperties. + BuildGraphInfo getBuildGraphInfo() const; + #ifdef QBS_ENABLE_PROJECT_FILE_UPDATES ErrorInfo addGroup(const ProductData &product, const QString &groupName); diff --git a/src/lib/corelib/api/projectdata.cpp b/src/lib/corelib/api/projectdata.cpp index 56700b8be..7c64bf6ff 100644 --- a/src/lib/corelib/api/projectdata.cpp +++ b/src/lib/corelib/api/projectdata.cpp @@ -45,21 +45,57 @@ #include #include #include +#include #include #include #include +#include +#include #include namespace qbs { +using namespace Internal; + +template static QJsonArray toJsonArray(const QList &list, + const QStringList &moduleProperties) +{ + QJsonArray jsonArray; + std::transform(list.begin(), list.end(), std::back_inserter(jsonArray), + [&moduleProperties](const T &v) { return v.toJson(moduleProperties);}); + return jsonArray; +} + +static QVariant getModuleProperty(const PropertyMap &properties, const QString &fullPropertyName) +{ + const int lastDotIndex = fullPropertyName.lastIndexOf(QLatin1Char('.')); + if (lastDotIndex == -1) + return QVariant(); + return properties.getModuleProperty(fullPropertyName.left(lastDotIndex), + fullPropertyName.mid(lastDotIndex + 1)); +} + +static void addModuleProperties(QJsonObject &obj, const PropertyMap &properties, + const QStringList &propertyNames) +{ + QJsonObject propertyValues; + for (const QString &prop : propertyNames) { + const QVariant v = getModuleProperty(properties, prop); + if (v.isValid()) + propertyValues.insert(prop, QJsonValue::fromVariant(v)); + } + if (!propertyValues.isEmpty()) + obj.insert(StringConstants::modulePropertiesKey(), propertyValues); +} + /*! * \class GroupData * \brief The \c GroupData class corresponds to the Group item in a qbs source file. */ -GroupData::GroupData() : d(new Internal::GroupDataPrivate) +GroupData::GroupData() : d(new GroupDataPrivate) { } @@ -81,6 +117,22 @@ bool GroupData::isValid() const return d->isValid; } +QJsonObject GroupData::toJson(const QStringList &moduleProperties) const +{ + QJsonObject obj; + if (isValid()) { + obj.insert(StringConstants::locationKey(), location().toJson()); + obj.insert(StringConstants::nameProperty(), name()); + obj.insert(StringConstants::prefixProperty(), prefix()); + obj.insert(StringConstants::isEnabledKey(), isEnabled()); + obj.insert(QStringLiteral("source-artifacts"), toJsonArray(sourceArtifacts(), {})); + obj.insert(QStringLiteral("source-artifacts-from-wildcards"), + toJsonArray(sourceArtifactsFromWildcards(), {})); + addModuleProperties(obj, properties(), moduleProperties); + } + return obj; +} + /*! * \brief The location at which the group is defined in the respective source file. */ @@ -204,7 +256,7 @@ bool operator<(const GroupData &lhs, const GroupData &rhs) * or it gets generated during the build process. */ -ArtifactData::ArtifactData() : d(new Internal::ArtifactDataPrivate) +ArtifactData::ArtifactData() : d(new ArtifactDataPrivate) { } @@ -226,6 +278,21 @@ bool ArtifactData::isValid() const return d->isValid; } +QJsonObject ArtifactData::toJson(const QStringList &moduleProperties) const +{ + QJsonObject obj; + if (isValid()) { + obj.insert(StringConstants::filePathKey(), filePath()); + obj.insert(QStringLiteral("file-tags"), QJsonArray::fromStringList(fileTags())); + obj.insert(QStringLiteral("is-generated"), isGenerated()); + obj.insert(QStringLiteral("is-executable"), isExecutable()); + obj.insert(QStringLiteral("is-target"), isTargetArtifact()); + obj.insert(QStringLiteral("install-data"), installData().toJson()); + addModuleProperties(obj, properties(), moduleProperties); + } + return obj; +} + /*! * \brief The full path of this file. */ @@ -256,8 +323,8 @@ bool ArtifactData::isExecutable() const { const bool isBundle = d->properties.getModuleProperty( QStringLiteral("bundle"), QStringLiteral("isBundle")).toBool(); - return Internal::isRunnableArtifact( - Internal::FileTags::fromStringList(d->fileTags), isBundle); + return isRunnableArtifact( + FileTags::fromStringList(d->fileTags), isBundle); } /*! @@ -309,7 +376,7 @@ bool operator<(const ArtifactData &ta1, const ArtifactData &ta2) * \brief The \c InstallData class provides the installation-related data of an artifact. */ -InstallData::InstallData() : d(new Internal::InstallDataPrivate) +InstallData::InstallData() : d(new InstallDataPrivate) { } @@ -331,6 +398,19 @@ bool InstallData::isValid() const return d->isValid; } +QJsonObject InstallData::toJson() const +{ + QJsonObject obj; + if (isValid()) { + obj.insert(QStringLiteral("is-installable"), isInstallable()); + if (isInstallable()) { + obj.insert(QStringLiteral("install-file-path"), installFilePath()); + obj.insert(QStringLiteral("install-root"), installRoot()); + } + } + return obj; +} + /*! \brief Returns true if and only if \c{qbs.install} is \c true for the artifact. */ @@ -348,7 +428,7 @@ bool InstallData::isInstallable() const QString InstallData::installDir() const { QBS_ASSERT(isValid(), return {}); - return Internal::FileInfo::path(installFilePath()); + return FileInfo::path(installFilePath()); } /*! @@ -392,7 +472,7 @@ QString InstallData::localInstallFilePath() const * \brief The \c ProductData class corresponds to the Product item in a qbs source file. */ -ProductData::ProductData() : d(new Internal::ProductDataPrivate) +ProductData::ProductData() : d(new ProductDataPrivate) { } @@ -414,6 +494,39 @@ bool ProductData::isValid() const return d->isValid; } +QJsonObject ProductData::toJson(const QStringList &propertyNames) const +{ + QJsonObject obj; + if (!isValid()) + return obj; + obj.insert(StringConstants::typeProperty(), QJsonArray::fromStringList(type())); + obj.insert(StringConstants::dependenciesProperty(), + QJsonArray::fromStringList(dependencies())); + obj.insert(StringConstants::nameProperty(), name()); + obj.insert(StringConstants::fullDisplayNameKey(), fullDisplayName()); + obj.insert(QStringLiteral("target-name"), targetName()); + obj.insert(StringConstants::versionProperty(), version()); + obj.insert(QStringLiteral("multiplex-configuration-id"), multiplexConfigurationId()); + obj.insert(StringConstants::locationKey(), location().toJson()); + obj.insert(StringConstants::buildDirectoryKey(), buildDirectory()); + obj.insert(QStringLiteral("generated-artifacts"), toJsonArray(generatedArtifacts(), + propertyNames)); + obj.insert(QStringLiteral("target-executable"), targetExecutable()); + QJsonArray groupArray; + for (const GroupData &g : groups()) { + const QStringList groupPropNames = g.properties() == moduleProperties() + ? QStringList() : propertyNames; + groupArray << g.toJson(groupPropNames); + } + obj.insert(QStringLiteral("groups"), groupArray); + obj.insert(QStringLiteral("properties"), QJsonObject::fromVariantMap(properties())); + obj.insert(StringConstants::isEnabledKey(), isEnabled()); + obj.insert(QStringLiteral("is-runnable"), isRunnable()); + obj.insert(QStringLiteral("is-multiplexed"), isMultiplexed()); + addModuleProperties(obj, moduleProperties(), propertyNames); + return obj; +} + /*! * \brief The product type, which is the list of file tags matching the product's target artifacts. */ @@ -445,7 +558,7 @@ QString ProductData::name() const */ QString ProductData::fullDisplayName() const { - return Internal::ResolvedProduct::fullDisplayName(name(), multiplexConfigurationId()); + return ResolvedProduct::fullDisplayName(name(), multiplexConfigurationId()); } /*! @@ -470,8 +583,8 @@ QString ProductData::version() const QString ProductData::profile() const { return d->moduleProperties.getModuleProperty( - Internal::StringConstants::qbsModule(), - Internal::StringConstants::profileProperty()).toString(); + StringConstants::qbsModule(), + StringConstants::profileProperty()).toString(); } QString ProductData::multiplexConfigurationId() const @@ -661,7 +774,7 @@ bool operator<(const ProductData &lhs, const ProductData &rhs) * \brief The products in this project. */ -ProjectData::ProjectData() : d(new Internal::ProjectDataPrivate) +ProjectData::ProjectData() : d(new ProjectDataPrivate) { } @@ -683,6 +796,19 @@ bool ProjectData::isValid() const return d->isValid; } +QJsonObject ProjectData::toJson(const QStringList &moduleProperties) const +{ + QJsonObject obj; + if (!isValid()) + return obj; + obj.insert(StringConstants::nameProperty(), name()); + obj.insert(StringConstants::locationKey(), location().toJson()); + obj.insert(StringConstants::isEnabledKey(), isEnabled()); + obj.insert(StringConstants::productsKey(), toJsonArray(products(), moduleProperties)); + obj.insert(QStringLiteral("sub-projects"), toJsonArray(subProjects(), moduleProperties)); + return obj; +} + /*! * \brief The name of this project. */ @@ -788,14 +914,14 @@ bool operator<(const ProjectData &lhs, const ProjectData &rhs) */ PropertyMap::PropertyMap() - : d(std::make_unique()) + : d(std::make_unique()) { - static Internal::PropertyMapPtr defaultInternalMap = Internal::PropertyMapInternal::create(); + static PropertyMapPtr defaultInternalMap = PropertyMapInternal::create(); d->m_map = defaultInternalMap; } PropertyMap::PropertyMap(const PropertyMap &other) - : d(std::make_unique(*other.d)) + : d(std::make_unique(*other.d)) { } @@ -806,7 +932,7 @@ PropertyMap::~PropertyMap() = default; PropertyMap &PropertyMap::operator =(const PropertyMap &other) { if (this != &other) - d = std::make_unique(*other.d); + d = std::make_unique(*other.d); return *this; } diff --git a/src/lib/corelib/api/projectdata.h b/src/lib/corelib/api/projectdata.h index 3bd1c4540..a285f8570 100644 --- a/src/lib/corelib/api/projectdata.h +++ b/src/lib/corelib/api/projectdata.h @@ -110,6 +110,7 @@ public: ~ArtifactData(); bool isValid() const; + QJsonObject toJson(const QStringList &moduleProperties = {}) const; QString filePath() const; QStringList fileTags() const; @@ -135,6 +136,7 @@ public: ~InstallData(); bool isValid() const; + QJsonObject toJson() const; bool isInstallable() const; QString installDir() const; @@ -162,6 +164,7 @@ public: ~GroupData(); bool isValid() const; + QJsonObject toJson(const QStringList &moduleProperties = {}) const; CodeLocation location() const; QString name() const; @@ -193,6 +196,7 @@ public: ~ProductData(); bool isValid() const; + QJsonObject toJson(const QStringList &propertyNames = {}) const; QStringList type() const; QStringList dependencies() const; @@ -235,6 +239,7 @@ public: ~ProjectData(); bool isValid() const; + QJsonObject toJson(const QStringList &moduleProperties = {}) const; QString name() const; CodeLocation location() const; diff --git a/src/lib/corelib/corelib.qbs b/src/lib/corelib/corelib.qbs index 92d7eb052..2f0ced926 100644 --- a/src/lib/corelib/corelib.qbs +++ b/src/lib/corelib/corelib.qbs @@ -418,6 +418,7 @@ QbsLibrary { "joblimits.cpp", "jsliterals.cpp", "jsliterals.h", + "jsonhelper.h", "installoptions.cpp", "launcherinterface.cpp", "launcherinterface.h", diff --git a/src/lib/corelib/tools/buildoptions.cpp b/src/lib/corelib/tools/buildoptions.cpp index 5507e0842..75417ab0b 100644 --- a/src/lib/corelib/tools/buildoptions.cpp +++ b/src/lib/corelib/tools/buildoptions.cpp @@ -38,6 +38,9 @@ ****************************************************************************/ #include "buildoptions.h" +#include "jsonhelper.h" + +#include #include #include @@ -413,4 +416,56 @@ bool operator==(const BuildOptions &bo1, const BuildOptions &bo2) && bo1.removeExistingInstallation() == bo2.removeExistingInstallation(); } +namespace Internal { +template<> JobLimits fromJson(const QJsonValue &limitsData) +{ + JobLimits limits; + const QJsonArray &limitsArray = limitsData.toArray(); + for (const QJsonValue &v : limitsArray) { + const QJsonObject limitData = v.toObject(); + QString pool; + int limit = 0; + setValueFromJson(pool, limitData, "pool"); + setValueFromJson(limit, limitData, "limit"); + if (!pool.isEmpty() && limit > 0) + limits.setJobLimit(pool, limit); + } + return limits; +} + +template<> CommandEchoMode fromJson(const QJsonValue &modeData) +{ + const QString modeString = modeData.toString(); + if (modeString == QLatin1String("silent")) + return CommandEchoModeSilent; + if (modeString == QLatin1String("command-line")) + return CommandEchoModeCommandLine; + if (modeString == QLatin1String("command-line-with-environment")) + return CommandEchoModeCommandLineWithEnvironment; + return CommandEchoModeSummary; +} +} // namespace Internal + +qbs::BuildOptions qbs::BuildOptions::fromJson(const QJsonObject &data) +{ + using namespace Internal; + BuildOptions opt; + setValueFromJson(opt.d->changedFiles, data, "changed-files"); + setValueFromJson(opt.d->filesToConsider, data, "files-to-consider"); + setValueFromJson(opt.d->activeFileTags, data, "active-file-tags"); + setValueFromJson(opt.d->jobLimits, data, "job-limits"); + setValueFromJson(opt.d->maxJobCount, data, "max-job-count"); + setValueFromJson(opt.d->dryRun, data, "dry-run"); + setValueFromJson(opt.d->keepGoing, data, "keep-going"); + setValueFromJson(opt.d->forceTimestampCheck, data, "check-timestamps"); + setValueFromJson(opt.d->forceOutputCheck, data, "check-outputs"); + setValueFromJson(opt.d->logElapsedTime, data, "log-time"); + setValueFromJson(opt.d->echoMode, data, "command-echo-mode"); + setValueFromJson(opt.d->install, data, "install"); + setValueFromJson(opt.d->removeExistingInstallation, data, "clean-install-root"); + setValueFromJson(opt.d->onlyExecuteRules, data, "only-execute-rules"); + setValueFromJson(opt.d->jobLimitsFromProjectTakePrecedence, data, "enforce-project-job-limits"); + return opt; +} + } // namespace qbs diff --git a/src/lib/corelib/tools/buildoptions.h b/src/lib/corelib/tools/buildoptions.h index cea89d0ea..bd0fb22cb 100644 --- a/src/lib/corelib/tools/buildoptions.h +++ b/src/lib/corelib/tools/buildoptions.h @@ -47,6 +47,7 @@ #include QT_BEGIN_NAMESPACE +class QJsonObject; class QStringList; QT_END_NAMESPACE @@ -61,6 +62,8 @@ public: BuildOptions &operator=(const BuildOptions &other); ~BuildOptions(); + static BuildOptions fromJson(const QJsonObject &data); + QStringList filesToConsider() const; void setFilesToConsider(const QStringList &files); diff --git a/src/lib/corelib/tools/cleanoptions.cpp b/src/lib/corelib/tools/cleanoptions.cpp index 4fbe77b5d..b888fb1e8 100644 --- a/src/lib/corelib/tools/cleanoptions.cpp +++ b/src/lib/corelib/tools/cleanoptions.cpp @@ -38,6 +38,8 @@ ****************************************************************************/ #include "cleanoptions.h" +#include "jsonhelper.h" + #include namespace qbs { @@ -151,4 +153,14 @@ void CleanOptions::setLogElapsedTime(bool log) d->logElapsedTime = log; } +qbs::CleanOptions qbs::CleanOptions::fromJson(const QJsonObject &data) +{ + CleanOptions opt; + using namespace Internal; + setValueFromJson(opt.d->dryRun, data, "dry-run"); + setValueFromJson(opt.d->keepGoing, data, "keep-going"); + setValueFromJson(opt.d->logElapsedTime, data, "log-time"); + return opt; +} + } // namespace qbs diff --git a/src/lib/corelib/tools/cleanoptions.h b/src/lib/corelib/tools/cleanoptions.h index 3f67cf5a5..7827697bb 100644 --- a/src/lib/corelib/tools/cleanoptions.h +++ b/src/lib/corelib/tools/cleanoptions.h @@ -43,6 +43,10 @@ #include +QT_BEGIN_NAMESPACE +class QJsonObject; +QT_END_NAMESPACE + namespace qbs { namespace Internal { class CleanOptionsPrivate; } @@ -56,6 +60,8 @@ public: CleanOptions &operator=(CleanOptions &&other) Q_DECL_NOEXCEPT; ~CleanOptions(); + static CleanOptions fromJson(const QJsonObject &data); + bool dryRun() const; void setDryRun(bool dryRun); diff --git a/src/lib/corelib/tools/codelocation.cpp b/src/lib/corelib/tools/codelocation.cpp index 2c6ade3b0..5eff378e1 100644 --- a/src/lib/corelib/tools/codelocation.cpp +++ b/src/lib/corelib/tools/codelocation.cpp @@ -41,9 +41,12 @@ #include #include #include +#include #include #include +#include +#include #include #include #include @@ -134,6 +137,18 @@ QString CodeLocation::toString() const return str; } +QJsonObject CodeLocation::toJson() const +{ + QJsonObject obj; + if (!filePath().isEmpty()) + obj.insert(Internal::StringConstants::filePathKey(), filePath()); + if (line() != -1) + obj.insert(QStringLiteral("line"), line()); + if (column() != -1) + obj.insert(QStringLiteral("column"), column()); + return obj; +} + void CodeLocation::load(Internal::PersistentPool &pool) { const bool isValid = pool.load(); diff --git a/src/lib/corelib/tools/codelocation.h b/src/lib/corelib/tools/codelocation.h index 3dc8f26b1..3e84ce2d1 100644 --- a/src/lib/corelib/tools/codelocation.h +++ b/src/lib/corelib/tools/codelocation.h @@ -47,6 +47,7 @@ QT_BEGIN_NAMESPACE class QDataStream; +class QJsonObject; class QString; QT_END_NAMESPACE @@ -70,6 +71,7 @@ public: bool isValid() const; QString toString() const; + QJsonObject toJson() const; void load(Internal::PersistentPool &pool); void store(Internal::PersistentPool &pool) const; diff --git a/src/lib/corelib/tools/error.cpp b/src/lib/corelib/tools/error.cpp index 185dc0531..fc0b9377e 100644 --- a/src/lib/corelib/tools/error.cpp +++ b/src/lib/corelib/tools/error.cpp @@ -41,7 +41,10 @@ #include "persistence.h" #include "qttools.h" +#include "stringconstants.h" +#include +#include #include #include @@ -156,6 +159,14 @@ QString ErrorItem::toString() const return str += description(); } +QJsonObject ErrorItem::toJson() const +{ + QJsonObject data; + data.insert(Internal::StringConstants::descriptionProperty(), description()); + data.insert(Internal::StringConstants::locationKey(), codeLocation().toJson()); + return data; +} + class ErrorInfo::ErrorInfoPrivate : public QSharedData { @@ -248,7 +259,7 @@ void ErrorInfo::prepend(const QString &description, const CodeLocation &location * Most often, there will be one element in this list, but there can be more e.g. to illustrate * how an error condition propagates through several source files. */ -QList ErrorInfo::items() const +const QList ErrorInfo::items() const { return d->items; } @@ -282,6 +293,17 @@ QString ErrorInfo::toString() const return lines.join(QLatin1Char('\n')); } +QJsonObject ErrorInfo::toJson() const +{ + QJsonObject data; + data.insert(QLatin1String("is-internal"), isInternalError()); + QJsonArray itemsArray; + for (const ErrorItem &item : items()) + itemsArray.append(item.toJson()); + data.insert(QLatin1String("items"), itemsArray); + return data; +} + /*! * \brief Returns true if this error represents a bug in qbs, false otherwise. */ diff --git a/src/lib/corelib/tools/error.h b/src/lib/corelib/tools/error.h index 4832499af..abad85bad 100644 --- a/src/lib/corelib/tools/error.h +++ b/src/lib/corelib/tools/error.h @@ -47,6 +47,7 @@ #include QT_BEGIN_NAMESPACE +class QJsonObject; template class QList; class QString; class QStringList; @@ -68,6 +69,7 @@ public: QString description() const; CodeLocation codeLocation() const; QString toString() const; + QJsonObject toJson() const; bool isBacktraceItem() const; @@ -97,10 +99,11 @@ public: void append(const ErrorItem &item); void append(const QString &description, const CodeLocation &location = CodeLocation()); void prepend(const QString &description, const CodeLocation &location = CodeLocation()); - QList items() const; + const QList items() const; bool hasError() const { return !items().empty(); } void clear(); QString toString() const; + QJsonObject toJson() const; bool isInternalError() const; bool hasLocation() const; diff --git a/src/lib/corelib/tools/installoptions.cpp b/src/lib/corelib/tools/installoptions.cpp index 5cddae4ad..93fd54efe 100644 --- a/src/lib/corelib/tools/installoptions.cpp +++ b/src/lib/corelib/tools/installoptions.cpp @@ -36,9 +36,13 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ + #include "installoptions.h" -#include "language/language.h" -#include + +#include "jsonhelper.h" +#include "stringconstants.h" + +#include #include #include @@ -230,4 +234,17 @@ void InstallOptions::setLogElapsedTime(bool logElapsedTime) d->logElapsedTime = logElapsedTime; } +qbs::InstallOptions qbs::InstallOptions::fromJson(const QJsonObject &data) +{ + using namespace Internal; + InstallOptions opt; + setValueFromJson(opt.d->installRoot, data, "install-root"); + setValueFromJson(opt.d->useSysroot, data, "use-sysroot"); + setValueFromJson(opt.d->removeExisting, data, "clean-install-root"); + setValueFromJson(opt.d->dryRun, data, "dry-run"); + setValueFromJson(opt.d->keepGoing, data, "keep-going"); + setValueFromJson(opt.d->logElapsedTime, data, "log-time"); + return opt; +} + } // namespace qbs diff --git a/src/lib/corelib/tools/installoptions.h b/src/lib/corelib/tools/installoptions.h index 69e00aae5..16511aa3d 100644 --- a/src/lib/corelib/tools/installoptions.h +++ b/src/lib/corelib/tools/installoptions.h @@ -44,6 +44,7 @@ #include QT_BEGIN_NAMESPACE +class QJsonObject; class QString; QT_END_NAMESPACE @@ -65,6 +66,8 @@ public: InstallOptions &operator=(InstallOptions &&other) Q_DECL_NOEXCEPT; ~InstallOptions(); + static InstallOptions fromJson(const QJsonObject &data); + static QString defaultInstallRoot(); QString installRoot() const; void setInstallRoot(const QString &installRoot); diff --git a/src/lib/corelib/tools/jsonhelper.h b/src/lib/corelib/tools/jsonhelper.h new file mode 100644 index 000000000..d87802c0a --- /dev/null +++ b/src/lib/corelib/tools/jsonhelper.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QBS_JSON_HELPER_H +#define QBS_JSON_HELPER_H + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace qbs { +namespace Internal { + +template inline T fromJson(const QJsonValue &v); +template<> inline bool fromJson(const QJsonValue &v) { return v.toBool(); } +template<> inline int fromJson(const QJsonValue &v) { return v.toInt(); } +template<> inline QString fromJson(const QJsonValue &v) { return v.toString(); } +template<> inline QStringList fromJson(const QJsonValue &v) +{ + const QJsonArray &jsonList = v.toArray(); + QStringList stringList; + std::transform(jsonList.begin(), jsonList.end(), std::back_inserter(stringList), + [](const QVariant &v) { return v.toString(); }); + return stringList; +} +template<> inline QVariantMap fromJson(const QJsonValue &v) { return v.toObject().toVariantMap(); } +template<> inline QProcessEnvironment fromJson(const QJsonValue &v) +{ + const QJsonObject obj = v.toObject(); + QProcessEnvironment env; + for (auto it = obj.begin(); it != obj.end(); ++it) + env.insert(it.key(), it.value().toString()); + return env; +} + +template inline void setValueFromJson(T &targetValue, const QJsonObject &data, + const char *jsonProperty) +{ + const QJsonValue v = data.value(QLatin1String(jsonProperty)); + if (!v.isNull()) + targetValue = fromJson(v); +} + +} // namespace Internal +} // namespace qbs + +#endif // Include guard diff --git a/src/lib/corelib/tools/processresult.cpp b/src/lib/corelib/tools/processresult.cpp index 12e45b251..3fb2f8dbc 100644 --- a/src/lib/corelib/tools/processresult.cpp +++ b/src/lib/corelib/tools/processresult.cpp @@ -39,6 +39,9 @@ #include "processresult.h" #include "processresult_p.h" +#include +#include + /*! * \class SetupProjectParameters * \brief The \c ProcessResult class describes a finished qbs process command. @@ -129,4 +132,31 @@ QStringList ProcessResult::stdErr() const return d->stdErr; } +static QJsonValue processErrorToJson(QProcess::ProcessError error) +{ + switch (error) { + case QProcess::FailedToStart: return QLatin1String("failed-to-start"); + case QProcess::Crashed: return QLatin1String("crashed"); + case QProcess::Timedout: return QLatin1String("timed-out"); + case QProcess::WriteError: return QLatin1String("write-error"); + case QProcess::ReadError: return QLatin1String("read-error"); + case QProcess::UnknownError: return QStringLiteral("unknown-error"); + } + return {}; // For dumb compilers. +} + +QJsonObject qbs::ProcessResult::toJson() const +{ + return QJsonObject{ + {QStringLiteral("success"), success()}, + {QStringLiteral("executable-file-path"), executableFilePath()}, + {QStringLiteral("arguments"), QJsonArray::fromStringList(arguments())}, + {QStringLiteral("working-directory"), workingDirectory()}, + {QStringLiteral("error"), processErrorToJson(error())}, + {QStringLiteral("exit-code"), exitCode()}, + {QStringLiteral("stdout"), QJsonArray::fromStringList(stdOut())}, + {QStringLiteral("stderr"), QJsonArray::fromStringList(stdErr())} + }; +} + } // namespace qbs diff --git a/src/lib/corelib/tools/processresult.h b/src/lib/corelib/tools/processresult.h index 2d2ebbfb4..92408aa31 100644 --- a/src/lib/corelib/tools/processresult.h +++ b/src/lib/corelib/tools/processresult.h @@ -46,6 +46,7 @@ #include QT_BEGIN_NAMESPACE +class QJsonObject; class QString; class QStringList; QT_END_NAMESPACE @@ -65,6 +66,8 @@ public: ProcessResult &operator=(const ProcessResult &other); ~ProcessResult(); + QJsonObject toJson() const; + bool success() const; QString executableFilePath() const; QStringList arguments() const; diff --git a/src/lib/corelib/tools/setupprojectparameters.cpp b/src/lib/corelib/tools/setupprojectparameters.cpp index 6d817c8f3..41af7b926 100644 --- a/src/lib/corelib/tools/setupprojectparameters.cpp +++ b/src/lib/corelib/tools/setupprojectparameters.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,7 @@ #include #include #include +#include namespace qbs { namespace Internal { @@ -69,14 +71,14 @@ public: , forceProbeExecution(false) , waitLockBuildGraph(false) , restoreBehavior(SetupProjectParameters::RestoreAndTrackChanges) - , propertyCheckingMode(ErrorHandlingMode::Relaxed) + , propertyCheckingMode(ErrorHandlingMode::Strict) , productErrorMode(ErrorHandlingMode::Strict) { } QString projectFilePath; QString topLevelProfile; - QString configurationName; + QString configurationName = QLatin1String("default"); QString buildRoot; QStringList searchPaths; QStringList pluginPaths; @@ -121,6 +123,47 @@ SetupProjectParameters &SetupProjectParameters::operator=(const SetupProjectPara return *this; } +namespace Internal { +template<> ErrorHandlingMode fromJson(const QJsonValue &v) +{ + if (v.toString() == QLatin1String("relaxed")) + return ErrorHandlingMode::Relaxed; + return ErrorHandlingMode::Strict; +} + +template<> SetupProjectParameters::RestoreBehavior fromJson(const QJsonValue &v) +{ + const QString value = v.toString(); + if (value == QLatin1String("restore-only")) + return SetupProjectParameters::RestoreOnly; + if (value == QLatin1String("resolve-only")) + return SetupProjectParameters::ResolveOnly; + return SetupProjectParameters::RestoreAndTrackChanges; +} +} // namespace Internal + +SetupProjectParameters SetupProjectParameters::fromJson(const QJsonObject &data) +{ + using namespace Internal; + SetupProjectParameters params; + setValueFromJson(params.d->topLevelProfile, data, "top-level-profile"); + setValueFromJson(params.d->configurationName, data, "configuration-name"); + setValueFromJson(params.d->projectFilePath, data, "project-file-path"); + setValueFromJson(params.d->buildRoot, data, "build-root"); + setValueFromJson(params.d->settingsBaseDir, data, "settings-directory"); + setValueFromJson(params.d->overriddenValues, data, "overridden-properties"); + setValueFromJson(params.d->dryRun, data, "dry-run"); + setValueFromJson(params.d->logElapsedTime, data, "log-time"); + setValueFromJson(params.d->forceProbeExecution, data, "force-probe-execution"); + setValueFromJson(params.d->waitLockBuildGraph, data, "wait-lock-build-graph"); + setValueFromJson(params.d->fallbackProviderEnabled, data, "fallback-provider-enabled"); + setValueFromJson(params.d->environment, data, "environment"); + setValueFromJson(params.d->restoreBehavior, data, "restore-behavior"); + setValueFromJson(params.d->propertyCheckingMode, data, "error-handling-mode"); + params.d->productErrorMode = params.d->propertyCheckingMode; + return params; +} + SetupProjectParameters &SetupProjectParameters::operator=(SetupProjectParameters &&other) Q_DECL_NOEXCEPT = default; /*! diff --git a/src/lib/corelib/tools/setupprojectparameters.h b/src/lib/corelib/tools/setupprojectparameters.h index cf3b200cb..a4d090ec5 100644 --- a/src/lib/corelib/tools/setupprojectparameters.h +++ b/src/lib/corelib/tools/setupprojectparameters.h @@ -71,6 +71,8 @@ public: SetupProjectParameters &operator=(const SetupProjectParameters &other); SetupProjectParameters &operator=(SetupProjectParameters &&other) Q_DECL_NOEXCEPT; + static SetupProjectParameters fromJson(const QJsonObject &data); + QString topLevelProfile() const; void setTopLevelProfile(const QString &profile); diff --git a/src/lib/corelib/tools/stringconstants.h b/src/lib/corelib/tools/stringconstants.h index cd41f3768..79cbcd125 100644 --- a/src/lib/corelib/tools/stringconstants.h +++ b/src/lib/corelib/tools/stringconstants.h @@ -69,6 +69,7 @@ public: QBS_STRING_CONSTANT(baseNameProperty, "baseName") QBS_STRING_CONSTANT(baseProfileProperty, "baseProfile") QBS_STRING_CONSTANT(buildDirectoryProperty, "buildDirectory") + QBS_STRING_CONSTANT(buildDirectoryKey, "build-directory") QBS_STRING_CONSTANT(builtByDefaultProperty, "builtByDefault") QBS_STRING_CONSTANT(classNameProperty, "className") QBS_STRING_CONSTANT(completeBaseNameProperty, "completeBaseName") @@ -90,11 +91,13 @@ public: static const QString &fileNameProperty() { return fileName(); } static const QString &filePathProperty() { return filePath(); } static const QString &filePathVar() { return filePath(); } + QBS_STRING_CONSTANT(filePathKey, "file-path") QBS_STRING_CONSTANT(fileTagsFilterProperty, "fileTagsFilter") QBS_STRING_CONSTANT(fileTagsProperty, "fileTags") QBS_STRING_CONSTANT(filesProperty, "files") QBS_STRING_CONSTANT(filesAreTargetsProperty, "filesAreTargets") QBS_STRING_CONSTANT(foundProperty, "found") + QBS_STRING_CONSTANT(fullDisplayNameKey, "full-display-name") QBS_STRING_CONSTANT(imports, "imports") static const QString &importsDir() { return imports(); } static const QString &importsProperty() { return imports(); } @@ -106,12 +109,16 @@ public: QBS_STRING_CONSTANT(installPrefixProperty, "installPrefix") QBS_STRING_CONSTANT(installDirProperty, "installDir") QBS_STRING_CONSTANT(installSourceBaseProperty, "installSourceBase") + QBS_STRING_CONSTANT(isEnabledKey, "is-enabled") QBS_STRING_CONSTANT(jobCountProperty, "jobCount") QBS_STRING_CONSTANT(jobPoolProperty, "jobPool") QBS_STRING_CONSTANT(lengthProperty, "length") QBS_STRING_CONSTANT(limitToSubProjectProperty, "limitToSubProject") + QBS_STRING_CONSTANT(locationKey, "location") + QBS_STRING_CONSTANT(messageKey, "message") QBS_STRING_CONSTANT(minimumQbsVersionProperty, "minimumQbsVersion") QBS_STRING_CONSTANT(moduleNameProperty, "moduleName") + QBS_STRING_CONSTANT(modulePropertiesKey, "module-properties") QBS_STRING_CONSTANT(moduleProviders, "moduleProviders") QBS_STRING_CONSTANT(multiplexByQbsPropertiesProperty, "multiplexByQbsProperties") QBS_STRING_CONSTANT(multiplexConfigurationIdProperty, "multiplexConfigurationId") @@ -135,6 +142,7 @@ public: QBS_STRING_CONSTANT(profileProperty, "profile") static const QString &profilesProperty() { return profiles(); } QBS_STRING_CONSTANT(productTypesProperty, "productTypes") + QBS_STRING_CONSTANT(productsKey, "products") QBS_STRING_CONSTANT(qbsSearchPathsProperty, "qbsSearchPaths") QBS_STRING_CONSTANT(referencesProperty, "references") QBS_STRING_CONSTANT(recursiveProperty, "recursive") @@ -149,7 +157,8 @@ public: QBS_STRING_CONSTANT(sourceDirectoryProperty, "sourceDirectory") QBS_STRING_CONSTANT(submodulesProperty, "submodules") QBS_STRING_CONSTANT(targetNameProperty, "targetName") - QBS_STRING_CONSTANT(typeProperty, "type") + static const QString &typeProperty() { return type(); } + QBS_STRING_CONSTANT(type, "type") QBS_STRING_CONSTANT(validateProperty, "validate") QBS_STRING_CONSTANT(versionProperty, "version") QBS_STRING_CONSTANT(versionAtLeastProperty, "versionAtLeast") diff --git a/src/lib/corelib/tools/tools.pri b/src/lib/corelib/tools/tools.pri index f9c6be9a5..89d752671 100644 --- a/src/lib/corelib/tools/tools.pri +++ b/src/lib/corelib/tools/tools.pri @@ -23,6 +23,7 @@ HEADERS += \ $$PWD/iosutils.h \ $$PWD/joblimits.h \ $$PWD/jsliterals.h \ + $$PWD/jsonhelper.h \ $$PWD/launcherinterface.h \ $$PWD/launcherpackets.h \ $$PWD/launchersocket.h \ diff --git a/tests/auto/api/tst_api.cpp b/tests/auto/api/tst_api.cpp index 36a98fdaa..05cfc728f 100644 --- a/tests/auto/api/tst_api.cpp +++ b/tests/auto/api/tst_api.cpp @@ -875,7 +875,7 @@ void TestApi::changeContent() buildJob.reset(project.buildAllProducts(buildOptions, defaultProducts(), this)); errorInfo = project.addGroup(newProjectData.products().front(), "blubb"); QVERIFY(errorInfo.hasError()); - QVERIFY2(errorInfo.toString().contains("in process"), qPrintable(errorInfo.toString())); + QVERIFY2(errorInfo.toString().contains("in progress"), qPrintable(errorInfo.toString())); waitForFinished(buildJob.get()); errorInfo = project.addGroup(newProjectData.products().front(), "blubb"); VERIFY_NO_ERROR(errorInfo); diff --git a/tests/auto/blackbox/testdata/qbs-session/file1.cpp b/tests/auto/blackbox/testdata/qbs-session/file1.cpp new file mode 100644 index 000000000..1d6ea3b78 --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/file1.cpp @@ -0,0 +1 @@ +void f1() {} diff --git a/tests/auto/blackbox/testdata/qbs-session/file2.cpp b/tests/auto/blackbox/testdata/qbs-session/file2.cpp new file mode 100644 index 000000000..8ccc02b45 --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/file2.cpp @@ -0,0 +1 @@ +void f2() {} diff --git a/tests/auto/blackbox/testdata/qbs-session/lib.cpp b/tests/auto/blackbox/testdata/qbs-session/lib.cpp new file mode 100644 index 000000000..8101b05dc --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/lib.cpp @@ -0,0 +1 @@ +void f() { } diff --git a/tests/auto/blackbox/testdata/qbs-session/lib.h b/tests/auto/blackbox/testdata/qbs-session/lib.h new file mode 100644 index 000000000..789447c02 --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/lib.h @@ -0,0 +1 @@ +void f(); diff --git a/tests/auto/blackbox/testdata/qbs-session/main.cpp b/tests/auto/blackbox/testdata/qbs-session/main.cpp new file mode 100644 index 000000000..654a5d65b --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/main.cpp @@ -0,0 +1,4 @@ +int main() +{ + int i; // Should trigger a warning and thus a process-exited message. +} diff --git a/tests/auto/blackbox/testdata/qbs-session/modules/mymodule/mymodule.qbs b/tests/auto/blackbox/testdata/qbs-session/modules/mymodule/mymodule.qbs new file mode 100644 index 000000000..ecf12b5a3 --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/modules/mymodule/mymodule.qbs @@ -0,0 +1,5 @@ +import qbs.Environment + +Module { + setupRunEnvironment: { Environment.putEnv("MY_MODULE", 1); } +} diff --git a/tests/auto/blackbox/testdata/qbs-session/qbs-session.qbs b/tests/auto/blackbox/testdata/qbs-session/qbs-session.qbs new file mode 100644 index 000000000..8496fb38e --- /dev/null +++ b/tests/auto/blackbox/testdata/qbs-session/qbs-session.qbs @@ -0,0 +1,25 @@ +Project { + StaticLibrary { + name: "theLib" + Depends { name: "cpp" } + cpp.cxxLanguageVersion: "c++11" + Group { + name: "sources" + files: "lib.cpp" + } + Group { + name: "headers" + files: "lib.h" + } + } + CppApplication { + name: "theApp" + consoleApplication: true + Depends { name: "mymodule" } + cpp.cxxLanguageVersion: "c++14" + cpp.warningLevel: "all" + files: "main.cpp" + install: true + } +} + diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp index a2e8238c9..3a90d8ccb 100644 --- a/tests/auto/blackbox/tst_blackbox.cpp +++ b/tests/auto/blackbox/tst_blackbox.cpp @@ -43,6 +43,7 @@ #include #include +#include #include #include #include @@ -53,6 +54,7 @@ #include #include +#include #include #include #include @@ -5308,6 +5310,648 @@ void TestBlackbox::qbsConfig() } } +static QJsonObject getNextSessionPacket(QProcess &session, QByteArray &data) +{ + int totalSize = -1; + QElapsedTimer timer; + timer.start(); + QByteArray msg; + while (totalSize == -1 || msg.size() < totalSize) { + if (data.isEmpty()) + session.waitForReadyRead(1000); + if (timer.elapsed() >= 10000) + return QJsonObject(); + data += session.readAllStandardOutput(); + if (totalSize == -1) { + static const QByteArray magicString = "qbsmsg:"; + const int magicStringOffset = data.indexOf(magicString); + if (magicStringOffset == -1) + continue; + const int sizeOffset = magicStringOffset + magicString.length(); + const int newlineOffset = data.indexOf('\n', sizeOffset); + if (newlineOffset == -1) + continue; + const QByteArray sizeString = data.mid(sizeOffset, newlineOffset - sizeOffset); + bool isNumber; + const int size = sizeString.toInt(&isNumber); + if (!isNumber || size <= 0) + return QJsonObject(); + data = data.mid(newlineOffset + 1); + totalSize = size; + } + const int bytesToTake = std::min(totalSize - msg.size(), data.size()); + msg += data.left(bytesToTake); + data = data.mid(bytesToTake); + } + return QJsonDocument::fromJson(QByteArray::fromBase64(msg)).object(); +} + +void TestBlackbox::qbsSession() +{ + QDir::setCurrent(testDataDir + "/qbs-session"); + QProcess sessionProc; + sessionProc.start(qbsExecutableFilePath, QStringList("session")); + + // Uncomment for debugging. + /* + connect(&sessionProc, &QProcess::readyReadStandardError, [&sessionProc] { + qDebug() << "stderr:" << sessionProc.readAllStandardError(); + }); + */ + + QVERIFY(sessionProc.waitForStarted()); + + const auto sendPacket = [&sessionProc](const QJsonObject &message) { + const QByteArray data = QJsonDocument(message).toJson().toBase64(); + sessionProc.write("qbsmsg:"); + sessionProc.write(QByteArray::number(data.length())); + sessionProc.write("\n"); + sessionProc.write(data); + }; + + static const auto envToJson = [](const QProcessEnvironment &env) { + QJsonObject envObj; + const QStringList keys = env.keys(); + for (const QString &key : keys) + envObj.insert(key, env.value(key)); + return envObj; + }; + + static const auto envFromJson = [](const QJsonValue &v) { + const QJsonObject obj = v.toObject(); + QProcessEnvironment env; + for (auto it = obj.begin(); it != obj.end(); ++it) + env.insert(it.key(), it.value().toString()); + return env; + }; + + QByteArray incomingData; + + // Wait for and verify hello packet. + QJsonObject receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type"), "hello"); + QCOMPARE(receivedMessage.value("api-level").toInt(), 1); + QCOMPARE(receivedMessage.value("api-compat-level").toInt(), 1); + + // Resolve & verify structure + QJsonObject resolveMessage; + resolveMessage.insert("type", "resolve-project"); + resolveMessage.insert("top-level-profile", profileName()); + resolveMessage.insert("configuration-name", "my-config"); + resolveMessage.insert("project-file-path", QDir::currentPath() + "/qbs-session.qbs"); + resolveMessage.insert("build-root", QDir::currentPath()); + resolveMessage.insert("settings-directory", settings()->baseDirectory()); + QJsonObject overriddenValues; + overriddenValues.insert("products.theLib.cpp.cxxLanguageVersion", "c++17"); + resolveMessage.insert("overridden-properties", overriddenValues); + resolveMessage.insert("environment", envToJson(QProcessEnvironment::systemEnvironment())); + resolveMessage.insert("data-mode", "only-if-changed"); + resolveMessage.insert("log-time", true); + resolveMessage.insert("module-properties", + QJsonArray::fromStringList({"cpp.cxxLanguageVersion"})); + sendPacket(resolveMessage); + bool receivedLogData = false; + bool receivedStartedSignal = false; + bool receivedProgressData = false; + bool receivedReply = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-resolved") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + const QJsonObject projectData = receivedMessage.value("project-data").toObject(); + QCOMPARE(projectData.value("name").toString(), "qbs-session"); + const QJsonArray products = projectData.value("products").toArray(); + QCOMPARE(products.size(), 2); + for (const QJsonValue &v : products) { + const QJsonObject product = v.toObject(); + const QString productName = product.value("name").toString(); + QVERIFY(!productName.isEmpty()); + QVERIFY2(product.value("is-enabled").toBool(), qPrintable(productName)); + bool theLib = false; + bool theApp = false; + if (productName == "theLib") + theLib = true; + else if (productName == "theApp") + theApp = true; + QVERIFY2(theLib || theApp, qPrintable(productName)); + const QJsonArray groups = product.value("groups").toArray(); + if (theLib) + QVERIFY(groups.size() >= 3); + else + QVERIFY(!groups.isEmpty()); + for (const QJsonValue &v : groups) { + const QJsonObject group = v.toObject(); + const QJsonArray sourceArtifacts + = group.value("source-artifacts").toArray(); + const auto findArtifact = [&sourceArtifacts](const QString fileName) { + for (const QJsonValue &v : sourceArtifacts) { + const QJsonObject artifact = v.toObject(); + if (QFileInfo(artifact.value("file-path").toString()).fileName() + == fileName) { + return artifact; + } + } + return QJsonObject(); + }; + const QString groupName = group.value("name").toString(); + const auto getCxxLanguageVersion = [&group, &product] { + QJsonObject moduleProperties = group.value("module-properties").toObject(); + if (moduleProperties.isEmpty()) + moduleProperties = product.value("module-properties").toObject(); + return moduleProperties.toVariantMap().value("cpp.cxxLanguageVersion") + .toStringList(); + }; + if (groupName == "sources") { + const QJsonObject artifact = findArtifact("lib.cpp"); + QVERIFY2(!artifact.isEmpty(), "lib.cpp"); + QCOMPARE(getCxxLanguageVersion(), {"c++17"}); + } else if (groupName == "headers") { + const QJsonObject artifact = findArtifact("lib.h"); + QVERIFY2(!artifact.isEmpty(), "lib.h"); + } else if (groupName == "theApp") { + const QJsonObject artifact = findArtifact("main.cpp"); + QVERIFY2(!artifact.isEmpty(), "main.cpp"); + QCOMPARE(getCxxLanguageVersion(), {"c++14"}); + } + } + } + break; + } else if (msgType == "log-data") { + if (receivedMessage.value("message").toString().contains("activity")) + receivedLogData = true; + } else if (msgType == "task-started") { + receivedStartedSignal = true; + } else if (msgType == "task-progress") { + receivedProgressData = true; + } else if (msgType != "new-max-progress") { + QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType))); + } + } + QVERIFY(receivedReply); + QVERIFY(receivedLogData); + QVERIFY(receivedStartedSignal); + QVERIFY(receivedProgressData); + + // First build: No install, log time, default command description. + QJsonObject buildRequest; + buildRequest.insert("type", "build-project"); + buildRequest.insert("log-time", true); + buildRequest.insert("install", false); + buildRequest.insert("data-mode", "only-if-changed"); + sendPacket(buildRequest); + receivedReply = false; + receivedLogData = false; + receivedStartedSignal = false; + receivedProgressData = false; + bool receivedCommandDescription = false; + bool receivedProcessResult = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-built") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + const QJsonObject projectData = receivedMessage.value("project-data").toObject(); + QCOMPARE(projectData.value("name").toString(), "qbs-session"); + } else if (msgType == "log-data") { + if (receivedMessage.value("message").toString().contains("activity")) + receivedLogData = true; + } else if (msgType == "task-started") { + receivedStartedSignal = true; + } else if (msgType == "task-progress") { + receivedProgressData = true; + } else if (msgType == "command-description") { + if (receivedMessage.value("message").toString().contains("compiling main.cpp")) + receivedCommandDescription = true; + } else if (msgType == "process-result") { + QCOMPARE(receivedMessage.value("exit-code").toInt(), 0); + receivedProcessResult = true; + } else if (msgType != "new-max-progress") { + QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType))); + } + } + QVERIFY(receivedReply); + QVERIFY(receivedLogData); + QVERIFY(receivedStartedSignal); + QVERIFY(receivedProgressData); + QVERIFY(receivedCommandDescription); + QVERIFY(receivedProcessResult); + const QString &exeFilePath = QDir::currentPath() + '/' + + relativeExecutableFilePath("theApp", "my-config"); + QVERIFY2(regularFileExists(exeFilePath), qPrintable(exeFilePath)); + const QString defaultInstallRoot = QDir::currentPath() + '/' + + relativeBuildDir("my-config") + "/install-root"; + QVERIFY2(!directoryExists(defaultInstallRoot), qPrintable(defaultInstallRoot)); + + // Clean. + QJsonObject cleanRequest; + cleanRequest.insert("type", "clean-project"); + cleanRequest.insert("settings-dir", settings()->baseDirectory()); + cleanRequest.insert("log-time", true); + sendPacket(cleanRequest); + receivedReply = false; + receivedLogData = false; + receivedStartedSignal = false; + receivedProgressData = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-cleaned") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + } else if (msgType == "log-data") { + if (receivedMessage.value("message").toString().contains("activity")) + receivedLogData = true; + } else if (msgType == "task-started") { + receivedStartedSignal = true; + } else if (msgType == "task-progress") { + receivedProgressData = true; + } else if (msgType != "new-max-progress") { + QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType))); + } + } + QVERIFY(receivedReply); + QVERIFY(receivedLogData); + QVERIFY(receivedStartedSignal); + QVERIFY(receivedProgressData); + QVERIFY2(!regularFileExists(exeFilePath), qPrintable(exeFilePath)); + + // Second build: Do not log the time, show command lines. + buildRequest.insert("log-time", false); + buildRequest.insert("command-echo-mode", "command-line"); + sendPacket(buildRequest); + receivedReply = false; + receivedLogData = false; + receivedStartedSignal = false; + receivedProgressData = false; + receivedCommandDescription = false; + receivedProcessResult = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-built") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + const QJsonObject projectData = receivedMessage.value("project-data").toObject(); + QVERIFY(projectData.isEmpty()); + } else if (msgType == "log-data") { + if (receivedMessage.value("message").toString().contains("activity")) + receivedLogData = true; + } else if (msgType == "task-started") { + receivedStartedSignal = true; + } else if (msgType == "task-progress") { + receivedProgressData = true; + } else if (msgType == "command-description") { + if (receivedMessage.value("message").toString().contains( + QDir::separator() + QString("main.cpp"))) { + receivedCommandDescription = true; + } + } else if (msgType == "process-result") { + QCOMPARE(receivedMessage.value("exit-code").toInt(), 0); + receivedProcessResult = true; + } else if (msgType != "new-max-progress") { + QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType))); + } + } + QVERIFY(receivedReply); + QVERIFY(!receivedLogData); + QVERIFY(receivedStartedSignal); + QVERIFY(receivedProgressData); + QVERIFY(receivedCommandDescription); + QVERIFY(receivedProcessResult); + QVERIFY2(regularFileExists(exeFilePath), qPrintable(exeFilePath)); + QVERIFY2(!directoryExists(defaultInstallRoot), qPrintable(defaultInstallRoot)); + + // Install. + QJsonObject installRequest; + installRequest.insert("type", "install-project"); + installRequest.insert("log-time", true); + const QString customInstallRoot = QDir::currentPath() + "/my-install-root"; + QVERIFY2(!QFile::exists(customInstallRoot), qPrintable(customInstallRoot)); + installRequest.insert("install-root", customInstallRoot); + sendPacket(installRequest); + receivedReply = false; + receivedLogData = false; + receivedStartedSignal = false; + receivedProgressData = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "install-done") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + } else if (msgType == "log-data") { + if (receivedMessage.value("message").toString().contains("activity")) + receivedLogData = true; + } else if (msgType == "task-started") { + receivedStartedSignal = true; + } else if (msgType == "task-progress") { + receivedProgressData = true; + } else if (msgType != "new-max-progress") { + QVERIFY2(false, qPrintable(QString("Unexpected message type '%1'").arg(msgType))); + } + } + QVERIFY(receivedReply); + QVERIFY(receivedLogData); + QVERIFY(receivedStartedSignal); + QVERIFY(receivedProgressData); + QVERIFY2(!directoryExists(defaultInstallRoot), qPrintable(defaultInstallRoot)); + QVERIFY2(directoryExists(customInstallRoot), qPrintable(customInstallRoot)); + + // Retrieve modified environment. + QJsonObject getRunEnvRequest; + getRunEnvRequest.insert("type", "get-run-environment"); + getRunEnvRequest.insert("product", "theApp"); + const QProcessEnvironment inEnv = QProcessEnvironment::systemEnvironment(); + QVERIFY(!inEnv.contains("MY_MODULE")); + getRunEnvRequest.insert("base-environment", envToJson(inEnv)); + sendPacket(getRunEnvRequest); + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type").toString(), QString("run-environment")); + QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + const QProcessEnvironment outEnv = envFromJson(receivedMessage.value("full-environment")); + QVERIFY(outEnv.keys().size() > inEnv.keys().size()); + QCOMPARE(outEnv.value("MY_MODULE"), QString("1")); + + // Add two files to library and re-build. + QJsonObject addFilesRequest; + addFilesRequest.insert("type", "add-files"); + addFilesRequest.insert("product", "theLib"); + addFilesRequest.insert("group", "sources"); + addFilesRequest.insert("files", + QJsonArray::fromStringList({QDir::currentPath() + "/file1.cpp", + QDir::currentPath() + "/file2.cpp"})); + sendPacket(addFilesRequest); + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type").toString(), QString("files-added")); + error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + QJsonObject projectData = receivedMessage.value("project-data").toObject(); + QJsonArray products = projectData.value("products").toArray(); + bool file1 = false; + bool file2 = false; + for (const QJsonValue &v : products) { + const QJsonObject product = v.toObject(); + const QString productName = product.value("full-display-name").toString(); + const QJsonArray groups = product.value("groups").toArray(); + for (const QJsonValue &v : groups) { + const QJsonObject group = v.toObject(); + const QString groupName = group.value("name").toString(); + const QJsonArray sourceArtifacts = group.value("source-artifacts").toArray(); + for (const QJsonValue &v : sourceArtifacts) { + const QString filePath = v.toObject().value("file-path").toString(); + if (filePath.endsWith("file1.cpp")) { + QCOMPARE(productName, QString("theLib")); + QCOMPARE(groupName, QString("sources")); + file1 = true; + } else if (filePath.endsWith("file2.cpp")) { + QCOMPARE(productName, QString("theLib")); + QCOMPARE(groupName, QString("sources")); + file2 = true; + } + } + } + } + QVERIFY(file1); + QVERIFY(file2); + receivedReply = false; + receivedProcessResult = false; + bool compiledFile1 = false; + bool compiledFile2 = false; + bool compiledMain = false; + bool compiledLib = false; + buildRequest.remove("command-echo-mode"); + sendPacket(buildRequest); + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-built") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + } else if (msgType == "command-description") { + const QString msg = receivedMessage.value("message").toString(); + if (msg.contains("compiling file1.cpp")) + compiledFile1 = true; + else if (msg.contains("compiling file2.cpp")) + compiledFile2 = true; + else if (msg.contains("compiling main.cpp")) + compiledMain = true; + else if (msg.contains("compiling lib.cpp")) + compiledLib = true; + } else if (msgType == "process-result") { + QCOMPARE(receivedMessage.value("exit-code").toInt(), 0); + receivedProcessResult = true; + } + } + QVERIFY(receivedReply); + QVERIFY(!receivedProcessResult); + QVERIFY(compiledFile1); + QVERIFY(compiledFile2); + QVERIFY(!compiledLib); + QVERIFY(!compiledMain); + + // Remove one of the newly added files again and re-build. + WAIT_FOR_NEW_TIMESTAMP(); + touch("file1.cpp"); + touch("file2.cpp"); + touch("main.cpp"); + QJsonObject removeFilesRequest; + removeFilesRequest.insert("type", "remove-files"); + removeFilesRequest.insert("product", "theLib"); + removeFilesRequest.insert("group", "sources"); + removeFilesRequest.insert("files", + QJsonArray::fromStringList({QDir::currentPath() + "/file1.cpp"})); + sendPacket(removeFilesRequest); + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type").toString(), QString("files-removed")); + error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + projectData = receivedMessage.value("project-data").toObject(); + products = projectData.value("products").toArray(); + file1 = false; + file2 = false; + for (const QJsonValue &v : products) { + const QJsonObject product = v.toObject(); + const QString productName = product.value("full-display-name").toString(); + const QJsonArray groups = product.value("groups").toArray(); + for (const QJsonValue &v : groups) { + const QJsonObject group = v.toObject(); + const QString groupName = group.value("name").toString(); + const QJsonArray sourceArtifacts = group.value("source-artifacts").toArray(); + for (const QJsonValue &v : sourceArtifacts) { + const QString filePath = v.toObject().value("file-path").toString(); + if (filePath.endsWith("file1.cpp")) { + file1 = true; + } else if (filePath.endsWith("file2.cpp")) { + QCOMPARE(productName, QString("theLib")); + QCOMPARE(groupName, QString("sources")); + file2 = true; + } + } + } + } + QVERIFY(!file1); + QVERIFY(file2); + receivedReply = false; + receivedProcessResult = false; + compiledFile1 = false; + compiledFile2 = false; + compiledMain = false; + compiledLib = false; + sendPacket(buildRequest); + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QVERIFY(!receivedMessage.isEmpty()); + const QString msgType = receivedMessage.value("type").toString(); + if (msgType == "project-built") { + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + } else if (msgType == "command-description") { + const QString msg = receivedMessage.value("message").toString(); + if (msg.contains("compiling file1.cpp")) + compiledFile1 = true; + else if (msg.contains("compiling file2.cpp")) + compiledFile2 = true; + else if (msg.contains("compiling main.cpp")) + compiledMain = true; + else if (msg.contains("compiling lib.cpp")) + compiledLib = true; + } else if (msgType == "process-result") { + QCOMPARE(receivedMessage.value("exit-code").toInt(), 0); + receivedProcessResult = true; + } + } + QVERIFY(receivedReply); + QVERIFY(receivedProcessResult); + QVERIFY(!compiledFile1); + QVERIFY(compiledFile2); + QVERIFY(!compiledLib); + QVERIFY(compiledMain); + + // Get generated files. + QJsonObject genFilesRequestPerFile; + genFilesRequestPerFile.insert("source-file", QDir::currentPath() + "/main.cpp"); + genFilesRequestPerFile.insert("tags", QJsonArray{QJsonValue("obj")}); + QJsonObject genFilesRequestPerProduct; + genFilesRequestPerProduct.insert("full-display-name", "theApp"); + genFilesRequestPerProduct.insert("requests", QJsonArray({genFilesRequestPerFile})); + QJsonObject genFilesRequest; + genFilesRequest.insert("type", "get-generated-files-for-sources"); + genFilesRequest.insert("products", QJsonArray({genFilesRequestPerProduct})); + sendPacket(genFilesRequest); + receivedReply = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type").toString(), QString("generated-files-for-sources")); + const QJsonArray products = receivedMessage.value("products").toArray(); + QCOMPARE(products.size(), 1); + const QJsonArray results = products.first().toObject().value("results").toArray(); + QCOMPARE(results.size(), 1); + const QJsonObject result = results.first().toObject(); + QCOMPARE(result.value("source-file"), QDir::currentPath() + "/main.cpp"); + const QJsonArray generatedFiles = result.value("generated-files").toArray(); + QCOMPARE(generatedFiles.count(), 1); + QCOMPARE(QFileInfo(generatedFiles.first().toString()).fileName(), + objectFileName("main.cpp", profileName())); + receivedReply = true; + } + QVERIFY(receivedReply); + + // Release project. + const QJsonObject releaseRequest{qMakePair(QString("type"), QJsonValue("release-project"))}; + sendPacket(releaseRequest); + receivedReply = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type").toString(), QString("project-released")); + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + receivedReply = true; + } + QVERIFY(receivedReply); + + // Get build graph info. + QJsonObject loadProjectMessage; + loadProjectMessage.insert("type", "resolve-project"); + loadProjectMessage.insert("configuration-name", "my-config"); + loadProjectMessage.insert("build-root", QDir::currentPath()); + loadProjectMessage.insert("settings-dir", settings()->baseDirectory()); + loadProjectMessage.insert("restore-behavior", "restore-only"); + loadProjectMessage.insert("data-mode", "only-if-changed"); + sendPacket(loadProjectMessage); + receivedReply = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + if (receivedMessage.value("type") != "project-resolved") + continue; + receivedReply = true; + const QJsonObject error = receivedMessage.value("error").toObject(); + if (!error.isEmpty()) + qDebug() << error; + QVERIFY(error.isEmpty()); + const QString bgFilePath = QDir::currentPath() + '/' + + relativeBuildGraphFilePath("my-config"); + const QJsonObject projectData = receivedMessage.value("project-data").toObject(); + QCOMPARE(projectData.value("build-graph-file-path").toString(), bgFilePath); + QCOMPARE(projectData.value("overridden-properties"), overriddenValues); + } + QVERIFY(receivedReply); + + // Send unknown request. + const QJsonObject unknownRequest({qMakePair(QString("type"), QJsonValue("blubb"))}); + sendPacket(unknownRequest); + receivedReply = false; + while (!receivedReply) { + receivedMessage = getNextSessionPacket(sessionProc, incomingData); + QCOMPARE(receivedMessage.value("type").toString(), QString("protocol-error")); + receivedReply = true; + } + QVERIFY(receivedReply); + + QJsonObject quitRequest; + quitRequest.insert("type", "quit"); + sendPacket(quitRequest); + QVERIFY(sessionProc.waitForFinished(3000)); +} + void TestBlackbox::radAfterIncompleteBuild_data() { QTest::addColumn("projectFileName"); diff --git a/tests/auto/blackbox/tst_blackbox.h b/tests/auto/blackbox/tst_blackbox.h index ba511ed10..382c65389 100644 --- a/tests/auto/blackbox/tst_blackbox.h +++ b/tests/auto/blackbox/tst_blackbox.h @@ -240,6 +240,7 @@ private slots: void protobuf(); void pseudoMultiplexing(); void qbsConfig(); + void qbsSession(); void qbsVersion(); void qtBug51237(); void radAfterIncompleteBuild(); diff --git a/tests/auto/shared.h b/tests/auto/shared.h index f40a7dbfb..8f85f5d6c 100644 --- a/tests/auto/shared.h +++ b/tests/auto/shared.h @@ -101,8 +101,9 @@ inline QString relativeBuildDir(const QString &configurationName = QString()) return !configurationName.isEmpty() ? configurationName : QLatin1String("default"); } -inline QString relativeBuildGraphFilePath() { - return relativeBuildDir() + QLatin1Char('/') + relativeBuildDir() + QLatin1String(".bg"); +inline QString relativeBuildGraphFilePath(const QString &configName = QString()) { + return relativeBuildDir(configName) + QLatin1Char('/') + relativeBuildDir(configName) + + QLatin1String(".bg"); } inline bool regularFileExists(const QString &filePath) @@ -215,9 +216,10 @@ inline QString relativeProductBuildDir(const QString &productName, return relativeBuildDir(configurationName) + '/' + dirName; } -inline QString relativeExecutableFilePath(const QString &productName) +inline QString relativeExecutableFilePath(const QString &productName, + const QString &configName = QString()) { - return relativeProductBuildDir(productName) + '/' + return relativeProductBuildDir(productName, configName) + '/' + qbs::Internal::HostOsInfo::appendExecutableSuffix(productName); } -- cgit v1.2.3