diff options
Diffstat (limited to 'src/lib/corelib/loader/moduleproviderloader.cpp')
-rw-r--r-- | src/lib/corelib/loader/moduleproviderloader.cpp | 416 |
1 files changed, 416 insertions, 0 deletions
diff --git a/src/lib/corelib/loader/moduleproviderloader.cpp b/src/lib/corelib/loader/moduleproviderloader.cpp new file mode 100644 index 000000000..d0789facd --- /dev/null +++ b/src/lib/corelib/loader/moduleproviderloader.cpp @@ -0,0 +1,416 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Copyright (C) 2021 Ivan Komissarov (abbapoh@gmail.com) +** 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 "moduleproviderloader.h" + +#include "itemreader.h" +#include "probesresolver.h" + +#include <language/builtindeclarations.h> +#include <language/evaluator.h> +#include <language/item.h> +#include <language/scriptengine.h> +#include <language/value.h> +#include <logging/categories.h> +#include <logging/translator.h> +#include <tools/fileinfo.h> +#include <tools/jsliterals.h> +#include <tools/scripttools.h> +#include <tools/setupprojectparameters.h> +#include <tools/stlutils.h> +#include <tools/stringconstants.h> + +#include <QtCore/qcryptographichash.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> + +namespace qbs { +namespace Internal { + +static QString getConfigHash(const QVariantMap& config) +{ + QJsonDocument doc; + doc.setObject(QJsonObject::fromVariantMap(config)); + return QString::fromLatin1( + QCryptographicHash::hash(doc.toJson(), QCryptographicHash::Sha1).toHex().left(16)); +} + +ModuleProviderLoader::ModuleProviderLoader(LoaderState &loaderState) + : m_loaderState(loaderState) {} + +ModuleProviderLoader::ModuleProviderResult ModuleProviderLoader::executeModuleProviders( + ProductContext &productContext, + const CodeLocation &dependsItemLocation, + const QualifiedId &moduleName) +{ + ModuleProviderLoader::ModuleProviderResult result; + try { + std::vector<Provider> providersToRun; + qCDebug(lcModuleLoader) << "Module" << moduleName.toString() + << "not found, checking for module providers"; + const auto providerNames = getModuleProviders(productContext.item); + if (providerNames) { + providersToRun = transformed<std::vector<Provider>>(*providerNames, [](const auto &name) { + return Provider{name, ModuleProviderLookup::Named}; }); + } else { + for (QualifiedId providerName = moduleName; !providerName.empty(); + providerName.pop_back()) { + providersToRun.push_back({providerName, ModuleProviderLookup::Scoped}); + } + } + result = executeModuleProvidersHelper( + productContext, dependsItemLocation, moduleName, providersToRun); + } catch (const ErrorInfo &error) { + auto ei = error; + ei.prepend( + Tr::tr("Error executing provider for module '%1':").arg(moduleName.toString()), + dependsItemLocation); + productContext.handleError(ei); + } + return result; +} + +ModuleProviderLoader::ModuleProviderResult ModuleProviderLoader::executeModuleProvidersHelper( + ProductContext &product, + const CodeLocation &dependsItemLocation, + const QualifiedId &moduleName, + const std::vector<Provider> &providers) +{ + if (providers.empty()) + return {}; + QStringList allSearchPaths; + ModuleProviderResult result; + setupModuleProviderConfig(product); + const auto qbsModule = evaluateQbsModule(product); + for (const auto &[name, lookupType] : providers) { + const auto &[info, fromCache] = findOrCreateProviderInfo( + product, dependsItemLocation, moduleName, name, lookupType, qbsModule); + if (info.providerFile.isEmpty()) { + if (lookupType == ModuleProviderLookup::Named) + throw ErrorInfo(Tr::tr("Unknown provider '%1'").arg(name.toString())); + continue; + } + if (fromCache) + qCDebug(lcModuleLoader) << "Re-using provider" << name.toString() << "from cache"; + + result.providerFound = true; + if (info.searchPaths.empty()) { + qCDebug(lcModuleLoader) + << "Module provider did run, but did not set up any modules."; + continue; + } + qCDebug(lcModuleLoader) << "Module provider added" << info.searchPaths.size() + << "new search path(s)"; + + allSearchPaths << info.searchPaths; + } + if (allSearchPaths.isEmpty()) + return result; + + result.searchPaths = std::move(allSearchPaths); + + return result; +} + +std::pair<const ModuleProviderInfo &, bool /*fromCache*/> +ModuleProviderLoader::findOrCreateProviderInfo( + ProductContext &product, const CodeLocation &dependsItemLocation, + const QualifiedId &moduleName, const QualifiedId &name, ModuleProviderLookup lookupType, + const QVariantMap &qbsModule) +{ + QBS_CHECK(product.providerConfig); + const QVariantMap config = product.providerConfig->value(name.toString()).toMap(); + std::lock_guard lock(m_loaderState.topLevelProject().moduleProvidersCacheLock()); + ModuleProvidersCacheKey cacheKey{name.toString(), {}, config, qbsModule, int(lookupType)}; + // TODO: get rid of non-eager providers and eliminate following if-logic + // first, try to find eager provider (stored with an empty module name) + if (ModuleProviderInfo *provider = m_loaderState.topLevelProject().moduleProvider(cacheKey)) + return {*provider, true}; + // second, try to find non-eager provider for a specific module name + std::get<1>(cacheKey) = moduleName.toString(); // override moduleName + if (ModuleProviderInfo *provider = m_loaderState.topLevelProject().moduleProvider(cacheKey)) + return {*provider, true}; + bool isEager = false; + ModuleProviderInfo info; + info.name = name; + info.config = config; + info.providerFile = findModuleProviderFile(name, lookupType); + if (!info.providerFile.isEmpty()) { + qCDebug(lcModuleLoader) << "Running provider" << name << "at" << info.providerFile; + std::tie(info.searchPaths, isEager) = evaluateModuleProvider( + product, + dependsItemLocation, + moduleName, + name, + info.providerFile, + config, + qbsModule); + info.transientOutput = m_loaderState.parameters().dryRun(); + } + std::get<1>(cacheKey) = isEager ? QString() : moduleName.toString(); + return {m_loaderState.topLevelProject().addModuleProvider(cacheKey, info), false}; +} + +void ModuleProviderLoader::setupModuleProviderConfig(ProductContext &product) +{ + if (product.providerConfig) + return; + QVariantMap providerConfig; + const ItemValueConstPtr configItemValue = + product.item->itemProperty(StringConstants::moduleProviders(), m_loaderState.itemPool()); + if (configItemValue) { + const std::function<void(const Item *, QualifiedId)> collectMap + = [this, &providerConfig, &collectMap](const Item *item, const QualifiedId &name) { + const Item::PropertyMap &props = item->properties(); + for (auto it = props.begin(); it != props.end(); ++it) { + QVariant value; + switch (it.value()->type()) { + case Value::ItemValueType: { + const auto childItem = static_cast<ItemValue *>(it.value().get())->item(); + childItem->setScope(item->scope()); + collectMap(childItem, QualifiedId(name) << it.key()); + continue; + } + case Value::JSSourceValueType: { + it.value()->setScope(item->scope(), {}); + const ScopedJsValue sv(m_loaderState.evaluator().engine()->context(), + m_loaderState.evaluator().value(item, it.key())); + value = getJsVariant(m_loaderState.evaluator().engine()->context(), sv); + break; + } + case Value::VariantValueType: + value = static_cast<VariantValue *>(it.value().get())->value(); + break; + } + QVariantMap m = providerConfig.value(name.toString()).toMap(); + m.insert(it.key(), value); + providerConfig.insert(name.toString(), m); + } + }; + configItemValue->item()->setScope(product.item); + collectMap(configItemValue->item(), QualifiedId()); + } + for (auto it = product.moduleProperties.begin(); it != product.moduleProperties.end(); ++it) { + if (!it.key().startsWith(QStringLiteral("moduleProviders."))) + continue; + const QString provider = it.key().mid(QStringLiteral("moduleProviders.").size()); + const QVariantMap providerConfigFromBuildConfig = it.value().toMap(); + if (providerConfigFromBuildConfig.empty()) + continue; + QVariantMap currentMapForProvider = providerConfig.value(provider).toMap(); + for (auto propIt = providerConfigFromBuildConfig.begin(); + propIt != providerConfigFromBuildConfig.end(); ++propIt) { + currentMapForProvider.insert(propIt.key(), propIt.value()); + } + providerConfig.insert(provider, currentMapForProvider); + } + product.providerConfig = providerConfig; +} + +std::optional<std::vector<QualifiedId>> ModuleProviderLoader::getModuleProviders(Item *item) +{ + while (item) { + const auto providers = m_loaderState.evaluator().optionalStringListValue( + item, StringConstants::qbsModuleProviders()); + if (providers) { + return transformed<std::vector<QualifiedId>>(*providers, [](const auto &provider) { + return QualifiedId::fromString(provider); }); + } + item = item->parent(); + } + return std::nullopt; +} + +QString ModuleProviderLoader::findModuleProviderFile( + const QualifiedId &name, ModuleProviderLookup lookupType) +{ + for (const QString &path : m_loaderState.itemReader().allSearchPaths()) { + QString fullPath = FileInfo::resolvePath(path, QStringLiteral("module-providers")); + switch (lookupType) { + case ModuleProviderLookup::Named: { + const auto result = + FileInfo::resolvePath(fullPath, name.toString() + QStringLiteral(".qbs")); + if (FileInfo::exists(result)) { + fullPath = result; + break; + } + [[fallthrough]]; + } + case ModuleProviderLookup::Scoped: + for (const QString &component : name) + fullPath = FileInfo::resolvePath(fullPath, component); + fullPath = FileInfo::resolvePath(fullPath, QStringLiteral("provider.qbs")); + break; + } + if (!FileInfo::exists(fullPath)) { + qCDebug(lcModuleLoader) << "No module provider found at" << fullPath; + continue; + } + return fullPath; + } + return {}; +} + +QVariantMap ModuleProviderLoader::evaluateQbsModule(ProductContext &product) const +{ + if (product.providerQbsModule) + return *product.providerQbsModule; + const QString properties[] = { + QStringLiteral("sysroot"), + QStringLiteral("toolchain"), + }; + const auto qbsItemValue = std::static_pointer_cast<ItemValue>( + product.item->property(StringConstants::qbsModule())); + QVariantMap result; + for (const auto &property : properties) { + const ScopedJsValue val(m_loaderState.evaluator().engine()->context(), + m_loaderState.evaluator().value(qbsItemValue->item(), property)); + auto value = getJsVariant(m_loaderState.evaluator().engine()->context(), val); + if (!value.isValid()) + continue; + + // The xcode module sets qbs.sysroot; the resulting value is bogus before the probes + // have run. + if (property == QLatin1String("sysroot") && !FileInfo::isAbsolute(value.toString())) + continue; + + result[property] = std::move(value); + } + return *(product.providerQbsModule = result); +} + +Item *ModuleProviderLoader::createProviderScope( + const ProductContext &product, const QVariantMap &qbsModule) +{ + const auto qbsItemValue = std::static_pointer_cast<ItemValue>( + product.item->property(StringConstants::qbsModule())); + + Item *fakeQbsModule = Item::create(&m_loaderState.itemPool(), ItemType::Scope); + + for (auto it = qbsModule.begin(), end = qbsModule.end(); it != end; ++it) { + fakeQbsModule->setProperty(it.key(), VariantValue::create(it.value())); + } + + Item *scope = Item::create(&m_loaderState.itemPool(), ItemType::Scope); + scope->setFile(qbsItemValue->item()->file()); + scope->setProperty(StringConstants::qbsModule(), ItemValue::create(fakeQbsModule)); + return scope; +} + +ModuleProviderLoader::EvaluationResult ModuleProviderLoader::evaluateModuleProvider( + ProductContext &product, + const CodeLocation &dependsItemLocation, + const QualifiedId &moduleName, + const QualifiedId &name, + const QString &providerFile, + const QVariantMap &moduleConfig, + const QVariantMap &qbsModule) +{ + qCDebug(lcModuleLoader) << "Instantiating module provider at" << providerFile; + const QString projectBuildDir = product.project->item->variantProperty( + StringConstants::buildDirectoryProperty())->value().toString(); + const QString searchPathBaseDir = ModuleProviderInfo::outputDirPath(projectBuildDir, name); + + // include qbs module into hash + auto jsConfig = moduleConfig; + jsConfig[StringConstants::qbsModule()] = qbsModule; + + QString outputBaseDir = searchPathBaseDir + QLatin1Char('/') + getConfigHash(jsConfig); + Item * const providerItem = m_loaderState.itemReader().setupItemFromFile( + providerFile, dependsItemLocation); + if (providerItem->type() != ItemType::ModuleProvider) { + throw ErrorInfo(Tr::tr("File '%1' declares an item of type '%2', " + "but '%3' was expected.") + .arg(providerFile, providerItem->typeName(), + BuiltinDeclarations::instance().nameForType(ItemType::ModuleProvider))); + } + + Item * const scope = createProviderScope(product, qbsModule); + for (auto it = providerItem->properties().begin(); it != providerItem->properties().end(); ++it) + it.value()->setScope(scope, {}); + providerItem->setProperty( + StringConstants::nameProperty(), + VariantValue::create(name.toString())); + providerItem->setProperty( + QStringLiteral("outputBaseDir"), + VariantValue::create(outputBaseDir)); + providerItem->overrideProperties(moduleConfig, name, + m_loaderState.parameters(), m_loaderState.logger()); + + const bool isEager = m_loaderState.evaluator().boolValue( + providerItem, StringConstants::isEagerProperty()); + if (!isEager) { + providerItem->setProperty( + StringConstants::moduleNameProperty(), + VariantValue::create(moduleName.toString())); + } + + ProbesResolver(m_loaderState).resolveProbes(product, providerItem); + + EvalContextSwitcher contextSwitcher(m_loaderState.evaluator().engine(), + EvalContext::ModuleProvider); + + checkAllowedValues(providerItem); + auto searchPaths = m_loaderState.evaluator().stringListValue( + providerItem, QStringLiteral("relativeSearchPaths")); + auto prependBaseDir = [&outputBaseDir](const auto &path) { + return outputBaseDir + QLatin1Char('/') + path; + }; + std::transform(searchPaths.begin(), searchPaths.end(), searchPaths.begin(), prependBaseDir); + return {searchPaths, isEager}; +} + +void ModuleProviderLoader::checkAllowedValues(Item *providerItem) +{ + for (const auto &propertyDeclaration : providerItem->propertyDeclarations()) { + if (!propertyDeclaration.shouldCheckAllowedValues()) + continue; + const auto &name = propertyDeclaration.name(); + if (name == QStringLiteral("relativeSearchPaths")) + continue; + const auto value = m_loaderState.evaluator().variantValue(providerItem, name); + const auto propertyValue = providerItem->property(name); + propertyDeclaration.checkAllowedValues( + value, propertyValue->location(), name, m_loaderState); + } +} + +} // namespace Internal +} // namespace qbs |