diff options
Diffstat (limited to 'src/lib/corelib/loader/projectresolver.cpp')
-rw-r--r-- | src/lib/corelib/loader/projectresolver.cpp | 1800 |
1 files changed, 1800 insertions, 0 deletions
diff --git a/src/lib/corelib/loader/projectresolver.cpp b/src/lib/corelib/loader/projectresolver.cpp new file mode 100644 index 000000000..564c2f3ea --- /dev/null +++ b/src/lib/corelib/loader/projectresolver.cpp @@ -0,0 +1,1800 @@ +/**************************************************************************** +** +** Copyright (C) 2016 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 "projectresolver.h" + +#include <jsextensions/jsextensions.h> +#include <jsextensions/moduleproperties.h> +#include <language/artifactproperties.h> +#include <language/builtindeclarations.h> +#include <language/evaluator.h> +#include <language/filecontext.h> +#include <language/item.h> +#include <language/language.h> +#include <language/propertymapinternal.h> +#include <language/resolvedfilecontext.h> +#include <language/scriptengine.h> +#include <language/value.h> +#include <logging/categories.h> +#include <logging/translator.h> +#include <tools/error.h> +#include <tools/fileinfo.h> +#include <tools/joblimits.h> +#include <tools/jsliterals.h> +#include <tools/profiling.h> +#include <tools/progressobserver.h> +#include <tools/scripttools.h> +#include <tools/qbsassert.h> +#include <tools/qttools.h> +#include <tools/setupprojectparameters.h> +#include <tools/stlutils.h> +#include <tools/stringconstants.h> + +#include <quickjs.h> + +#include <QtCore/qdir.h> +#include <QtCore/qregularexpression.h> + +#include <algorithm> +#include <memory> +#include <queue> + +namespace qbs { +namespace Internal { + +extern bool debugProperties; + +static const FileTag unknownFileTag() +{ + static const FileTag tag("unknown-file-tag"); + return tag; +} + +struct ProjectResolver::ProjectContext +{ + ProjectContext *parentContext = nullptr; + ResolvedProjectPtr project; + std::vector<FileTaggerConstPtr> fileTaggers; + std::vector<RulePtr> rules; + JobLimits jobLimits; + ResolvedModulePtr dummyModule; +}; + +struct ProjectResolver::ProductContext +{ + ResolvedProductPtr product; + QString buildDirectory; + Item *item = nullptr; + using ArtifactPropertiesInfo = std::pair<ArtifactPropertiesPtr, std::vector<CodeLocation>>; + QHash<QStringList, ArtifactPropertiesInfo> artifactPropertiesPerFilter; + ProjectResolver::FileLocations sourceArtifactLocations; + GroupConstPtr currentGroup; +}; + +struct ProjectResolver::ModuleContext +{ + ResolvedModulePtr module; + JobLimits jobLimits; +}; + +class CancelException { }; + + +ProjectResolver::ProjectResolver(Evaluator *evaluator, ProjectTreeBuilder::Result loadResult, + SetupProjectParameters setupParameters, Logger &logger) + : m_evaluator(evaluator) + , m_logger(logger) + , m_engine(m_evaluator->engine()) + , m_progressObserver(nullptr) + , m_setupParams(std::move(setupParameters)) + , m_loadResult(std::move(loadResult)) +{ + QBS_CHECK(FileInfo::isAbsolute(m_setupParams.buildRoot())); +} + +ProjectResolver::~ProjectResolver() = default; + +void ProjectResolver::setProgressObserver(ProgressObserver *observer) +{ + m_progressObserver = observer; +} + +static void checkForDuplicateProductNames(const TopLevelProjectConstPtr &project) +{ + const std::vector<ResolvedProductPtr> allProducts = project->allProducts(); + for (size_t i = 0; i < allProducts.size(); ++i) { + const ResolvedProductConstPtr product1 = allProducts.at(i); + const QString productName = product1->uniqueName(); + for (size_t j = i + 1; j < allProducts.size(); ++j) { + const ResolvedProductConstPtr product2 = allProducts.at(j); + if (product2->uniqueName() == productName) { + ErrorInfo error; + error.append(Tr::tr("Duplicate product name '%1'.").arg(product1->name)); + error.append(Tr::tr("First product defined here."), product1->location); + error.append(Tr::tr("Second product defined here."), product2->location); + throw error; + } + } + } +} + +TopLevelProjectPtr ProjectResolver::resolve() +{ + TimedActivityLogger projectResolverTimer(m_logger, Tr::tr("ProjectResolver"), + m_setupParams.logElapsedTime()); + qCDebug(lcProjectResolver) << "resolving" << m_loadResult.root->file()->filePath(); + + m_productContext = nullptr; + m_moduleContext = nullptr; + m_elapsedTimeModPropEval = m_elapsedTimeAllPropEval = m_elapsedTimeGroups = 0; + TopLevelProjectPtr tlp; + try { + tlp = resolveTopLevelProject(); + printProfilingInfo(); + } catch (const CancelException &) { + throw ErrorInfo(Tr::tr("Project resolving canceled for configuration '%1'.") + .arg(TopLevelProject::deriveId(m_setupParams.finalBuildConfigurationTree()))); + } + return tlp; +} + +void ProjectResolver::checkCancelation() const +{ + if (m_progressObserver && m_progressObserver->canceled()) + throw CancelException(); +} + +QString ProjectResolver::verbatimValue(const ValueConstPtr &value, bool *propertyWasSet) const +{ + QString result; + if (value && value->type() == Value::JSSourceValueType) { + const JSSourceValueConstPtr sourceValue = std::static_pointer_cast<const JSSourceValue>( + value); + result = sourceCodeForEvaluation(sourceValue); + if (propertyWasSet) + *propertyWasSet = !sourceValue->isBuiltinDefaultValue(); + } else { + if (propertyWasSet) + *propertyWasSet = false; + } + return result; +} + +QString ProjectResolver::verbatimValue(Item *item, const QString &name, bool *propertyWasSet) const +{ + return verbatimValue(item->property(name), propertyWasSet); +} + +void ProjectResolver::ignoreItem(Item *item, ProjectContext *projectContext) +{ + Q_UNUSED(item); + Q_UNUSED(projectContext); +} + +static void makeSubProjectNamesUniqe(const ResolvedProjectPtr &parentProject) +{ + Set<QString> subProjectNames; + Set<ResolvedProjectPtr> projectsInNeedOfNameChange; + for (const ResolvedProjectPtr &p : qAsConst(parentProject->subProjects)) { + if (!subProjectNames.insert(p->name).second) + projectsInNeedOfNameChange << p; + makeSubProjectNamesUniqe(p); + } + while (!projectsInNeedOfNameChange.empty()) { + auto it = projectsInNeedOfNameChange.begin(); + while (it != projectsInNeedOfNameChange.end()) { + const ResolvedProjectPtr p = *it; + p->name += QLatin1Char('_'); + if (subProjectNames.insert(p->name).second) { + it = projectsInNeedOfNameChange.erase(it); + } else { + ++it; + } + } + } +} + +TopLevelProjectPtr ProjectResolver::resolveTopLevelProject() +{ + if (m_progressObserver) + m_progressObserver->setMaximum(int(m_loadResult.productInfos.size())); + TopLevelProjectPtr project = TopLevelProject::create(); + project->buildDirectory = TopLevelProject::deriveBuildDirectory(m_setupParams.buildRoot(), + TopLevelProject::deriveId(m_setupParams.finalBuildConfigurationTree())); + project->buildSystemFiles = m_loadResult.qbsFiles; + project->profileConfigs = m_loadResult.profileConfigs; + project->probes = m_loadResult.projectProbes; + project->moduleProviderInfo = m_loadResult.storedModuleProviderInfo; + ProjectContext projectContext; + projectContext.project = project; + + resolveProject(m_loadResult.root, &projectContext); + ErrorInfo accumulatedErrors; + for (const ErrorInfo &e : m_queuedErrors) + appendError(accumulatedErrors, e); + if (accumulatedErrors.hasError()) + throw accumulatedErrors; + + project->setBuildConfiguration(m_setupParams.finalBuildConfigurationTree()); + project->overriddenValues = m_setupParams.overriddenValues(); + project->canonicalFilePathResults = m_engine->canonicalFilePathResults(); + project->fileExistsResults = m_engine->fileExistsResults(); + project->directoryEntriesResults = m_engine->directoryEntriesResults(); + project->fileLastModifiedResults = m_engine->fileLastModifiedResults(); + project->environment = m_engine->environment(); + project->buildSystemFiles.unite(m_engine->imports()); + makeSubProjectNamesUniqe(project); + resolveProductDependencies(); + collectExportedProductDependencies(); + checkForDuplicateProductNames(project); + + for (const ResolvedProductPtr &product : project->allProducts()) { + if (!product->enabled) + continue; + + applyFileTaggers(product); + matchArtifactProperties(product, product->allEnabledFiles()); + + // Let a positive value of qbs.install imply the file tag "installable". + for (const SourceArtifactPtr &artifact : product->allFiles()) { + if (artifact->properties->qbsPropertyValue(StringConstants::installProperty()).toBool()) + artifact->fileTags += "installable"; + } + } + project->warningsEncountered = m_logger.warnings(); + return project; +} + +void ProjectResolver::resolveProject(Item *item, ProjectContext *projectContext) +{ + checkCancelation(); + + if (projectContext->parentContext) + projectContext->project->enabled = projectContext->parentContext->project->enabled; + projectContext->project->location = item->location(); + try { + resolveProjectFully(item, projectContext); + } catch (const ErrorInfo &error) { + if (!projectContext->project->enabled) { + qCDebug(lcProjectResolver) << "error resolving project" + << projectContext->project->location << error.toString(); + return; + } + if (m_setupParams.productErrorMode() == ErrorHandlingMode::Strict) + throw; + m_logger.printWarning(error); + } +} + +void ProjectResolver::resolveProjectFully(Item *item, ProjectResolver::ProjectContext *projectContext) +{ + projectContext->project->enabled = projectContext->project->enabled + && m_evaluator->boolValue(item, StringConstants::conditionProperty()); + projectContext->project->name = m_evaluator->stringValue(item, StringConstants::nameProperty()); + if (projectContext->project->name.isEmpty()) + projectContext->project->name = FileInfo::baseName(item->location().filePath()); // FIXME: Must also be changed in item? + QVariantMap projectProperties; + if (!projectContext->project->enabled) { + projectProperties.insert(StringConstants::profileProperty(), + m_evaluator->stringValue(item, + StringConstants::profileProperty())); + projectContext->project->setProjectProperties(projectProperties); + return; + } + + projectContext->dummyModule = ResolvedModule::create(); + + for (Item::PropertyDeclarationMap::const_iterator it + = item->propertyDeclarations().constBegin(); + it != item->propertyDeclarations().constEnd(); ++it) { + if (it.value().flags().testFlag(PropertyDeclaration::PropertyNotAvailableInConfig)) + continue; + const ValueConstPtr v = item->property(it.key()); + QBS_ASSERT(v && v->type() != Value::ItemValueType, continue); + const ScopedJsValue sv(m_engine->context(), m_evaluator->value(item, it.key())); + projectProperties.insert(it.key(), getJsVariant(m_engine->context(), sv)); + } + projectContext->project->setProjectProperties(projectProperties); + + static const ItemFuncMap mapping = { + { ItemType::Project, &ProjectResolver::resolveProject }, + { ItemType::SubProject, &ProjectResolver::resolveSubProject }, + { ItemType::Product, &ProjectResolver::resolveProduct }, + { ItemType::Probe, &ProjectResolver::ignoreItem }, + { ItemType::FileTagger, &ProjectResolver::resolveFileTagger }, + { ItemType::JobLimit, &ProjectResolver::resolveJobLimit }, + { ItemType::Rule, &ProjectResolver::resolveRule }, + { ItemType::PropertyOptions, &ProjectResolver::ignoreItem } + }; + + for (Item * const child : item->children()) { + try { + callItemFunction(mapping, child, projectContext); + } catch (const ErrorInfo &e) { + m_queuedErrors.push_back(e); + } + } + + for (const ResolvedProductPtr &product : projectContext->project->products) + postProcess(product, projectContext); +} + +void ProjectResolver::resolveSubProject(Item *item, ProjectResolver::ProjectContext *projectContext) +{ + ProjectContext subProjectContext = createProjectContext(projectContext); + + Item * const projectItem = item->child(ItemType::Project); + if (projectItem) { + resolveProject(projectItem, &subProjectContext); + return; + } + + // No project item was found, which means the project was disabled. + subProjectContext.project->enabled = false; + Item * const propertiesItem = item->child(ItemType::PropertiesInSubProject); + if (propertiesItem) { + subProjectContext.project->name + = m_evaluator->stringValue(propertiesItem, StringConstants::nameProperty()); + } +} + +class ProjectResolver::ProductContextSwitcher +{ +public: + ProductContextSwitcher(ProjectResolver *resolver, ProductContext *newContext, + ProgressObserver *progressObserver) + : m_resolver(resolver), m_progressObserver(progressObserver) + { + QBS_CHECK(!m_resolver->m_productContext); + m_resolver->m_productContext = newContext; + } + + ~ProductContextSwitcher() + { + if (m_progressObserver) + m_progressObserver->incrementProgressValue(); + m_resolver->m_productContext = nullptr; + } + +private: + ProjectResolver * const m_resolver; + ProgressObserver * const m_progressObserver; +}; + +void ProjectResolver::resolveProduct(Item *item, ProjectContext *projectContext) +{ + checkCancelation(); + m_evaluator->clearPropertyDependencies(); + ProductContext productContext; + productContext.item = item; + ResolvedProductPtr product = ResolvedProduct::create(); + product->enabled = projectContext->project->enabled; + product->moduleProperties = PropertyMapInternal::create(); + product->project = projectContext->project; + productContext.product = product; + product->location = item->location(); + ProductContextSwitcher contextSwitcher(this, &productContext, m_progressObserver); + const auto errorFromDelayedError = [&] { + ProjectTreeBuilder::Result::ProductInfo &pi = m_loadResult.productInfos[item]; + if (pi.delayedError.hasError()) { + ErrorInfo errorInfo; + + // First item is "main error", gets prepended again in the catch clause. + const QList<ErrorItem> &items = pi.delayedError.items(); + for (int i = 1; i < items.size(); ++i) + errorInfo.append(items.at(i)); + + pi.delayedError.clear(); + return errorInfo; + } + return ErrorInfo(); + }; + + // Even if we previously encountered an error, try to continue for as long as possible + // to provide IDEs with useful data (e.g. the list of files). + // If we encounter a follow-up error, suppress it and report the original one instead. + try { + resolveProductFully(item, projectContext); + if (const ErrorInfo error = errorFromDelayedError(); error.hasError()) + throw error; + } catch (ErrorInfo e) { + if (const ErrorInfo error = errorFromDelayedError(); error.hasError()) + e = error; + QString mainErrorString = !product->name.isEmpty() + ? Tr::tr("Error while handling product '%1':").arg(product->name) + : Tr::tr("Error while handling product:"); + ErrorInfo fullError(mainErrorString, item->location()); + appendError(fullError, e); + if (!product->enabled) { + qCDebug(lcProjectResolver) << fullError.toString(); + return; + } + if (m_setupParams.productErrorMode() == ErrorHandlingMode::Strict) + throw fullError; + m_logger.printWarning(fullError); + m_logger.printWarning(ErrorInfo(Tr::tr("Product '%1' had errors and was disabled.") + .arg(product->name), item->location())); + product->enabled = false; + } +} + +void ProjectResolver::resolveProductFully(Item *item, ProjectContext *projectContext) +{ + const ResolvedProductPtr product = m_productContext->product; + m_productItemMap.insert(product, item); + projectContext->project->products.push_back(product); + product->name = m_evaluator->stringValue(item, StringConstants::nameProperty()); + + // product->buildDirectory() isn't valid yet, because the productProperties map is not ready. + m_productContext->buildDirectory + = m_evaluator->stringValue(item, StringConstants::buildDirectoryProperty()); + product->multiplexConfigurationId + = m_evaluator->stringValue(item, StringConstants::multiplexConfigurationIdProperty()); + qCDebug(lcProjectResolver) << "resolveProduct" << product->uniqueName(); + m_productsByItem.insert(item, product); + product->enabled = product->enabled + && m_evaluator->boolValue(item, StringConstants::conditionProperty()); + ProjectTreeBuilder::Result::ProductInfo &pi = m_loadResult.productInfos[item]; + gatherProductTypes(product.get(), item); + product->targetName = m_evaluator->stringValue(item, StringConstants::targetNameProperty()); + product->sourceDirectory = m_evaluator->stringValue( + item, StringConstants::sourceDirectoryProperty()); + product->destinationDirectory = m_evaluator->stringValue( + item, StringConstants::destinationDirProperty()); + + if (product->destinationDirectory.isEmpty()) { + product->destinationDirectory = m_productContext->buildDirectory; + } else { + product->destinationDirectory = FileInfo::resolvePath( + product->topLevelProject()->buildDirectory, + product->destinationDirectory); + } + product->probes = pi.probes; + createProductConfig(product.get()); + product->productProperties.insert(StringConstants::destinationDirProperty(), + product->destinationDirectory); + ModuleProperties::init(m_evaluator->engine(), m_evaluator->scriptValue(item), product.get()); + + QList<Item *> subItems = item->children(); + const ValuePtr filesProperty = item->property(StringConstants::filesProperty()); + if (filesProperty) { + Item *fakeGroup = Item::create(item->pool(), ItemType::Group); + fakeGroup->setFile(item->file()); + fakeGroup->setLocation(item->location()); + fakeGroup->setScope(item); + fakeGroup->setProperty(StringConstants::nameProperty(), VariantValue::create(product->name)); + fakeGroup->setProperty(StringConstants::filesProperty(), filesProperty); + fakeGroup->setProperty(StringConstants::excludeFilesProperty(), + item->property(StringConstants::excludeFilesProperty())); + fakeGroup->setProperty(StringConstants::overrideTagsProperty(), + VariantValue::falseValue()); + fakeGroup->setupForBuiltinType(m_setupParams.deprecationWarningMode(), m_logger); + subItems.prepend(fakeGroup); + } + + static const ItemFuncMap mapping = { + { ItemType::Depends, &ProjectResolver::ignoreItem }, + { ItemType::Rule, &ProjectResolver::resolveRule }, + { ItemType::FileTagger, &ProjectResolver::resolveFileTagger }, + { ItemType::JobLimit, &ProjectResolver::resolveJobLimit }, + { ItemType::Group, &ProjectResolver::resolveGroup }, + { ItemType::Product, &ProjectResolver::resolveShadowProduct }, + { ItemType::Export, &ProjectResolver::resolveExport }, + { ItemType::Probe, &ProjectResolver::ignoreItem }, + { ItemType::PropertyOptions, &ProjectResolver::ignoreItem } + }; + + for (Item * const child : qAsConst(subItems)) + callItemFunction(mapping, child, projectContext); + + for (const ProjectContext *p = projectContext; p; p = p->parentContext) { + JobLimits tempLimits = p->jobLimits; + product->jobLimits = tempLimits.update(product->jobLimits); + } + + resolveModules(item, projectContext); + + for (const FileTag &t : qAsConst(product->fileTags)) + m_productsByType[t].push_back(product); +} + +void ProjectResolver::resolveModules(const Item *item, ProjectContext *projectContext) +{ + JobLimits jobLimits; + for (const Item::Module &m : item->modules()) { + resolveModule(m.name, m.item, m.productInfo.has_value(), m.parameters, jobLimits, + projectContext); + } + for (int i = 0; i < jobLimits.count(); ++i) { + const JobLimit &moduleJobLimit = jobLimits.jobLimitAt(i); + if (m_productContext->product->jobLimits.getLimit(moduleJobLimit.pool()) == -1) + m_productContext->product->jobLimits.setJobLimit(moduleJobLimit); + } +} + +void ProjectResolver::resolveModule(const QualifiedId &moduleName, Item *item, bool isProduct, + const QVariantMap ¶meters, JobLimits &jobLimits, + ProjectContext *projectContext) +{ + checkCancelation(); + if (!item->isPresentModule()) + return; + + ModuleContext * const oldModuleContext = m_moduleContext; + ModuleContext moduleContext; + moduleContext.module = ResolvedModule::create(); + m_moduleContext = &moduleContext; + + const ResolvedModulePtr &module = moduleContext.module; + module->name = moduleName.toString(); + module->isProduct = isProduct; + module->product = m_productContext->product.get(); + module->setupBuildEnvironmentScript.initialize( + scriptFunctionValue(item, StringConstants::setupBuildEnvironmentProperty())); + module->setupRunEnvironmentScript.initialize( + scriptFunctionValue(item, StringConstants::setupRunEnvironmentProperty())); + + for (const Item::Module &m : item->modules()) { + if (m.item->isPresentModule()) + module->moduleDependencies += m.name.toString(); + } + + m_productContext->product->modules.push_back(module); + if (!parameters.empty()) + m_productContext->product->moduleParameters[module] = parameters; + + static const ItemFuncMap mapping { + { ItemType::Group, &ProjectResolver::ignoreItem }, + { ItemType::Rule, &ProjectResolver::resolveRule }, + { ItemType::FileTagger, &ProjectResolver::resolveFileTagger }, + { ItemType::JobLimit, &ProjectResolver::resolveJobLimit }, + { ItemType::Scanner, &ProjectResolver::resolveScanner }, + { ItemType::PropertyOptions, &ProjectResolver::ignoreItem }, + { ItemType::Depends, &ProjectResolver::ignoreItem }, + { ItemType::Parameter, &ProjectResolver::ignoreItem }, + { ItemType::Properties, &ProjectResolver::ignoreItem }, + { ItemType::Probe, &ProjectResolver::ignoreItem } + }; + for (Item *child : item->children()) + callItemFunction(mapping, child, projectContext); + for (int i = 0; i < moduleContext.jobLimits.count(); ++i) { + const JobLimit &newJobLimit = moduleContext.jobLimits.jobLimitAt(i); + const int oldLimit = jobLimits.getLimit(newJobLimit.pool()); + if (oldLimit == -1 || oldLimit > newJobLimit.limit()) + jobLimits.setJobLimit(newJobLimit); + } + + m_moduleContext = oldModuleContext; +} + +void ProjectResolver::gatherProductTypes(ResolvedProduct *product, Item *item) +{ + const VariantValuePtr type = item->variantProperty(StringConstants::typeProperty()); + if (type) + product->fileTags = FileTags::fromStringList(type->value().toStringList()); +} + +SourceArtifactPtr ProjectResolver::createSourceArtifact(const ResolvedProductPtr &rproduct, + const QString &fileName, const GroupPtr &group, bool wildcard, + const CodeLocation &filesLocation, FileLocations *fileLocations, + ErrorInfo *errorInfo) +{ + const QString &baseDir = FileInfo::path(group->location.filePath()); + const QString absFilePath = QDir::cleanPath(FileInfo::resolvePath(baseDir, fileName)); + if (!wildcard && !FileInfo(absFilePath).exists()) { + if (errorInfo) + errorInfo->append(Tr::tr("File '%1' does not exist.").arg(absFilePath), filesLocation); + rproduct->missingSourceFiles << absFilePath; + return {}; + } + if (group->enabled && fileLocations) { + CodeLocation &loc = (*fileLocations)[std::make_pair(group->targetOfModule, absFilePath)]; + if (loc.isValid()) { + if (errorInfo) { + errorInfo->append(Tr::tr("Duplicate source file '%1'.").arg(absFilePath)); + errorInfo->append(Tr::tr("First occurrence is here."), loc); + errorInfo->append(Tr::tr("Next occurrence is here."), filesLocation); + } + return {}; + } + loc = filesLocation; + } + SourceArtifactPtr artifact = SourceArtifactInternal::create(); + artifact->absoluteFilePath = absFilePath; + artifact->fileTags = group->fileTags; + artifact->overrideFileTags = group->overrideTags; + artifact->properties = group->properties; + artifact->targetOfModule = group->targetOfModule; + (wildcard ? group->wildcards->files : group->files).push_back(artifact); + return artifact; +} + +static QualifiedIdSet propertiesToEvaluate(std::deque<QualifiedId> initialProps, + const PropertyDependencies &deps) +{ + std::deque<QualifiedId> remainingProps = std::move(initialProps); + QualifiedIdSet allProperties; + while (!remainingProps.empty()) { + const QualifiedId prop = remainingProps.front(); + remainingProps.pop_front(); + const auto insertResult = allProperties.insert(prop); + if (!insertResult.second) + continue; + transform(deps.value(prop), remainingProps, [](const QualifiedId &id) { return id; }); + } + return allProperties; +} + +QVariantMap ProjectResolver::resolveAdditionalModuleProperties(const Item *group, + const QVariantMap ¤tValues) +{ + // Step 1: Retrieve the properties directly set in the group + const ModulePropertiesPerGroup &mp = mapValue( + m_loadResult.productInfos, m_productContext->item).modulePropertiesSetInGroups; + const auto it = mp.find(group); + if (it == mp.end()) + return {}; + const QualifiedIdSet &propsSetInGroup = it->second; + + // Step 2: Gather all properties that depend on these properties. + const QualifiedIdSet &propsToEval = propertiesToEvaluate( + rangeTo<std::deque<QualifiedId>>(propsSetInGroup), + m_evaluator->propertyDependencies()); + + // Step 3: Evaluate all these properties and replace their values in the map + QVariantMap modulesMap = currentValues; + QHash<QString, QStringList> propsPerModule; + for (auto fullPropName : propsToEval) { + const QString moduleName + = QualifiedId(fullPropName.mid(0, fullPropName.size() - 1)).toString(); + propsPerModule[moduleName] << fullPropName.last(); + } + EvalCacheEnabler cachingEnabler(m_evaluator, m_productContext->product->sourceDirectory); + for (const Item::Module &module : group->modules()) { + const QString &fullModName = module.name.toString(); + const QStringList propsForModule = propsPerModule.take(fullModName); + if (propsForModule.empty()) + continue; + QVariantMap reusableValues = modulesMap.value(fullModName).toMap(); + for (const QString &prop : qAsConst(propsForModule)) + reusableValues.remove(prop); + modulesMap.insert(fullModName, + evaluateProperties(module.item, module.item, reusableValues, true, true)); + } + return modulesMap; +} + +void ProjectResolver::resolveGroup(Item *item, ProjectContext *projectContext) +{ + checkCancelation(); + const bool parentEnabled = m_productContext->currentGroup + ? m_productContext->currentGroup->enabled + : m_productContext->product->enabled; + const bool isEnabled = parentEnabled + && m_evaluator->boolValue(item, StringConstants::conditionProperty()); + try { + resolveGroupFully(item, projectContext, isEnabled); + } catch (const ErrorInfo &error) { + if (!isEnabled) { + qCDebug(lcProjectResolver) << "error resolving group at" << item->location() + << error.toString(); + return; + } + if (m_setupParams.productErrorMode() == ErrorHandlingMode::Strict) + throw; + m_logger.printWarning(error); + } +} + +void ProjectResolver::resolveGroupFully(Item *item, ProjectResolver::ProjectContext *projectContext, + bool isEnabled) +{ + AccumulatingTimer groupTimer(m_setupParams.logElapsedTime() + ? &m_elapsedTimeGroups : nullptr); + + const auto getGroupPropertyMap = [this, item](const ArtifactProperties *existingProps) { + PropertyMapPtr moduleProperties; + bool newPropertyMapRequired = false; + if (existingProps) + moduleProperties = existingProps->propertyMap(); + if (!moduleProperties) { + newPropertyMapRequired = true; + moduleProperties = m_productContext->currentGroup + ? m_productContext->currentGroup->properties + : m_productContext->product->moduleProperties; + } + const QVariantMap newModuleProperties + = resolveAdditionalModuleProperties(item, moduleProperties->value()); + if (!newModuleProperties.empty()) { + if (newPropertyMapRequired) + moduleProperties = PropertyMapInternal::create(); + moduleProperties->setValue(newModuleProperties); + } + return moduleProperties; + }; + + QStringList files = m_evaluator->stringListValue(item, StringConstants::filesProperty()); + bool fileTagsSet; + const FileTags fileTags = m_evaluator->fileTagsValue(item, StringConstants::fileTagsProperty(), + &fileTagsSet); + const QStringList fileTagsFilter + = m_evaluator->stringListValue(item, StringConstants::fileTagsFilterProperty()); + if (!fileTagsFilter.empty()) { + if (Q_UNLIKELY(!files.empty())) + throw ErrorInfo(Tr::tr("Group.files and Group.fileTagsFilters are exclusive."), + item->location()); + + if (!isEnabled) + return; + + ProductContext::ArtifactPropertiesInfo &apinfo + = m_productContext->artifactPropertiesPerFilter[fileTagsFilter]; + if (apinfo.first) { + const auto it = std::find_if(apinfo.second.cbegin(), apinfo.second.cend(), + [item](const CodeLocation &loc) { + return item->location().filePath() == loc.filePath(); + }); + if (it != apinfo.second.cend()) { + ErrorInfo error(Tr::tr("Conflicting fileTagsFilter in Group items.")); + error.append(Tr::tr("First item"), *it); + error.append(Tr::tr("Second item"), item->location()); + throw error; + } + } else { + apinfo.first = ArtifactProperties::create(); + apinfo.first->setFileTagsFilter(FileTags::fromStringList(fileTagsFilter)); + m_productContext->product->artifactProperties.push_back(apinfo.first); + } + apinfo.second.push_back(item->location()); + apinfo.first->setPropertyMapInternal(getGroupPropertyMap(apinfo.first.get())); + apinfo.first->addExtraFileTags(fileTags); + return; + } + QStringList patterns; + for (int i = files.size(); --i >= 0;) { + if (FileInfo::isPattern(files[i])) + patterns.push_back(files.takeAt(i)); + } + GroupPtr group = ResolvedGroup::create(); + bool prefixWasSet = false; + group->prefix = m_evaluator->stringValue(item, StringConstants::prefixProperty(), QString(), + &prefixWasSet); + if (!prefixWasSet && m_productContext->currentGroup) + group->prefix = m_productContext->currentGroup->prefix; + if (!group->prefix.isEmpty()) { + for (auto it = files.rbegin(), end = files.rend(); it != end; ++it) + it->prepend(group->prefix); + } + group->location = item->location(); + group->enabled = isEnabled; + group->properties = getGroupPropertyMap(nullptr); + group->fileTags = fileTags; + group->overrideTags = m_evaluator->boolValue(item, StringConstants::overrideTagsProperty()); + if (group->overrideTags && fileTagsSet) { + if (group->fileTags.empty() ) + group->fileTags.insert(unknownFileTag()); + } else if (m_productContext->currentGroup) { + group->fileTags.unite(m_productContext->currentGroup->fileTags); + } + + const CodeLocation filesLocation = item->property(StringConstants::filesProperty())->location(); + const VariantValueConstPtr moduleProp = item->variantProperty( + StringConstants::modulePropertyInternal()); + if (moduleProp) + group->targetOfModule = moduleProp->value().toString(); + ErrorInfo fileError; + if (!patterns.empty()) { + group->wildcards = std::make_unique<SourceWildCards>(); + SourceWildCards *wildcards = group->wildcards.get(); + wildcards->group = group.get(); + wildcards->excludePatterns = m_evaluator->stringListValue( + item, StringConstants::excludeFilesProperty()); + wildcards->patterns = patterns; + const Set<QString> files = wildcards->expandPatterns(group, + FileInfo::path(item->file()->filePath()), + projectContext->project->topLevelProject()->buildDirectory); + for (const QString &fileName : files) + createSourceArtifact(m_productContext->product, fileName, group, true, filesLocation, + &m_productContext->sourceArtifactLocations, &fileError); + } + + for (const QString &fileName : qAsConst(files)) { + createSourceArtifact(m_productContext->product, fileName, group, false, filesLocation, + &m_productContext->sourceArtifactLocations, &fileError); + } + if (fileError.hasError()) { + if (group->enabled) { + if (m_setupParams.productErrorMode() == ErrorHandlingMode::Strict) + throw ErrorInfo(fileError); + m_logger.printWarning(fileError); + } else { + qCDebug(lcProjectResolver) << "error for disabled group:" << fileError.toString(); + } + } + group->name = m_evaluator->stringValue(item, StringConstants::nameProperty()); + if (group->name.isEmpty()) + group->name = Tr::tr("Group %1").arg(m_productContext->product->groups.size()); + m_productContext->product->groups.push_back(group); + + class GroupContextSwitcher { + public: + GroupContextSwitcher(ProductContext &context, const GroupConstPtr &newGroup) + : m_context(context), m_oldGroup(context.currentGroup) { + m_context.currentGroup = newGroup; + } + ~GroupContextSwitcher() { m_context.currentGroup = m_oldGroup; } + private: + ProductContext &m_context; + const GroupConstPtr m_oldGroup; + }; + GroupContextSwitcher groupSwitcher(*m_productContext, group); + for (Item * const childItem : item->children()) + resolveGroup(childItem, projectContext); +} + +void ProjectResolver::adaptExportedPropertyValues(const Item *shadowProductItem) +{ + ExportedModule &m = m_productContext->product->exportedModule; + const QVariantList prefixList = m.propertyValues.take( + StringConstants::prefixMappingProperty()).toList(); + const QString shadowProductName = m_evaluator->stringValue( + shadowProductItem, StringConstants::nameProperty()); + const QString shadowProductBuildDir = m_evaluator->stringValue( + shadowProductItem, StringConstants::buildDirectoryProperty()); + QVariantMap prefixMap; + for (const QVariant &v : prefixList) { + const QVariantMap o = v.toMap(); + prefixMap.insert(o.value(QStringLiteral("prefix")).toString(), + o.value(QStringLiteral("replacement")).toString()); + } + const auto valueRefersToImportingProduct + = [shadowProductName, shadowProductBuildDir](const QString &value) { + return value.toLower().contains(shadowProductName.toLower()) + || value.contains(shadowProductBuildDir); + }; + static const auto stringMapper = [](const QVariantMap &mappings, const QString &value) + -> QString { + for (auto it = mappings.cbegin(); it != mappings.cend(); ++it) { + if (value.startsWith(it.key())) + return it.value().toString() + value.mid(it.key().size()); + } + return value; + }; + const auto stringListMapper = [&valueRefersToImportingProduct]( + const QVariantMap &mappings, const QStringList &value) -> QStringList { + QStringList result; + result.reserve(value.size()); + for (const QString &s : value) { + if (!valueRefersToImportingProduct(s)) + result.push_back(stringMapper(mappings, s)); + } + return result; + }; + const std::function<QVariant(const QVariantMap &, const QVariant &)> mapper + = [&stringListMapper, &mapper]( + const QVariantMap &mappings, const QVariant &value) -> QVariant { + switch (static_cast<QMetaType::Type>(value.userType())) { + case QMetaType::QString: + return stringMapper(mappings, value.toString()); + case QMetaType::QStringList: + return stringListMapper(mappings, value.toStringList()); + case QMetaType::QVariantMap: { + QVariantMap m = value.toMap(); + for (auto it = m.begin(); it != m.end(); ++it) + it.value() = mapper(mappings, it.value()); + return m; + } + default: + return value; + } + }; + for (auto it = m.propertyValues.begin(); it != m.propertyValues.end(); ++it) + it.value() = mapper(prefixMap, it.value()); + for (auto it = m.modulePropertyValues.begin(); it != m.modulePropertyValues.end(); ++it) + it.value() = mapper(prefixMap, it.value()); + for (ExportedModuleDependency &dep : m.moduleDependencies) { + for (auto it = dep.moduleProperties.begin(); it != dep.moduleProperties.end(); ++it) + it.value() = mapper(prefixMap, it.value()); + } +} + +void ProjectResolver::collectExportedProductDependencies() +{ + ResolvedProductPtr dummyProduct = ResolvedProduct::create(); + dummyProduct->enabled = false; + for (const auto &exportingProductInfo : qAsConst(m_productExportInfo)) { + const ResolvedProductPtr exportingProduct = exportingProductInfo.first; + if (!exportingProduct->enabled) + continue; + Item * const importingProductItem = exportingProductInfo.second; + + std::vector<std::pair<ResolvedProductPtr, QVariantMap>> directDeps; + for (const Item::Module &m : importingProductItem->modules()) { + if (m.name.toString() != exportingProduct->name) + continue; + for (const Item::Module &dep : m.item->modules()) { + if (dep.productInfo) { + directDeps.emplace_back(m_productsByItem.value(dep.productInfo->item), + m.parameters); + } + } + } + for (const auto &dep : directDeps) { + if (!contains(exportingProduct->exportedModule.productDependencies, + dep.first->uniqueName())) { + exportingProduct->exportedModule.productDependencies.push_back( + dep.first->uniqueName()); + } + if (!dep.second.isEmpty()) { + exportingProduct->exportedModule.dependencyParameters.insert(dep.first, + dep.second); + } + } + auto &productDeps = exportingProduct->exportedModule.productDependencies; + std::sort(productDeps.begin(), productDeps.end()); + } +} + +void ProjectResolver::resolveShadowProduct(Item *item, ProjectResolver::ProjectContext *) +{ + if (!m_productContext->product->enabled) + return; + for (const auto &m : item->modules()) { + if (m.name.toString() != m_productContext->product->name) + continue; + collectPropertiesForExportItem(m.item); + for (const auto &dep : m.item->modules()) + collectPropertiesForModuleInExportItem(dep); + break; + } + try { + adaptExportedPropertyValues(item); + } catch (const ErrorInfo &) {} + m_productExportInfo.emplace_back(m_productContext->product, item); +} + +void ProjectResolver::setupExportedProperties(const Item *item, const QString &namePrefix, + std::vector<ExportedProperty> &properties) +{ + const auto &props = item->properties(); + for (auto it = props.cbegin(); it != props.cend(); ++it) { + const QString qualifiedName = namePrefix.isEmpty() + ? it.key() : namePrefix + QLatin1Char('.') + it.key(); + if ((item->type() == ItemType::Export || item->type() == ItemType::Properties) + && qualifiedName == StringConstants::prefixMappingProperty()) { + continue; + } + const ValuePtr &v = it.value(); + if (v->type() == Value::ItemValueType) { + setupExportedProperties(std::static_pointer_cast<ItemValue>(v)->item(), + qualifiedName, properties); + continue; + } + ExportedProperty exportedProperty; + exportedProperty.fullName = qualifiedName; + exportedProperty.type = item->propertyDeclaration(it.key()).type(); + if (v->type() == Value::VariantValueType) { + exportedProperty.sourceCode = toJSLiteral( + std::static_pointer_cast<VariantValue>(v)->value()); + } else { + QBS_CHECK(v->type() == Value::JSSourceValueType); + const JSSourceValue * const sv = static_cast<JSSourceValue *>(v.get()); + exportedProperty.sourceCode = sv->sourceCode().toString(); + } + const ItemDeclaration itemDecl + = BuiltinDeclarations::instance().declarationsForType(item->type()); + PropertyDeclaration propertyDecl; + const auto itemProperties = itemDecl.properties(); + for (const PropertyDeclaration &decl : itemProperties) { + if (decl.name() == it.key()) { + propertyDecl = decl; + exportedProperty.isBuiltin = true; + break; + } + } + + // Do not add built-in properties that were left at their default value. + if (!exportedProperty.isBuiltin || m_evaluator->isNonDefaultValue(item, it.key())) + properties.push_back(exportedProperty); + } + + // Order the list of properties, so the output won't look so random. + static const auto less = [](const ExportedProperty &p1, const ExportedProperty &p2) -> bool { + const int p1ComponentCount = p1.fullName.count(QLatin1Char('.')); + const int p2ComponentCount = p2.fullName.count(QLatin1Char('.')); + if (p1.isBuiltin && !p2.isBuiltin) + return true; + if (!p1.isBuiltin && p2.isBuiltin) + return false; + if (p1ComponentCount < p2ComponentCount) + return true; + if (p1ComponentCount > p2ComponentCount) + return false; + return p1.fullName < p2.fullName; + }; + std::sort(properties.begin(), properties.end(), less); +} + +static bool usesImport(const ExportedProperty &prop, const QRegularExpression ®ex) +{ + return prop.sourceCode.indexOf(regex) != -1; +} + +static bool usesImport(const ExportedItem &item, const QRegularExpression ®ex) +{ + return any_of(item.properties, + [regex](const ExportedProperty &p) { return usesImport(p, regex); }) + || any_of(item.children, + [regex](const ExportedItemPtr &child) { return usesImport(*child, regex); }); +} + +static bool usesImport(const ExportedModule &module, const QString &name) +{ + // Imports are used in three ways: + // (1) var f = new TextFile(...); + // (2) var path = FileInfo.joinPaths(...) + // (3) var obj = DataCollection; + const QString pattern = QStringLiteral("\\b%1\\b"); + + const QRegularExpression regex(pattern.arg(name)); // std::regex is much slower + return any_of(module.m_properties, + [regex](const ExportedProperty &p) { return usesImport(p, regex); }) + || any_of(module.children, + [regex](const ExportedItemPtr &child) { return usesImport(*child, regex); }); +} + +static QString getLineAtLocation(const CodeLocation &loc, const QString &content) +{ + int pos = 0; + int currentLine = 1; + while (currentLine < loc.line()) { + while (content.at(pos++) != QLatin1Char('\n')) + ; + ++currentLine; + } + const int eolPos = content.indexOf(QLatin1Char('\n'), pos); + return content.mid(pos, eolPos - pos); +} + +void ProjectResolver::resolveExport(Item *exportItem, ProjectContext *) +{ + ExportedModule &exportedModule = m_productContext->product->exportedModule; + setupExportedProperties(exportItem, QString(), exportedModule.m_properties); + static const auto cmpFunc = [](const ExportedProperty &p1, const ExportedProperty &p2) { + return p1.fullName < p2.fullName; + }; + std::sort(exportedModule.m_properties.begin(), exportedModule.m_properties.end(), cmpFunc); + + transform(exportItem->children(), exportedModule.children, + [&exportedModule, this](const auto &child) { + return resolveExportChild(child, exportedModule); }); + + for (const JsImport &jsImport : exportItem->file()->jsImports()) { + if (usesImport(exportedModule, jsImport.scopeName)) { + exportedModule.importStatements << getLineAtLocation(jsImport.location, + exportItem->file()->content()); + } + } + const auto builtInImports = JsExtensions::extensionNames(); + for (const QString &builtinImport: builtInImports) { + if (usesImport(exportedModule, builtinImport)) + exportedModule.importStatements << QStringLiteral("import qbs.") + builtinImport; + } + exportedModule.importStatements.sort(); +} + +// TODO: This probably wouldn't be necessary if we had item serialization. +std::unique_ptr<ExportedItem> ProjectResolver::resolveExportChild(const Item *item, + const ExportedModule &module) +{ + std::unique_ptr<ExportedItem> exportedItem(new ExportedItem); + + // This is the type of the built-in base item. It may turn out that we need to support + // derived items under Export. In that case, we probably need a new Item member holding + // the original type name. + exportedItem->name = item->typeName(); + + transform(item->children(), exportedItem->children, [&module, this](const auto &child) { + return resolveExportChild(child, module); }); + + setupExportedProperties(item, QString(), exportedItem->properties); + return exportedItem; +} + +QString ProjectResolver::sourceCodeAsFunction(const JSSourceValueConstPtr &value, + const PropertyDeclaration &decl) const +{ + QString &scriptFunction = m_scriptFunctions[std::make_pair(value->sourceCode(), + decl.functionArgumentNames())]; + if (!scriptFunction.isNull()) + return scriptFunction; + const QString args = decl.functionArgumentNames().join(QLatin1Char(',')); + if (value->hasFunctionForm()) { + // Insert the argument list. + scriptFunction = value->sourceCodeForEvaluation(); + scriptFunction.insert(10, args); + // Remove the function application "()" that has been + // added in ItemReaderASTVisitor::visitStatement. + scriptFunction.chop(2); + } else { + scriptFunction = QLatin1String("(function(") + args + QLatin1String("){return ") + + value->sourceCode().toString() + QLatin1String(";})"); + } + return scriptFunction; +} + +QString ProjectResolver::sourceCodeForEvaluation(const JSSourceValueConstPtr &value) const +{ + QString &code = m_sourceCode[value->sourceCode()]; + if (!code.isNull()) + return code; + code = value->sourceCodeForEvaluation(); + return code; +} + +ScriptFunctionPtr ProjectResolver::scriptFunctionValue(Item *item, const QString &name) const +{ + JSSourceValuePtr value = item->sourceProperty(name); + ScriptFunctionPtr &script = m_scriptFunctionMap[value ? value->location() : CodeLocation()]; + if (!script.get()) { + script = ScriptFunction::create(); + const PropertyDeclaration decl = item->propertyDeclaration(name); + script->sourceCode = sourceCodeAsFunction(value, decl); + script->location = value->location(); + script->fileContext = resolvedFileContext(value->file()); + } + return script; +} + +ResolvedFileContextPtr ProjectResolver::resolvedFileContext(const FileContextConstPtr &ctx) const +{ + ResolvedFileContextPtr &result = m_fileContextMap[ctx]; + if (!result) + result = ResolvedFileContext::create(*ctx); + return result; +} + +void ProjectResolver::resolveRule(Item *item, ProjectContext *projectContext) +{ + checkCancelation(); + + if (!m_evaluator->boolValue(item, StringConstants::conditionProperty())) + return; + + RulePtr rule = Rule::create(); + + // read artifacts + bool hasArtifactChildren = false; + for (Item * const child : item->children()) { + if (Q_UNLIKELY(child->type() != ItemType::Artifact)) { + throw ErrorInfo(Tr::tr("'Rule' can only have children of type 'Artifact'."), + child->location()); + } + hasArtifactChildren = true; + resolveRuleArtifact(rule, child); + } + + rule->name = m_evaluator->stringValue(item, StringConstants::nameProperty()); + rule->prepareScript.initialize( + scriptFunctionValue(item, StringConstants::prepareProperty())); + rule->outputArtifactsScript.initialize( + scriptFunctionValue(item, StringConstants::outputArtifactsProperty())); + rule->outputFileTags = m_evaluator->fileTagsValue( + item, StringConstants::outputFileTagsProperty()); + if (rule->outputArtifactsScript.isValid()) { + if (hasArtifactChildren) + throw ErrorInfo(Tr::tr("The Rule.outputArtifacts script is not allowed in rules " + "that contain Artifact items."), + item->location()); + } + if (!hasArtifactChildren && rule->outputFileTags.empty()) { + throw ErrorInfo(Tr::tr("A rule needs to have Artifact items or a non-empty " + "outputFileTags property."), item->location()); + } + rule->multiplex = m_evaluator->boolValue(item, StringConstants::multiplexProperty()); + rule->alwaysRun = m_evaluator->boolValue(item, StringConstants::alwaysRunProperty()); + rule->inputs = m_evaluator->fileTagsValue(item, StringConstants::inputsProperty()); + rule->inputsFromDependencies + = m_evaluator->fileTagsValue(item, StringConstants::inputsFromDependenciesProperty()); + bool requiresInputsSet = false; + rule->requiresInputs = m_evaluator->boolValue(item, StringConstants::requiresInputsProperty(), + &requiresInputsSet); + if (!requiresInputsSet) + rule->requiresInputs = rule->declaresInputs(); + rule->auxiliaryInputs + = m_evaluator->fileTagsValue(item, StringConstants::auxiliaryInputsProperty()); + rule->excludedInputs + = m_evaluator->fileTagsValue(item, StringConstants::excludedInputsProperty()); + if (rule->excludedInputs.empty()) { + rule->excludedInputs = m_evaluator->fileTagsValue( + item, StringConstants::excludedAuxiliaryInputsProperty()); + } + rule->explicitlyDependsOn + = m_evaluator->fileTagsValue(item, StringConstants::explicitlyDependsOnProperty()); + rule->explicitlyDependsOnFromDependencies = m_evaluator->fileTagsValue( + item, StringConstants::explicitlyDependsOnFromDependenciesProperty()); + rule->module = m_moduleContext ? m_moduleContext->module : projectContext->dummyModule; + if (!rule->multiplex && !rule->declaresInputs()) { + throw ErrorInfo(Tr::tr("Rule has no inputs, but is not a multiplex rule."), + item->location()); + } + if (!rule->multiplex && !rule->requiresInputs) { + throw ErrorInfo(Tr::tr("Rule.requiresInputs is false for non-multiplex rule."), + item->location()); + } + if (!rule->declaresInputs() && rule->requiresInputs) { + throw ErrorInfo(Tr::tr("Rule.requiresInputs is true, but the rule " + "does not declare any input tags."), item->location()); + } + if (m_productContext) { + rule->product = m_productContext->product.get(); + m_productContext->product->rules.push_back(rule); + } else { + projectContext->rules.push_back(rule); + } +} + +void ProjectResolver::resolveRuleArtifact(const RulePtr &rule, Item *item) +{ + RuleArtifactPtr artifact = RuleArtifact::create(); + rule->artifacts.push_back(artifact); + artifact->location = item->location(); + + if (const auto sourceProperty = item->sourceProperty(StringConstants::filePathProperty())) + artifact->filePathLocation = sourceProperty->location(); + + artifact->filePath = verbatimValue(item, StringConstants::filePathProperty()); + artifact->fileTags = m_evaluator->fileTagsValue(item, StringConstants::fileTagsProperty()); + artifact->alwaysUpdated = m_evaluator->boolValue(item, + StringConstants::alwaysUpdatedProperty()); + + QualifiedIdSet seenBindings; + for (Item *obj = item; obj; obj = obj->prototype()) { + for (QMap<QString, ValuePtr>::const_iterator it = obj->properties().constBegin(); + it != obj->properties().constEnd(); ++it) + { + if (it.value()->type() != Value::ItemValueType) + continue; + resolveRuleArtifactBinding(artifact, + std::static_pointer_cast<ItemValue>(it.value())->item(), + QStringList(it.key()), &seenBindings); + } + } +} + +void ProjectResolver::resolveRuleArtifactBinding(const RuleArtifactPtr &ruleArtifact, + Item *item, + const QStringList &namePrefix, + QualifiedIdSet *seenBindings) +{ + for (QMap<QString, ValuePtr>::const_iterator it = item->properties().constBegin(); + it != item->properties().constEnd(); ++it) + { + const QStringList name = QStringList(namePrefix) << it.key(); + if (it.value()->type() == Value::ItemValueType) { + resolveRuleArtifactBinding(ruleArtifact, + std::static_pointer_cast<ItemValue>(it.value())->item(), name, + seenBindings); + } else if (it.value()->type() == Value::JSSourceValueType) { + const auto insertResult = seenBindings->insert(name); + if (!insertResult.second) + continue; + JSSourceValuePtr sourceValue = std::static_pointer_cast<JSSourceValue>(it.value()); + RuleArtifact::Binding rab; + rab.name = name; + rab.code = sourceCodeForEvaluation(sourceValue); + rab.location = sourceValue->location(); + ruleArtifact->bindings.push_back(rab); + } else { + QBS_ASSERT(!"unexpected value type", continue); + } + } +} + +void ProjectResolver::resolveFileTagger(Item *item, ProjectContext *projectContext) +{ + checkCancelation(); + if (!m_evaluator->boolValue(item, StringConstants::conditionProperty())) + return; + std::vector<FileTaggerConstPtr> &fileTaggers = m_productContext + ? m_productContext->product->fileTaggers + : projectContext->fileTaggers; + const QStringList patterns = m_evaluator->stringListValue(item, + StringConstants::patternsProperty()); + if (patterns.empty()) + throw ErrorInfo(Tr::tr("FileTagger.patterns must be a non-empty list."), item->location()); + + const FileTags fileTags = m_evaluator->fileTagsValue(item, StringConstants::fileTagsProperty()); + if (fileTags.empty()) + throw ErrorInfo(Tr::tr("FileTagger.fileTags must not be empty."), item->location()); + + for (const QString &pattern : patterns) { + if (pattern.isEmpty()) + throw ErrorInfo(Tr::tr("A FileTagger pattern must not be empty."), item->location()); + } + + const int priority = m_evaluator->intValue(item, StringConstants::priorityProperty()); + fileTaggers.push_back(FileTagger::create(patterns, fileTags, priority)); +} + +void ProjectResolver::resolveJobLimit(Item *item, ProjectResolver::ProjectContext *projectContext) +{ + if (!m_evaluator->boolValue(item, StringConstants::conditionProperty())) + return; + const QString jobPool = m_evaluator->stringValue(item, StringConstants::jobPoolProperty()); + if (jobPool.isEmpty()) + throw ErrorInfo(Tr::tr("A JobLimit item needs to have a non-empty '%1' property.") + .arg(StringConstants::jobPoolProperty()), item->location()); + bool jobCountWasSet; + const int jobCount = m_evaluator->intValue(item, StringConstants::jobCountProperty(), -1, + &jobCountWasSet); + if (!jobCountWasSet) { + throw ErrorInfo(Tr::tr("A JobLimit item needs to have a '%1' property.") + .arg(StringConstants::jobCountProperty()), item->location()); + } + if (jobCount < 0) { + throw ErrorInfo(Tr::tr("A JobLimit item must have a non-negative '%1' property.") + .arg(StringConstants::jobCountProperty()), item->location()); + } + JobLimits &jobLimits = m_moduleContext + ? m_moduleContext->jobLimits + : m_productContext ? m_productContext->product->jobLimits + : projectContext->jobLimits; + JobLimit jobLimit(jobPool, jobCount); + const int oldLimit = jobLimits.getLimit(jobPool); + if (oldLimit == -1 || oldLimit > jobCount) + jobLimits.setJobLimit(jobLimit); +} + +void ProjectResolver::resolveScanner(Item *item, ProjectResolver::ProjectContext *projectContext) +{ + checkCancelation(); + if (!m_evaluator->boolValue(item, StringConstants::conditionProperty())) { + qCDebug(lcProjectResolver) << "scanner condition is false"; + return; + } + + ResolvedScannerPtr scanner = ResolvedScanner::create(); + scanner->module = m_moduleContext ? m_moduleContext->module : projectContext->dummyModule; + scanner->inputs = m_evaluator->fileTagsValue(item, StringConstants::inputsProperty()); + scanner->recursive = m_evaluator->boolValue(item, StringConstants::recursiveProperty()); + scanner->searchPathsScript.initialize( + scriptFunctionValue(item, StringConstants::searchPathsProperty())); + scanner->scanScript.initialize( + scriptFunctionValue(item, StringConstants::scanProperty())); + m_productContext->product->scanners.push_back(scanner); +} + +void ProjectResolver::matchArtifactProperties(const ResolvedProductPtr &product, + const std::vector<SourceArtifactPtr> &artifacts) +{ + for (const SourceArtifactPtr &artifact : artifacts) { + for (const auto &artifactProperties : product->artifactProperties) { + if (!artifact->isTargetOfModule() + && artifact->fileTags.intersects(artifactProperties->fileTagsFilter())) { + artifact->properties = artifactProperties->propertyMap(); + } + } + } +} + +void ProjectResolver::printProfilingInfo() +{ + if (!m_setupParams.logElapsedTime()) + return; + m_logger.qbsLog(LoggerInfo, true) << "\t" << Tr::tr("All property evaluation took %1.") + .arg(elapsedTimeString(m_elapsedTimeAllPropEval)); + m_logger.qbsLog(LoggerInfo, true) << "\t" << Tr::tr("Module property evaluation took %1.") + .arg(elapsedTimeString(m_elapsedTimeModPropEval)); + m_logger.qbsLog(LoggerInfo, true) << "\t" + << Tr::tr("Resolving groups (without module property " + "evaluation) took %1.") + .arg(elapsedTimeString(m_elapsedTimeGroups)); +} + +class TempScopeSetter +{ +public: + TempScopeSetter(const ValuePtr &value, Item *newScope) : m_value(value), m_oldScope(value->scope()) + { + value->setScope(newScope, {}); + } + ~TempScopeSetter() { if (m_value) m_value->setScope(m_oldScope, {}); } + + TempScopeSetter(const TempScopeSetter &) = delete; + TempScopeSetter &operator=(const TempScopeSetter &) = delete; + TempScopeSetter &operator=(TempScopeSetter &&) = delete; + + TempScopeSetter(TempScopeSetter &&other) noexcept + : m_value(std::move(other.m_value)), m_oldScope(other.m_oldScope) + { + other.m_value.reset(); + other.m_oldScope = nullptr; + } + +private: + ValuePtr m_value; + Item *m_oldScope; +}; + +void ProjectResolver::collectPropertiesForExportItem(Item *productModuleInstance) +{ + if (!productModuleInstance->isPresentModule()) + return; + Item * const exportItem = productModuleInstance->prototype(); + QBS_CHECK(exportItem); + QBS_CHECK(exportItem->type() == ItemType::Export); + const ItemDeclaration::Properties exportDecls = BuiltinDeclarations::instance() + .declarationsForType(ItemType::Export).properties(); + ExportedModule &exportedModule = m_productContext->product->exportedModule; + const auto &props = exportItem->properties(); + for (auto it = props.begin(); it != props.end(); ++it) { + const auto match + = [it](const PropertyDeclaration &decl) { return decl.name() == it.key(); }; + if (it.key() != StringConstants::prefixMappingProperty() && + std::find_if(exportDecls.begin(), exportDecls.end(), match) != exportDecls.end()) { + continue; + } + if (it.value()->type() == Value::ItemValueType) { + collectPropertiesForExportItem(it.key(), it.value(), productModuleInstance, + exportedModule.modulePropertyValues); + } else { + TempScopeSetter tss(it.value(), productModuleInstance); + evaluateProperty(exportItem, it.key(), it.value(), exportedModule.propertyValues, + false); + } + } +} + +// Collects module properties assigned to in other (higher-level) modules. +void ProjectResolver::collectPropertiesForModuleInExportItem(const Item::Module &module) +{ + if (!module.item->isPresentModule()) + return; + ExportedModule &exportedModule = m_productContext->product->exportedModule; + if (module.productInfo || module.name.first() == StringConstants::qbsModule()) + return; + const auto checkName = [module](const ExportedModuleDependency &d) { + return module.name.toString() == d.name; + }; + if (any_of(exportedModule.moduleDependencies, checkName)) + return; + + Item *modulePrototype = module.item->prototype(); + while (modulePrototype && modulePrototype->type() != ItemType::Module) + modulePrototype = modulePrototype->prototype(); + if (!modulePrototype) // Can happen for broken products in relaxed mode. + return; + const Item::PropertyMap &props = modulePrototype->properties(); + ExportedModuleDependency dep; + dep.name = module.name.toString(); + for (auto it = props.begin(); it != props.end(); ++it) { + if (it.value()->type() == Value::ItemValueType) + collectPropertiesForExportItem(it.key(), it.value(), module.item, dep.moduleProperties); + } + exportedModule.moduleDependencies.push_back(dep); + + for (const auto &dep : module.item->modules()) + collectPropertiesForModuleInExportItem(dep); +} + +void ProjectResolver::resolveProductDependencies() +{ + for (auto it = m_productsByItem.cbegin(); it != m_productsByItem.cend(); ++it) { + const ResolvedProductPtr &product = it.value(); + for (const Item::Module &module : it.key()->modules()) { + if (!module.productInfo) + continue; + const ResolvedProductPtr &dep = m_productsByItem.value(module.productInfo->item); + QBS_CHECK(dep); + QBS_CHECK(dep != product); + it.value()->dependencies << dep; + it.value()->dependencyParameters.insert(dep, module.parameters); // TODO: Streamline this with normal module dependencies? + } + + // TODO: We might want to keep the topological sorting and get rid of "module module dependencies". + std::sort(product->dependencies.begin(),product->dependencies.end(), + [](const ResolvedProductPtr &p1, const ResolvedProductPtr &p2) { + return p1->fullDisplayName() < p2->fullDisplayName(); + }); + } +} + +void ProjectResolver::postProcess(const ResolvedProductPtr &product, + ProjectContext *projectContext) const +{ + product->fileTaggers << projectContext->fileTaggers; + std::sort(std::begin(product->fileTaggers), std::end(product->fileTaggers), + [] (const FileTaggerConstPtr &a, const FileTaggerConstPtr &b) { + return a->priority() > b->priority(); + }); + for (const RulePtr &rule : projectContext->rules) { + RulePtr clonedRule = rule->clone(); + clonedRule->product = product.get(); + product->rules.push_back(clonedRule); + } +} + +void ProjectResolver::applyFileTaggers(const ResolvedProductPtr &product) const +{ + for (const SourceArtifactPtr &artifact : product->allEnabledFiles()) + applyFileTaggers(artifact, product); +} + +void ProjectResolver::applyFileTaggers(const SourceArtifactPtr &artifact, + const ResolvedProductConstPtr &product) +{ + if (!artifact->overrideFileTags || artifact->fileTags.empty()) { + const QString fileName = FileInfo::fileName(artifact->absoluteFilePath); + const FileTags fileTags = product->fileTagsForFileName(fileName); + artifact->fileTags.unite(fileTags); + if (artifact->fileTags.empty()) + artifact->fileTags.insert(unknownFileTag()); + qCDebug(lcProjectResolver) << "adding file tags" << artifact->fileTags + << "to" << fileName; + } +} + +QVariantMap ProjectResolver::evaluateModuleValues(Item *item, bool lookupPrototype) +{ + AccumulatingTimer modPropEvalTimer(m_setupParams.logElapsedTime() + ? &m_elapsedTimeModPropEval : nullptr); + QVariantMap moduleValues; + for (const Item::Module &module : item->modules()) { + if (!module.item->isPresentModule()) + continue; + const QString fullName = module.name.toString(); + moduleValues[fullName] = evaluateProperties(module.item, lookupPrototype, true); + } + + return moduleValues; +} + +QVariantMap ProjectResolver::evaluateProperties(Item *item, bool lookupPrototype, bool checkErrors) +{ + const QVariantMap tmplt; + return evaluateProperties(item, item, tmplt, lookupPrototype, checkErrors); +} + +QVariantMap ProjectResolver::evaluateProperties(const Item *item, const Item *propertiesContainer, + const QVariantMap &tmplt, bool lookupPrototype, bool checkErrors) +{ + AccumulatingTimer propEvalTimer(m_setupParams.logElapsedTime() + ? &m_elapsedTimeAllPropEval : nullptr); + QVariantMap result = tmplt; + for (QMap<QString, ValuePtr>::const_iterator it = propertiesContainer->properties().begin(); + it != propertiesContainer->properties().end(); ++it) { + checkCancelation(); + evaluateProperty(item, it.key(), it.value(), result, checkErrors); + } + return lookupPrototype && propertiesContainer->prototype() + ? evaluateProperties(item, propertiesContainer->prototype(), result, true, checkErrors) + : result; +} + +void ProjectResolver::evaluateProperty(const Item *item, const QString &propName, + const ValuePtr &propValue, QVariantMap &result, bool checkErrors) +{ + JSContext * const ctx = m_engine->context(); + switch (propValue->type()) { + case Value::ItemValueType: + { + // Ignore items. Those point to module instances + // and are handled in evaluateModuleValues(). + break; + } + case Value::JSSourceValueType: + { + if (result.contains(propName)) + break; + const PropertyDeclaration pd = item->propertyDeclaration(propName); + if (pd.flags().testFlag(PropertyDeclaration::PropertyNotAvailableInConfig)) { + break; + } + const ScopedJsValue scriptValue(ctx, m_evaluator->property(item, propName)); + if (JsException ex = m_evaluator->engine()->checkAndClearException(propValue->location())) { + if (checkErrors) + throw ex.toErrorInfo(); + } + + // NOTE: Loses type information if scriptValue.isUndefined == true, + // as such QScriptValues become invalid QVariants. + QVariant v; + if (JS_IsFunction(ctx, scriptValue)) { + v = getJsString(ctx, scriptValue); + } else { + v = getJsVariant(ctx, scriptValue); + QVariantMap m = v.toMap(); + if (m.contains(StringConstants::importScopeNamePropertyInternal())) { + QVariantMap tmp = m; + const ScopedJsValue proto(ctx, JS_GetPrototype(ctx, scriptValue)); + m = getJsVariant(ctx, proto).toMap(); + for (auto it = tmp.begin(); it != tmp.end(); ++it) + m.insert(it.key(), it.value()); + v = m; + } + } + + if (pd.type() == PropertyDeclaration::Path && v.isValid()) { + v = v.toString(); + } else if (pd.type() == PropertyDeclaration::PathList + || pd.type() == PropertyDeclaration::StringList) { + v = v.toStringList(); + } else if (pd.type() == PropertyDeclaration::VariantList) { + v = v.toList(); + } + checkAllowedValues(v, propValue->location(), pd, propName); + result[propName] = v; + break; + } + case Value::VariantValueType: + { + if (result.contains(propName)) + break; + VariantValuePtr vvp = std::static_pointer_cast<VariantValue>(propValue); + QVariant v = vvp->value(); + + const PropertyDeclaration pd = item->propertyDeclaration(propName); + if (v.isNull() && !pd.isScalar()) // QTBUG-51237 + v = QStringList(); + + checkAllowedValues(v, propValue->location(), pd, propName); + result[propName] = v; + break; + } + } +} + +void ProjectResolver::checkAllowedValues( + const QVariant &value, const CodeLocation &loc, const PropertyDeclaration &decl, + const QString &key) const +{ + const auto type = decl.type(); + if (type != PropertyDeclaration::String && type != PropertyDeclaration::StringList) + return; + + if (value.isNull()) + return; + + const auto &allowedValues = decl.allowedValues(); + if (allowedValues.isEmpty()) + return; + + const auto checkValue = [this, &loc, &allowedValues, &key](const QString &value) + { + if (!allowedValues.contains(value)) { + const auto message = Tr::tr("Value '%1' is not allowed for property '%2'.") + .arg(value, key); + ErrorInfo error(message, loc); + handlePropertyError(error, m_setupParams, m_logger); + } + }; + + if (type == PropertyDeclaration::StringList) { + const auto strings = value.toStringList(); + for (const auto &string: strings) { + checkValue(string); + } + } else if (type == PropertyDeclaration::String) { + checkValue(value.toString()); + } +} + +void ProjectResolver::collectPropertiesForExportItem(const QualifiedId &moduleName, + const ValuePtr &value, Item *moduleInstance, QVariantMap &moduleProps) +{ + QBS_CHECK(value->type() == Value::ItemValueType); + Item * const itemValueItem = std::static_pointer_cast<ItemValue>(value)->item(); + if (itemValueItem->propertyDeclarations().isEmpty()) { + for (const Item::Module &module : moduleInstance->modules()) { + if (module.name == moduleName) { + itemValueItem->setPropertyDeclarations(module.item->propertyDeclarations()); + break; + } + } + } + if (itemValueItem->type() == ItemType::ModuleInstancePlaceholder) { + struct EvalPreparer { + EvalPreparer(Item *valueItem, const QualifiedId &moduleName) + : valueItem(valueItem), + hadName(!!valueItem->variantProperty(StringConstants::nameProperty())) + { + if (!hadName) { + // Evaluator expects a name here. + valueItem->setProperty(StringConstants::nameProperty(), + VariantValue::create(moduleName.toString())); + } + } + ~EvalPreparer() + { + if (!hadName) + valueItem->setProperty(StringConstants::nameProperty(), VariantValuePtr()); + } + Item * const valueItem; + const bool hadName; + }; + EvalPreparer ep(itemValueItem, moduleName); + std::vector<TempScopeSetter> tss; + for (const ValuePtr &v : itemValueItem->properties()) + tss.emplace_back(v, moduleInstance); + moduleProps.insert(moduleName.toString(), evaluateProperties(itemValueItem, false, false)); + return; + } + QBS_CHECK(itemValueItem->type() == ItemType::ModulePrefix); + const Item::PropertyMap &props = itemValueItem->properties(); + for (auto it = props.begin(); it != props.end(); ++it) { + QualifiedId fullModuleName = moduleName; + fullModuleName << it.key(); + collectPropertiesForExportItem(fullModuleName, it.value(), moduleInstance, moduleProps); + } +} + +void ProjectResolver::createProductConfig(ResolvedProduct *product) +{ + EvalCacheEnabler cachingEnabler(m_evaluator, m_productContext->product->sourceDirectory); + product->moduleProperties->setValue(evaluateModuleValues(m_productContext->item)); + product->productProperties = evaluateProperties(m_productContext->item, m_productContext->item, + QVariantMap(), true, true); +} + +void ProjectResolver::callItemFunction(const ItemFuncMap &mappings, Item *item, + ProjectContext *projectContext) +{ + const ItemFuncPtr f = mappings.value(item->type()); + QBS_CHECK(f); + if (item->type() == ItemType::Project) { + ProjectContext subProjectContext = createProjectContext(projectContext); + (this->*f)(item, &subProjectContext); + } else { + (this->*f)(item, projectContext); + } +} + +ProjectResolver::ProjectContext ProjectResolver::createProjectContext(ProjectContext *parentProjectContext) const +{ + ProjectContext subProjectContext; + subProjectContext.parentContext = parentProjectContext; + subProjectContext.project = ResolvedProject::create(); + parentProjectContext->project->subProjects.push_back(subProjectContext.project); + subProjectContext.project->parentProject = parentProjectContext->project; + return subProjectContext; +} + +} // namespace Internal +} // namespace qbs |