aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/appendix/json-api.qdoc817
-rw-r--r--doc/appendix/qbs-porting.qdoc2
-rw-r--r--doc/config/style/qt5-sidebar.html3
-rw-r--r--doc/doc.qbs1
-rw-r--r--doc/qbs.qdoc7
-rw-r--r--doc/reference/cli/builtin/cli-session.qdoc54
-rw-r--r--doc/reference/cli/cli-options.qdocinc1
-rw-r--r--src/app/qbs/commandlinefrontend.cpp6
-rw-r--r--src/app/qbs/parser/commandlineparser.cpp1
-rw-r--r--src/app/qbs/parser/commandpool.cpp3
-rw-r--r--src/app/qbs/parser/commandtype.h2
-rw-r--r--src/app/qbs/parser/parsercommand.cpp23
-rw-r--r--src/app/qbs/parser/parsercommand.h14
-rw-r--r--src/app/qbs/qbs.pro8
-rw-r--r--src/app/qbs/qbs.qbs8
-rw-r--r--src/app/qbs/session.cpp751
-rw-r--r--src/app/qbs/session.h51
-rw-r--r--src/app/qbs/sessionpacket.cpp113
-rw-r--r--src/app/qbs/sessionpacket.h70
-rw-r--r--src/app/qbs/sessionpacketreader.cpp85
-rw-r--r--src/app/qbs/sessionpacketreader.h70
-rw-r--r--src/app/qbs/stdinreader.cpp146
-rw-r--r--src/app/qbs/stdinreader.h66
-rw-r--r--src/lib/corelib/api/project.cpp22
-rw-r--r--src/lib/corelib/api/project.h3
-rw-r--r--src/lib/corelib/api/projectdata.cpp156
-rw-r--r--src/lib/corelib/api/projectdata.h5
-rw-r--r--src/lib/corelib/corelib.qbs1
-rw-r--r--src/lib/corelib/tools/buildoptions.cpp55
-rw-r--r--src/lib/corelib/tools/buildoptions.h3
-rw-r--r--src/lib/corelib/tools/cleanoptions.cpp12
-rw-r--r--src/lib/corelib/tools/cleanoptions.h6
-rw-r--r--src/lib/corelib/tools/codelocation.cpp15
-rw-r--r--src/lib/corelib/tools/codelocation.h2
-rw-r--r--src/lib/corelib/tools/error.cpp24
-rw-r--r--src/lib/corelib/tools/error.h5
-rw-r--r--src/lib/corelib/tools/installoptions.cpp21
-rw-r--r--src/lib/corelib/tools/installoptions.h3
-rw-r--r--src/lib/corelib/tools/jsonhelper.h89
-rw-r--r--src/lib/corelib/tools/processresult.cpp30
-rw-r--r--src/lib/corelib/tools/processresult.h3
-rw-r--r--src/lib/corelib/tools/setupprojectparameters.cpp47
-rw-r--r--src/lib/corelib/tools/setupprojectparameters.h2
-rw-r--r--src/lib/corelib/tools/stringconstants.h11
-rw-r--r--src/lib/corelib/tools/tools.pri1
-rw-r--r--tests/auto/api/tst_api.cpp2
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/file1.cpp1
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/file2.cpp1
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/lib.cpp1
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/lib.h1
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/main.cpp4
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/modules/mymodule/mymodule.qbs5
-rw-r--r--tests/auto/blackbox/testdata/qbs-session/qbs-session.qbs25
-rw-r--r--tests/auto/blackbox/tst_blackbox.cpp644
-rw-r--r--tests/auto/blackbox/tst_blackbox.h1
-rw-r--r--tests/auto/shared.h10
56 files changed, 3477 insertions, 36 deletions
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:" <payload length> [<meta data>] <line feed> <payload>
+ \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 @@
<li><a href="reference.html">Reference</a></li>
<li><a href="building-qbs.html">Appendix A: Building Qbs</a></li>
<li><a href="porting-to-qbs.html">Appendix B: Migrating from Other Build Systems</a></li>
- <li><a href="attributions.html">Appendix C: Code Attributions</a></li>
+ <li><a href="json-api.html">Appendix C: The JSON API</a></li>
+ <li><a href="attributions.html">Appendix D: Code Attributions</a></li>
</ul>
</div>
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<Command *> 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<CommandLineOption::Type> 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 <api/jobs.h>
+#include <api/project.h>
+#include <api/projectdata.h>
+#include <api/runenvironment.h>
+#include <logging/ilogsink.h>
+#include <tools/buildoptions.h>
+#include <tools/cleanoptions.h>
+#include <tools/error.h>
+#include <tools/installoptions.h>
+#include <tools/jsonhelper.h>
+#include <tools/preferences.h>
+#include <tools/processresult.h>
+#include <tools/qbsassert.h>
+#include <tools/settings.h>
+#include <tools/setupprojectparameters.h>
+#include <tools/stringconstants.h>
+
+#include <QtCore/qcoreapplication.h>
+#include <QtCore/qdir.h>
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qobject.h>
+#include <QtCore/qprocess.h>
+
+#include <algorithm>
+#include <cstdlib>
+#include <iostream>
+#include <memory>
+
+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<ProductData> getProductsByName(const QStringList &productNames) const;
+ ProductData getProductByName(const QString &productName) const;
+
+ struct ProductSelection {
+ ProductSelection(Project::ProductSelection s) : selection(s) {}
+ ProductSelection(const QList<ProductData> &p) : products(p) {}
+
+ Project::ProductSelection selection = Project::ProductSelectionDefaultOnly;
+ QList<ProductData> 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<Settings> 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<SetupProjectJob *>(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<ProductData> getProductsByNameForProject(const ProjectData &project,
+ QStringList &productNames)
+{
+ QList<ProductData> 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<ProductData> Session::getProductsByName(const QStringList &productNames) const
+{
+ QStringList remainingNames = productNames;
+ return getProductsByNameForProject(m_projectData, remainingNames);
+}
+
+ProductData Session::getProductByName(const QString &productName) const
+{
+ const QList<ProductData> 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<QProcessEnvironment>(
+ request.value(QLatin1String("base-environment")));
+ const QStringList config = fromJson<QStringList>(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<QStringList>(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<QStringList>(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<QStringList>(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 <session.moc>
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 <tools/qbsassert.h>
+#include <tools/stringconstants.h>
+#include <tools/version.h>
+
+#include <QtCore/qdebug.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qjsonvalue.h>
+#include <QtCore/qstring.h>
+
+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 <QtCore/qbytearray.h>
+#include <QtCore/qjsonobject.h>
+
+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 <QtCore/qjsonobject.h>
+#include <QtCore/qobject.h>
+
+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 <tools/hostosinfo.h>
+
+#include <QtCore/qfile.h>
+#include <QtCore/qsocketnotifier.h>
+
+#include <cerrno>
+#include <cstring>
+
+#ifdef Q_OS_WIN32
+#include <qt_windows.h>
+#include <QtCore/qtimer.h>
+#else
+#include <fcntl.h>
+#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<DWORD>(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 <QtCore/qobject.h>
+
+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 <tools/fileinfo.h>
#include <tools/jsliterals.h>
#include <tools/qbsassert.h>
+#include <tools/stringconstants.h>
#include <tools/qttools.h>
#include <tools/stringconstants.h>
#include <QtCore/qdir.h>
+#include <QtCore/qjsonarray.h>
+#include <QtCore/qjsonobject.h>
#include <algorithm>
namespace qbs {
+using namespace Internal;
+
+template<typename T> static QJsonArray toJsonArray(const QList<T> &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<Internal::PropertyMapPrivate>())
+ : d(std::make_unique<PropertyMapPrivate>())
{
- 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<Internal::PropertyMapPrivate>(*other.d))
+ : d(std::make_unique<PropertyMapPrivate>(*other.d))
{
}
@@ -806,7 +932,7 @@ PropertyMap::~PropertyMap() = default;
PropertyMap &PropertyMap::operator =(const PropertyMap &other)
{
if (this != &other)
- d = std::make_unique<Internal::PropertyMapPrivate>(*other.d);
+ d = std::make_unique<PropertyMapPrivate>(*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 <QtCore/qjsonobject.h>
#include <QtCore/qshareddata.h>
#include <QtCore/qthread.h>
@@ -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 <QtCore/qshareddata.h>
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 <QtCore/qshareddata.h>
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 <QtCore/qshareddata.h>
+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 <tools/fileinfo.h>
#include <tools/persistence.h>
#include <tools/qbsassert.h>
+#include <tools/stringconstants.h>
#include <QtCore/qdatastream.h>
#include <QtCore/qdir.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsonvalue.h>
#include <QtCore/qregexp.h>
#include <QtCore/qshareddata.h>
#include <QtCore/qstring.h>
@@ -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<bool>();
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 <QtCore/qjsonarray.h>
+#include <QtCore/qjsonobject.h>
#include <QtCore/qshareddata.h>
#include <QtCore/qstringlist.h>
@@ -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<ErrorItem> ErrorInfo::items() const
+const QList<ErrorItem> 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 <QtCore/qshareddata.h>
QT_BEGIN_NAMESPACE
+class QJsonObject;
template <class T> 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<ErrorItem> items() const;
+ const QList<ErrorItem> 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 <tools/stringconstants.h>
+
+#include "jsonhelper.h"
+#include "stringconstants.h"
+
+#include <language/language.h>
#include <QtCore/qdir.h>
#include <QtCore/qshareddata.h>
@@ -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 <QtCore/qshareddata.h>
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 <QtCore/qjsonarray.h>
+#include <QtCore/qjsonobject.h>
+#include <QtCore/qjsonvalue.h>
+#include <QtCore/qprocess.h>
+#include <QtCore/qstringlist.h>
+#include <QtCore/qvariant.h>
+
+#include <algorithm>
+#include <iterator>
+
+namespace qbs {
+namespace Internal {
+
+template<typename T> 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<typename T> inline void setValueFromJson(T &targetValue, const QJsonObject &data,
+ const char *jsonProperty)
+{
+ const QJsonValue v = data.value(QLatin1String(jsonProperty));
+ if (!v.isNull())
+ targetValue = fromJson<T>(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 <QtCore/qjsonarray.h>
+#include <QtCore/qjsonobject.h>
+
/*!
* \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 <QtCore/qprocess.h>
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 <logging/translator.h>
#include <tools/buildgraphlocker.h>
#include <tools/installoptions.h>
+#include <tools/jsonhelper.h>
#include <tools/profile.h>
#include <tools/qbsassert.h>
#include <tools/scripttools.h>
@@ -50,6 +51,7 @@
#include <QtCore/qdir.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qprocess.h>
+#include <QtCore/qjsonobject.h>
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 <tools/version.h>
#include <QtCore/qdebug.h>
+#include <QtCore/qelapsedtimer.h>
#include <QtCore/qjsonarray.h>
#include <QtCore/qjsondocument.h>
#include <QtCore/qjsonobject.h>
@@ -53,6 +54,7 @@
#include <QtCore/qtemporarydir.h>
#include <QtCore/qtemporaryfile.h>
+#include <algorithm>
#include <functional>
#include <regex>
#include <utility>
@@ -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<QString>("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);
}