summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@qt.io>2018-03-06 10:46:26 +0100
committerChristian Kandeler <christian.kandeler@qt.io>2018-03-14 08:53:13 +0000
commit2acaba8ea211e1ba00c2e2844aa00ca16c7a04f4 (patch)
tree6be71fac638be27609fb6b196ce73d058780c67f
parent149e20aca1e401ba18bbae602df2caa7dc68c493 (diff)
downloadqbs-2acaba8ea211e1ba00c2e2844aa00ca16c7a04f4.tar.gz
Add module Exporter.qbs
This module generates qbs modules from products, providing an interface to them for use by external projects. [ChangeLog] Added new module "Exporter.qbs" for creating qbs modules from products. Task-number: QBS-1231 Change-Id: I9f0cf04b441aaf279cf19a84fd94d97a8cea9de8 Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
-rw-r--r--doc/reference/modules/exporter-qbs-module.qdoc130
-rw-r--r--qbs-resources/imports/QbsLibrary.qbs23
-rw-r--r--qbs-resources/modules/qbsbuildconfig/qbsbuildconfig.qbs2
-rw-r--r--share/qbs/modules/Exporter/qbs/qbsexporter.js268
-rw-r--r--share/qbs/modules/Exporter/qbs/qbsexporter.qbs78
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/consumer.cpp6
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/consumer.qbs14
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/exports-qbs.qbs120
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/helper.cpp.in6
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/helper.js1
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/mylib.cpp5
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/mylib.h17
-rw-r--r--tests/auto/blackbox/testdata/exports-qbs/tool.cpp18
-rw-r--r--tests/auto/blackbox/tst_blackbox.cpp59
-rw-r--r--tests/auto/blackbox/tst_blackbox.h1
15 files changed, 746 insertions, 2 deletions
diff --git a/doc/reference/modules/exporter-qbs-module.qdoc b/doc/reference/modules/exporter-qbs-module.qdoc
new file mode 100644
index 000000000..610b02dab
--- /dev/null
+++ b/doc/reference/modules/exporter-qbs-module.qdoc
@@ -0,0 +1,130 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 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
+ \qmltype Exporter.qbs
+ \inqmlmodule QbsModules
+ \since Qbs 1.12
+
+ \brief Provides support for generating \QBS modules from products.
+
+ The Exporter.qbs module contains the properties and rules to create a \QBS module from
+ the \l Export item of a \l Product.
+
+ Such a module acts as your product's interface to other projects written in \QBS.
+ For instance, suppose you are creating a library. To allow other products both within
+ and outside of your project to make use of it, you would write something like the following:
+ \code
+ DynamicLibrary {
+ name: "mylibrary"
+ qbs.installPrefix: "/opt/mylibrary"
+ Depends { name: "Exporter.qbs" }
+ property string headersInstallDir: "include"
+ // ...
+ Group {
+ name: "API headers"
+ files: ["mylib.h"]
+ qbs.install: true
+ qbs.installDir: headersInstallDir
+ }
+ Group {
+ fileTagsFilter: ["Exporter.qbs.module"]
+ qbs.installDir: "qbs/modules/mylibrary"
+ }
+ Export {
+ Depends { name: "cpp" }
+ cpp.includePaths: [product.sourceDirectory]
+ prefixMapping: [{
+ prefix: product.sourceDirectory,
+ replacement: FileInfo.joinPaths(qbs.installPrefix, product.headersInstallDir)
+ }]
+ }
+ }
+ \endcode
+ To build against this library, from within your project or any other one, you simply
+ declare a \l{Depends}{dependency}:
+ \code
+ Depends { name: "mylibrary" }
+ \endcode
+
+ \section2 Relevant File Tags
+ \target filetags-exporter-qbs
+
+ \table
+ \header
+ \li Tag
+ \li Since
+ \li Description
+ \row
+ \li \c{"Exporter.qbs.module"}
+ \li 1.12.0
+ \li This tag is attached to the generated module file.
+ \endtable
+*/
+
+/*!
+ \qmlproperty stringList Exporter.qbs::artifactTypes
+
+ Artifacts that match these tags will become \l{Group::filesAreTargets}{target artifacts}
+ of the generated module, so they can get picked up by the rules of depending products.
+
+ If you do not specify anything here, all installed artifacts of the product are considered.
+ \note You can only limit the default set of artifacts by setting this property, but you
+ cannot extend it, because only artifacts that are to be installed are considered.
+
+ \defaultvalue \c undefined
+*/
+
+/*!
+ \qmlproperty string Exporter.qbs::additionalContent
+
+ The value of this property will be copied verbatim into the generated module.
+
+ \defaultvalue \c undefined
+*/
+
+/*!
+ \qmlproperty stringList Exporter.qbs::excludedDependencies
+
+ Normally, all \l Depends items in the \l Export item are copied into the generated
+ module. However, if there are any exported dependencies that only make sense for
+ products in the same project, then you can enter their names into this array, and they
+ will get filtered out.
+ \note You should strive to structure your projects in such a way that you do not need to set
+ this property.
+
+ \defaultvalue \c undefined
+*/
+
+/*!
+ \qmlproperty string Exporter.qbs::fileName
+
+ The name of the generated module file.
+
+ \defaultvalue \c {product.targetName + ".qbs"}
+*/
diff --git a/qbs-resources/imports/QbsLibrary.qbs b/qbs-resources/imports/QbsLibrary.qbs
index 986cc0318..7855a3f7b 100644
--- a/qbs-resources/imports/QbsLibrary.qbs
+++ b/qbs-resources/imports/QbsLibrary.qbs
@@ -1,10 +1,11 @@
import qbs
import qbs.FileInfo
+import qbs.Utilities
QbsProduct {
Depends { name: "cpp" }
version: qbsversion.version
- type: Qt.core.staticBuild ? "staticlibrary" : "dynamiclibrary"
+ type: libType
targetName: (qbs.enableDebugCode && qbs.targetOS.contains("windows")) ? (name + 'd') : name
destinationDirectory: FileInfo.joinPaths(project.buildDirectory,
qbs.targetOS.contains("windows") ? "bin" : qbsbuildconfig.libDirName)
@@ -17,8 +18,12 @@ QbsProduct {
cpp.visibility: "minimal"
property bool visibilityType: Qt.core.staticBuild ? "static" : "dynamic"
property string headerInstallPrefix: "/include/qbs"
+ property bool hasExporter: Utilities.versionCompare(qbs.version, "1.12") >= 0
+ property bool generateQbsModule: install && qbsbuildconfig.generateQbsModules && hasExporter
+ property stringList libType: [Qt.core.staticBuild ? "staticlibrary" : "dynamiclibrary"]
+ Depends { name: "Exporter.qbs"; condition: generateQbsModule }
Group {
- fileTagsFilter: product.type.concat("dynamiclibrary_symlink")
+ fileTagsFilter: libType.concat("dynamiclibrary_symlink")
.concat(qbs.buildVariant === "debug" ? ["debuginfo_dll"] : [])
qbs.install: install
qbs.installSourceBase: destinationDirectory
@@ -30,6 +35,11 @@ QbsProduct {
qbs.install: install
qbs.installDir: qbsbuildconfig.importLibInstallDir
}
+ Group {
+ fileTagsFilter: "Exporter.qbs.module"
+ qbs.install: install
+ qbs.installDir: FileInfo.joinPaths(qbsbuildconfig.qbsModulesBaseDir, product.name)
+ }
Properties {
condition: qbs.targetOS.contains("darwin")
@@ -41,6 +51,15 @@ QbsProduct {
Depends { name: "cpp" }
Depends { name: "Qt"; submodules: ["core"] }
+ Properties {
+ condition: product.hasExporter
+ prefixMapping: [{
+ prefix: product.sourceDirectory,
+ replacement: FileInfo.joinPaths(product.qbs.installPrefix,
+ product.headerInstallPrefix)
+ }]
+ }
+
cpp.includePaths: [product.sourceDirectory]
cpp.defines: product.visibilityType === "static" ? ["QBS_STATIC_LIB"] : []
}
diff --git a/qbs-resources/modules/qbsbuildconfig/qbsbuildconfig.qbs b/qbs-resources/modules/qbsbuildconfig/qbsbuildconfig.qbs
index a1781400d..dd079a106 100644
--- a/qbs-resources/modules/qbsbuildconfig/qbsbuildconfig.qbs
+++ b/qbs-resources/modules/qbsbuildconfig/qbsbuildconfig.qbs
@@ -16,7 +16,9 @@ Module {
property bool installManPage: qbs.targetOS.contains("unix")
property bool installHtml: true
property bool installQch: false
+ property bool generateQbsModules: installApiHeaders
property string docInstallDir: "share/doc/qbs/html"
+ property string qbsModulesBaseDir: FileInfo.joinPaths(libDirName, "qbs", "modules")
property string relativeLibexecPath: "../" + libexecInstallDir
property string relativePluginsPath: "../" + libDirName
property string relativeSearchPath: ".."
diff --git a/share/qbs/modules/Exporter/qbs/qbsexporter.js b/share/qbs/modules/Exporter/qbs/qbsexporter.js
new file mode 100644
index 000000000..e71622607
--- /dev/null
+++ b/share/qbs/modules/Exporter/qbs/qbsexporter.js
@@ -0,0 +1,268 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing
+**
+** This file is part of Qbs.
+**
+** 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 http://www.qt.io/terms-conditions. For further information
+** use the contact form at http://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 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+
+var FileInfo = require("qbs.FileInfo");
+var ModUtils = require("qbs.ModUtils");
+
+function tagListToString(tagList)
+{
+ return JSON.stringify(tagList);
+}
+
+function stringToTagList(tagListString)
+{
+ return JSON.parse(tagListString);
+}
+
+function writeTargetArtifactGroup(output, tagList, artifactList, moduleInstallDir, moduleFile)
+{
+ // Do not add our qbs module file itself.
+ if (tagListToString(tagList) === tagListToString(output.fileTags))
+ return;
+
+ moduleFile.writeLine(" Group {");
+ moduleFile.writeLine(" filesAreTargets: true");
+ var filteredTagList = tagList.filter(function(t) { return t !== "installable"; });
+ moduleFile.writeLine(" fileTags: " + JSON.stringify(filteredTagList));
+ moduleFile.writeLine(" files: [");
+ for (i = 0; i < artifactList.length; ++i) {
+ var artifact = artifactList[i];
+ var installedArtifactFilePath = ModUtils.artifactInstalledFilePath(artifact);
+
+ // Use relative file paths for relocatability.
+ var relativeInstalledArtifactFilePath = FileInfo.relativePath(moduleInstallDir,
+ installedArtifactFilePath);
+ moduleFile.writeLine(" " + JSON.stringify(relativeInstalledArtifactFilePath)
+ + ",");
+ }
+ moduleFile.writeLine(" ]");
+ moduleFile.writeLine(" }");
+
+}
+
+function writeTargetArtifactGroups(product, output, moduleFile)
+{
+ var relevantArtifacts = [];
+ for (var i = 0; i < (product.Exporter.qbs._artifactTypes || []).length; ++i) {
+ var tag = product.Exporter.qbs._artifactTypes[i];
+ var artifactsForTag = product.artifacts[tag] || [];
+ for (var j = 0; j < artifactsForTag.length; ++j) {
+ if (!relevantArtifacts.contains(artifactsForTag[j]))
+ relevantArtifacts.push(artifactsForTag[j]);
+ }
+ }
+ var artifactsByTags = {};
+ var artifactCount = relevantArtifacts ? relevantArtifacts.length : 0;
+ for (i = 0; i < artifactCount; ++i) {
+ var artifact = relevantArtifacts[i];
+ if (!artifact.fileTags.contains("installable"))
+ continue;
+
+ // Put all artifacts with the same set of file tags into the same group, so we don't
+ // create more groups than necessary.
+ var key = tagListToString(artifact.fileTags);
+ var currentList = artifactsByTags[key];
+ if (currentList)
+ currentList.push(artifact);
+ else
+ currentList = [artifact];
+ artifactsByTags[key] = currentList;
+ }
+ var moduleInstallDir = FileInfo.path(ModUtils.artifactInstalledFilePath(output));
+ for (var tagListKey in artifactsByTags) {
+ writeTargetArtifactGroup(output, stringToTagList(tagListKey), artifactsByTags[tagListKey],
+ moduleInstallDir, moduleFile);
+ }
+}
+
+function checkValuePrefix(name, value, forbiddenPrefix, prefixDescription)
+{
+ if (value.startsWith(forbiddenPrefix)) {
+ throw "Value '" + value + "' for exported property '" + name + "' in product '"
+ + product.name + "' points into " + prefixDescription + ".\n"
+ + "Did you forget to set the prefixMapping property in an Export item?";
+ }
+}
+
+function stringifyValue(project, product, moduleInstallDir, name, value)
+{
+ if (Array.isArray(value)) {
+ var repr = "[";
+ for (var i = 0; i < value.length; ++i) {
+ repr += stringifyValue(project, product, moduleInstallDir, name, value[i]) + ", ";
+ }
+ repr += "]";
+ return repr;
+ }
+ if (typeof(value) !== "string")
+ return JSON.stringify(value);
+
+ // Catch user oversights: Paths that point into the project source or build directories
+ // make no sense in the module.
+ if (!value.startsWith(product.qbs.installRoot)) {
+ checkValuePrefix(name, value, project.buildDirectory, "project build directory");
+ checkValuePrefix(name, value, project.sourceDirectory, "project source directory");
+ }
+
+ // Adapt file paths pointing into the install dir, that is, make them relative to the
+ // module file for relocatability. We accept them with or without the install root.
+ // The latter form will typically be a result of applying the prefixMapping property,
+ // while the first one could be an untransformed path, for instance if the project
+ // file is written in such a way that include paths are picked up from the installed
+ // location rather than the source directory.
+ var valuePrefixToStrip;
+ var fullInstallPrefix = FileInfo.joinPaths(product.qbs.installRoot, product.qbs.installPrefix);
+ if (fullInstallPrefix.length > 1 && value.startsWith(fullInstallPrefix)) {
+ valuePrefixToStrip = fullInstallPrefix;
+ } else {
+ var installPrefix = FileInfo.joinPaths("/", product.qbs.installPrefix);
+ if (installPrefix.length > 1 && value.startsWith(installPrefix))
+ valuePrefixToStrip = installPrefix;
+ }
+ if (valuePrefixToStrip) {
+ var deployedModuleInstallDir = moduleInstallDir.slice(fullInstallPrefix.length);
+ return "FileInfo.cleanPath(FileInfo.joinPaths(path, FileInfo.relativePath("
+ + JSON.stringify(deployedModuleInstallDir) + ", "
+ + JSON.stringify(value.slice(valuePrefixToStrip.length)) + ")))";
+ }
+
+ return JSON.stringify(value);
+}
+
+function writeProperty(project, product, moduleInstallDir, prop, indentation, considerValue,
+ moduleFile)
+{
+ var line = indentation;
+ var separatorIndex = prop.name.lastIndexOf(".");
+ var isModuleProperty = separatorIndex !== -1;
+ var needsDeclaration = !prop.isBuiltin && !isModuleProperty;
+ if (needsDeclaration)
+ line += "property " + prop.type + " ";
+ var moduleName;
+ if (isModuleProperty) {
+ moduleName = prop.name.slice(0, separatorIndex);
+ if ((product.Exporter.qbs.excludedDependencies || []).contains(moduleName))
+ return;
+ }
+ line += prop.name + ": ";
+
+ // We emit the literal value, unless the source code clearly refers to values from inside the
+ // original project, in which case the evaluated value is used.
+ if (considerValue && /(project|product)\./.test(prop.sourceCode)) {
+ var value;
+ if (isModuleProperty) {
+ var propertyName = prop.name.slice(separatorIndex + 1);
+ value = product.exports[moduleName][propertyName];
+ } else {
+ value = product.exports[prop.name];
+ }
+ line += stringifyValue(project, product, moduleInstallDir, prop.name, value);
+ } else {
+ line += prop.sourceCode.replace(/importingProduct\./g, "product.");
+ }
+ moduleFile.writeLine(line);
+}
+
+function writeProperties(project, product, moduleInstallDir, list, indentation, considerValue,
+ moduleFile)
+{
+ for (var i = 0; i < list.length; ++i) {
+ writeProperty(project, product, moduleInstallDir, list[i], indentation, considerValue,
+ moduleFile);
+ }
+}
+
+// This writes properties set on other modules in the Export item, i.e. property assignments
+// like "cpp.includePaths: '...'".
+function writeModuleProperties(project, product, output, moduleFile)
+{
+ var moduleInstallDir = FileInfo.path(ModUtils.artifactInstalledFilePath(output));
+ var filteredProps = product.exports.properties.filter(function(p) {
+ return p.name !== "name";
+ });
+
+ // The right-hand side can refer to values from the exporting product, in which case
+ // the evaluated value, rather than the source code, needs to go into the module file.
+ var considerValues = true;
+ writeProperties(project, product, moduleInstallDir, filteredProps, " ", considerValues,
+ moduleFile);
+}
+
+function writeItem(product, item, indentation, moduleFile)
+{
+ moduleFile.writeLine(indentation + item.name + " {");
+ var newIndentation = indentation + " ";
+
+ // These are sub-items of the Export item, whose properties entirely live in the context
+ // of the importing product. Therefore, they must never use pre-evaluated values.
+ var considerValues = false;
+ writeProperties(undefined, product, undefined, item.properties, newIndentation, considerValues,
+ moduleFile)
+
+ for (var i = 0; i < item.childItems.length; ++i)
+ writeItem(product, item.childItems[i], newIndentation, moduleFile);
+ moduleFile.writeLine(indentation + "}");
+}
+
+function isExcludedDependency(product, childItem)
+{
+ if ((product.Exporter.qbs.excludedDependencies || []).length === 0)
+ return false;
+ if (childItem.name !== "Depends")
+ return false;
+ for (var i = 0; i < childItem.properties.length; ++i) {
+ var prop = childItem.properties[i];
+ var unquotedRhs = prop.sourceCode.slice(1, -1);
+ if (prop.name === "name" && product.Exporter.qbs.excludedDependencies.contains(unquotedRhs))
+ return true;
+ }
+ return false;
+}
+
+function writeChildItems(product, moduleFile)
+{
+ for (var i = 0; i < product.exports.childItems.length; ++i) {
+ var item = product.exports.childItems[i];
+ if (!isExcludedDependency(product, item))
+ writeItem(product, item, " ", moduleFile);
+ }
+}
+
+function writeImportStatements(product, moduleFile)
+{
+ var imports = product.exports.imports;
+
+ // We potentially use FileInfo ourselves when transforming paths in stringifyValue().
+ if (!imports.contains("import qbs.FileInfo"))
+ imports.push("import qbs.FileInfo");
+
+ for (var i = 0; i < product.exports.imports.length; ++i)
+ moduleFile.writeLine(product.exports.imports[i]);
+}
diff --git a/share/qbs/modules/Exporter/qbs/qbsexporter.qbs b/share/qbs/modules/Exporter/qbs/qbsexporter.qbs
new file mode 100644
index 000000000..6cdc55891
--- /dev/null
+++ b/share/qbs/modules/Exporter/qbs/qbsexporter.qbs
@@ -0,0 +1,78 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: http://www.qt.io/licensing
+**
+** This file is part of Qbs.
+**
+** 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 http://www.qt.io/terms-conditions. For further information
+** use the contact form at http://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 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+****************************************************************************/
+
+import qbs
+import qbs.FileInfo
+import qbs.TextFile
+
+import "qbsexporter.js" as HelperFunctions
+
+Module {
+ property stringList artifactTypes
+ property string fileName: product.targetName + ".qbs"
+ property stringList excludedDependencies
+ property string additionalContent
+
+ property stringList _artifactTypes: artifactTypes ? artifactTypes : ["installable"]
+
+ additionalProductTypes: ["Exporter.qbs.module"]
+
+ Rule {
+ multiplex: true
+ requiresInputs: false
+
+ // Make sure we only run when all other artifacts are already present.
+ inputs: product.type.filter(function(t) { return t !== "Exporter.qbs.module"; })
+
+ Artifact {
+ filePath: product.Exporter.qbs.fileName
+ fileTags: ["Exporter.qbs.module"]
+ qbs.install: true
+ }
+ prepare: {
+ var cmd = new JavaScriptCommand();
+ cmd.description = "Creating " + output.fileName;
+ cmd.sourceCode = function() {
+ var f = new TextFile(output.filePath, TextFile.WriteOnly);
+ f.writeLine("import qbs");
+ HelperFunctions.writeImportStatements(product, f);
+ f.writeLine("\nModule {");
+ HelperFunctions.writeModuleProperties(project, product, output, f);
+ HelperFunctions.writeTargetArtifactGroups(product, output, f);
+ HelperFunctions.writeChildItems(product, f);
+ if (product.Exporter.qbs.additionalContent)
+ f.writeLine(product.Exporter.qbs.additionalContent);
+ f.writeLine("}");
+ f.close();
+ };
+ return [cmd];
+ }
+ }
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/consumer.cpp b/tests/auto/blackbox/testdata/exports-qbs/consumer.cpp
new file mode 100644
index 000000000..25338b611
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/consumer.cpp
@@ -0,0 +1,6 @@
+void helper();
+
+int main()
+{
+ helper();
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/consumer.qbs b/tests/auto/blackbox/testdata/exports-qbs/consumer.qbs
new file mode 100644
index 000000000..57f2adc15
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/consumer.qbs
@@ -0,0 +1,14 @@
+import qbs
+
+CppApplication {
+ name: "consumer"
+ qbsSearchPaths: "default/install-root/usr/qbs"
+ property string outTag: "cpp"
+ Depends { name: "MyLib" }
+ Depends { name: "MyTool" }
+ files: ["consumer.cpp"]
+ Group {
+ files: ["helper.cpp.in"]
+ fileTags: ["cpp.in"]
+ }
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/exports-qbs.qbs b/tests/auto/blackbox/testdata/exports-qbs/exports-qbs.qbs
new file mode 100644
index 000000000..b84c44b5f
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/exports-qbs.qbs
@@ -0,0 +1,120 @@
+import qbs
+import qbs.FileInfo
+
+import "helper.js" as Helper
+
+Project {
+ property string installPrefix: "/usr"
+ Product {
+ name: "local"
+ Export {
+ property bool dummy
+ Depends { name: "cpp" }
+ cpp.includePaths: ["/somelocaldir/include"]
+ }
+ }
+
+ CppApplication {
+ name: "MyTool"
+ consoleApplication: true
+ property stringList toolTags: ["MyTool.tool"]
+ Depends { name: "Exporter.qbs" }
+ Exporter.qbs.artifactTypes: ["installable", "blubb"]
+ files: ["tool.cpp"]
+ qbs.installPrefix: project.installPrefix
+ Group {
+ files: ["helper.js"]
+ qbs.install: true
+ qbs.installDir: "qbs/modules/MyTool"
+ }
+
+ Group {
+ fileTagsFilter: ["application"]
+ qbs.install: true
+ qbs.installDir: "bin"
+ fileTags: toolTags
+ }
+ Group {
+ fileTagsFilter: ["Exporter.qbs.module"]
+ qbs.installDir: "qbs/modules/MyTool"
+ }
+
+ Export {
+ property stringList toolTags: product.toolTags
+ property stringList outTags: [importingProduct.outTag]
+ Rule {
+ inputs: Helper.toolInputs()
+ explicitlyDependsOn: toolTags
+
+ outputFileTags: parent.outTags
+ outputArtifacts: [{
+ filePath: FileInfo.completeBaseName(input.fileName),
+ fileTags: product.MyTool.outTags
+ }]
+ prepare: {
+ var cmd = new Command(explicitlyDependsOn["MyTool.tool"][0].filePath,
+ [input.filePath, output.filePath]);
+ cmd.description = input.fileName + " -> " + output.fileName;
+ return [cmd];
+ }
+ }
+ }
+ }
+
+ DynamicLibrary {
+ name: "MyLib"
+ multiplexByQbsProperties: ["buildVariants"]
+ aggregate: false
+ qbs.buildVariants: ["debug", "release"]
+ qbs.installPrefix: project.installPrefix
+ Depends { name: "cpp" }
+ Depends { name: "Exporter.qbs" }
+ Exporter.qbs.fileName: name + "_" + qbs.buildVariant + ".qbs"
+ Exporter.qbs.excludedDependencies: ["local"]
+ Exporter.qbs.additionalContent: " condition: qbs.buildVariant === '"
+ + qbs.buildVariant + "'"
+ property string headersInstallDir: "include"
+ cpp.defines: ["MYLIB_BUILD"]
+ cpp.variantSuffix: qbs.buildVariant === "debug" ? "d" : ""
+ Properties {
+ condition: qbs.targetOS.contains("darwin")
+ bundle.isBundle: false
+ }
+ files: ["mylib.cpp"]
+ Group {
+ name: "API headers"
+ files: ["mylib.h"]
+ qbs.install: true
+ qbs.installDir: headersInstallDir
+ }
+ Group {
+ fileTagsFilter: ["dynamiclibrary", "dynamiclibrary_import"]
+ qbs.install: true
+ qbs.installDir: "lib"
+ }
+ Group {
+ fileTagsFilter: ["Exporter.qbs.module"]
+ qbs.install: true
+ qbs.installDir: "qbs/modules/MyLib"
+ }
+
+ Export {
+ Depends { name: "cpp" }
+ property string includeDir: product.sourceDirectory
+ Properties {
+ condition: true
+ cpp.includePaths: [includeDir]
+ cpp.dynamicLibraries: []
+ }
+ cpp.dynamicLibraries: ["nosuchlib"]
+ Depends { name: "local" }
+ local.dummy: true
+ prefixMapping: [{
+ prefix: includeDir,
+ replacement: FileInfo.joinPaths(qbs.installPrefix, product.headersInstallDir)
+ }]
+ }
+ }
+
+ references: ["consumer.qbs"]
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/helper.cpp.in b/tests/auto/blackbox/testdata/exports-qbs/helper.cpp.in
new file mode 100644
index 000000000..21c1f8ee6
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/helper.cpp.in
@@ -0,0 +1,6 @@
+#include <mylib.h>
+
+void helper()
+{
+ MyLib::f();
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/helper.js b/tests/auto/blackbox/testdata/exports-qbs/helper.js
new file mode 100644
index 000000000..17c4e91c0
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/helper.js
@@ -0,0 +1 @@
+function toolInputs() { return ["cpp.in"]; }
diff --git a/tests/auto/blackbox/testdata/exports-qbs/mylib.cpp b/tests/auto/blackbox/testdata/exports-qbs/mylib.cpp
new file mode 100644
index 000000000..f3dc6a435
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/mylib.cpp
@@ -0,0 +1,5 @@
+#include "mylib.h"
+
+namespace MyLib {
+void f() {}
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/mylib.h b/tests/auto/blackbox/testdata/exports-qbs/mylib.h
new file mode 100644
index 000000000..9f5f8269e
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/mylib.h
@@ -0,0 +1,17 @@
+#if defined(_WIN32) || defined(WIN32)
+# define DLL_EXPORT __declspec(dllexport)
+# define DLL_IMPORT __declspec(dllimport)
+#else
+# define DLL_EXPORT __attribute__((visibility("default")))
+# define DLL_IMPORT __attribute__((visibility("default")))
+# endif
+
+#ifdef MYLIB_BUILD
+#define MYLIB_EXPORT DLL_EXPORT
+#else
+#define MYLIB_EXPORT DLL_IMPORT
+#endif
+
+namespace MyLib {
+MYLIB_EXPORT void f();
+}
diff --git a/tests/auto/blackbox/testdata/exports-qbs/tool.cpp b/tests/auto/blackbox/testdata/exports-qbs/tool.cpp
new file mode 100644
index 000000000..4657033fd
--- /dev/null
+++ b/tests/auto/blackbox/testdata/exports-qbs/tool.cpp
@@ -0,0 +1,18 @@
+#include <cstdlib>
+#include <fstream>
+
+int main(int argc, char *argv[])
+{
+ if (argc != 3)
+ return EXIT_FAILURE;
+ std::ifstream in(argv[1]);
+ if (!in)
+ return EXIT_FAILURE;
+ std::ofstream out(argv[2]);
+ if (!out)
+ return EXIT_FAILURE;
+ char ch;
+ while (in.get(ch))
+ out.put(ch);
+ return in.eof() && out ? EXIT_SUCCESS : EXIT_FAILURE;
+}
diff --git a/tests/auto/blackbox/tst_blackbox.cpp b/tests/auto/blackbox/tst_blackbox.cpp
index bc2d69572..7402a5eb5 100644
--- a/tests/auto/blackbox/tst_blackbox.cpp
+++ b/tests/auto/blackbox/tst_blackbox.cpp
@@ -3270,6 +3270,65 @@ void TestBlackbox::exportToOutsideSearchPath()
m_qbsStderr.constData());
}
+void TestBlackbox::exportsQbs()
+{
+ QDir::setCurrent(testDataDir + "/exports-qbs");
+
+ // First we build exportable products and use them (as products) inside
+ // the original project.
+ QCOMPARE(runQbs(QStringList{"-f", "exports-qbs.qbs", "--command-echo-mode", "command-line"}),
+ 0);
+ QVERIFY2(m_qbsStdout.contains("somelocaldir"), m_qbsStdout.constData());
+
+ // Now we build an external product against the modules that were just installed.
+ // We try debug and release mode; one module exists for each of them.
+ QbsRunParameters paramsExternalBuild(QStringList{"-f", "consumer.qbs",
+ "--command-echo-mode", "command-line",
+ "modules.qbs.buildVariant:debug",});
+ paramsExternalBuild.buildDirectory = QDir::currentPath() + "/external-consumer-debug";
+ QCOMPARE(runQbs(paramsExternalBuild), 0);
+ QVERIFY2(!m_qbsStdout.contains("somelocaldir"), m_qbsStdout.constData());
+
+ paramsExternalBuild.arguments = QStringList{"-f", "consumer.qbs",
+ "modules.qbs.buildVariant:release"};
+ paramsExternalBuild.buildDirectory = QDir::currentPath() + "/external-consumer-release";
+ QCOMPARE(runQbs(paramsExternalBuild), 0);
+
+ // Trying to build with an unsupported build variant must fail.
+ paramsExternalBuild.arguments = QStringList{"-f", "consumer.qbs",
+ "modules.qbs.buildVariant:unknown"};
+ paramsExternalBuild.buildDirectory = QDir::currentPath() + "/external-consumer-profile";
+ paramsExternalBuild.expectFailure = true;
+ QVERIFY(runQbs(paramsExternalBuild) != 0);
+ QVERIFY2(m_qbsStderr.contains("MyLib could not be loaded"), m_qbsStderr.constData());
+
+ // Removing the condition from the generated module leaves us with two conflicting
+ // candidates.
+ QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{ "-f", "exports-qbs.qbs",
+ "modules.Exporter.qbs.additionalContent:''"})), 0);
+ QCOMPARE(runQbs(), 0);
+ QVERIFY(runQbs(paramsExternalBuild) != 0);
+ QVERIFY2(m_qbsStderr.contains("There is more than one equally prioritized candidate "
+ "for module 'MyLib'."), m_qbsStderr.constData());
+
+ // Change tracking for accesses to product.exports (negative).
+ QCOMPARE(runQbs(QbsRunParameters("resolve", QStringList{"-f", "exports-qbs.qbs"})), 0);
+ QCOMPARE(runQbs(), 0);
+ QVERIFY2(m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
+ WAIT_FOR_NEW_TIMESTAMP();
+ touch("exports-qbs.qbs");
+ QCOMPARE(runQbs(QStringList({"-p", "MyTool"})), 0);
+ if (HostOsInfo::isMacosHost())
+ QEXPECT_FAIL("", "darwin-specific rules have fake dependencies on 'qbs' tag", Continue);
+ QVERIFY2(!m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
+
+ // Change tracking for accesses to product.exports (positive).
+ WAIT_FOR_NEW_TIMESTAMP();
+ REPLACE_IN_FILE("exports-qbs.qbs", "product.toolTags", "[]");
+ QCOMPARE(runQbs(QStringList({"-p", "MyTool"})), 0);
+ QVERIFY2(m_qbsStdout.contains("Creating MyTool.qbs"), m_qbsStdout.constData());
+}
+
void TestBlackbox::externalLibs()
{
QDir::setCurrent(testDataDir + "/external-libs");
diff --git a/tests/auto/blackbox/tst_blackbox.h b/tests/auto/blackbox/tst_blackbox.h
index 8f8bf40ba..a1b5dcc0b 100644
--- a/tests/auto/blackbox/tst_blackbox.h
+++ b/tests/auto/blackbox/tst_blackbox.h
@@ -102,6 +102,7 @@ private slots:
void exportedPropertyInDisabledProduct_data();
void exportRule();
void exportToOutsideSearchPath();
+ void exportsQbs();
void externalLibs();
void fileDependencies();
void generatedArtifactAsInputToDynamicRule();