summaryrefslogtreecommitdiff
path: root/src/lib/corelib/language/projecttreebuilder.cpp
diff options
context:
space:
mode:
authorChristian Kandeler <christian.kandeler@qt.io>2023-02-10 12:27:03 +0100
committerChristian Kandeler <christian.kandeler@qt.io>2023-04-20 08:09:46 +0000
commitfb52fed84a1510a7de0172e643d6fd66a780e2e8 (patch)
tree9fcf258fa8e8d8279a3c98d8e6fa37f3ae5c3dc0 /src/lib/corelib/language/projecttreebuilder.cpp
parente178052c763dbe18304a101d6f96e79881081e1a (diff)
downloadqbs-fb52fed84a1510a7de0172e643d6fd66a780e2e8.tar.gz
Rewrite ModuleLoader
=================== Problem description =================== The ModuleLoader class as it exists before this patch has a number of serious problems: - It instantiates modules over and over again everywhere a Depends item appears. The different instances are later merged in a hopelessly obscure and error-prone way. - It seriously pollutes the module scopes so that sloppily written project files appear to work even though they shouldn't. - Dependencies between products are handled twice: Once using the normal module instantiation code (with the Export item acting like a Module item), and also with a parallel mechanism that does strange, seemingly redundant things especially regarding transitive dependencies, which appear to introduce enormous run-time overhead. It is also split up between ModuleLoader and ProjectResolver, adding even more confusion. - The code is messy, seriously under-documented and hardly understood even by its original authors. It presents a huge obstacle to potential contributors. ================= Patch description ================= - Each module is instantiated once per product. Property values are merged on the fly. Special handling for dependencies between products are kept to the absolutely required minimum. - There are no more extra passes for establishing inter-product dependencies. Instead, whenever an unhandled dependency is encountered, processing the current product is paused and resumed once the dependency is ready, with the product state getting saved and restored in between so no work is done twice. - The ModuleLoader class now really only locates and loads modules. The new main class is called ProjectTreeBuilder, and we have split off small, self-contained pieces wherever possible. This process will be continued in follow-up patches (see next section). ======= Outlook ======= The ProjectTreeBuilder ist still too large and should be split up further into small, easily digestible parts with clear responsibilities, until the top-level code becomes tidy and self-documenting. In the end, it can probably be merged with ProjectResolver and Loader. However, this first requires the tight coupling between ProjectTreeBuilder/ModuleProviderLoader/ProbesResolver/ProjectResolver to be broken; otherwise we can't produce clean interfaces. As this would involve touching a lot of otherwise unrelated code, it is out of scope for this patch. ================= Benchmarking data ================= We first present wall-time clock results gathered by running "qbs resolve --log-time" for qbs itself and Qt Creator on macOS and Windows. The numbers are the average of several runs, with outliers removed. Then the same for a simple QML project using a static Qt on Linux (this case is special because our current way of handling plugins causes a huge amount of modules to be loaded). Finally, we show the output of the qbs_benchmarker tool for resolving qbs and Qt Creator on Linux. The data shows a speed-up that is hardly noticeable for simple projects, but increases sharply with project complexity. This suggests that our new way of resolving does not suffer anymore from the non-linear slowdown when the number of dependencies gets large. Resolving qbs on Windows: Before this patch: ModuleLoader 3.6s, ProjectResolver 870ms With this patch: ProjectTreeBuilder 3.6s, ProjectResolver 840ms Resolving Qt Creator on Windows: Before this patch: ModuleLoader 17s, ProjectResolver 6.8s With this patch: ProjectTreeBuilder 10.0s, ProjectResolver 6.5s Resolving qbs on macOS: Before this patch: ModuleLoader 4.0s, ProjectResolver 2.3s With this patch: ProjectTreeBuilder 4.0s, ProjectResolver 2.3s Resolving Qt Creator on macOS: Before this patch: ModuleLoader 32.0s, ProjectResolver 15.6s With this patch: ProjectTreeBuilder 23.0s, ProjectResolver 15.3s Note that the above numbers are for an initial resolve, so they include the time for running Probes. The speed-up for re-resolving (with cached Probes) is even higher, in particular on macOS, where Probes take excessively long. Resolving with static Qt on Linux (QBS-1521): Before this patch: ModuleLoader 36s, ProjectResolver 18s With this patch: ProjectTreeBuilder 1.5s, ProjectResolver 14s Output of qbs_benchmarker for resolving qbs on Linux: Old instruction count: 10029744668 New instruction count: 9079802661 Relative change: -10 % Old peak memory usage: 69881840 Bytes New peak memory usage: 82434624 Bytes Relative change: +17 % Output of qbs_benchmarker for resolving Qt Creator on Linux: Old instruction count: 87364681688 New instruction count: 53634332869 Relative change: -39 % Old peak memory usage: 578458840 Bytes New peak memory usage: 567271960 Bytes Relative change: -2 % I don't know where the increased memory footprint for a small project comes from, but since it goes away for larger projects, it doesn't seem worth investigating. Fixes: QBS-1037 Task-number: QBS-1521 Change-Id: Ieeebce8a7ff68cdffc15d645e2342ece2426fa94 Reviewed-by: Ivan Komissarov <ABBAPOH@gmail.com> Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
Diffstat (limited to 'src/lib/corelib/language/projecttreebuilder.cpp')
-rw-r--r--src/lib/corelib/language/projecttreebuilder.cpp2293
1 files changed, 2293 insertions, 0 deletions
diff --git a/src/lib/corelib/language/projecttreebuilder.cpp b/src/lib/corelib/language/projecttreebuilder.cpp
new file mode 100644
index 000000000..dedf26a88
--- /dev/null
+++ b/src/lib/corelib/language/projecttreebuilder.cpp
@@ -0,0 +1,2293 @@
+/****************************************************************************
+**
+** Copyright (C) 2023 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 "projecttreebuilder.h"
+
+#include "builtindeclarations.h"
+#include "evaluator.h"
+#include "filecontext.h"
+#include "groupshandler.h"
+#include "itemreader.h"
+#include "language.h"
+#include "localprofiles.h"
+#include "moduleinstantiator.h"
+#include "moduleloader.h"
+#include "modulepropertymerger.h"
+#include "moduleproviderloader.h"
+#include "probesresolver.h"
+#include "productitemmultiplexer.h"
+#include "scriptengine.h"
+#include "value.h"
+
+#include <logging/categories.h>
+#include <logging/translator.h>
+#include <tools/fileinfo.h>
+#include <tools/filetime.h>
+#include <tools/preferences.h>
+#include <tools/progressobserver.h>
+#include <tools/profile.h>
+#include <tools/profiling.h>
+#include <tools/scripttools.h>
+#include <tools/settings.h>
+#include <tools/setupprojectparameters.h>
+#include <tools/stringconstants.h>
+
+#include <QDir>
+#include <QDirIterator>
+#include <QFileInfo>
+
+#include <list>
+#include <memory>
+#include <optional>
+#include <queue>
+#include <utility>
+#include <vector>
+
+namespace qbs::Internal {
+
+using ShadowProductInfo = std::pair<bool, QString>;
+enum class Deferral { Allowed, NotAllowed };
+enum class HandleDependency { Use, Ignore, Defer };
+
+class SearchPathsManager {
+public:
+ SearchPathsManager(ItemReader &itemReader, const QStringList &extraSearchPaths);
+ SearchPathsManager(ItemReader &itemReader, ProductContext &product);
+ ~SearchPathsManager();
+
+private:
+ SearchPathsManager(ItemReader &itemReader, ProductContext *product,
+ const QStringList &extraSearchPaths);
+
+ ItemReader &m_itemReader;
+ ProductContext * const m_product = nullptr;
+ size_t m_oldSize{0};
+};
+
+class TimingData {
+public:
+ qint64 prepareProducts = 0;
+ qint64 productDependencies = 0;
+ qint64 moduleProviders = 0;
+ qint64 handleProducts = 0;
+ qint64 propertyChecking = 0;
+};
+
+class ProjectTreeBuilder::Private
+{
+public:
+ Private(const SetupProjectParameters &parameters, ItemPool &itemPool, Evaluator &evaluator,
+ Logger &logger)
+ : parameters(parameters), itemPool(itemPool), evaluator(evaluator), logger(logger) {}
+
+ Item *loadTopLevelProjectItem();
+ void checkOverriddenValues();
+ void collectNameFromOverride(const QString &overrideString);
+ Item *loadItemFromFile(const QString &filePath, const CodeLocation &referencingLocation);
+ Item *wrapInProjectIfNecessary(Item *item);
+ void handleTopLevelProject(Result &loadResult, const Set<QString> &referencedFilePaths);
+ void handleProject(Result *loadResult, TopLevelProjectContext *topLevelProjectContext,
+ Item *projectItem, const Set<QString> &referencedFilePaths);
+ void prepareProduct(ProjectContext &projectContext, Item *productItem);
+ void handleNextProduct(TopLevelProjectContext &tlp);
+ void handleProduct(ProductContext &productContext, Deferral deferral);
+ bool resolveDependencies(ProductContext &product, Deferral deferral);
+ void setSearchPathsForProduct(ProductContext &product);
+ void handleSubProject(ProjectContext &projectContext, Item *projectItem,
+ const Set<QString> &referencedFilePaths);
+ void initProductProperties(const ProductContext &product);
+ void printProfilingInfo();
+ void checkProjectNamesInOverrides(const TopLevelProjectContext &tlp);
+ void checkProductNamesInOverrides();
+ void collectProductsByName(const TopLevelProjectContext &topLevelProject);
+ void adjustDependsItemForMultiplexing(const ProductContext &product, Item *dependsItem);
+ ShadowProductInfo getShadowProductInfo(const ProductContext &product) const;
+ void handleProductError(const ErrorInfo &error, ProductContext &productContext);
+ void handleModuleSetupError(ProductContext &product, const Item::Module &module,
+ const ErrorInfo &error);
+ bool checkItemCondition(Item *item);
+ QStringList readExtraSearchPaths(Item *item, bool *wasSet = nullptr);
+ QList<Item *> multiplexProductItem(ProductContext &dummyContext, Item *productItem);
+ void checkCancelation() const;
+ QList<Item *> loadReferencedFile(const QString &relativePath,
+ const CodeLocation &referencingLocation,
+ const Set<QString> &referencedFilePaths,
+ ProductContext &dummyContext);
+ void copyProperties(const Item *sourceProject, Item *targetProject);
+ bool mergeExportItems(ProductContext &productContext);
+ bool checkExportItemCondition(Item *exportItem, const ProductContext &product);
+
+ Item *loadBaseModule(ProductContext &product, Item *item);
+
+ struct LoadModuleResult {
+ Item *moduleItem = nullptr;
+ ProductContext *product = nullptr;
+ HandleDependency handleDependency = HandleDependency::Use;
+ };
+ LoadModuleResult loadModule(ProductContext &product, Item *loadingItem,
+ const ProductContext::ResolvedAndMultiplexedDependsItem &dependency,
+ Deferral deferral);
+ Item *searchAndLoadModuleFile(ProductContext *productContext,
+ const CodeLocation &dependsItemLocation,
+ const QualifiedId &moduleName,
+ FallbackMode fallbackMode, bool isRequired);
+
+ std::optional<ProductContext::ResolvedDependsItem>
+ resolveDependsItem(const ProductContext &product, Item *dependsItem);
+ std::queue<ProductContext::ResolvedAndMultiplexedDependsItem>
+ multiplexDependency(const ProductContext &product,
+ const ProductContext::ResolvedDependsItem &dependency);
+ static bool haveSameSubProject(const ProductContext &p1, const ProductContext &p2);
+ QVariantMap extractParameters(Item *dependsItem) const;
+
+ const SetupProjectParameters &parameters;
+ ItemPool &itemPool;
+ Evaluator &evaluator;
+ Logger &logger;
+ ProgressObserver *progressObserver = nullptr;
+ TimingData timingData;
+ ItemReader reader{logger};
+ ProbesResolver probesResolver{&evaluator, logger};
+ ModuleProviderLoader moduleProviderLoader{&reader, &evaluator, &probesResolver, logger};
+ ModuleLoader moduleLoader{parameters, reader, evaluator, logger};
+ ModulePropertyMerger propertyMerger{parameters, evaluator, logger};
+ ModuleInstantiator moduleInstantiator{parameters, itemPool, propertyMerger, logger};
+ ProductItemMultiplexer multiplexer{parameters, evaluator, logger, [this](Item *productItem) {
+ return moduleInstantiator.retrieveQbsItem(productItem);
+ }};
+ GroupsHandler groupsHandler{parameters, moduleInstantiator, evaluator, logger};
+ LocalProfiles localProfiles{parameters, evaluator, logger};
+ FileTime lastResolveTime;
+ QVariantMap storedProfiles;
+
+ Set<Item *> disabledItems;
+ std::unique_ptr<Settings> settings;
+ Set<QString> projectNamesUsedInOverrides;
+ Set<QString> productNamesUsedInOverrides;
+ Set<QString> disabledProjects;
+ Set<QString> erroneousProducts;
+ std::multimap<QString, ProductContext *> productsByName;
+
+ // For fast look-up when resolving Depends.productTypes.
+ // The contract is that it contains fully handled, error-free, enabled products.
+ std::multimap<FileTag, ProductContext *> productsByType;
+
+ Version qbsVersion;
+ Item *tempScopeItem = nullptr;
+ QHash<std::pair<QString, QualifiedId>, std::optional<QString>> existingModulePathCache;
+ QMap<QString, QStringList> moduleDirListCache;
+
+private:
+ class TempBaseModuleAttacher {
+ public:
+ TempBaseModuleAttacher(Private *d, ProductContext &product);
+ ~TempBaseModuleAttacher() { drop(); }
+ void drop();
+ Item *tempBaseModuleItem() const { return m_tempBaseModule; }
+
+ private:
+ Item * const m_productItem;
+ ValuePtr m_origQbsValue;
+ Item *m_tempBaseModule = nullptr;
+ };
+};
+
+ProjectTreeBuilder::ProjectTreeBuilder(const SetupProjectParameters &parameters, ItemPool &itemPool,
+ Evaluator &evaluator, Logger &logger)
+ : d(new Private(parameters, itemPool, evaluator, logger))
+{
+ d->reader.setDeprecationWarningMode(parameters.deprecationWarningMode());
+ d->reader.setEnableTiming(parameters.logElapsedTime());
+ d->moduleProviderLoader.setProjectParameters(parameters);
+ d->probesResolver.setProjectParameters(parameters);
+ d->settings = std::make_unique<Settings>(parameters.settingsDirectory());
+}
+
+ProjectTreeBuilder::~ProjectTreeBuilder() { delete d; }
+
+void ProjectTreeBuilder::setProgressObserver(ProgressObserver *progressObserver)
+{
+ d->progressObserver = progressObserver;
+}
+
+void ProjectTreeBuilder::setSearchPaths(const QStringList &searchPaths)
+{
+ d->reader.setSearchPaths(searchPaths);
+ qCDebug(lcModuleLoader) << "initial search paths:" << searchPaths;
+}
+
+void ProjectTreeBuilder::setOldProjectProbes(const std::vector<ProbeConstPtr> &oldProbes)
+{
+ d->probesResolver.setOldProjectProbes(oldProbes);
+}
+
+void ProjectTreeBuilder::setOldProductProbes(
+ const QHash<QString, std::vector<ProbeConstPtr>> &oldProbes)
+{
+ d->probesResolver.setOldProductProbes(oldProbes);
+}
+
+void ProjectTreeBuilder::setLastResolveTime(const FileTime &time) { d->lastResolveTime = time; }
+
+void ProjectTreeBuilder::setStoredProfiles(const QVariantMap &profiles)
+{
+ d->storedProfiles = profiles;
+}
+
+void ProjectTreeBuilder::setStoredModuleProviderInfo(
+ const StoredModuleProviderInfo &moduleProviderInfo)
+{
+ d->moduleProviderLoader.setStoredModuleProviderInfo(moduleProviderInfo);
+}
+
+ProjectTreeBuilder::Result ProjectTreeBuilder::load()
+{
+ TimedActivityLogger mainTimer(d->logger, Tr::tr("ProjectTreeBuilder"),
+ d->parameters.logElapsedTime());
+ qCDebug(lcModuleLoader) << "load" << d->parameters.projectFilePath();
+
+ d->checkOverriddenValues();
+ d->reader.setPool(&d->itemPool);
+
+ Result result;
+ result.profileConfigs = d->storedProfiles;
+ result.root = d->loadTopLevelProjectItem();
+ d->handleTopLevelProject(result, {QDir::cleanPath(d->parameters.projectFilePath())});
+
+ result.qbsFiles = d->reader.filesRead() - d->moduleProviderLoader.tempQbsFiles();
+ for (auto it = d->localProfiles.profiles().begin(); it != d->localProfiles.profiles().end();
+ ++it) {
+ result.profileConfigs.remove(it.key());
+ }
+
+ d->printProfilingInfo();
+
+ return result;
+}
+
+Item *ProjectTreeBuilder::Private::loadTopLevelProjectItem()
+{
+ const QStringList topLevelSearchPaths
+ = parameters.finalBuildConfigurationTree()
+ .value(StringConstants::projectPrefix()).toMap()
+ .value(StringConstants::qbsSearchPathsProperty()).toStringList();
+ SearchPathsManager searchPathsManager(reader, topLevelSearchPaths);
+ Item * const root = loadItemFromFile(parameters.projectFilePath(), CodeLocation());
+ if (!root)
+ return {};
+
+ switch (root->type()) {
+ case ItemType::Product:
+ return wrapInProjectIfNecessary(root);
+ case ItemType::Project:
+ return root;
+ default:
+ throw ErrorInfo(Tr::tr("The top-level item must be of type 'Project' or 'Product', but it"
+ " is of type '%1'.").arg(root->typeName()), root->location());
+ }
+}
+
+void ProjectTreeBuilder::Private::checkOverriddenValues()
+{
+ static const auto matchesPrefix = [](const QString &key) {
+ static const QStringList prefixes({StringConstants::projectPrefix(),
+ QStringLiteral("projects"),
+ QStringLiteral("products"), QStringLiteral("modules"),
+ StringConstants::moduleProviders(),
+ StringConstants::qbsModule()});
+ for (const auto &prefix : prefixes) {
+ if (key.startsWith(prefix + QLatin1Char('.')))
+ return true;
+ }
+ return false;
+ };
+ const QVariantMap &overriddenValues = parameters.overriddenValues();
+ for (auto it = overriddenValues.begin(); it != overriddenValues.end(); ++it) {
+ if (matchesPrefix(it.key())) {
+ collectNameFromOverride(it.key());
+ continue;
+ }
+
+ ErrorInfo e(Tr::tr("Property override key '%1' not understood.").arg(it.key()));
+ e.append(Tr::tr("Please use one of the following:"));
+ e.append(QLatin1Char('\t') + Tr::tr("projects.<project-name>.<property-name>:value"));
+ e.append(QLatin1Char('\t') + Tr::tr("products.<product-name>.<property-name>:value"));
+ e.append(QLatin1Char('\t') + Tr::tr("modules.<module-name>.<property-name>:value"));
+ e.append(QLatin1Char('\t') + Tr::tr("products.<product-name>.<module-name>."
+ "<property-name>:value"));
+ e.append(QLatin1Char('\t') + Tr::tr("moduleProviders.<provider-name>."
+ "<property-name>:value"));
+ handlePropertyError(e, parameters, logger);
+ }
+}
+
+void ProjectTreeBuilder::Private::collectNameFromOverride(const QString &overrideString)
+{
+ static const auto extract = [](const QString &prefix, const QString &overrideString) {
+ if (!overrideString.startsWith(prefix))
+ return QString();
+ const int startPos = prefix.length();
+ const int endPos = overrideString.lastIndexOf(StringConstants::dot());
+ if (endPos == -1)
+ return QString();
+ return overrideString.mid(startPos, endPos - startPos);
+ };
+ const QString &projectName = extract(StringConstants::projectsOverridePrefix(), overrideString);
+ if (!projectName.isEmpty()) {
+ projectNamesUsedInOverrides.insert(projectName);
+ return;
+ }
+ const QString &productName = extract(StringConstants::productsOverridePrefix(), overrideString);
+ if (!productName.isEmpty()) {
+ productNamesUsedInOverrides.insert(productName.left(
+ productName.indexOf(StringConstants::dot())));
+ return;
+ }
+}
+
+Item *ProjectTreeBuilder::Private::loadItemFromFile(const QString &filePath,
+ const CodeLocation &referencingLocation)
+{
+ return reader.setupItemFromFile(filePath, referencingLocation, evaluator);
+}
+
+Item *ProjectTreeBuilder::Private::wrapInProjectIfNecessary(Item *item)
+{
+ if (item->type() == ItemType::Project)
+ return item;
+ Item *prj = Item::create(item->pool(), ItemType::Project);
+ Item::addChild(prj, item);
+ prj->setFile(item->file());
+ prj->setLocation(item->location());
+ prj->setupForBuiltinType(parameters.deprecationWarningMode(), logger);
+ return prj;
+}
+
+void ProjectTreeBuilder::Private::handleTopLevelProject(Result &loadResult,
+ const Set<QString> &referencedFilePaths)
+{
+ TopLevelProjectContext tlp;
+ tlp.buildDirectory = TopLevelProject::deriveBuildDirectory(
+ parameters.buildRoot(),
+ TopLevelProject::deriveId(parameters.finalBuildConfigurationTree()));
+ Item * const projectItem = loadResult.root;
+ projectItem->setProperty(StringConstants::sourceDirectoryProperty(),
+ VariantValue::create(QFileInfo(projectItem->file()->filePath())
+ .absolutePath()));
+ projectItem->setProperty(StringConstants::buildDirectoryProperty(),
+ VariantValue::create(tlp.buildDirectory));
+ projectItem->setProperty(StringConstants::profileProperty(),
+ VariantValue::create(parameters.topLevelProfile()));
+ handleProject(&loadResult, &tlp, projectItem, referencedFilePaths);
+ checkProjectNamesInOverrides(tlp);
+ collectProductsByName(tlp);
+ checkProductNamesInOverrides();
+
+ for (ProjectContext * const projectContext : qAsConst(tlp.projects)) {
+ for (ProductContext &productContext : projectContext->products)
+ tlp.productsToHandle.emplace_back(&productContext, -1);
+ }
+ while (!tlp.productsToHandle.empty())
+ handleNextProduct(tlp);
+
+ loadResult.projectProbes = tlp.probes;
+ loadResult.storedModuleProviderInfo = moduleProviderLoader.storedModuleProviderInfo();
+
+ reader.clearExtraSearchPathsStack();
+ AccumulatingTimer timer(parameters.logElapsedTime()
+ ? &timingData.propertyChecking : nullptr);
+ checkPropertyDeclarations(projectItem, disabledItems, parameters, logger);
+}
+
+void ProjectTreeBuilder::Private::handleProject(
+ Result *loadResult, TopLevelProjectContext *topLevelProjectContext, Item *projectItem,
+ const Set<QString> &referencedFilePaths)
+{
+ auto p = std::make_unique<ProjectContext>();
+ auto &projectContext = *p;
+ projectContext.topLevelProject = topLevelProjectContext;
+ projectContext.result = loadResult;
+ ItemValuePtr itemValue = ItemValue::create(projectItem);
+ projectContext.scope = Item::create(&itemPool, ItemType::Scope);
+ projectContext.scope->setFile(projectItem->file());
+ projectContext.scope->setProperty(StringConstants::projectVar(), itemValue);
+ ProductContext dummyProductContext;
+ dummyProductContext.project = &projectContext;
+ dummyProductContext.moduleProperties = parameters.finalBuildConfigurationTree();
+ dummyProductContext.profileModuleProperties = dummyProductContext.moduleProperties;
+ dummyProductContext.profileName = parameters.topLevelProfile();
+ loadBaseModule(dummyProductContext, projectItem);
+
+ projectItem->overrideProperties(parameters.overriddenValuesTree(),
+ StringConstants::projectPrefix(), parameters, logger);
+ projectContext.name = evaluator.stringValue(projectItem, StringConstants::nameProperty());
+ if (projectContext.name.isEmpty()) {
+ projectContext.name = FileInfo::baseName(projectItem->location().filePath());
+ projectItem->setProperty(StringConstants::nameProperty(),
+ VariantValue::create(projectContext.name));
+ }
+ projectItem->overrideProperties(parameters.overriddenValuesTree(),
+ StringConstants::projectsOverridePrefix() + projectContext.name,
+ parameters, logger);
+ if (!checkItemCondition(projectItem)) {
+ disabledProjects.insert(projectContext.name);
+ return;
+ }
+ topLevelProjectContext->projects.push_back(p.release());
+ SearchPathsManager searchPathsManager(reader, readExtraSearchPaths(projectItem)
+ << projectItem->file()->dirPath());
+ projectContext.searchPathsStack = reader.extraSearchPathsStack();
+ projectContext.item = projectItem;
+
+ const QString minVersionStr
+ = evaluator.stringValue(projectItem, StringConstants::minimumQbsVersionProperty(),
+ QStringLiteral("1.3.0"));
+ const Version minVersion = Version::fromString(minVersionStr);
+ if (!minVersion.isValid()) {
+ throw ErrorInfo(Tr::tr("The value '%1' of Project.minimumQbsVersion "
+ "is not a valid version string.")
+ .arg(minVersionStr), projectItem->location());
+ }
+ if (!qbsVersion.isValid())
+ qbsVersion = Version::fromString(QLatin1String(QBS_VERSION));
+ if (qbsVersion < minVersion) {
+ throw ErrorInfo(Tr::tr("The project requires at least qbs version %1, but "
+ "this is qbs version %2.").arg(minVersion.toString(),
+ qbsVersion.toString()));
+ }
+
+ for (Item * const child : projectItem->children())
+ child->setScope(projectContext.scope);
+
+ probesResolver.resolveProbes(&dummyProductContext, projectItem);
+ projectContext.topLevelProject->probes << dummyProductContext.info.probes;
+
+ localProfiles.collectProfilesFromItems(projectItem, projectContext.scope);
+
+ QList<Item *> multiplexedProducts;
+ for (Item * const child : projectItem->children()) {
+ if (child->type() == ItemType::Product)
+ multiplexedProducts << multiplexProductItem(dummyProductContext, child);
+ }
+ for (Item * const additionalProductItem : qAsConst(multiplexedProducts))
+ Item::addChild(projectItem, additionalProductItem);
+
+ const QList<Item *> originalChildren = projectItem->children();
+ for (Item * const child : originalChildren) {
+ switch (child->type()) {
+ case ItemType::Product:
+ prepareProduct(projectContext, child);
+ break;
+ case ItemType::SubProject:
+ handleSubProject(projectContext, child, referencedFilePaths);
+ break;
+ case ItemType::Project:
+ copyProperties(projectItem, child);
+ handleProject(loadResult, topLevelProjectContext, child, referencedFilePaths);
+ break;
+ default:
+ break;
+ }
+ }
+
+ const QStringList refs = evaluator.stringListValue(
+ projectItem, StringConstants::referencesProperty());
+ const CodeLocation referencingLocation
+ = projectItem->property(StringConstants::referencesProperty())->location();
+ QList<Item *> additionalProjectChildren;
+ for (const QString &filePath : refs) {
+ try {
+ additionalProjectChildren << loadReferencedFile(
+ filePath, referencingLocation, referencedFilePaths, dummyProductContext);
+ } catch (const ErrorInfo &error) {
+ if (parameters.productErrorMode() == ErrorHandlingMode::Strict)
+ throw;
+ logger.printWarning(error);
+ }
+ }
+ for (Item * const subItem : qAsConst(additionalProjectChildren)) {
+ Item::addChild(projectContext.item, subItem);
+ switch (subItem->type()) {
+ case ItemType::Product:
+ prepareProduct(projectContext, subItem);
+ break;
+ case ItemType::Project:
+ copyProperties(projectItem, subItem);
+ handleProject(loadResult, topLevelProjectContext, subItem,
+ Set<QString>(referencedFilePaths) << subItem->file()->filePath());
+ break;
+ default:
+ break;
+ }
+ }
+
+}
+
+void ProjectTreeBuilder::Private::prepareProduct(ProjectContext &projectContext, Item *productItem)
+{
+ AccumulatingTimer timer(parameters.logElapsedTime()
+ ? &timingData.prepareProducts : nullptr);
+ checkCancelation();
+ qCDebug(lcModuleLoader) << "prepareProduct" << productItem->file()->filePath();
+
+ ProductContext productContext;
+ productContext.item = productItem;
+ productContext.project = &projectContext;
+
+ // Retrieve name, profile and multiplex id.
+ productContext.name = evaluator.stringValue(productItem, StringConstants::nameProperty());
+ QBS_CHECK(!productContext.name.isEmpty());
+ const ItemValueConstPtr qbsItemValue = productItem->itemProperty(StringConstants::qbsModule());
+ if (qbsItemValue && qbsItemValue->item()->hasProperty(StringConstants::profileProperty())) {
+ TempBaseModuleAttacher tbma(this, productContext);
+ productContext.profileName = evaluator.stringValue(
+ tbma.tempBaseModuleItem(), StringConstants::profileProperty(), QString());
+ } else {
+ productContext.profileName = parameters.topLevelProfile();
+ }
+ productContext.multiplexConfigurationId = evaluator.stringValue(
+ productItem, StringConstants::multiplexConfigurationIdProperty());
+ QBS_CHECK(!productContext.profileName.isEmpty());
+
+ // Set up full module property map based on the profile.
+ const auto it = projectContext.result->profileConfigs.constFind(productContext.profileName);
+ QVariantMap flatConfig;
+ if (it == projectContext.result->profileConfigs.constEnd()) {
+ const Profile profile(productContext.profileName, settings.get(), localProfiles.profiles());
+ if (!profile.exists()) {
+ ErrorInfo error(Tr::tr("Profile '%1' does not exist.").arg(profile.name()),
+ productItem->location());
+ handleProductError(error, productContext);
+ return;
+ }
+ flatConfig = SetupProjectParameters::expandedBuildConfiguration(
+ profile, parameters.configurationName());
+ projectContext.result->profileConfigs.insert(productContext.profileName, flatConfig);
+ } else {
+ flatConfig = it.value().toMap();
+ }
+ productContext.profileModuleProperties = SetupProjectParameters::finalBuildConfigurationTree(
+ flatConfig, {});
+ productContext.moduleProperties = SetupProjectParameters::finalBuildConfigurationTree(
+ flatConfig, parameters.overriddenValues());
+ initProductProperties(productContext);
+
+ // Set up product scope. This is mainly for using the "product" and "project"
+ // variables in some contexts.
+ ItemValuePtr itemValue = ItemValue::create(productItem);
+ productContext.scope = Item::create(&itemPool, ItemType::Scope);
+ productContext.scope->setProperty(StringConstants::productVar(), itemValue);
+ productContext.scope->setFile(productItem->file());
+ productContext.scope->setScope(productContext.project->scope);
+
+ const bool hasExportItems = mergeExportItems(productContext);
+
+ setScopeForDescendants(productItem, productContext.scope);
+
+ projectContext.products.push_back(productContext);
+
+ if (!hasExportItems || getShadowProductInfo(productContext).first)
+ return;
+
+ // This "shadow product" exists only to pull in a dependency on the actual product
+ // and nothing else, thus providing us with the pure environment that we need to
+ // evaluate the product's exported properties in isolation in the project resolver.
+ Item * const importer = Item::create(productItem->pool(), ItemType::Product);
+ importer->setProperty(QStringLiteral("name"),
+ VariantValue::create(StringConstants::shadowProductPrefix()
+ + productContext.name));
+ importer->setFile(productItem->file());
+ importer->setLocation(productItem->location());
+ importer->setScope(projectContext.scope);
+ importer->setupForBuiltinType(parameters.deprecationWarningMode(), logger);
+ Item * const dependsItem = Item::create(productItem->pool(), ItemType::Depends);
+ dependsItem->setProperty(QStringLiteral("name"), VariantValue::create(productContext.name));
+ dependsItem->setProperty(QStringLiteral("required"), VariantValue::create(false));
+ dependsItem->setFile(importer->file());
+ dependsItem->setLocation(importer->location());
+ dependsItem->setupForBuiltinType(parameters.deprecationWarningMode(), logger);
+ Item::addChild(importer, dependsItem);
+ Item::addChild(productItem, importer);
+ prepareProduct(projectContext, importer);
+}
+
+void ProjectTreeBuilder::Private::handleNextProduct(TopLevelProjectContext &tlp)
+{
+ auto [product, queueSizeOnInsert] = tlp.productsToHandle.front();
+ tlp.productsToHandle.pop_front();
+
+ // If the queue of in-progress products has shrunk since the last time we tried handling
+ // this product, there has been forward progress and we can allow a deferral.
+ const Deferral deferral = queueSizeOnInsert == -1
+ || queueSizeOnInsert > int(tlp.productsToHandle.size())
+ ? Deferral::Allowed : Deferral::NotAllowed;
+
+ reader.setExtraSearchPathsStack(product->project->searchPathsStack);
+ try {
+ handleProduct(*product, deferral);
+ if (product->name.startsWith(StringConstants::shadowProductPrefix()))
+ tlp.probes << product->info.probes;
+ } catch (const ErrorInfo &err) {
+ handleProductError(err, *product);
+ }
+
+ // The search paths stack can change during dependency resolution (due to module providers);
+ // check that we've rolled back all the changes
+ QBS_CHECK(reader.extraSearchPathsStack() == product->project->searchPathsStack);
+
+ // If we encountered a dependency to an in-progress product or to a bulk dependency,
+ // we defer handling this product if it hasn't failed yet and there is still forward progress.
+ if (!product->info.delayedError.hasError() && !product->resolveDependenciesState.empty())
+ tlp.productsToHandle.emplace_back(product, int(tlp.productsToHandle.size()));
+}
+
+void ProjectTreeBuilder::Private::handleProduct(ProductContext &product, Deferral deferral)
+{
+ checkCancelation();
+
+ AccumulatingTimer timer(parameters.logElapsedTime() ? &timingData.handleProducts : nullptr);
+ if (product.info.delayedError.hasError())
+ return;
+
+ if (!resolveDependencies(product, deferral))
+ return;
+
+ // Run probes for modules and product.
+ for (const Item::Module &module : product.item->modules()) {
+ if (!module.item->isPresentModule())
+ continue;
+ if (module.productInfo && disabledItems.contains(module.productInfo->item)) {
+ createNonPresentModule(itemPool, module.name.toString(),
+ QLatin1String("module's exporting product is disabled"),
+ module.item);
+ continue;
+ }
+ try {
+ probesResolver.resolveProbes(&product, module.item);
+ if (module.versionRange.minimum.isValid()
+ || module.versionRange.maximum.isValid()) {
+ if (module.versionRange.maximum.isValid()
+ && module.versionRange.minimum >= module.versionRange.maximum) {
+ throw ErrorInfo(Tr::tr("Impossible version constraint [%1,%2) set for module "
+ "'%3'").arg(module.versionRange.minimum.toString(),
+ module.versionRange.maximum.toString(),
+ module.name.toString()));
+ }
+ const Version moduleVersion = Version::fromString(
+ evaluator.stringValue(module.item,
+ StringConstants::versionProperty()));
+ if (moduleVersion < module.versionRange.minimum) {
+ throw ErrorInfo(Tr::tr("Module '%1' has version %2, but it needs to be "
+ "at least %3.").arg(module.name.toString(),
+ moduleVersion.toString(),
+ module.versionRange.minimum.toString()));
+ }
+ if (module.versionRange.maximum.isValid()
+ && moduleVersion >= module.versionRange.maximum) {
+ throw ErrorInfo(Tr::tr("Module '%1' has version %2, but it needs to be "
+ "lower than %3.").arg(module.name.toString(),
+ moduleVersion.toString(),
+ module.versionRange.maximum.toString()));
+ }
+ }
+ } catch (const ErrorInfo &error) {
+ handleModuleSetupError(product, module, error);
+ if (product.info.delayedError.hasError())
+ return;
+ }
+ }
+ probesResolver.resolveProbes(&product, product.item);
+
+ // After the probes have run, we can switch on the evaluator cache.
+ FileTags fileTags = evaluator.fileTagsValue(product.item, StringConstants::typeProperty());
+ EvalCacheEnabler cacheEnabler(&evaluator, evaluator.stringValue(
+ product.item,
+ StringConstants::sourceDirectoryProperty()));
+
+ // Run module validation scripts.
+ for (const Item::Module &module : product.item->modules()) {
+ if (!module.item->isPresentModule())
+ continue;
+ try {
+ evaluator.boolValue(module.item, StringConstants::validateProperty());
+ for (const auto &dep : module.item->modules()) {
+ if (dep.required && !dep.item->isPresentModule()) {
+ throw ErrorInfo(Tr::tr("Module '%1' depends on module '%2', which was not "
+ "loaded successfully")
+ .arg(module.name.toString(), dep.name.toString()));
+ }
+ }
+ fileTags += evaluator.fileTagsValue(
+ module.item, StringConstants::additionalProductTypesProperty());
+ } catch (const ErrorInfo &error) {
+ handleModuleSetupError(product, module, error);
+ if (product.info.delayedError.hasError())
+ return;
+ }
+ }
+
+ // Disable modules that have been pulled in only by now-disabled modules.
+ // Note that this has to happen in the reverse order compared to the other loops,
+ // with the leaves checked last.
+ for (auto it = product.item->modules().rbegin(); it != product.item->modules().rend(); ++it) {
+ const Item::Module &module = *it;
+ if (!module.item->isPresentModule())
+ continue;
+ bool hasPresentLoadingItem = false;
+ for (const Item * const loadingItem : module.loadingItems) {
+ if (loadingItem == product.item) {
+ hasPresentLoadingItem = true;
+ break;
+ }
+ if (!loadingItem->isPresentModule())
+ continue;
+ if (loadingItem->prototype() && loadingItem->prototype()->type() == ItemType::Export) {
+ QBS_CHECK(loadingItem->prototype()->parent()->type() == ItemType::Product);
+ if (disabledItems.contains(loadingItem->prototype()->parent()))
+ continue;
+ }
+ hasPresentLoadingItem = true;
+ break;
+ }
+ if (!hasPresentLoadingItem) {
+ createNonPresentModule(itemPool, module.name.toString(),
+ QLatin1String("imported only by disabled module(s)"),
+ module.item);
+ continue;
+ }
+ }
+
+ // Now do the canonical module property values merge. Note that this will remove
+ // previously attached values from modules that failed validation.
+ // Evaluator cache entries that could potentially change due to this will be purged.
+ propertyMerger.doFinalMerge(product.item);
+
+ const bool enabled = checkItemCondition(product.item);
+ moduleLoader.checkDependencyParameterDeclarations({product.item, product.name, {}, {}});
+
+ groupsHandler.setupGroups(product.item, product.scope);
+ product.info.modulePropertiesSetInGroups = groupsHandler.modulePropertiesSetInGroups();
+ disabledItems.unite(groupsHandler.disabledGroups());
+
+ // Collect the full list of fileTags, including the values contributed by modules.
+ if (!product.info.delayedError.hasError() && enabled) {
+ for (const FileTag &tag : fileTags)
+ productsByType.insert({tag, &product});
+ product.item->setProperty(StringConstants::typeProperty(),
+ VariantValue::create(sorted(fileTags.toStringList())));
+ }
+ product.project->result->productInfos[product.item] = product.info;
+}
+
+bool ProjectTreeBuilder::Private::resolveDependencies(ProductContext &product, Deferral deferral)
+{
+ AccumulatingTimer timer(parameters.logElapsedTime()
+ ? &timingData.productDependencies : nullptr);
+
+ // Initialize the state with the direct Depends items of the product item.
+ // This branch is executed once per product, while the function might be entered
+ // multiple times due to deferrals.
+ if (product.resolveDependenciesState.empty()) {
+ setSearchPathsForProduct(product);
+ std::queue<Item *> topLevelDependsItems;
+ for (Item * const child : product.item->children()) {
+ if (child->type() == ItemType::Depends)
+ topLevelDependsItems.push(child);
+ }
+ product.resolveDependenciesState.push_front({product.item, {}, topLevelDependsItems, });
+ product.resolveDependenciesState.front().pendingResolvedDependencies.push(
+ ProductContext::ResolvedAndMultiplexedDependsItem::makeBaseDependency());
+ }
+
+ SearchPathsManager searchPathsMgr(reader, product);
+
+ while (!product.resolveDependenciesState.empty()) {
+fixme:
+ auto &state = product.resolveDependenciesState.front();
+ while (!state.pendingResolvedDependencies.empty()) {
+ QBS_CHECK(!state.currentDependsItem);
+ const auto dependency = state.pendingResolvedDependencies.front();
+ try {
+ const LoadModuleResult res = loadModule(product, state.loadingItem, dependency,
+ deferral);
+ switch (res.handleDependency) {
+ case HandleDependency::Defer:
+ QBS_CHECK(deferral == Deferral::Allowed);
+ if (res.product)
+ state.pendingResolvedDependencies.front().product = res.product;
+ return false;
+ case HandleDependency::Ignore:
+ state.pendingResolvedDependencies.pop();
+ continue;
+ case HandleDependency::Use:
+ if (dependency.name.toString() == StringConstants::qbsModule()) {
+ state.pendingResolvedDependencies.pop();
+ continue;
+ }
+ break;
+ }
+
+ QBS_CHECK(res.moduleItem);
+ std::queue<Item *> moduleDependsItems;
+ for (Item * const child : res.moduleItem->children()) {
+ if (child->type() == ItemType::Depends)
+ moduleDependsItems.push(child);
+ }
+
+ state.pendingResolvedDependencies.pop();
+ product.resolveDependenciesState.push_front(
+ {res.moduleItem, dependency, moduleDependsItems, {}, {},
+ dependency.requiredGlobally || state.requiredByLoadingItem});
+ product.resolveDependenciesState.front().pendingResolvedDependencies.push(
+ ProductContext::ResolvedAndMultiplexedDependsItem::makeBaseDependency());
+ break;
+ } catch (const ErrorInfo &e) {
+ if (dependency.name.toString() == StringConstants::qbsModule())
+ throw e;
+
+ if (!dependency.requiredLocally) {
+ state.pendingResolvedDependencies.pop();
+ continue;
+ }
+
+ // See QBS-1338 for why we do not abort handling the product.
+ state.pendingResolvedDependencies.pop();
+ Item::Modules &modules = product.item->modules();
+ while (product.resolveDependenciesState.size() > 1) {
+ const auto loadingItemModule = std::find_if(
+ modules.begin(), modules.end(), [&](const Item::Module &m) {
+ return m.item == product.resolveDependenciesState.front().loadingItem;
+ });
+ for (auto it = loadingItemModule; it != modules.end(); ++it) {
+ createNonPresentModule(itemPool, it->name.toString(),
+ QLatin1String("error in Depends chain"), it->item);
+ }
+ modules.erase(loadingItemModule, modules.end());
+ product.resolveDependenciesState.pop_front();
+ }
+ handleProductError(e, product);
+ goto fixme;
+ }
+ }
+ if (&state != &product.resolveDependenciesState.front())
+ continue;
+
+ if (state.currentDependsItem) {
+ QBS_CHECK(state.pendingResolvedDependencies.empty());
+
+ // We postpone handling Depends.productTypes for as long as possible, because
+ // the full type of a product becomes available only after its modules have been loaded.
+ if (!state.currentDependsItem->productTypes.empty() && deferral == Deferral::Allowed)
+ return false;
+
+ state.pendingResolvedDependencies = multiplexDependency(product,
+ *state.currentDependsItem);
+ state.currentDependsItem.reset();
+ continue;
+ }
+
+ while (!state.pendingDependsItems.empty()) {
+ QBS_CHECK(!state.currentDependsItem);
+ QBS_CHECK(state.pendingResolvedDependencies.empty());
+ Item * const dependsItem = state.pendingDependsItems.front();
+ state.pendingDependsItems.pop();
+ adjustDependsItemForMultiplexing(product, dependsItem);
+ state.currentDependsItem = resolveDependsItem(product, dependsItem);
+ if (!state.currentDependsItem)
+ continue;
+ state.currentDependsItem->requiredGlobally = state.currentDependsItem->requiredLocally
+ && state.loadingItemOrigin.requiredGlobally;
+ goto fixme;
+ }
+
+ QBS_CHECK(!state.currentDependsItem);
+ QBS_CHECK(state.pendingResolvedDependencies.empty());
+ QBS_CHECK(state.pendingDependsItems.empty());
+
+ // This ensures a sorted module list in the product (dependers after dependencies).
+ if (product.resolveDependenciesState.size() > 1) {
+ QBS_CHECK(state.loadingItem->type() == ItemType::ModuleInstance);
+ Item::Modules &modules = product.item->modules();
+ const auto loadingItemModule = std::find_if(modules.begin(), modules.end(),
+ [&](const Item::Module &m) {
+ return m.item == state.loadingItem;
+ });
+ QBS_CHECK(loadingItemModule != modules.end());
+ const Item::Module tempModule = *loadingItemModule;
+ modules.erase(loadingItemModule);
+ modules.push_back(tempModule);
+ }
+ product.resolveDependenciesState.pop_front();
+ }
+ return true;
+}
+
+void ProjectTreeBuilder::Private::setSearchPathsForProduct(ProductContext &product)
+{
+ QBS_CHECK(product.searchPaths.isEmpty());
+
+ product.searchPaths = readExtraSearchPaths(product.item);
+ Settings settings(parameters.settingsDirectory());
+ const QStringList prefsSearchPaths = Preferences(&settings, product.profileModuleProperties)
+ .searchPaths();
+ const QStringList &currentSearchPaths = reader.allSearchPaths();
+ for (const QString &p : prefsSearchPaths) {
+ if (!currentSearchPaths.contains(p) && FileInfo(p).exists())
+ product.searchPaths << p;
+ }
+}
+
+void ProjectTreeBuilder::Private::handleSubProject(
+ ProjectContext &projectContext, Item *projectItem, const Set<QString> &referencedFilePaths)
+{
+ qCDebug(lcModuleLoader) << "handleSubProject" << projectItem->file()->filePath();
+
+ Item * const propertiesItem = projectItem->child(ItemType::PropertiesInSubProject);
+ if (!checkItemCondition(projectItem))
+ return;
+ if (propertiesItem) {
+ propertiesItem->setScope(projectItem);
+ if (!checkItemCondition(propertiesItem))
+ return;
+ }
+
+ Item *loadedItem = nullptr;
+ QString subProjectFilePath;
+ try {
+ const QString projectFileDirPath = FileInfo::path(projectItem->file()->filePath());
+ const QString relativeFilePath
+ = evaluator.stringValue(projectItem, StringConstants::filePathProperty());
+ subProjectFilePath = FileInfo::resolvePath(projectFileDirPath, relativeFilePath);
+ if (referencedFilePaths.contains(subProjectFilePath))
+ throw ErrorInfo(Tr::tr("Cycle detected while loading subproject file '%1'.")
+ .arg(relativeFilePath), projectItem->location());
+ loadedItem = loadItemFromFile(subProjectFilePath, projectItem->location());
+ } catch (const ErrorInfo &error) {
+ if (parameters.productErrorMode() == ErrorHandlingMode::Strict)
+ throw;
+ logger.printWarning(error);
+ return;
+ }
+
+ loadedItem = wrapInProjectIfNecessary(loadedItem);
+ const bool inheritProperties = evaluator.boolValue(
+ projectItem, StringConstants::inheritPropertiesProperty());
+
+ if (inheritProperties)
+ copyProperties(projectItem->parent(), loadedItem);
+ if (propertiesItem) {
+ const Item::PropertyMap &overriddenProperties = propertiesItem->properties();
+ for (auto it = overriddenProperties.begin(); it != overriddenProperties.end(); ++it)
+ loadedItem->setProperty(it.key(), it.value());
+ }
+
+ Item::addChild(projectItem, loadedItem);
+ projectItem->setScope(projectContext.scope);
+ handleProject(projectContext.result, projectContext.topLevelProject, loadedItem,
+ Set<QString>(referencedFilePaths) << subProjectFilePath);
+}
+
+void ProjectTreeBuilder::Private::initProductProperties(const ProductContext &product)
+{
+ QString buildDir = ResolvedProduct::deriveBuildDirectoryName(product.name,
+ product.multiplexConfigurationId);
+ buildDir = FileInfo::resolvePath(product.project->topLevelProject->buildDirectory, buildDir);
+ product.item->setProperty(StringConstants::buildDirectoryProperty(),
+ VariantValue::create(buildDir));
+ const QString sourceDir = QFileInfo(product.item->file()->filePath()).absolutePath();
+ product.item->setProperty(StringConstants::sourceDirectoryProperty(),
+ VariantValue::create(sourceDir));
+}
+
+void ProjectTreeBuilder::Private::printProfilingInfo()
+{
+ if (!parameters.logElapsedTime())
+ return;
+ logger.qbsLog(LoggerInfo, true) << " "
+ << Tr::tr("Project file loading and parsing took %1.")
+ .arg(elapsedTimeString(reader.elapsedTime()));
+ logger.qbsLog(LoggerInfo, true) << " "
+ << Tr::tr("Preparing products took %1.")
+ .arg(elapsedTimeString(timingData.prepareProducts));
+ logger.qbsLog(LoggerInfo, true) << " "
+ << Tr::tr("Handling products took %1.")
+ .arg(elapsedTimeString(timingData.handleProducts));
+ logger.qbsLog(LoggerInfo, true) << " "
+ << Tr::tr("Setting up product dependencies took %1.")
+ .arg(elapsedTimeString(timingData.productDependencies));
+ logger.qbsLog(LoggerInfo, true) << " "
+ << Tr::tr("Running module providers took %1.")
+ .arg(elapsedTimeString(timingData.moduleProviders));
+ moduleInstantiator.printProfilingInfo(6);
+ propertyMerger.printProfilingInfo(6);
+ groupsHandler.printProfilingInfo(4);
+ probesResolver.printProfilingInfo(4);
+ logger.qbsLog(LoggerInfo, true) << " "
+ << Tr::tr("Property checking took %1.")
+ .arg(elapsedTimeString(timingData.propertyChecking));
+}
+
+void ProjectTreeBuilder::Private::checkProjectNamesInOverrides(const TopLevelProjectContext &tlp)
+{
+ for (const QString &projectNameInOverride : projectNamesUsedInOverrides) {
+ if (disabledProjects.contains(projectNameInOverride))
+ continue;
+ if (!any_of(tlp.projects, [&projectNameInOverride](const ProjectContext *p) {
+ return p->name == projectNameInOverride; })) {
+ handlePropertyError(Tr::tr("Unknown project '%1' in property override.")
+ .arg(projectNameInOverride), parameters, logger);
+ }
+ }
+}
+
+void ProjectTreeBuilder::Private::checkProductNamesInOverrides()
+{
+ for (const QString &productNameInOverride : productNamesUsedInOverrides) {
+ if (erroneousProducts.contains(productNameInOverride))
+ continue;
+ if (!any_of(productsByName, [&productNameInOverride](
+ const std::pair<QString, ProductContext *> &elem) {
+ // In an override string such as "a.b.c:d, we cannot tell whether we have a product
+ // "a" and a module "b.c" or a product "a.b" and a module "c", so we need to take
+ // care not to emit false positives here.
+ return elem.first == productNameInOverride
+ || elem.first.startsWith(productNameInOverride + StringConstants::dot());
+ })) {
+ handlePropertyError(Tr::tr("Unknown product '%1' in property override.")
+ .arg(productNameInOverride), parameters, logger);
+ }
+ }
+}
+
+void ProjectTreeBuilder::Private::collectProductsByName(
+ const TopLevelProjectContext &topLevelProject)
+{
+ for (ProjectContext * const project : topLevelProject.projects) {
+ for (ProductContext &product : project->products)
+ productsByName.insert({product.name, &product});
+ }
+}
+
+void ProjectTreeBuilder::Private::adjustDependsItemForMultiplexing(const ProductContext &product,
+ Item *dependsItem)
+{
+ const QString name = evaluator.stringValue(dependsItem, StringConstants::nameProperty());
+ const bool productIsMultiplexed = !product.multiplexConfigurationId.isEmpty();
+ if (name == product.name) {
+ QBS_CHECK(!productIsMultiplexed); // This product must be an aggregator.
+ return;
+ }
+ const auto productRange = productsByName.equal_range(name);
+ if (productRange.first == productRange.second)
+ return; // Dependency is a module. Nothing to adjust.
+
+ bool profilesPropertyIsSet;
+ const QStringList profiles = evaluator.stringListValue(
+ dependsItem, StringConstants::profilesProperty(), &profilesPropertyIsSet);
+
+ std::vector<const ProductContext *> multiplexedDependencies;
+ bool hasNonMultiplexedDependency = false;
+ for (auto it = productRange.first; it != productRange.second; ++it) {
+ if (!it->second->multiplexConfigurationId.isEmpty())
+ multiplexedDependencies.push_back(it->second);
+ else
+ hasNonMultiplexedDependency = true;
+ }
+ bool hasMultiplexedDependencies = !multiplexedDependencies.empty();
+
+ static const auto multiplexConfigurationIntersects = [](const QVariantMap &lhs,
+ const QVariantMap &rhs) {
+ QBS_CHECK(!lhs.isEmpty() && !rhs.isEmpty());
+ for (auto lhsProperty = lhs.constBegin(); lhsProperty != lhs.constEnd(); lhsProperty++) {
+ const auto rhsProperty = rhs.find(lhsProperty.key());
+ const bool isCommonProperty = rhsProperty != rhs.constEnd();
+ if (isCommonProperty && lhsProperty.value() != rhsProperty.value())
+ return false;
+ }
+ return true;
+ };
+
+ // These are the allowed cases:
+ // (1) Normal dependency with no multiplexing whatsoever.
+ // (2) Both product and dependency are multiplexed.
+ // (2a) The profiles property is not set, we want to depend on the best
+ // matching variant.
+ // (2b) The profiles property is set, we want to depend on all variants
+ // with a matching profile.
+ // (3) The product is not multiplexed, but the dependency is.
+ // (3a) The profiles property is not set, the dependency has an aggregator.
+ // We want to depend on the aggregator.
+ // (3b) The profiles property is not set, the dependency does not have an
+ // aggregator. We want to depend on all the multiplexed variants.
+ // (3c) The profiles property is set, we want to depend on all variants
+ // with a matching profile regardless of whether an aggregator exists or not.
+ // (4) The product is multiplexed, but the dependency is not. We don't have to adapt
+ // any Depends items.
+ // (5) The product is a "shadow product". In that case, we know which product
+ // it should have a dependency on, and we make sure we depend on that.
+
+ // (1) and (4)
+ if (!hasMultiplexedDependencies)
+ return;
+
+ // (3a)
+ if (!productIsMultiplexed && hasNonMultiplexedDependency && !profilesPropertyIsSet)
+ return;
+
+ QStringList multiplexIds;
+ const ShadowProductInfo shadowProductInfo = getShadowProductInfo(product);
+ const bool isShadowProduct = shadowProductInfo.first && shadowProductInfo.second == name;
+ const auto productMultiplexConfig
+ = multiplexer.multiplexIdToVariantMap(product.multiplexConfigurationId);
+
+ for (const ProductContext *dependency : multiplexedDependencies) {
+ const bool depMatchesShadowProduct = isShadowProduct
+ && dependency->item == product.item->parent();
+ const QString depMultiplexId = dependency->multiplexConfigurationId;
+ if (depMatchesShadowProduct) { // (5)
+ dependsItem->setProperty(StringConstants::multiplexConfigurationIdsProperty(),
+ VariantValue::create(depMultiplexId));
+ return;
+ }
+ if (productIsMultiplexed && !profilesPropertyIsSet) { // 2a
+ if (dependency->multiplexConfigurationId == product.multiplexConfigurationId) {
+ const ValuePtr &multiplexId = product.item->property(
+ StringConstants::multiplexConfigurationIdProperty());
+ dependsItem->setProperty(StringConstants::multiplexConfigurationIdsProperty(),
+ multiplexId);
+ return;
+
+ }
+ // Otherwise collect partial matches and decide later
+ const auto dependencyMultiplexConfig
+ = multiplexer.multiplexIdToVariantMap(dependency->multiplexConfigurationId);
+
+ if (multiplexConfigurationIntersects(dependencyMultiplexConfig, productMultiplexConfig))
+ multiplexIds << dependency->multiplexConfigurationId;
+ } else {
+ // (2b), (3b) or (3c)
+ const bool profileMatch = !profilesPropertyIsSet || profiles.empty()
+ || profiles.contains(dependency->profileName);
+ if (profileMatch)
+ multiplexIds << depMultiplexId;
+ }
+ }
+ if (multiplexIds.empty()) {
+ const QString productName = ProductItemMultiplexer::fullProductDisplayName(
+ product.name, product.multiplexConfigurationId);
+ throw ErrorInfo(Tr::tr("Dependency from product '%1' to product '%2' not fulfilled. "
+ "There are no eligible multiplex candidates.").arg(productName,
+ name),
+ dependsItem->location());
+ }
+
+ // In case of (2a), at most 1 match is allowed
+ if (productIsMultiplexed && !profilesPropertyIsSet && multiplexIds.size() > 1) {
+ const QString productName = ProductItemMultiplexer::fullProductDisplayName(
+ product.name, product.multiplexConfigurationId);
+ QStringList candidateNames;
+ for (const auto &id : qAsConst(multiplexIds))
+ candidateNames << ProductItemMultiplexer::fullProductDisplayName(name, id);
+ throw ErrorInfo(Tr::tr("Dependency from product '%1' to product '%2' is ambiguous. "
+ "Eligible multiplex candidates: %3.").arg(
+ productName, name, candidateNames.join(QLatin1String(", "))),
+ dependsItem->location());
+ }
+
+ dependsItem->setProperty(StringConstants::multiplexConfigurationIdsProperty(),
+ VariantValue::create(multiplexIds));
+}
+
+ShadowProductInfo ProjectTreeBuilder::Private::getShadowProductInfo(
+ const ProductContext &product) const
+{
+ const bool isShadowProduct = product.name.startsWith(StringConstants::shadowProductPrefix());
+ return std::make_pair(isShadowProduct, isShadowProduct
+ ? product.name.mid(StringConstants::shadowProductPrefix().size())
+ : QString());
+}
+
+void ProjectTreeBuilder::Private::handleProductError(const ErrorInfo &error,
+ ProductContext &productContext)
+{
+ const bool alreadyHadError = productContext.info.delayedError.hasError();
+ if (!alreadyHadError) {
+ productContext.info.delayedError.append(Tr::tr("Error while handling product '%1':")
+ .arg(productContext.name),
+ productContext.item->location());
+ }
+ if (error.isInternalError()) {
+ if (alreadyHadError) {
+ qCDebug(lcModuleLoader()) << "ignoring subsequent internal error" << error.toString()
+ << "in product" << productContext.name;
+ return;
+ }
+ }
+ const auto errorItems = error.items();
+ for (const ErrorItem &ei : errorItems)
+ productContext.info.delayedError.append(ei.description(), ei.codeLocation());
+ productContext.project->result->productInfos[productContext.item] = productContext.info;
+ disabledItems << productContext.item;
+ erroneousProducts.insert(productContext.name);
+}
+
+void ProjectTreeBuilder::Private::handleModuleSetupError(
+ ProductContext &product, const Item::Module &module, const ErrorInfo &error)
+{
+ if (module.required) {
+ handleProductError(error, product);
+ } else {
+ qCDebug(lcModuleLoader()) << "non-required module" << module.name.toString()
+ << "found, but not usable in product" << product.name
+ << error.toString();
+ createNonPresentModule(itemPool, module.name.toString(),
+ QStringLiteral("failed validation"), module.item);
+ }
+}
+
+bool ProjectTreeBuilder::Private::checkItemCondition(Item *item)
+{
+ if (evaluator.boolValue(item, StringConstants::conditionProperty()))
+ return true;
+ disabledItems += item;
+ return false;
+}
+
+QStringList ProjectTreeBuilder::Private::readExtraSearchPaths(Item *item, bool *wasSet)
+{
+ QStringList result;
+ const QStringList paths = evaluator.stringListValue(
+ item, StringConstants::qbsSearchPathsProperty(), wasSet);
+ const JSSourceValueConstPtr prop = item->sourceProperty(
+ StringConstants::qbsSearchPathsProperty());
+
+ // Value can come from within a project file or as an overridden value from the user
+ // (e.g command line).
+ const QString basePath = FileInfo::path(prop ? prop->file()->filePath()
+ : parameters.projectFilePath());
+ for (const QString &path : paths)
+ result += FileInfo::resolvePath(basePath, path);
+ return result;
+}
+
+QList<Item *> ProjectTreeBuilder::Private::multiplexProductItem(ProductContext &dummyContext,
+ Item *productItem)
+{
+ // Overriding the product item properties must be done here already, because multiplexing
+ // properties might depend on product properties.
+ const QString &nameKey = StringConstants::nameProperty();
+ QString productName = evaluator.stringValue(productItem, nameKey);
+ if (productName.isEmpty()) {
+ productName = FileInfo::completeBaseName(productItem->file()->filePath());
+ productItem->setProperty(nameKey, VariantValue::create(productName));
+ }
+ productItem->overrideProperties(parameters.overriddenValuesTree(),
+ StringConstants::productsOverridePrefix() + productName,
+ parameters, logger);
+ dummyContext.item = productItem;
+ TempBaseModuleAttacher tbma(this, dummyContext);
+ return multiplexer.multiplex(productName, productItem, tbma.tempBaseModuleItem(),
+ [&] { tbma.drop(); });
+}
+
+void ProjectTreeBuilder::Private::checkCancelation() const
+{
+ if (progressObserver && progressObserver->canceled()) {
+ throw ErrorInfo(Tr::tr("Project resolving canceled for configuration %1.")
+ .arg(TopLevelProject::deriveId(
+ parameters.finalBuildConfigurationTree())));
+ }
+}
+
+QList<Item *> ProjectTreeBuilder::Private::loadReferencedFile(
+ const QString &relativePath, const CodeLocation &referencingLocation,
+ const Set<QString> &referencedFilePaths, ProductContext &dummyContext)
+{
+ QString absReferencePath = FileInfo::resolvePath(FileInfo::path(referencingLocation.filePath()),
+ relativePath);
+ if (FileInfo(absReferencePath).isDir()) {
+ QString qbsFilePath;
+
+ QDirIterator dit(absReferencePath, StringConstants::qbsFileWildcards());
+ while (dit.hasNext()) {
+ if (!qbsFilePath.isEmpty()) {
+ throw ErrorInfo(Tr::tr("Referenced directory '%1' contains more than one "
+ "qbs file.").arg(absReferencePath), referencingLocation);
+ }
+ qbsFilePath = dit.next();
+ }
+ if (qbsFilePath.isEmpty()) {
+ throw ErrorInfo(Tr::tr("Referenced directory '%1' does not contain a qbs file.")
+ .arg(absReferencePath), referencingLocation);
+ }
+ absReferencePath = qbsFilePath;
+ }
+ if (referencedFilePaths.contains(absReferencePath))
+ throw ErrorInfo(Tr::tr("Cycle detected while referencing file '%1'.").arg(relativePath),
+ referencingLocation);
+ Item * const subItem = loadItemFromFile(absReferencePath, referencingLocation);
+ if (subItem->type() != ItemType::Project && subItem->type() != ItemType::Product) {
+ ErrorInfo error(Tr::tr("Item type should be 'Product' or 'Project', but is '%1'.")
+ .arg(subItem->typeName()));
+ error.append(Tr::tr("Item is defined here."), subItem->location());
+ error.append(Tr::tr("File is referenced here."), referencingLocation);
+ throw error;
+ }
+ subItem->setScope(dummyContext.project->scope);
+ subItem->setParent(dummyContext.project->item);
+ QList<Item *> loadedItems;
+ loadedItems << subItem;
+ if (subItem->type() == ItemType::Product) {
+ localProfiles.collectProfilesFromItems(subItem, dummyContext.project->scope);
+ loadedItems << multiplexProductItem(dummyContext, subItem);
+ }
+ return loadedItems;
+}
+
+void ProjectTreeBuilder::Private::copyProperties(const Item *sourceProject, Item *targetProject)
+{
+ if (!sourceProject)
+ return;
+ const QList<PropertyDeclaration> builtinProjectProperties
+ = BuiltinDeclarations::instance().declarationsForType(ItemType::Project).properties();
+ Set<QString> builtinProjectPropertyNames;
+ for (const PropertyDeclaration &p : builtinProjectProperties)
+ builtinProjectPropertyNames << p.name();
+
+ for (auto it = sourceProject->propertyDeclarations().begin();
+ it != sourceProject->propertyDeclarations().end(); ++it) {
+
+ // We must not inherit built-in properties such as "name",
+ // but there are exceptions.
+ if (it.key() == StringConstants::qbsSearchPathsProperty()
+ || it.key() == StringConstants::profileProperty()
+ || it.key() == StringConstants::buildDirectoryProperty()
+ || it.key() == StringConstants::sourceDirectoryProperty()
+ || it.key() == StringConstants::minimumQbsVersionProperty()) {
+ const JSSourceValueConstPtr &v = targetProject->sourceProperty(it.key());
+ QBS_ASSERT(v, continue);
+ if (v->sourceCode() == StringConstants::undefinedValue())
+ sourceProject->copyProperty(it.key(), targetProject);
+ continue;
+ }
+
+ if (builtinProjectPropertyNames.contains(it.key()))
+ continue;
+
+ if (targetProject->hasOwnProperty(it.key()))
+ continue; // Ignore stuff the target project already has.
+
+ targetProject->setPropertyDeclaration(it.key(), it.value());
+ sourceProject->copyProperty(it.key(), targetProject);
+ }
+}
+
+static void mergeParameters(QVariantMap &dst, const QVariantMap &src)
+{
+ for (auto it = src.begin(); it != src.end(); ++it) {
+ if (it.value().userType() == QMetaType::QVariantMap) {
+ QVariant &vdst = dst[it.key()];
+ QVariantMap mdst = vdst.toMap();
+ mergeParameters(mdst, it.value().toMap());
+ vdst = mdst;
+ } else {
+ dst[it.key()] = it.value();
+ }
+ }
+}
+
+static void adjustParametersScopes(Item *item, Item *scope)
+{
+ if (item->type() == ItemType::ModuleParameters) {
+ item->setScope(scope);
+ return;
+ }
+
+ for (const auto &value : item->properties()) {
+ if (value->type() == Value::ItemValueType)
+ adjustParametersScopes(std::static_pointer_cast<ItemValue>(value)->item(), scope);
+ }
+}
+
+static void mergeProperty(Item *dst, const QString &name, const ValuePtr &value)
+{
+ if (value->type() == Value::ItemValueType) {
+ const ItemValueConstPtr itemValue = std::static_pointer_cast<ItemValue>(value);
+ const Item * const valueItem = itemValue->item();
+ Item * const subItem = dst->itemProperty(name, itemValue)->item();
+ for (auto it = valueItem->properties().begin(); it != valueItem->properties().end(); ++it)
+ mergeProperty(subItem, it.key(), it.value());
+ return;
+ }
+
+ // If the property already exists, set up the base value.
+ if (value->type() == Value::JSSourceValueType) {
+ const auto jsValue = static_cast<JSSourceValue *>(value.get());
+ if (jsValue->isBuiltinDefaultValue())
+ return;
+ const ValuePtr baseValue = dst->property(name);
+ if (baseValue) {
+ QBS_CHECK(baseValue->type() == Value::JSSourceValueType);
+ const JSSourceValuePtr jsBaseValue = std::static_pointer_cast<JSSourceValue>(
+ baseValue->clone());
+ jsValue->setBaseValue(jsBaseValue);
+ std::vector<JSSourceValue::Alternative> alternatives = jsValue->alternatives();
+ jsValue->clearAlternatives();
+ for (JSSourceValue::Alternative &a : alternatives) {
+ a.value->setBaseValue(jsBaseValue);
+ jsValue->addAlternative(a);
+ }
+ }
+ }
+ dst->setProperty(name, value);
+}
+
+bool ProjectTreeBuilder::Private::mergeExportItems(ProductContext &productContext)
+{
+ std::vector<Item *> exportItems;
+ QList<Item *> children = productContext.item->children();
+
+ const auto isExport = [](Item *item) { return item->type() == ItemType::Export; };
+ std::copy_if(children.cbegin(), children.cend(), std::back_inserter(exportItems), isExport);
+ qbs::Internal::removeIf(children, isExport);
+
+ // Note that we do not return if there are no Export items: The "merged" item becomes the
+ // "product module", which always needs to exist, regardless of whether the product sources
+ // actually contain an Export item or not.
+ if (!exportItems.empty())
+ productContext.item->setChildren(children);
+
+ Item *merged = Item::create(productContext.item->pool(), ItemType::Export);
+ const QString &nameKey = StringConstants::nameProperty();
+ const ValuePtr nameValue = VariantValue::create(productContext.name);
+ merged->setProperty(nameKey, nameValue);
+ Set<FileContextConstPtr> filesWithExportItem;
+ QVariantMap defaultParameters;
+ for (Item * const exportItem : qAsConst(exportItems)) {
+ checkCancelation();
+ if (Q_UNLIKELY(filesWithExportItem.contains(exportItem->file())))
+ throw ErrorInfo(Tr::tr("Multiple Export items in one product are prohibited."),
+ exportItem->location());
+ exportItem->setProperty(nameKey, nameValue);
+ if (!checkExportItemCondition(exportItem, productContext))
+ continue;
+ filesWithExportItem += exportItem->file();
+ for (Item * const child : exportItem->children()) {
+ if (child->type() == ItemType::Parameters) {
+ adjustParametersScopes(child, child);
+ mergeParameters(defaultParameters,
+ getJsVariant(evaluator.engine()->context(),
+ evaluator.scriptValue(child)).toMap());
+ } else {
+ Item::addChild(merged, child);
+ }
+ }
+ const Item::PropertyDeclarationMap &decls = exportItem->propertyDeclarations();
+ for (auto it = decls.constBegin(); it != decls.constEnd(); ++it) {
+ const PropertyDeclaration &newDecl = it.value();
+ const PropertyDeclaration &existingDecl = merged->propertyDeclaration(it.key());
+ if (existingDecl.isValid() && existingDecl.type() != newDecl.type()) {
+ ErrorInfo error(Tr::tr("Export item in inherited item redeclares property "
+ "'%1' with different type.").arg(it.key()), exportItem->location());
+ handlePropertyError(error, parameters, logger);
+ }
+ merged->setPropertyDeclaration(newDecl.name(), newDecl);
+ }
+ for (QMap<QString, ValuePtr>::const_iterator it = exportItem->properties().constBegin();
+ it != exportItem->properties().constEnd(); ++it) {
+ mergeProperty(merged, it.key(), it.value());
+ }
+ }
+ merged->setFile(exportItems.empty()
+ ? productContext.item->file() : exportItems.back()->file());
+ merged->setLocation(exportItems.empty()
+ ? productContext.item->location() : exportItems.back()->location());
+ Item::addChild(productContext.item, merged);
+ merged->setupForBuiltinType(parameters.deprecationWarningMode(), logger);
+
+ productContext.mergedExportItem = merged;
+ productContext.defaultParameters = defaultParameters;
+ return !exportItems.empty();
+}
+
+// TODO: This seems dubious. Can we merge the conditions instead?
+bool ProjectTreeBuilder::Private::checkExportItemCondition(Item *exportItem,
+ const ProductContext &product)
+{
+ class ScopeHandler {
+ public:
+ ScopeHandler(Item *exportItem, const ProductContext &productContext, Item **cachedScopeItem)
+ : m_exportItem(exportItem)
+ {
+ if (!*cachedScopeItem)
+ *cachedScopeItem = Item::create(exportItem->pool(), ItemType::Scope);
+ Item * const scope = *cachedScopeItem;
+ QBS_CHECK(productContext.item->file());
+ scope->setFile(productContext.item->file());
+ scope->setScope(productContext.item);
+ productContext.project->scope->copyProperty(StringConstants::projectVar(), scope);
+ productContext.scope->copyProperty(StringConstants::productVar(), scope);
+ QBS_CHECK(!exportItem->scope());
+ exportItem->setScope(scope);
+ }
+ ~ScopeHandler() { m_exportItem->setScope(nullptr); }
+
+ private:
+ Item * const m_exportItem;
+ } scopeHandler(exportItem, product, &tempScopeItem);
+ return checkItemCondition(exportItem);
+}
+
+static void checkForModuleNamePrefixCollision(
+ const Item *product, const ProductContext::ResolvedAndMultiplexedDependsItem &dependency)
+{
+ if (!product)
+ return;
+
+ for (const Item::Module &m : product->modules()) {
+ if (m.name.length() == dependency.name.length()
+ || m.name.front() != dependency.name.front()) {
+ continue;
+ }
+ QualifiedId shortName;
+ QualifiedId longName;
+ if (m.name < dependency.name) {
+ shortName = m.name;
+ longName = dependency.name;
+ } else {
+ shortName = dependency.name;
+ longName = m.name;
+ }
+ throw ErrorInfo(Tr::tr("The name of module '%1' is equal to the first component of the "
+ "name of module '%2', which is not allowed")
+ .arg(shortName.toString(), longName.toString()), dependency.location());
+ }
+}
+
+// Note: This function is never called for regular loading of the base module into a product,
+// but only for the special cases of loading the dummy base module into a project
+// and temporarily providing a base module for product multiplexing.
+Item *ProjectTreeBuilder::Private::loadBaseModule(ProductContext &product, Item *item)
+{
+ const QualifiedId baseModuleName(StringConstants::qbsModule());
+ const auto baseDependency
+ = ProductContext::ResolvedAndMultiplexedDependsItem::makeBaseDependency();
+ Item * const moduleItem = loadModule(product, item, baseDependency, Deferral::NotAllowed)
+ .moduleItem;
+ if (Q_UNLIKELY(!moduleItem))
+ throw ErrorInfo(Tr::tr("Cannot load base qbs module."));
+ return moduleItem;
+}
+
+ProjectTreeBuilder::Private::LoadModuleResult
+ProjectTreeBuilder::Private::loadModule(ProductContext &product, Item *loadingItem,
+ const ProductContext::ResolvedAndMultiplexedDependsItem &dependency,
+ Deferral deferral)
+{
+ qCDebug(lcModuleLoader) << "loadModule name:" << dependency.name.toString()
+ << "id:" << dependency.id();
+
+ QBS_CHECK(loadingItem);
+
+ const auto findExistingModule = [&dependency](Item *item) -> std::pair<Item::Module *, Item *> {
+ if (!item) // Happens if and only if called via loadBaseModule().
+ return {};
+ Item *moduleWithSameName = nullptr;
+ for (Item::Module &m : item->modules()) {
+ if (m.name != dependency.name)
+ continue;
+ if (!m.productInfo) {
+ QBS_CHECK(!dependency.product);
+ return {&m, m.item};
+ }
+ if ((dependency.profile.isEmpty() || (m.productInfo->profile == dependency.profile))
+ && m.productInfo->multiplexId == dependency.multiplexId) {
+ return {&m, m.item};
+ }
+ moduleWithSameName = m.item;
+ }
+ return {nullptr, moduleWithSameName};
+ };
+ ProductContext *productDep = nullptr;
+ Item *moduleItem = nullptr;
+ static const auto displayName = [](const ProductContext &p) {
+ return ProductItemMultiplexer::fullProductDisplayName(p.name, p.multiplexConfigurationId);
+ };
+ const auto &[existingModule, moduleWithSameName] = findExistingModule(product.item);
+ if (existingModule) {
+ // Merge version range and required property. These will be checked again
+ // after probes resolving.
+ if (existingModule->name.toString() != StringConstants::qbsModule()) {
+ moduleLoader.forwardParameterDeclarations(dependency.item, product.item->modules());
+
+ // TODO: Use priorities like for property values. See QBS-1300.
+ mergeParameters(existingModule->parameters, dependency.parameters);
+
+ existingModule->versionRange.narrowDown(dependency.versionRange);
+ existingModule->required |= dependency.requiredGlobally;
+ if (int(product.resolveDependenciesState.size())
+ > existingModule->maxDependsChainLength) {
+ existingModule->maxDependsChainLength = product.resolveDependenciesState.size();
+ }
+ }
+ QBS_CHECK(existingModule->item);
+ moduleItem = existingModule->item;
+ if (!contains(existingModule->loadingItems, loadingItem))
+ existingModule->loadingItems.push_back(loadingItem);
+ } else if (dependency.product) {
+ // We have already done the look-up.
+ productDep = dependency.product;
+ } else {
+ // First check if there's a matching product.
+ const auto candidates = productsByName.equal_range(dependency.name.toString());
+ for (auto it = candidates.first; it != candidates.second; ++it) {
+ const auto candidate = it->second;
+ if (candidate->multiplexConfigurationId != dependency.multiplexId)
+ continue;
+ if (!dependency.profile.isEmpty() && dependency.profile != candidate->profileName)
+ continue;
+ if (dependency.limitToSubProject && !haveSameSubProject(product, *candidate))
+ continue;
+ productDep = candidate;
+ break;
+ }
+ if (!productDep) {
+ // If we can tell that this is supposed to be a product dependency, we can skip
+ // the module look-up.
+ if (!dependency.profile.isEmpty() || !dependency.multiplexId.isEmpty()) {
+ if (dependency.requiredGlobally) {
+ if (!dependency.profile.isEmpty()) {
+ throw ErrorInfo(Tr::tr("Product '%1' depends on '%2', which does not exist "
+ "for the requested profile '%3'.")
+ .arg(displayName(product), dependency.displayName(),
+ dependency.profile),
+ product.item->location());
+ }
+ throw ErrorInfo(Tr::tr("Product '%1' depends on '%2', which does not exist.")
+ .arg(displayName(product), dependency.displayName()),
+ product.item->location());
+ }
+ } else {
+ // No matching product found, look for a "real" module.
+ moduleItem = searchAndLoadModuleFile(&product, dependency.location(),
+ dependency.name, dependency.fallbackMode,
+ dependency.requiredGlobally);
+
+ if (moduleItem) {
+ Item * const proto = moduleItem;
+ moduleItem = moduleItem->clone();
+ moduleItem->setPrototype(proto); // For parameter declarations.
+ } else if (dependency.requiredGlobally) {
+ throw ErrorInfo(Tr::tr("Dependency '%1' not found for product '%2'.")
+ .arg(dependency.name.toString(), displayName(product)),
+ dependency.location());
+ }
+ }
+ }
+ }
+
+ if (productDep && dependency.checkProduct) {
+ if (productDep == &product) {
+ throw ErrorInfo(Tr::tr("Dependency '%1' refers to itself.")
+ .arg(dependency.name.toString()),
+ dependency.location());
+ }
+
+ if (any_of(product.project->topLevelProject->productsToHandle, [productDep](const auto &e) {
+ return e.first == productDep;
+ })) {
+ if (deferral == Deferral::Allowed)
+ return {nullptr, productDep, HandleDependency::Defer};
+ ErrorInfo e;
+ e.append(Tr::tr("Cyclic dependencies detected:"));
+ e.append(Tr::tr("First product is '%1'.")
+ .arg(displayName(product)), product.item->location());
+ e.append(Tr::tr("Second product is '%1'.")
+ .arg(displayName(*productDep)), productDep->item->location());
+ e.append(Tr::tr("Requested here."), dependency.location());
+ throw e;
+ }
+
+ // This covers both the case of user-disabled products and products with errors.
+ // The latter are force-disabled in handleProductError().
+ if (disabledItems.contains(productDep->item)) {
+ if (dependency.requiredGlobally) {
+ ErrorInfo e;
+ e.append(Tr::tr("Product '%1' depends on '%2',")
+ .arg(displayName(product), displayName(*productDep)),
+ product.item->location());
+ e.append(Tr::tr("but product '%1' is disabled.").arg(displayName(*productDep)),
+ productDep->item->location());
+ throw e;
+ }
+ productDep = nullptr;
+ }
+ }
+
+ if (productDep) {
+ QBS_CHECK(productDep->mergedExportItem);
+ moduleItem = productDep->mergedExportItem->clone();
+ moduleItem->setParent(nullptr);
+
+ // Needed for isolated Export item evaluation.
+ moduleItem->setPrototype(productDep->mergedExportItem);
+ }
+
+ if (moduleItem) {
+ for (auto it = product.resolveDependenciesState.begin();
+ it != product.resolveDependenciesState.end(); ++it) {
+ Item *itemToCheck = moduleItem;
+ if (it->loadingItem != itemToCheck) {
+ if (!productDep)
+ continue;
+ itemToCheck = productDep->item;
+ }
+ if (it->loadingItem != itemToCheck)
+ continue;
+ ErrorInfo e;
+ e.append(Tr::tr("Cyclic dependencies detected:"));
+ while (true) {
+ e.append(it->loadingItemOrigin.name.toString(),
+ it->loadingItemOrigin.location());
+ if (it->loadingItem->type() == ItemType::ModuleInstance) {
+ createNonPresentModule(itemPool, it->loadingItemOrigin.name.toString(),
+ QLatin1String("cyclic dependency"), it->loadingItem);
+ }
+ if (it == product.resolveDependenciesState.begin())
+ break;
+ --it;
+ }
+ e.append(dependency.name.toString(), dependency.location());
+ throw e;
+ }
+ checkForModuleNamePrefixCollision(product.item, dependency);
+ }
+
+ // Can only happen with multiplexing.
+ if (moduleWithSameName && moduleWithSameName != moduleItem)
+ QBS_CHECK(productDep);
+
+ QString loadingName;
+ if (loadingItem == product.item) {
+ loadingName = product.name;
+ } else if (product.resolveDependenciesState.size() > 0) {
+ const auto &loadingItemOrigin = product.resolveDependenciesState.front().loadingItemOrigin;
+ loadingName = loadingItemOrigin.name.toString() + loadingItemOrigin.multiplexId
+ + loadingItemOrigin.profile;
+ }
+ moduleInstantiator.instantiate({
+ product.item, product.name, loadingItem, loadingName, moduleItem, moduleWithSameName,
+ productDep ? productDep->item : nullptr, product.scope, product.project->scope,
+ dependency.name, dependency.id(), bool(existingModule)});
+
+ // At this point, a null module item is only possible for a non-required dependency.
+ // Note that we still needed to to the instantiation above, as that injects the module
+ // name into the surrounding item for the ".present" check.
+ if (!moduleItem) {
+ QBS_CHECK(!dependency.requiredGlobally);
+ return {nullptr, nullptr, HandleDependency::Ignore};
+ }
+
+ const auto createModule = [&] {
+ Item::Module m;
+ m.item = moduleItem;
+ if (productDep) {
+ m.productInfo.emplace(productDep->item, productDep->multiplexConfigurationId,
+ productDep->profileName);
+ }
+ m.name = dependency.name;
+ m.required = dependency.requiredLocally;
+ m.versionRange = dependency.versionRange;
+ return m;
+ };
+ const auto addLocalModule = [&] {
+ if (loadingItem->type() == ItemType::ModuleInstance
+ && !findExistingModule(loadingItem).first) {
+ loadingItem->addModule(createModule());
+ }
+ };
+
+ // The module has already been loaded, so we don't need to add it to the product's list of
+ // modules, nor do we need to handle its dependencies. The only thing we might need to
+ // do is to add it to the "local" list of modules of the loading item, if it isn't in there yet.
+ if (existingModule) {
+ addLocalModule();
+ return {nullptr, nullptr, HandleDependency::Ignore};
+ }
+
+ qCDebug(lcModuleLoader) << "module loaded:" << dependency.name.toString();
+ if (product.item) {
+ Item::Module module = createModule();
+
+ if (module.name.toString() != StringConstants::qbsModule()) {
+ // TODO: Why do we have default parameters only for Export items and
+ // property declarations only for modules? Does that make any sense?
+ if (productDep)
+ module.parameters = productDep->defaultParameters;
+ mergeParameters(module.parameters, dependency.parameters);
+ }
+ module.required = dependency.requiredGlobally;
+ module.loadingItems.push_back(loadingItem);
+ module.maxDependsChainLength = product.resolveDependenciesState.size();
+ product.item->addModule(module);
+ addLocalModule();
+ }
+ return {moduleItem, nullptr, HandleDependency::Use};
+}
+
+bool ProjectTreeBuilder::Private::haveSameSubProject(const ProductContext &p1,
+ const ProductContext &p2)
+{
+ for (const Item *otherParent = p2.item->parent(); otherParent;
+ otherParent = otherParent->parent()) {
+ if (otherParent == p1.item->parent())
+ return true;
+ }
+ return false;
+}
+
+static Item::PropertyMap filterItemProperties(const Item::PropertyMap &properties)
+{
+ Item::PropertyMap result;
+ for (auto it = properties.begin(); it != properties.end(); ++it) {
+ if (it.value()->type() == Value::ItemValueType)
+ result.insert(it.key(), it.value());
+ }
+ return result;
+}
+
+static QVariantMap safeToVariant(JSContext *ctx, const JSValue &v)
+{
+ QVariantMap result;
+ handleJsProperties(ctx, v, [&](const JSAtom &prop, const JSPropertyDescriptor &desc) {
+ const JSValue u = desc.value;
+ if (JS_IsError(ctx, u))
+ throw ErrorInfo(getJsString(ctx, u));
+ const QString name = getJsString(ctx, prop);
+ result[name] = (JS_IsObject(u) && !JS_IsArray(ctx, u) && !JS_IsRegExp(ctx, u))
+ ? safeToVariant(ctx, u) : getJsVariant(ctx, u);
+ });
+ return result;
+}
+
+QVariantMap ProjectTreeBuilder::Private::extractParameters(Item *dependsItem) const
+{
+ QVariantMap result;
+ const Item::PropertyMap &itemProperties = filterItemProperties(dependsItem->properties());
+ if (itemProperties.empty())
+ return result;
+ auto origProperties = dependsItem->properties();
+
+ // TODO: This is not exception-safe. Also, can't we do the item value check along the
+ // way, without allocationg an extra map and exchanging the list of children?
+ dependsItem->setProperties(itemProperties);
+
+ JSValue sv = evaluator.scriptValue(dependsItem);
+ try {
+ result = safeToVariant(evaluator.engine()->context(), sv);
+ } catch (const ErrorInfo &exception) {
+ auto ei = exception;
+ ei.prepend(Tr::tr("Error in dependency parameter."), dependsItem->location());
+ throw ei;
+ }
+ dependsItem->setProperties(origProperties);
+ return result;
+}
+
+// TODO Move the search & load stuff into ModuleLoader once we untangled the data types
+struct PrioritizedItem
+{
+ PrioritizedItem(Item *item, int priority, int searchPathIndex)
+ : item(item), priority(priority), searchPathIndex(searchPathIndex) { }
+
+ Item * const item;
+ int priority = 0;
+ const int searchPathIndex;
+};
+
+static Item *chooseModuleCandidate(const std::vector<PrioritizedItem> &candidates,
+ const QString &moduleName)
+{
+ // TODO: This should also consider the version requirement.
+
+ auto maxIt = std::max_element(
+ candidates.begin(), candidates.end(),
+ [] (const PrioritizedItem &a, const PrioritizedItem &b) {
+ if (a.priority < b.priority)
+ return true;
+ if (a.priority > b.priority)
+ return false;
+ return a.searchPathIndex > b.searchPathIndex;
+ });
+
+ size_t nmax = std::count_if(
+ candidates.begin(), candidates.end(),
+ [maxIt] (const PrioritizedItem &i) {
+ return i.priority == maxIt->priority && i.searchPathIndex == maxIt->searchPathIndex;
+ });
+
+ if (nmax > 1) {
+ ErrorInfo e(Tr::tr("There is more than one equally prioritized candidate for module '%1'.")
+ .arg(moduleName));
+ for (size_t i = 0; i < candidates.size(); ++i) {
+ const auto candidate = candidates.at(i);
+ if (candidate.priority == maxIt->priority) {
+ //: The %1 denotes the number of the candidate.
+ e.append(Tr::tr("candidate %1").arg(i + 1), candidates.at(i).item->location());
+ }
+ }
+ throw e;
+ }
+
+ return maxIt->item;
+}
+
+Item *ProjectTreeBuilder::Private::searchAndLoadModuleFile(
+ ProductContext *productContext, const CodeLocation &dependsItemLocation,
+ const QualifiedId &moduleName, FallbackMode fallbackMode, bool isRequired)
+{
+ const auto findExistingModulePath = [&](const QString &searchPath) {
+ // isFileCaseCorrect is a very expensive call on macOS, so we cache the value for the
+ // modules and search paths we've already processed
+ auto &moduleInfo = existingModulePathCache[{searchPath, moduleName}];
+ if (moduleInfo)
+ return *moduleInfo;
+
+ QString dirPath = searchPath + QStringLiteral("/modules");
+ for (const QString &moduleNamePart : moduleName) {
+ dirPath = FileInfo::resolvePath(dirPath, moduleNamePart);
+ if (!FileInfo::exists(dirPath) || !FileInfo::isFileCaseCorrect(dirPath)) {
+ return *(moduleInfo = QString());
+ }
+ }
+
+ return *(moduleInfo = dirPath);
+ };
+ const auto findExistingModulePaths = [&] {
+ const QStringList &searchPaths = reader.allSearchPaths();
+ QStringList result;
+ result.reserve(searchPaths.size());
+ for (const auto &path: searchPaths) {
+ const QString dirPath = findExistingModulePath(path);
+ if (!dirPath.isEmpty())
+ result.append(dirPath);
+ }
+ return result;
+ };
+
+ auto existingPaths = findExistingModulePaths();
+ if (existingPaths.isEmpty()) { // no suitable names found, try to use providers
+ AccumulatingTimer providersTimer(
+ parameters.logElapsedTime() ? &timingData.moduleProviders : nullptr);
+ auto result = moduleProviderLoader.executeModuleProviders(
+ *productContext,
+ dependsItemLocation,
+ moduleName,
+ fallbackMode);
+ if (result.providerAddedSearchPaths) {
+ qCDebug(lcModuleLoader) << "Re-checking for module" << moduleName.toString()
+ << "with newly added search paths from module provider";
+ existingPaths = findExistingModulePaths();
+ }
+ }
+
+ const auto getModuleFileNames = [&](const QString &dirPath) -> QStringList & {
+ QStringList &moduleFileNames = moduleDirListCache[dirPath];
+ if (moduleFileNames.empty()) {
+ QDirIterator dirIter(dirPath, StringConstants::qbsFileWildcards());
+ while (dirIter.hasNext())
+ moduleFileNames += dirIter.next();
+ }
+ return moduleFileNames;
+ };
+
+ const QString fullName = moduleName.toString();
+ bool triedToLoadModule = false;
+ std::vector<PrioritizedItem> candidates;
+ candidates.reserve(size_t(existingPaths.size()));
+ for (int i = 0; i < existingPaths.size(); ++i) {
+ const QString &dirPath = existingPaths.at(i);
+ QStringList &moduleFileNames = getModuleFileNames(dirPath);
+ for (auto it = moduleFileNames.begin(); it != moduleFileNames.end(); ) {
+ const QString &filePath = *it;
+ const auto [module, triedToLoad] = moduleLoader.loadModuleFile(
+ {productContext->item, productContext->name, productContext->profileName,
+ productContext->profileModuleProperties},
+ fullName, filePath);
+ if (module)
+ candidates.emplace_back(module, 0, i);
+ if (!triedToLoad)
+ it = moduleFileNames.erase(it);
+ else
+ ++it;
+ triedToLoadModule = triedToLoadModule || triedToLoad;
+ }
+ }
+
+ if (candidates.empty()) {
+ if (!isRequired)
+ return createNonPresentModule(itemPool, fullName, QStringLiteral("not found"), nullptr);
+ if (Q_UNLIKELY(triedToLoadModule)) {
+ throw ErrorInfo(Tr::tr("Module %1 could not be loaded.").arg(fullName),
+ dependsItemLocation);
+ }
+ return nullptr;
+ }
+
+ Item *moduleItem;
+ if (candidates.size() == 1) {
+ moduleItem = candidates.at(0).item;
+ } else {
+ for (auto &candidate : candidates) {
+ candidate.priority = evaluator.intValue(candidate.item,
+ StringConstants::priorityProperty(),
+ candidate.priority);
+ }
+ moduleItem = chooseModuleCandidate(candidates, fullName);
+ }
+
+ const QString fullProductName = ProductItemMultiplexer::fullProductDisplayName(
+ productContext->name, productContext->multiplexConfigurationId);
+ moduleLoader.checkProfileErrorsForModule(moduleItem, fullName, fullProductName,
+ productContext->profileName);
+ return moduleItem;
+}
+
+std::optional<ProductContext::ResolvedDependsItem>
+ProjectTreeBuilder::Private::resolveDependsItem(const ProductContext &product, Item *dependsItem)
+{
+ if (!checkItemCondition(dependsItem)) {
+ qCDebug(lcModuleLoader) << "Depends item disabled, ignoring.";
+ return {};
+ }
+
+ const QString name = evaluator.stringValue(dependsItem, StringConstants::nameProperty());
+ if (name == StringConstants::qbsModule()) // Redundant
+ return {};
+
+ bool submodulesPropertySet;
+ const QStringList submodules = evaluator.stringListValue(
+ dependsItem, StringConstants::submodulesProperty(), &submodulesPropertySet);
+ if (submodules.empty() && submodulesPropertySet) {
+ qCDebug(lcModuleLoader) << "Ignoring Depends item with empty submodules list.";
+ return {};
+ }
+ if (Q_UNLIKELY(submodules.size() > 1 && !dependsItem->id().isEmpty())) {
+ QString msg = Tr::tr("A Depends item with more than one module cannot have an id.");
+ throw ErrorInfo(msg, dependsItem->location());
+ }
+ bool productTypesWasSet;
+ const QStringList productTypes = evaluator.stringListValue(
+ dependsItem, StringConstants::productTypesProperty(), &productTypesWasSet);
+ if (!name.isEmpty() && !productTypes.isEmpty()) {
+ throw ErrorInfo(Tr::tr("The name and productTypes are mutually exclusive "
+ "in a Depends item"), dependsItem->location());
+ }
+ if (productTypes.isEmpty() && productTypesWasSet) {
+ qCDebug(lcModuleLoader) << "Ignoring Depends item with empty productTypes list.";
+ return {};
+ }
+ if (name.isEmpty() && !productTypesWasSet) {
+ throw ErrorInfo(Tr::tr("Either name or productTypes must be set in a Depends item"),
+ dependsItem->location());
+ }
+
+ const FallbackMode fallbackMode = parameters.fallbackProviderEnabled()
+ && evaluator.boolValue(dependsItem, StringConstants::enableFallbackProperty())
+ ? FallbackMode::Enabled : FallbackMode::Disabled;
+
+ bool profilesPropertyWasSet = false;
+ std::optional<QStringList> profiles;
+ bool required = true;
+ if (productTypes.isEmpty()) {
+ const QStringList profileList = evaluator.stringListValue(
+ dependsItem, StringConstants::profilesProperty(), &profilesPropertyWasSet);
+ if (profilesPropertyWasSet)
+ profiles.emplace(profileList);
+ required = evaluator.boolValue(dependsItem, StringConstants::requiredProperty());
+ }
+ const Version minVersion = Version::fromString(
+ evaluator.stringValue(dependsItem, StringConstants::versionAtLeastProperty()));
+ const Version maxVersion = Version::fromString(
+ evaluator.stringValue(dependsItem, StringConstants::versionBelowProperty()));
+ const bool limitToSubProject = evaluator.boolValue(
+ dependsItem, StringConstants::limitToSubProjectProperty());
+ const QStringList multiplexIds = evaluator.stringListValue(
+ dependsItem, StringConstants::multiplexConfigurationIdsProperty());
+ adjustParametersScopes(dependsItem, dependsItem);
+ moduleLoader.forwardParameterDeclarations(dependsItem, product.item->modules());
+ const QVariantMap parameters = extractParameters(dependsItem);
+
+ return ProductContext::ResolvedDependsItem{dependsItem, QualifiedId::fromString(name),
+ submodules, FileTags::fromStringList(productTypes), multiplexIds, profiles,
+ {minVersion, maxVersion}, parameters, limitToSubProject, fallbackMode, required};
+}
+
+// Potentially multiplexes a dependency along Depends.productTypes, Depends.subModules and
+// Depends.profiles, as well as internally set up multiplexing axes.
+// Each entry in the resulting queue corresponds to exactly one product or module to pull in.
+std::queue<ProductContext::ResolvedAndMultiplexedDependsItem>
+ProjectTreeBuilder::Private::multiplexDependency(
+ const ProductContext &product, const ProductContext::ResolvedDependsItem &dependency)
+{
+ std::queue<ProductContext::ResolvedAndMultiplexedDependsItem> dependencies;
+ if (!dependency.productTypes.empty()) {
+ std::vector<ProductContext *> matchingProducts;
+ for (const FileTag &typeTag : dependency.productTypes) {
+ const auto range = productsByType.equal_range(typeTag);
+ for (auto it = range.first; it != range.second; ++it) {
+ if (it->second != &product
+ && it->second->name != product.name
+ && (!dependency.limitToSubProject
+ || haveSameSubProject(product, *it->second))) {
+ matchingProducts.push_back(it->second);
+ }
+ }
+ }
+ if (matchingProducts.empty()) {
+ qCDebug(lcModuleLoader) << "Depends.productTypes does not match anything."
+ << dependency.item->location();
+ return {};
+ }
+ for (ProductContext * const match : matchingProducts)
+ dependencies.emplace(match, dependency);
+ return dependencies;
+ }
+
+ const QStringList profiles = dependency.profiles && !dependency.profiles->isEmpty()
+ ? *dependency.profiles : QStringList(QString());
+ const QStringList multiplexIds = !dependency.multiplexIds.isEmpty()
+ ? dependency.multiplexIds : QStringList(QString());
+ const QStringList subModules = !dependency.subModules.isEmpty()
+ ? dependency.subModules : QStringList(QString());
+ for (const QString &profile : profiles) {
+ for (const QString &multiplexId : multiplexIds) {
+ for (const QString &subModule : subModules) {
+ QualifiedId name = dependency.name;
+ if (!subModule.isEmpty())
+ name << subModule.split(QLatin1Char('.'));
+ dependencies.emplace(dependency, name, profile, multiplexId);
+ }
+ }
+ }
+ return dependencies;
+}
+
+SearchPathsManager::SearchPathsManager(ItemReader &itemReader, const QStringList &extraSearchPaths)
+ : SearchPathsManager(itemReader, nullptr, extraSearchPaths) {}
+
+SearchPathsManager::SearchPathsManager(ItemReader &itemReader, ProductContext &product)
+ : SearchPathsManager(itemReader, &product, product.searchPaths) {}
+
+SearchPathsManager::SearchPathsManager(ItemReader &itemReader, ProductContext *product,
+ const QStringList &extraSearchPaths)
+ : m_itemReader(itemReader), m_product(product),
+ m_oldSize(itemReader.extraSearchPathsStack().size())
+{
+ if (!extraSearchPaths.isEmpty())
+ m_itemReader.pushExtraSearchPaths(extraSearchPaths);
+}
+
+SearchPathsManager::~SearchPathsManager()
+{
+ while (m_itemReader.extraSearchPathsStack().size() > m_oldSize) {
+ if (m_product && m_itemReader.extraSearchPathsStack().size() > m_oldSize + 1) {
+ for (const QString &pathFromProvider : m_itemReader.extraSearchPathsStack().back())
+ m_product->searchPaths.prepend(pathFromProvider);
+ }
+ m_itemReader.popExtraSearchPaths();
+ }
+}
+
+QString ProductContext::uniqueName() const
+{
+ return ResolvedProduct::uniqueName(name, multiplexConfigurationId);
+}
+
+QString ProductContext::ResolvedAndMultiplexedDependsItem::id() const
+{
+ if (!item) {
+ QBS_CHECK(name.toString() == StringConstants::qbsModule());
+ return {};
+ }
+ return item->id();
+}
+
+CodeLocation ProductContext::ResolvedAndMultiplexedDependsItem::location() const
+{
+ if (!item) {
+ QBS_CHECK(name.toString() == StringConstants::qbsModule());
+ return {};
+ }
+ return item->location();
+}
+
+QString ProductContext::ResolvedAndMultiplexedDependsItem::displayName() const
+{
+ return ProductItemMultiplexer::fullProductDisplayName(name.toString(), multiplexId);
+}
+
+ProjectTreeBuilder::Private::TempBaseModuleAttacher::TempBaseModuleAttacher(
+ ProjectTreeBuilder::Private *d, ProductContext &product)
+ : m_productItem(product.item)
+{
+ const ValuePtr qbsValue = m_productItem->property(StringConstants::qbsModule());
+
+ // Cloning is necessary because the original value will get "instantiated" now.
+ if (qbsValue)
+ m_origQbsValue = qbsValue->clone();
+
+ m_tempBaseModule = d->loadBaseModule(product, m_productItem);
+}
+
+void ProjectTreeBuilder::Private::TempBaseModuleAttacher::drop()
+{
+ if (!m_tempBaseModule)
+ return;
+
+ // "Unload" the qbs module again.
+ if (m_origQbsValue)
+ m_productItem->setProperty(StringConstants::qbsModule(), m_origQbsValue);
+ else
+ m_productItem->removeProperty(StringConstants::qbsModule());
+ m_productItem->removeModules();
+ m_tempBaseModule = nullptr;
+}
+
+} // namespace qbs::Internal