diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2018-03-06 10:46:26 +0100 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2018-03-14 08:53:13 +0000 |
commit | 2acaba8ea211e1ba00c2e2844aa00ca16c7a04f4 (patch) | |
tree | 6be71fac638be27609fb6b196ce73d058780c67f | |
parent | 149e20aca1e401ba18bbae602df2caa7dc68c493 (diff) | |
download | qbs-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.qdoc | 130 | ||||
-rw-r--r-- | qbs-resources/imports/QbsLibrary.qbs | 23 | ||||
-rw-r--r-- | qbs-resources/modules/qbsbuildconfig/qbsbuildconfig.qbs | 2 | ||||
-rw-r--r-- | share/qbs/modules/Exporter/qbs/qbsexporter.js | 268 | ||||
-rw-r--r-- | share/qbs/modules/Exporter/qbs/qbsexporter.qbs | 78 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/consumer.cpp | 6 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/consumer.qbs | 14 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/exports-qbs.qbs | 120 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/helper.cpp.in | 6 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/helper.js | 1 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/mylib.cpp | 5 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/mylib.h | 17 | ||||
-rw-r--r-- | tests/auto/blackbox/testdata/exports-qbs/tool.cpp | 18 | ||||
-rw-r--r-- | tests/auto/blackbox/tst_blackbox.cpp | 59 | ||||
-rw-r--r-- | tests/auto/blackbox/tst_blackbox.h | 1 |
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(); |