diff options
Diffstat (limited to 'src/lib/language/loader.cpp')
-rw-r--r-- | src/lib/language/loader.cpp | 2647 |
1 files changed, 2647 insertions, 0 deletions
diff --git a/src/lib/language/loader.cpp b/src/lib/language/loader.cpp new file mode 100644 index 000000000..4fcd4abb3 --- /dev/null +++ b/src/lib/language/loader.cpp @@ -0,0 +1,2647 @@ +/************************************************************************** +** +** This file is part of the Qt Build Suite +** +** Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (info@qt.nokia.com) +** +** +** GNU Lesser General Public License Usage +** +** This file may be used under the terms of the GNU Lesser General Public +** License version 2.1 as published by the Free Software Foundation and +** appearing in the file LICENSE.LGPL included in the packaging of this file. +** Please review the following information to ensure the GNU Lesser General +** Public License version 2.1 requirements will be met: +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Nokia gives you certain additional +** rights. These rights are described in the Nokia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU General +** Public License version 3.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of this +** file. +** Please review the following information to ensure the GNU General +** Public License version 3.0 requirements will be met: +** http://www.gnu.org/copyleft/gpl.html. +** +** Other Usage +** Alternatively, this file may be used in accordance with the terms and +** conditions contained in a signed written agreement between you and Nokia. +** +**************************************************************************/ + + +#include "loader.h" + +#include "language.h" +#include <tools/error.h> +#include <tools/settings.h> +#include <tools/fileinfo.h> +#include <tools/runenvironment.h> +#include <tools/scripttools.h> +#include <tools/logger.h> + +#include <QtCore/QCoreApplication> +#include <QtCore/QSettings> +#include <QtCore/QStringList> +#include <QtCore/QVariant> +#include <QtCore/QDirIterator> + +#include <QtScript/QScriptEngine> +#include <QtScript/QScriptProgram> +#include <QtScript/QScriptValueIterator> + +#include <parser/qmljsparser_p.h> +#include <parser/qmljsnodepool_p.h> +#include <parser/qmljsengine_p.h> +#include <parser/qmljslexer_p.h> + +QT_BEGIN_NAMESPACE +static uint qHash(const QStringList &list) +{ + uint hash = 0; + foreach (const QString &n, list) + hash ^= qHash(n); + return hash; +} +QT_END_NAMESPACE + +using namespace QmlJS::AST; + + +namespace qbs { + +const QString dumpIndent(" "); + +PropertyDeclaration::PropertyDeclaration() + : type(UnknownType) + , flags(DefaultFlags) +{ +} + +PropertyDeclaration::PropertyDeclaration(const QString &name, Type type, Flags flags) + : name(name) + , type(type) + , flags(flags) +{ +} + +PropertyDeclaration::~PropertyDeclaration() +{ +} + +LanguageObject::LanguageObject(ProjectFile *owner) + : file(owner) +{ + file->registerLanguageObject(this); +} + +LanguageObject::LanguageObject(const LanguageObject &other) + : id(other.id) + , prototype(other.prototype) + , prototypeFileName(other.prototypeFileName) + , prototypeLocation(other.prototypeLocation) + , file(other.file) + , bindings(other.bindings) + , functions(other.functions) + , propertyDeclarations(other.propertyDeclarations) +{ + file->registerLanguageObject(this); + children.reserve(other.children.size()); + for (int i = 0; i < other.children.size(); ++i) + children.append(new LanguageObject(*other.children.at(i))); +} + +LanguageObject::~LanguageObject() +{ + if (!file->isDestructing()) + file->registerLanguageObject(this); +} + +ScopeChain::ScopeChain(QScriptEngine *engine, const QSharedPointer<Scope> &root) + : QScriptClass(engine) +{ + m_value = engine->newObject(this); + m_globalObject = engine->globalObject(); + if (root) + m_scopes.append(root); +} + +ScopeChain::~ScopeChain() +{ +} + +ScopeChain *ScopeChain::clone() const +{ + ScopeChain *s = new ScopeChain(engine(), m_scopes.last()); + s->m_scopes = m_scopes; + return s; +} + +QScriptValue ScopeChain::value() +{ + return m_value; +} + +Scope::Ptr ScopeChain::first() const +{ + return m_scopes.first(); +} + +Scope::Ptr ScopeChain::last() const +{ + return m_scopes.last(); +} + +ScopeChain *ScopeChain::prepend(const QSharedPointer<Scope> &newTop) +{ + if (!newTop) + return this; + m_scopes.prepend(newTop); + return this; +} + +QSharedPointer<Scope> ScopeChain::findNonEmpty(const QString &name) const +{ + foreach (const Scope::Ptr &scope, m_scopes) { + if (scope->name() == name && !scope->properties.isEmpty()) + return scope; + } + return Scope::Ptr(); +} + +QSharedPointer<Scope> ScopeChain::find(const QString &name) const +{ + foreach (const Scope::Ptr &scope, m_scopes) { + if (scope->name() == name) + return scope; + } + return Scope::Ptr(); +} + +Property ScopeChain::lookupProperty(const QString &name) const +{ + foreach (const Scope::Ptr &scope, m_scopes) { + Property p = scope->properties.value(name); + if (p.isValid()) + return p; + } + return Property(); +} + +ScopeChain::QueryFlags ScopeChain::queryProperty(const QScriptValue &object, const QScriptString &name, + QueryFlags flags, uint *id) +{ + Q_UNUSED(object); + Q_UNUSED(name); + Q_UNUSED(id); + return (HandlesReadAccess | HandlesWriteAccess) & flags; +} + +QScriptValue ScopeChain::property(const QScriptValue &object, const QScriptString &name, uint id) +{ + Q_UNUSED(object); + Q_UNUSED(id); + QScriptValue value; + foreach (const Scope::Ptr &scope, m_scopes) { + value = scope->value.property(name); + if (value.isError()) { + engine()->clearExceptions(); + } else if (value.isValid()) { + return value; + } + } + value = m_globalObject.property(name); + if (!value.isValid() || (value.isUndefined() && name.toString() != QLatin1String("undefined"))) { + QString msg("Undefined property '%1'"); + value = engine()->currentContext()->throwError(msg.arg(name.toString())); + } + return value; +} + +void ScopeChain::setProperty(QScriptValue &, const QScriptString &name, uint, const QScriptValue &) +{ + QString msg("Removing or setting property '%1' in a binding is invalid."); + engine()->currentContext()->throwError(msg.arg(name.toString())); +} + +Property::Property(EvaluationObject * object) + : scope(object->scope) +{ +} + +Property::Property(const QScriptValue &scriptValue) + : value(scriptValue) +{ +} + +static QScriptValue evaluate(QScriptEngine *engine, const QScriptProgram &expression) +{ + QScriptValue result = engine->evaluate(expression); + if (engine->hasUncaughtException()) { + QString errorMessage = engine->uncaughtException().toString(); + int errorLine = engine->uncaughtExceptionLineNumber(); + engine->clearExceptions(); + throw Error(errorMessage, expression.fileName(), errorLine); + } + if (result.isError()) + throw Error(result.toString()); + return result; +} + +std::set<Scope *> Scope::scopesWithEvaluatedProperties; + +Scope::Scope(QScriptEngine *engine, const QString &name) + : QScriptClass(engine) + , m_name(name) +{ +} + +QSharedPointer<Scope> Scope::create(QScriptEngine *engine, const QString &name, ProjectFile *owner) +{ + QSharedPointer<Scope> obj(new Scope(engine, name)); + obj->value = engine->newObject(obj.data()); + owner->registerScope(obj); + return obj; +} + +Scope::~Scope() +{ +} + +QString Scope::name() const +{ + return m_name; +} + +static const bool debugProperties = false; + +Scope::QueryFlags Scope::queryProperty(const QScriptValue &object, const QScriptString &name, + QueryFlags flags, uint *id) +{ + const QString nameString = name.toString(); + if (properties.contains(nameString)) { + *id = 0; + return (HandlesReadAccess | HandlesWriteAccess) & flags; + } + if (fallbackScope && fallbackScope.data()->queryProperty(object, name, flags, id)) { + *id = 1; + return (HandlesReadAccess | HandlesWriteAccess) & flags; + } + + QScriptValue proto = value.prototype(); + if (proto.isValid()) { + QScriptValue v = proto.property(name); + if (!v.isValid()) { + *id = 2; + return (HandlesReadAccess | HandlesWriteAccess) & flags; + } + } + + if (debugProperties) + qbsTrace() << "PROPERTIES: we don't handle " << name.toString(); + return 0; +} + +QScriptValue Scope::property(const QScriptValue &object, const QScriptString &name, uint id) +{ + if (id == 1) + return fallbackScope.data()->property(object, name, 0); + else if (id == 2) { + QString msg = "Property %0.%1 is undefined."; + return engine()->currentContext()->throwError(msg.arg(m_name, name)); + } + + const QString nameString = name.toString(); + + Property property = properties.value(nameString); + + if (debugProperties) + qbsTrace() << "PROPERTIES: evaluating " << nameString; + + if (!property.isValid()) { + if (debugProperties) + qbsTrace() << " : no such property"; + return QScriptValue(); // does this raise an error? + } + + if (property.scope) { + if (debugProperties) + qbsTrace() << " : object property"; + return property.scope->value; + } + + if (property.value.isValid()) { + if (debugProperties) + qbsTrace() << " : pre-evaluated property: " << property.value.toVariant(); + return property.value; + } + + // evaluate now + if (debugProperties) + qbsTrace() << " : evaluating now: " << property.valueSource.sourceCode(); + QScriptContext *context = engine()->currentContext(); + const QScriptValue oldActivation = context->activationObject(); + const QString &sourceCode = property.valueSource.sourceCode(); + + // evaluate base properties + QLatin1String baseValueName("base"); + const bool usesBaseProperty = sourceCode.contains(baseValueName); + if (usesBaseProperty) { + foreach (const Property &baseProperty, property.baseProperties) { + context->setActivationObject(baseProperty.scopeChain->value()); + QScriptValue baseValue; + try { + baseValue = evaluate(engine(), baseProperty.valueSource); + } + catch (Error &e) + { + baseValue = engine()->currentContext()->throwError("error while evaluating:\n" + e.toString()); + } + engine()->globalObject().setProperty(baseValueName, baseValue); + } + } + + context->setActivationObject(property.scopeChain->value()); + + QLatin1String oldValueName("outer"); + const bool usesOldProperty = fallbackScope && sourceCode.contains(oldValueName); + if (usesOldProperty) { + QScriptValue oldValue = fallbackScope.data()->value.property(name); + if (oldValue.isValid() && !oldValue.isError()) + engine()->globalObject().setProperty(oldValueName, oldValue); + } + + QScriptValue result; + // Do not throw exceptions through the depths of the script engine. + try { + result = evaluate(engine(), property.valueSource); + } + catch (Error &e) + { + result = engine()->currentContext()->throwError("error while evaluating:\n" + e.toString()); + } + + if (debugProperties) { + qbsTrace() << "PROPERTIES: evaluated " << nameString << " to " << result.toVariant() << " " << result.toString(); + if (result.isError()) + qbsTrace() << " was error!"; + } + + Scope::scopesWithEvaluatedProperties.insert(this); + property.value = result; + properties.insert(nameString, property); + + if (usesOldProperty) + engine()->globalObject().setProperty(oldValueName, engine()->undefinedValue()); + if (usesBaseProperty) + engine()->globalObject().setProperty(baseValueName, engine()->undefinedValue()); + context->setActivationObject(oldActivation); + + return result; +} + +QScriptValue Scope::property(const QString &name) const +{ + QScriptValue result = value.property(name); + if (result.isError()) + throw Error(result.toString()); + return result; +} + +bool Scope::boolValue(const QString &name, bool defaultValue) const +{ + QScriptValue scriptValue = property(name); + if (scriptValue.isBool()) + return scriptValue.toBool(); + return defaultValue; +} + +QString Scope::stringValue(const QString &name) const +{ + QScriptValue scriptValue = property(name); + if (scriptValue.isString()) + return scriptValue.toString(); + QVariant v = scriptValue.toVariant(); + if (v.type() == QVariant::String) { + return v.toString(); + } else if (v.type() == QVariant::StringList) { + const QStringList lst = v.toStringList(); + if (lst.count() == 1) + return lst.first(); + } + return QString(); +} + +QStringList Scope::stringListValue(const QString &name) const +{ + QScriptValue scriptValue = property(name); + if (scriptValue.isString()) { + return QStringList(scriptValue.toString()); + } else if (scriptValue.isArray()) { + QStringList lst; + int i=0; + forever { + QScriptValue item = scriptValue.property(i++); + if (!item.isValid()) + break; + if (!item.isString()) + continue; + lst.append(item.toString()); + } + return lst; + } + return QStringList(); +} + +QString Scope::verbatimValue(const QString &name) const +{ + const Property &property = properties.value(name); + return property.valueSource.sourceCode(); +} + +void Scope::dump(const QByteArray &aIndent) const +{ + QByteArray indent = aIndent; + printf("%sScope: {\n", indent.constData()); + indent.append(dumpIndent); + printf("%sName: '%s'\n", indent.constData(), qPrintable(m_name)); + if (!properties.isEmpty()) { + printf("%sProperties: [\n", indent.constData()); + indent.append(dumpIndent); + foreach (const QString &propertyName, properties.keys()) { + QScriptValue scriptValue = property(propertyName); + QString propertyValue; + if (scriptValue.isString()) + propertyValue = stringValue(propertyName); + else if (scriptValue.isArray()) + propertyValue = stringListValue(propertyName).join(", "); + else if (scriptValue.isBool()) + propertyValue = boolValue(propertyName) ? "true" : "false"; + else + propertyValue = verbatimValue(propertyName); + printf("%s'%s': %s\n", indent.constData(), qPrintable(propertyName), qPrintable(propertyValue)); + } + indent.chop(dumpIndent.length()); + printf("%s]\n", indent.constData()); + } + if (!declarations.isEmpty()) + printf("%sPropertyDeclarations: [%s]\n", indent.constData(), qPrintable(QStringList(declarations.keys()).join(", "))); + + indent.chop(dumpIndent.length()); + printf("%s}\n", indent.constData()); +} + +EvaluationObject::EvaluationObject(LanguageObject *instantiatingObject) +{ + instantiatingObject->file->registerEvaluationObject(this); + objects.append(instantiatingObject); +} + +EvaluationObject::~EvaluationObject() +{ + ProjectFile *file = instantiatingObject()->file; + if (!file->isDestructing()) + file->unregisterEvaluationObject(this); +} + +LanguageObject *EvaluationObject::instantiatingObject() const +{ + return objects.first(); +} + +void EvaluationObject::dump(QByteArray &indent) +{ + printf("%sEvaluationObject: {\n", indent.constData()); + indent.append(dumpIndent); + printf("%sProtoType: '%s'\n", indent.constData(), qPrintable(prototype)); + if (!modules.isEmpty()) { + printf("%sModules: [\n", indent.constData()); + indent.append(dumpIndent); + foreach (const QSharedPointer<Module> module, modules) + module->dump(indent); + indent.chop(dumpIndent.length()); + printf("%s]\n", indent.constData()); + } + scope->dump(indent); + foreach (EvaluationObject *child, children) + child->dump(indent); + indent.chop(dumpIndent.length()); + printf("%s}\n", indent.constData()); +} + +Module::Module() + : object(0) +{ +} + +Module::~Module() +{ +} + +ProjectFile *Module::file() const +{ + return object->instantiatingObject()->file; +} + +void Module::dump(QByteArray &indent) +{ + printf("%s'%s': %s\n", indent.constData(), qPrintable(name), qPrintable(dependsLocation.fileName)); +} + +static QStringList resolvePaths(const QStringList &paths, const QString &base) +{ + QStringList resolved; + foreach (const QString &path, paths) { + QString resolvedPath = FileInfo::resolvePath(base, path); + resolvedPath = QDir::cleanPath(resolvedPath); + resolved += resolvedPath; + } + return resolved; +} + + +static const char szLoaderPropertyName[] = "qbs_loader_ptr"; +static const QLatin1String name_FileTagger("FileTagger"); +static const QLatin1String name_Rule("Rule"); +static const QLatin1String name_Transformer("Transformer"); +static const QLatin1String name_TransformProperties("TransformProperties"); +static const QLatin1String name_Artifact("Artifact"); +static const QLatin1String name_Group("Group"); +static const QLatin1String name_Project("Project"); +static const QLatin1String name_Product("Product"); +static const QLatin1String name_ProductModule("ProductModule"); +static const QLatin1String name_Module("Module"); +static const QLatin1String name_Properties("Properties"); +static const QLatin1String name_PropertyOptions("PropertyOptions"); +static const QLatin1String name_Depends("Depends"); +static const QLatin1String name_moduleSearchPaths("moduleSearchPaths"); +static const uint hashName_FileTagger = qHash(name_FileTagger); +static const uint hashName_Rule = qHash(name_Rule); +static const uint hashName_Transformer = qHash(name_Transformer); +static const uint hashName_TransformProperties = qHash(name_TransformProperties); +static const uint hashName_Artifact = qHash(name_Artifact); +static const uint hashName_Group = qHash(name_Group); +static const uint hashName_Project = qHash(name_Project); +static const uint hashName_Product = qHash(name_Product); +static const uint hashName_ProductModule = qHash(name_ProductModule); +static const uint hashName_Module = qHash(name_Module); +static const uint hashName_Properties = qHash(name_Properties); +static const uint hashName_PropertyOptions = qHash(name_PropertyOptions); +static const uint hashName_Depends = qHash(name_Depends); +QHash<QString, PropertyDeclaration> Loader::m_dependsPropertyDeclarations; + +static const QLatin1String name_productPropertyScope("product property scope"); +static const QLatin1String name_projectPropertyScope("project property scope"); + +Loader::Loader() +{ + m_settings = Settings::create(); + + QVariant v; + v.setValue(static_cast<void*>(this)); + m_engine.setProperty(szLoaderPropertyName, v); + m_engine.pushContext(); // this preserves the original global object + + m_jsFunction_getHostOS = m_engine.newFunction(js_getHostOS, 0); + m_jsFunction_getHostDefaultArchitecture = m_engine.newFunction(js_getHostDefaultArchitecture, 0); + m_jsFunction_configurationValue = m_engine.newFunction(js_configurationValue, 2); + + if (m_dependsPropertyDeclarations.isEmpty()) { + QList<PropertyDeclaration> depends; + depends += PropertyDeclaration("name", PropertyDeclaration::String); + depends += PropertyDeclaration("submodules", PropertyDeclaration::Variant); + depends += PropertyDeclaration("condition", PropertyDeclaration::Boolean); + depends += PropertyDeclaration("required", PropertyDeclaration::Boolean); + depends += PropertyDeclaration("failureMessage", PropertyDeclaration::String); + foreach (const PropertyDeclaration &pd, depends) + m_dependsPropertyDeclarations.insert(pd.name, pd); + } +} + +Loader::~Loader() +{ +} + +static bool compare(const QStringList &list, const QString &value) +{ + if (list.size() != 1) + return false; + return list.first() == value; +} + +void Loader::setSearchPaths(const QStringList &searchPaths) +{ + m_searchPaths = searchPaths; +} + +ProjectFile::Ptr Loader::loadProject(const QString &fileName) +{ + m_settings->loadProjectSettings(fileName); + m_project = parseFile(fileName); + return m_project; +} + +static void setPathAndFilePath(const Scope::Ptr &scope, const QString &filePath, const QString &prefix = QString()) +{ + QString filePathPropertyName("filePath"); + QString pathPropertyName("path"); + if (!prefix.isEmpty()) { + filePathPropertyName = prefix + QLatin1String("FilePath"); + pathPropertyName = prefix + QLatin1String("Path"); + } + scope->properties.insert(filePathPropertyName, Property(QScriptValue(filePath))); + scope->properties.insert(pathPropertyName, Property(QScriptValue(FileInfo::path(filePath)))); +} + +Scope::Ptr Loader::buildFileContext(ProjectFile *file) +{ + Scope::Ptr context = Scope::create(&m_engine, QLatin1String("global file context"), file); + setPathAndFilePath(context, file->fileName, QLatin1String("local")); + evaluateImports(context, file->jsImports); + + return context; +} + +void Loader::resolveInheritance(LanguageObject *object, EvaluationObject *evaluationObject, + ScopeChain::Ptr moduleScope, const QVariantMap &userProperties) +{ + if (object->prototypeFileName.isEmpty()) { + if (object->prototype.size() != 1) + throw Error("prototype with dots does not resolve to a file", object->prototypeLocation); + evaluationObject->prototype = object->prototype.first(); + + setupInternalPrototype(evaluationObject); + + // once we know something is a project/product, add a property to + // the correct scope + if (evaluationObject->prototype == name_Project) { + if (Scope::Ptr projectPropertyScope = moduleScope->find(name_projectPropertyScope)) + projectPropertyScope->properties.insert("project", Property(evaluationObject)); + } + else if (evaluationObject->prototype == name_Product) { + if (Scope::Ptr productPropertyScope = moduleScope->find(name_productPropertyScope)) + productPropertyScope->properties.insert("product", Property(evaluationObject)); + } + + return; + } + + // load prototype (cache result) + ProjectFile::Ptr file = parseFile(object->prototypeFileName); + + // recurse to prototype's prototype + if (evaluationObject->objects.contains(file->root)) { + QString msg("circular prototypes in instantiation of '%1', '%2' recurred"); + throw Error(msg.arg(evaluationObject->instantiatingObject()->prototype.join("."), + object->prototype.join("."))); + } + evaluationObject->objects.append(file->root); + resolveInheritance(file->root, evaluationObject, moduleScope, userProperties); + + // ### expensive, and could be shared among all builds of this prototype instance + Scope::Ptr context = buildFileContext(file.data()); + + // project and product scopes are always available + ScopeChain::Ptr scopeChain(new ScopeChain(&m_engine, context)); + if (Scope::Ptr projectPropertyScope = moduleScope->findNonEmpty(name_projectPropertyScope)) + scopeChain->prepend(projectPropertyScope); + if (Scope::Ptr productPropertyScope = moduleScope->findNonEmpty(name_productPropertyScope)) + scopeChain->prepend(productPropertyScope); + + scopeChain->prepend(evaluationObject->scope); + + // having a module scope enables resolving of Depends blocks + if (moduleScope) + evaluateDependencies(file->root, evaluationObject, scopeChain, moduleScope, userProperties); + + fillEvaluationObject(scopeChain, file->root, evaluationObject->scope, evaluationObject, userProperties); + +// QByteArray indent; +// evaluationObject->dump(indent); +} + +static bool checkFileCondition(QScriptEngine *engine, const ScopeChain::Ptr &scope, const ProjectFile *file) +{ + static const bool debugCondition = false; + if (debugCondition) + qbsTrace() << "Checking condition"; + + const Binding &condition = file->root->bindings.value(QStringList("condition")); + if (!condition.isValid()) + return true; + + QScriptContext *context = engine->currentContext(); + const QScriptValue oldActivation = context->activationObject(); + context->setActivationObject(scope->value()); + + if (debugCondition) + qbsTrace() << " code is: " << condition.valueSource.sourceCode(); + const QScriptValue value = evaluate(engine, condition.valueSource); + bool result = false; + if (value.isBool()) + result = value.toBool(); + else + throw Error(QString("Condition return type must be boolean."), CodeLocation(condition.valueSource.fileName(), condition.valueSource.firstLineNumber())); + if (debugCondition) + qbsTrace() << " result: " << value.toString(); + + context->setActivationObject(oldActivation); + return result; +} + +static void applyFunctions(QScriptEngine *engine, LanguageObject *object, EvaluationObject *evaluationObject, ScopeChain::Ptr scope) +{ + if (object->functions.isEmpty()) + return; + + // set the activation object to the correct scope + QScriptValue oldActivation = engine->currentContext()->activationObject(); + engine->currentContext()->setActivationObject(scope->value()); + + foreach (const Function &func, object->functions) { + Property property; + property.value = evaluate(engine, func.source); + evaluationObject->scope->properties.insert(func.name, property); + } + + engine->currentContext()->setActivationObject(oldActivation); +} + +static void testIfValueIsAllowed(const QVariant &value, const QVariant &allowedValues, + const QString &propertyName, const CodeLocation &location) +{ + bool valueIsAllowed = false; + + if (value.type() == QVariant::String && allowedValues.type() == QVariant::List) + valueIsAllowed = allowedValues.toStringList().contains(value.toString()); + else if (value.type() == QVariant::Int && allowedValues.type() == QVariant::List) + valueIsAllowed = allowedValues.toList().contains(value); + else { + const QString msg = "The combination of the type of Property '%1' and the type of its allowedValues is not supported"; + throw Error(msg.arg(propertyName), location); + } + + if (!valueIsAllowed) { + const QString msg = "Value '%1' is not allowed for Property '%2'"; + throw Error(msg.arg(value.toString()).arg(propertyName), location); // TODO: print out allowed values? + } +} + +static void applyBinding(LanguageObject *object, const Binding &binding, const ScopeChain::Ptr &scopeChain) +{ + CodeLocation bindingLocation(binding.valueSource.fileName(), + binding.valueSource.firstLineNumber()); + Scope *target; + if (binding.name.size() == 1) { + target = scopeChain->first().data(); // assume the top scope is the 'current' one + } else { + if (compare(object->prototype, name_Artifact)) + return; + QScriptValue targetValue = scopeChain->value().property(binding.name.first()); + if (!targetValue.isValid() || targetValue.isError()) { + QString msg = "Binding '%1' failed, no property '%2' in the scope of %3"; + throw Error(msg.arg(binding.name.join("."), + binding.name.first(), + scopeChain->first()->name()), + bindingLocation); + } + target = dynamic_cast<Scope *>(targetValue.scriptClass()); + if (!target) { + QString msg = "Binding '%1' failed, property '%2' in the scope of %3 has no properties"; + throw Error(msg.arg(binding.name.join("."), + binding.name.first(), + scopeChain->first()->name()), + bindingLocation); + } + } + + for (int i = 1; i < binding.name.size() - 1; ++i) { + Scope *oldTarget = target; + const QString &bindingName = binding.name.at(i); + const QScriptValue &value = target->property(bindingName); + if (!value.isValid()) { + QString msg = "Binding '%1' failed, no property '%2' in %3"; + throw Error(msg.arg(binding.name.join("."), + binding.name.at(i), + target->name()), + bindingLocation); + } + target = dynamic_cast<Scope *>(value.scriptClass()); + if (!target) { + QString msg = "Binding '%1' failed, property '%2' in %3 has no properties"; + throw Error(msg.arg(binding.name.join("."), + bindingName, + oldTarget->name()), + bindingLocation); + } + } + + const QString name = binding.name.last(); + + if (!target->declarations.contains(name)) { + QString msg = "Binding '%1' failed, no property '%2' in %3"; + throw Error(msg.arg(binding.name.join("."), + name, + target->name()), + bindingLocation); + } + + Property newProperty; + newProperty.valueSource = binding.valueSource; + newProperty.scopeChain = scopeChain; + + Property &property = target->properties[name]; + if (!property.valueSource.isNull()) { + newProperty.baseProperties += property.baseProperties; + property.baseProperties.clear(); + newProperty.baseProperties += property; + } + property = newProperty; + + const PropertyDeclaration &decl = object->propertyDeclarations.value(name); + // ### testIfValueIsAllowed is wrong here... + if (!decl.allowedValues.isNull()) + testIfValueIsAllowed(target->property(name).toVariant(), decl.allowedValues, name, bindingLocation); +} + +static void applyBindings(LanguageObject *object, ScopeChain::Ptr scopeChain) +{ + foreach (const Binding &binding, object->bindings) + applyBinding(object, binding, scopeChain); +} + +void Loader::fillEvaluationObjectForProperties(const ScopeChain::Ptr &scope, LanguageObject *object, Scope::Ptr ids, EvaluationObject *evaluationObject, const QVariantMap &userProperties) +{ + if (!object->children.isEmpty()) + throw Error("Properties block may not have children", object->children.first()->prototypeLocation); + + const QStringList conditionName("condition"); + Binding condition = object->bindings.value(conditionName); + if (!condition.isValid()) + throw Error("Properties block must have a condition property", object->prototypeLocation); + + LanguageObject *ifCopy = new LanguageObject(*object); + + // adjust bindings to be if (condition) { original-source } + QMutableHashIterator<QStringList, Binding> it(ifCopy->bindings); + while (it.hasNext()) { + it.next(); + if (it.key() == conditionName) { + it.remove(); + continue; + } + Binding &binding = it.value(); + binding.valueSource = QScriptProgram( + QString("if (%1) { %2 }").arg( + condition.valueSource.sourceCode(), + binding.valueSource.sourceCode()), + binding.valueSource.fileName(), + binding.valueSource.firstLineNumber()); + } + + fillEvaluationObject(scope, ifCopy, ids, evaluationObject, userProperties); +} + +void Loader::setupInternalPrototype(EvaluationObject *evaluationObject) +{ + // special builtins + static QHash<QString, QList<PropertyDeclaration> > builtinDeclarations; + if (builtinDeclarations.isEmpty()) { + builtinDeclarations.insert(name_Depends, m_dependsPropertyDeclarations.values()); + PropertyDeclaration conditionProperty("condition", PropertyDeclaration::Boolean); + + QList<PropertyDeclaration> project; + project += PropertyDeclaration("references", PropertyDeclaration::Variant); + project += PropertyDeclaration(name_moduleSearchPaths, PropertyDeclaration::Variant); + builtinDeclarations.insert(name_Project, project); + + QList<PropertyDeclaration> product; + product += PropertyDeclaration("type", PropertyDeclaration::String); + product += PropertyDeclaration("name", PropertyDeclaration::String); + product += PropertyDeclaration("destination", PropertyDeclaration::String); + product += PropertyDeclaration("files", PropertyDeclaration::Variant, PropertyDeclaration::PropertyNotAvailableInConfig); + product += PropertyDeclaration("module", PropertyDeclaration::Variant); + product += PropertyDeclaration("modules", PropertyDeclaration::Variant); + product += PropertyDeclaration(name_moduleSearchPaths, PropertyDeclaration::Variant); + builtinDeclarations.insert(name_Product, product); + + QList<PropertyDeclaration> fileTagger; + fileTagger += PropertyDeclaration("pattern", PropertyDeclaration::String); + fileTagger += PropertyDeclaration("fileTags", PropertyDeclaration::Variant); + builtinDeclarations.insert(name_FileTagger, fileTagger); + + QList<PropertyDeclaration> group; + group += conditionProperty; + group += PropertyDeclaration("files", PropertyDeclaration::Variant, PropertyDeclaration::PropertyNotAvailableInConfig); + group += PropertyDeclaration("fileTags", PropertyDeclaration::Variant, PropertyDeclaration::PropertyNotAvailableInConfig); + group += PropertyDeclaration("prefix", PropertyDeclaration::Variant, PropertyDeclaration::PropertyNotAvailableInConfig); + builtinDeclarations.insert(name_Group, group); + + QList<PropertyDeclaration> artifact; + artifact += conditionProperty; + artifact += PropertyDeclaration("fileName", PropertyDeclaration::Verbatim); + artifact += PropertyDeclaration("fileTags", PropertyDeclaration::Variant); + builtinDeclarations.insert(name_Artifact, artifact); + + QList<PropertyDeclaration> rule; + rule += PropertyDeclaration("multiplex", PropertyDeclaration::Boolean); + rule += PropertyDeclaration("inputs", PropertyDeclaration::Variant); + rule += PropertyDeclaration("usings", PropertyDeclaration::Variant); + rule += PropertyDeclaration("explicitlyDependsOn", PropertyDeclaration::Variant); + rule += PropertyDeclaration("prepare", PropertyDeclaration::Verbatim); + builtinDeclarations.insert(name_Rule, rule); + + QList<PropertyDeclaration> transformer; + transformer += PropertyDeclaration("inputs", PropertyDeclaration::Variant); + transformer += PropertyDeclaration("prepare", PropertyDeclaration::Verbatim); + transformer += conditionProperty; + builtinDeclarations.insert(name_Transformer, transformer); + + QList<PropertyDeclaration> transformProperties; + builtinDeclarations.insert(name_TransformProperties, transformProperties); + + QList<PropertyDeclaration> productModule; + builtinDeclarations.insert(name_ProductModule, productModule); + + QList<PropertyDeclaration> module; + module += PropertyDeclaration("name", PropertyDeclaration::String); + module += PropertyDeclaration("setupBuildEnvironment", PropertyDeclaration::Verbatim); + module += PropertyDeclaration("setupRunEnvironment", PropertyDeclaration::Verbatim); + module += PropertyDeclaration("additionalProductFileTags", PropertyDeclaration::Variant); + module += conditionProperty; + builtinDeclarations.insert(name_Module, module); + + QList<PropertyDeclaration> propertyOptions; + propertyOptions += PropertyDeclaration("name", PropertyDeclaration::String); + propertyOptions += PropertyDeclaration("allowedValues", PropertyDeclaration::Variant); + propertyOptions += PropertyDeclaration("description", PropertyDeclaration::String); + builtinDeclarations.insert(name_PropertyOptions, propertyOptions); + } + + if (!builtinDeclarations.contains(evaluationObject->prototype)) + throw Error(QString("Type name '%1' is unknown.").arg(evaluationObject->prototype), + evaluationObject->instantiatingObject()->prototypeLocation); + + foreach (const PropertyDeclaration &pd, builtinDeclarations.value(evaluationObject->prototype)) { + evaluationObject->scope->declarations.insert(pd.name, pd); + evaluationObject->scope->properties.insert(pd.name, Property(m_engine.undefinedValue())); + } +} + +void Loader::fillEvaluationObject(const ScopeChain::Ptr &scope, LanguageObject *object, Scope::Ptr ids, EvaluationObject *evaluationObject, const QVariantMap &userProperties) +{ + // fill subobjects recursively + foreach (LanguageObject *child, object->children) { + // 'Properties' objects are treated specially, they don't introduce a scope + // and don't get added as a child object + if (compare(child->prototype, name_Properties)) { + fillEvaluationObjectForProperties(scope, child, ids, evaluationObject, userProperties); + continue; + } + + // 'Depends' blocks are already handled before this function is called + // and should not appear in the children list + if (compare(child->prototype, name_Depends)) + continue; + + EvaluationObject *childEvObject = new EvaluationObject(child); + const QString propertiesName = child->prototype.join(QLatin1String(".")); + childEvObject->scope = Scope::create(&m_engine, propertiesName, object->file); + + resolveInheritance(child, childEvObject); // ### need to pass 'moduleScope' for product/project property scopes + const uint childPrototypeHash = qHash(childEvObject->prototype); + + ScopeChain::Ptr childScope(scope->clone()->prepend(childEvObject->scope)); + + if (!child->id.isEmpty()) { + ids->properties.insert(child->id, Property(childEvObject)); + } + + // for Group and ProjectModule, add new module instances + const bool isProductModule = (childPrototypeHash == hashName_ProductModule); + const bool isArtifact = (childPrototypeHash == hashName_Artifact); + if (isProductModule || isArtifact || childPrototypeHash == hashName_Group) { + QHashIterator<QString, Module::Ptr> moduleIt(evaluationObject->modules); + while (moduleIt.hasNext()) { + moduleIt.next(); + Module::Ptr module = moduleIt.value(); + if (module->id.isEmpty()) + continue; + Scope::Ptr moduleInstance = Scope::create(&m_engine, module->object->scope->name(), module->file()); + if (isProductModule) { + // A ProductModule does not inherit module values set in the product + // but has its own module instance. + ScopeChain::Ptr moduleScope(new ScopeChain(&m_engine)); + moduleScope->prepend(scope->findNonEmpty(name_productPropertyScope)); + moduleScope->prepend(scope->findNonEmpty(name_projectPropertyScope)); + moduleScope->prepend(childEvObject->scope); + module = loadModule(module->file(), module->id, module->name, moduleScope, userProperties, module->dependsLocation); + childEvObject->modules.insert(module->name, module); + } + if (!isArtifact) + moduleInstance->fallbackScope = module->object->scope; + moduleInstance->declarations = module->object->scope->declarations; + Property property(moduleInstance); + childEvObject->scope->properties.insert(module->id, property); + } + } + + // for TransformProperties, add declarations to parent + if (childPrototypeHash == hashName_TransformProperties) { + for (QHash<QString, PropertyDeclaration>::const_iterator it = child->propertyDeclarations.begin(); + it != child->propertyDeclarations.end(); ++it) { + evaluationObject->scope->declarations.insert(it.key(), it.value()); + } + } + + fillEvaluationObject(childScope, child, ids, childEvObject, userProperties); + evaluationObject->children.append(childEvObject); + } + + fillEvaluationObjectBasics(scope, object, evaluationObject); +} + +void Loader::fillEvaluationObjectBasics(const ScopeChain::Ptr &scopeChain, LanguageObject *object, EvaluationObject *evaluationObject) +{ + // append the property declarations + foreach (const PropertyDeclaration &pd, object->propertyDeclarations) + if (!evaluationObject->scope->declarations.contains(pd.name)) + evaluationObject->scope->declarations.insert(pd.name, pd); + + applyFunctions(&m_engine, object, evaluationObject, scopeChain); + applyBindings(object, scopeChain); +} + +void Loader::evaluateImports(Scope::Ptr target, const JsImports &jsImports) +{ + for (JsImports::const_iterator importIt = jsImports.begin(); + importIt != jsImports.end(); ++importIt) { + + QScriptValue targetObject = m_engine.newObject(); + foreach (const QString &fileName, importIt.value()) { + QScriptValue importResult = m_jsImports.value(fileName); + if (importResult.isValid()) { + targetObject = importResult; + } else { + QFile file(fileName); + if (!file.open(QFile::ReadOnly)) { + QString msg("Couldn't open js import '%1'."); + // ### location + throw Error(msg.arg(fileName)); + continue; + } + const QString source = QTextStream(&file).readAll(); + file.close(); + const QScriptProgram program(source, fileName); + importResult = addJSImport(&m_engine, program, targetObject); + if (importResult.isError()) + throw Error(QLatin1String("error while evaluating import: ") + importResult.toString()); + + m_jsImports.insert(fileName, targetObject); + } + } + + target->properties.insert(importIt.key(), Property(targetObject)); + } +} + +void Loader::evaluatePropertyOptions(LanguageObject *object) +{ + foreach (LanguageObject *child, object->children) { + if (child->prototype.last() != name_PropertyOptions) + continue; + + const Binding nameBinding = child->bindings.value(QStringList("name")); + + if (!nameBinding.isValid()) + throw Error(name_PropertyOptions + " needs to define a 'name'"); + + const QScriptValue nameScriptValue = evaluate(&m_engine, nameBinding.valueSource); + const QString name = nameScriptValue.toString(); + + if (!object->propertyDeclarations.contains(name)) + throw Error(QString("no propery with name '%1' found").arg(name)); + + PropertyDeclaration &decl = object->propertyDeclarations[name]; + + const Binding allowedValuesBinding = child->bindings.value(QStringList("allowedValues")); + if (allowedValuesBinding.isValid()) { + const QScriptValue allowedValuesScriptValue = evaluate(&m_engine, allowedValuesBinding.valueSource); + decl.allowedValues = allowedValuesScriptValue.toVariant(); + } + + const Binding descriptionBinding = child->bindings.value(QStringList("description")); + if (descriptionBinding.isValid()) { + const QScriptValue description = evaluate(&m_engine, descriptionBinding.valueSource); + decl.description = description.toString(); + } + } +} + +Module::Ptr Loader::loadModule(ProjectFile *file, const QString &moduleId, const QString &moduleName, + ScopeChain::Ptr moduleBaseScope, const QVariantMap &userProperties, + const CodeLocation &dependsLocation) +{ + const bool isBaseModule = (moduleName == "qbs"); + + Module::Ptr module = Module::Ptr(new Module); + module->id = moduleId; + module->name = moduleName; + module->dependsLocation = dependsLocation; + module->object = new EvaluationObject(file->root); + const QString propertiesName = QString("module %1").arg(moduleName); + module->object->scope = Scope::create(&m_engine, propertiesName, file); + + resolveInheritance(file->root, module->object, moduleBaseScope, userProperties); + if (module->object->prototype != name_Module) + return Module::Ptr(); + + module->object->scope->properties.insert("name", Property(m_engine.toScriptValue(moduleName))); + module->context = buildFileContext(file); + + ScopeChain::Ptr moduleScope(moduleBaseScope->clone()); + moduleScope->prepend(module->context); + moduleScope->prepend(module->object->scope); + if (isBaseModule) { + // setup helper properties of the base module + Property p; + p.value = m_jsFunction_getHostOS; + module->object->scope->properties.insert("getHostOS", p); + p.value = m_jsFunction_getHostDefaultArchitecture; + module->object->scope->properties.insert("getHostDefaultArchitecture", p); + p.value = m_jsFunction_configurationValue; + module->object->scope->properties.insert("configurationValue", p); + } + + evaluatePropertyOptions(file->root); + evaluateDependencies(file->root, module->object, moduleScope, moduleBaseScope, userProperties, !isBaseModule); + if (!module->object->unknownModules.isEmpty()) { + QString msg; + foreach (const UnknownModule &missingModule, module->object->unknownModules) { + msg.append(Error(QString("Module '%1' cannot be loaded.").arg(missingModule.name), + missingModule.dependsLocation).toString()); + msg.append("\n"); + } + throw Error(msg); + } + buildModulesProperty(module->object); + + if (checkFileCondition(&m_engine, moduleScope, file)) { + qbsTrace() << "loading module '" << moduleName << "' from " << file->fileName; + if (!file->root->id.isEmpty()) + module->context->properties.insert(file->root->id, Property(module->object)); + fillEvaluationObject(moduleScope, file->root, module->object->scope, module->object, userProperties); + + // override properties given on the command line + const QVariantMap userModuleProperties = userProperties.value(moduleName).toMap(); + for (QVariantMap::const_iterator vmit = userModuleProperties.begin(); vmit != userModuleProperties.end(); ++vmit) { + if (!module->object->scope->properties.contains(vmit.key())) + throw Error("Unknown property: " + module->id + '.' + vmit.key()); + module->object->scope->properties.insert(vmit.key(), Property(m_engine.toScriptValue(vmit.value()))); + + const PropertyDeclaration &decl = module->object->scope->declarations.value(vmit.key()); + if (!decl.allowedValues.isNull()) + testIfValueIsAllowed(vmit.value(), decl.allowedValues, vmit.key(), dependsLocation); + } + + return module; + } + + return Module::Ptr(); +} + +/// load all module.qbs files, checking their conditions +Module::Ptr Loader::loadModule(const QString &moduleId, const QString &moduleName, ScopeChain::Ptr moduleBaseScope, + const QVariantMap &userProperties, const CodeLocation &dependsLocation, + const QStringList &extraSearchPaths) +{ + Q_ASSERT(!moduleName.isEmpty()); + + Module::Ptr module; + QStringList searchPaths = extraSearchPaths; + + const QString searchSubDir("modules"); + foreach (const QString &path, m_searchPaths) + searchPaths += FileInfo::resolvePath(path, searchSubDir); + + foreach (const QString &path, searchPaths) { + QString dirPath = FileInfo::resolvePath(path, moduleName); + QFileInfo dirInfo(dirPath); + if (!dirInfo.isDir()) { + bool found = false; +#ifndef Q_OS_WIN + // On case sensitive file systems try to find the path. + QStringList subPaths = moduleName.split("/", QString::SkipEmptyParts); + QDir dir(path); + if (!dir.cd(searchSubDir)) + continue; + do { + QStringList lst = dir.entryList(QStringList(subPaths.takeFirst()), QDir::Dirs); + if (lst.count() != 1) + break; + if (!dir.cd(lst.first())) + break; + if (subPaths.isEmpty()) { + found = true; + dirPath = dir.absolutePath(); + } + } while (!found); +#endif + if (!found) + continue; + } + + QDirIterator dirIter(dirPath, QStringList("*.qbs")); + while (dirIter.hasNext()) { + QString fileName = dirIter.next(); + ProjectFile::Ptr file = parseFile(fileName); + if (!file) + throw Error("Error while parsing file: " + fileName, dependsLocation); + + module = loadModule(file.data(), moduleId, moduleName, moduleBaseScope, userProperties, dependsLocation); + if (module) + break; + } + if (module) + break; + } + + return module; +} + +void Loader::evaluateDependencies(LanguageObject *object, EvaluationObject *evaluationObject, const ScopeChain::Ptr &localScope, + ScopeChain::Ptr moduleScope, const QVariantMap &userProperties, bool loadBaseModule) +{ + // check for additional module search paths in the product + Binding searchPathsBinding = object->bindings.value(QStringList(name_moduleSearchPaths)); + if (searchPathsBinding.isValid()) + applyBinding(object, searchPathsBinding, localScope); + + // if none found, check for additional module search paths in the project + QStringList extraSearchPaths; + Property projectProperty = localScope->lookupProperty("project"); + if (projectProperty.isValid() && projectProperty.scope) { + extraSearchPaths = projectProperty.scope->stringListValue(name_moduleSearchPaths); + // ### depends on the project.path property + extraSearchPaths = resolvePaths(extraSearchPaths, projectProperty.scope->stringValue("path")); + } + + if (loadBaseModule) { + Module::Ptr baseModule = loadModule("qbs", "qbs", moduleScope, userProperties, CodeLocation(object->file->fileName)); + if (!baseModule) + throw Error("Cannot load the qbs base module."); + evaluationObject->modules.insert(baseModule->name, baseModule); + evaluationObject->scope->properties.insert(baseModule->id, Property(baseModule->object)); + } + + foreach (LanguageObject *child, object->children) { + if (compare(child->prototype, name_Depends)) { + QList<UnknownModule> unknownModules; + foreach (const Module::Ptr &m, evaluateDependency(evaluationObject, child, moduleScope, extraSearchPaths, &unknownModules, userProperties)) { + evaluationObject->modules.insert(m->name, m); + evaluationObject->scope->properties.insert(m->id, Property(m->object)); + } + evaluationObject->unknownModules.append(unknownModules); + } + } +} + +void Loader::buildModulesProperty(EvaluationObject *evaluationObject) +{ + // set up a XXX.modules property + Scope::Ptr modules = Scope::create(&m_engine, QLatin1String("modules property"), evaluationObject->instantiatingObject()->file); + for (QHash<QString, Module::Ptr>::const_iterator it = evaluationObject->modules.begin(); + it != evaluationObject->modules.end(); ++it) + { + modules->properties.insert(it.key(), Property(it.value()->object)); + modules->declarations.insert(it.key(), PropertyDeclaration(it.key(), PropertyDeclaration::Variant)); + } + evaluationObject->scope->properties.insert("modules", Property(modules)); + evaluationObject->scope->declarations.insert("modules", PropertyDeclaration("modules", PropertyDeclaration::Variant)); +} + +QList<Module::Ptr> Loader::evaluateDependency(EvaluationObject *parentEObj, LanguageObject *depends, ScopeChain::Ptr moduleScope, + const QStringList &extraSearchPaths, + QList<UnknownModule> *unknownModules, const QVariantMap &userProperties) +{ + const CodeLocation dependsLocation = depends->prototypeLocation; + + // check for the use of undeclared properties + foreach (const Binding &binding, depends->bindings) { + if (binding.name.count() > 1) + throw Error("Bindings with dots are forbidden in Depends.", dependsLocation); + if (!m_dependsPropertyDeclarations.contains(binding.name.first())) + throw Error(QString("There's no property '%1' in Depends.").arg(binding.name.first()), + CodeLocation(depends->file->fileName, binding.valueSource.firstLineNumber())); + } + + // check condition + Binding binding = depends->bindings.value(QStringList("condition")); + if (binding.isValid()) { + QScriptValue v = evaluate(&m_engine, binding.valueSource); + if (!v.toBool()) + return QList<Module::Ptr>(); + } + + bool isRequired = true; + binding = depends->bindings.value(QStringList("required")); + if (!binding.valueSource.isNull()) + isRequired = evaluate(&m_engine, binding.valueSource).toBool(); + + QString failureMessage; + binding = depends->bindings.value(QStringList("failureMessage")); + if (!binding.valueSource.isNull()) + failureMessage = evaluate(&m_engine, binding.valueSource).toString(); + + QString moduleName; + binding = depends->bindings.value(QStringList("name")); + if (!binding.valueSource.isNull()) { + moduleName = evaluate(&m_engine, binding.valueSource).toString(); + } else { + moduleName = depends->id; + } + + if (parentEObj->modules.contains(moduleName)) { + // If the module is already in the target object then don't add it a second time. + // This is for the case where you have the same module in the instantiating object + // and in a base object. + // + // ---Foo.qbs--- + // Product { + // cpp.defines: ["BEAGLE"] + // Depends { name: "cpp" } + // } + // + // ---bar.qbp--- + // Foo { + // Depends { name: "cpp" } + // } + // + return QList<Module::Ptr>(); + } + + QString moduleId = depends->id; + if (moduleId.isEmpty()) + moduleId = moduleName; + + QStringList subModules; + Binding subModulesBinding = depends->bindings.value(QStringList("submodules")); + if (!subModulesBinding.valueSource.isNull()) + subModules = evaluate(&m_engine, subModulesBinding.valueSource).toVariant().toStringList(); + + QStringList fullModuleIds; + QStringList fullModuleNames; + if (subModules.isEmpty()) { + fullModuleIds.append(moduleId); + fullModuleNames.append(moduleName.toLower().replace('.', "/")); + } else { + foreach (const QString &subModuleName, subModules) { + fullModuleIds.append(moduleId + "." + subModuleName); + fullModuleNames.append(moduleName.toLower().replace('.', "/") + "/" + subModuleName.toLower().replace('.', "/")); + } + } + + QList<Module::Ptr> modules; + unknownModules->clear(); + for (int i=0; i < fullModuleNames.count(); ++i) { + const QString &fullModuleName = fullModuleNames.at(i); + Module::Ptr module = loadModule(fullModuleIds.at(i), fullModuleName, moduleScope, userProperties, dependsLocation, extraSearchPaths); + if (module) { + modules.append(module); + } else { + UnknownModule unknownModule; + unknownModule.name = fullModuleName; + unknownModule.required = isRequired; + unknownModule.failureMessage = failureMessage; + unknownModule.dependsLocation = dependsLocation; + unknownModules->append(unknownModule); + } + } + return modules; +} + +static void findModuleDependencies_impl(const Module::Ptr &module, QHash<QString, ProjectFile *> &result) +{ + QString moduleName = module->name; + ProjectFile *file = module->file(); + ProjectFile *otherFile = result.value(moduleName); + if (otherFile && otherFile != file) { + throw Error(QString("two different versions of '%1' were included: '%2' and '%3'").arg( + moduleName, file->fileName, otherFile->fileName)); + } else if (otherFile) { + return; + } + + result.insert(moduleName, file); + foreach (const Module::Ptr &depModule, module->object->modules) { + findModuleDependencies_impl(depModule, result); + } +} + +static QHash<QString, ProjectFile *> findModuleDependencies(EvaluationObject *root) +{ + QHash<QString, ProjectFile *> result; + foreach (const Module::Ptr &module, root->modules) { + foreach (const Module::Ptr &depModule, module->object->modules) { + findModuleDependencies_impl(depModule, result); + } + } + return result; +} + +static QVariantMap evaluateAll(const ResolvedProduct::Ptr &rproduct, const Scope::Ptr &properties) +{ + QVariantMap result; + + if (properties->fallbackScope) + result = evaluateAll(rproduct, properties->fallbackScope); + + typedef QHash<QString, PropertyDeclaration>::const_iterator iter; + iter end = properties->declarations.end(); + for (iter it = properties->declarations.begin(); it != end; ++it) { + const PropertyDeclaration &decl = it.value(); + if (decl.type == PropertyDeclaration::Verbatim || decl.flags.testFlag(PropertyDeclaration::PropertyNotAvailableInConfig)) + continue; + + Property property = properties->properties.value(it.key()); + if (!property.isValid()) + continue; + + QVariant value; + if (property.scope) { + value = evaluateAll(rproduct, property.scope); + } else { + value = properties->property(it.key()).toVariant(); + } + + if (decl.type == PropertyDeclaration::Paths) { + QStringList lst = value.toStringList(); + value = resolvePaths(lst, rproduct->sourceDirectory); + } + + result.insert(it.key(), value); + } + return result; +} + +static void clearCachedValues() +{ + QScriptValue nullScriptValue; + const std::set<Scope *>::const_iterator scopeEnd = Scope::scopesWithEvaluatedProperties.end(); + for (std::set<Scope *>::const_iterator it = Scope::scopesWithEvaluatedProperties.begin(); it != scopeEnd; ++it) { + const QHash<QString, Property>::iterator propertiesEnd = (*it)->properties.end(); + for (QHash<QString, Property>::iterator pit = (*it)->properties.begin(); pit != propertiesEnd; ++pit) { + Property &property = pit.value(); + if (!property.valueSource.isNull()) + property.value = nullScriptValue; + } + } + Scope::scopesWithEvaluatedProperties.clear(); +} + +int Loader::productCount(Configuration::Ptr userProperties) +{ + Q_ASSERT(hasLoaded()); + + LanguageObject *object = m_project->root; + EvaluationObject *evaluationObject = new EvaluationObject(object); + + const QString propertiesName = object->prototype.join("."); + evaluationObject->scope = Scope::create(&m_engine, propertiesName, m_project->root->file); + + Scope::Ptr productProperty = Scope::create(&m_engine, name_productPropertyScope, m_project->root->file); + Scope::Ptr projectProperty = Scope::create(&m_engine, name_projectPropertyScope, m_project->root->file); + + // for the 'product' and 'project' property available to the modules + ScopeChain::Ptr moduleScope(new ScopeChain(&m_engine)); + moduleScope->prepend(productProperty); + moduleScope->prepend(projectProperty); + + ScopeChain::Ptr localScope(new ScopeChain(&m_engine)); + localScope->prepend(productProperty); + localScope->prepend(projectProperty); + localScope->prepend(evaluationObject->scope); + + resolveInheritance(object, evaluationObject, moduleScope, userProperties->value()); + + if (evaluationObject->prototype != name_Project) + return 0; + + fillEvaluationObjectBasics(localScope, object, evaluationObject); + QStringList referencedProducts = evaluationObject->scope->stringListValue("references"); + + setPathAndFilePath(evaluationObject->scope, object->file->fileName); + int productChildrenCount = 0; + foreach (LanguageObject *child, object->children) { + EvaluationObject *eoChild = new EvaluationObject(child); + eoChild->scope = Scope::Ptr(evaluationObject->scope); + resolveInheritance(child, eoChild, moduleScope, userProperties->value()); + if (eoChild->prototype == name_Product) + ++productChildrenCount; + } + + return referencedProducts.count() + productChildrenCount; +} + +ResolvedProject::Ptr Loader::resolveProject(const QString &buildDirectoryRoot, + Configuration::Ptr userProperties, + QFutureInterface<bool> &futureInterface, + bool resolveProductDependencies) +{ + if (qbsLogLevel(LoggerTrace)) + qbsTrace() << "[LDR] resolving " << m_project->fileName; + ResolvedProject::Ptr rproject(new ResolvedProject); + rproject->qbsFile = m_project->fileName; + + Scope::Ptr context = buildFileContext(m_project.data()); + ScopeChain::Ptr scope(new ScopeChain(&m_engine, context)); + + ResolvedModule::Ptr dummyModule(new ResolvedModule); + dummyModule->jsImports = m_project->jsImports; + QList<Rule::Ptr> globalRules; + + ProjectData products; + resolveTopLevel(rproject, + m_project->root, + m_project->fileName, + &products, + &globalRules, + userProperties, + scope, + dummyModule, + futureInterface); + + QSet<QString> uniqueStrings; + QMultiMap<QString, ResolvedProduct::Ptr> resolvedProducts; + QHash<ResolvedProduct::Ptr, ProductData>::iterator it = products.begin(); + for (; it != products.end(); ++it) { + futureInterface.setProgressValue(futureInterface.progressValue() + 1); + ResolvedProduct::Ptr rproduct = it.key(); + ProductData &data = it.value(); + Scope *productProps = data.product->scope.data(); + + rproduct->name = productProps->stringValue("name"); + QString buildDirectory = FileInfo::resolvePath(buildDirectoryRoot, rproject->id); + + // insert property "buildDirectory" + { + Property p(m_engine.toScriptValue(buildDirectory)); + productProps->properties.insert("buildDirectory", p); + } + + rproduct->fileTags = productProps->stringListValue("type"); + rproduct->destinationDirectory = productProps->stringValue("destination"); + rproduct->buildDirectory = buildDirectory; + foreach (const Rule::Ptr &rule, globalRules) + rproduct->rules.insert(rule); + const QString lowerProductName = rproduct->name.toLower(); + uniqueStrings.insert(lowerProductName); + resolvedProducts.insert(lowerProductName, rproduct); + + // resolve the modules for this product + for (QHash<QString, Module::Ptr>::const_iterator modIt = data.product->modules.begin(); + modIt != data.product->modules.end(); ++modIt) + { + resolveModule(rproduct, modIt.key(), modIt.value()->object); + } + + QList<EvaluationObject *> unresolvedChildren = resolveCommonItems(data.product->children, rproduct, dummyModule); + + // build the product's configuration + rproduct->configuration = Configuration::Ptr(new Configuration); + QVariantMap productCfg = evaluateAll(rproduct, data.product->scope); + rproduct->configuration->setValue(productCfg); + + // handle the 'Product.files' property + { + QScriptValue files = data.product->scope->property("files"); + if (files.isValid()) { + resolveGroup(rproduct, data.product, data.product); + } + } + + foreach (EvaluationObject *child, unresolvedChildren) { + const uint prototypeNameHash = qHash(child->prototype); + if (prototypeNameHash == hashName_Group) { + resolveGroup(rproduct, data.product, child); + } else if (prototypeNameHash == hashName_ProductModule) { + child->scope->properties.insert("product", Property(data.product)); + resolveProductModule(rproduct, data.product, child); + data.usedProductsFromProductModule += child->unknownModules; + } + } + + // Apply file taggers and merge duplicate artifacts. + QHash<QString, SourceArtifact::Ptr> uniqueArtifacts; + foreach (const SourceArtifact::Ptr &artifact, rproduct->sources) { + if (artifact->fileTags.isEmpty()) { + artifact->fileTags = rproduct->fileTagsForFileName(artifact->absoluteFilePath); + if (!artifact->fileTags.isEmpty() && qbsLogLevel(LoggerTrace)) + qbsTrace() << "[LDR] adding file tags " << artifact->fileTags << " to " << FileInfo::fileName(artifact->absoluteFilePath); + } + SourceArtifact::Ptr existing = uniqueArtifacts.value(artifact->absoluteFilePath); + if (existing) { + + // if an artifact is in the product and in a group, prefer the group configuration + if (existing->configuration == rproduct->configuration) { + existing->configuration = artifact->configuration; + } else if (artifact->configuration != rproduct->configuration) { + throw Error(QString("Artifact '%1' is in more than one group.").arg(artifact->absoluteFilePath), + CodeLocation(m_project->fileName)); + } + + existing->fileTags.unite(artifact->fileTags); + } + else + uniqueArtifacts.insert(artifact->absoluteFilePath, artifact); + } + rproduct->sources = uniqueArtifacts.values().toSet(); + + // fix up source artifact configurations + foreach (SourceArtifact::Ptr artifact, rproduct->sources) + if (!artifact->configuration) + artifact->configuration = rproduct->configuration; + + rproject->products.insert(rproduct); + } + + // Change build directory for products with the same name. + foreach (const QString &name, uniqueStrings) { + QList<ResolvedProduct::Ptr> products = resolvedProducts.values(name); + if (products.count() < 2) + continue; + foreach (ResolvedProduct::Ptr product, products) { + product->buildDirectory.append('/'); + product->buildDirectory.append(product->fileTags.join("-")); + } + } + + // Resolve inter-product dependencies. + if (resolveProductDependencies) { + + // Collect product dependencies from ProductModules. + bool productDependenciesAdded; + do { + productDependenciesAdded = false; + foreach (ResolvedProduct::Ptr rproduct, rproject->products) { + ProductData &productData = products[rproduct]; + foreach (const UnknownModule &unknownModule, productData.usedProducts) { + const QString &usedProductName = unknownModule.name; + QList<ResolvedProduct::Ptr> usedProductCandidates = resolvedProducts.values(usedProductName); + if (usedProductCandidates.count() < 1) { + if (!unknownModule.required) { + if (!unknownModule.failureMessage.isEmpty()) + qbsWarning() << unknownModule.failureMessage; + continue; + } + throw Error(QString("Product dependency '%1' not found.").arg(usedProductName), + CodeLocation(m_project->fileName)); + } + if (usedProductCandidates.count() > 1) + throw Error(QString("Product dependency '%1' is ambiguous.").arg(usedProductName), + CodeLocation(m_project->fileName)); + ResolvedProduct::Ptr usedProduct = usedProductCandidates.first(); + const ProductData &usedProductData = products.value(usedProduct); + bool added; + productData.addUsedProducts(usedProductData.usedProductsFromProductModule, &added); + if (added) + productDependenciesAdded = true; + } + } + } while (productDependenciesAdded); + + // Resolve all inter-product dependencies. + foreach (ResolvedProduct::Ptr rproduct, rproject->products) { + foreach (const UnknownModule &unknownModule, products.value(rproduct).usedProducts) { + const QString &usedProductName = unknownModule.name; + QList<ResolvedProduct::Ptr> usedProductCandidates = resolvedProducts.values(usedProductName); + if (usedProductCandidates.count() < 1) { + if (!unknownModule.required) { + if (!unknownModule.failureMessage.isEmpty()) + qbsWarning() << unknownModule.failureMessage; + continue; + } + throw Error(QString("Product dependency '%1' not found.").arg(usedProductName), + CodeLocation(m_project->fileName)); + } + if (usedProductCandidates.count() > 1) + throw Error(QString("Product dependency '%1' is ambiguous.").arg(usedProductName), + CodeLocation(m_project->fileName)); + ResolvedProduct::Ptr usedProduct = usedProductCandidates.first(); + rproduct->uses.insert(usedProduct); + + // insert the configuration of the ProductModule into the product's configuration + const QVariantMap productModuleConfig = m_productModules.value(usedProductName); + if (productModuleConfig.isEmpty()) + continue; + + QVariantMap productConfigValue = rproduct->configuration->value(); + QVariantMap modules = productConfigValue.value("modules").toMap(); + modules.insert(usedProductName, productModuleConfig); + productConfigValue.insert("modules", modules); + rproduct->configuration->setValue(productConfigValue); + + // insert the configuration of the ProductModule into the artifact configurations + foreach (SourceArtifact::Ptr artifact, rproduct->sources) { + QVariantMap sourceArtifactConfigValue = artifact->configuration->value(); + QVariantMap modules = sourceArtifactConfigValue.value("modules").toMap(); + modules.insert(usedProductName, productModuleConfig); + sourceArtifactConfigValue.insert("modules", modules); + artifact->configuration->setValue(sourceArtifactConfigValue); + } + } + } + } + + // Create the unaltered configuration for this project from all used modules. + { + rproject->configuration = Configuration::Ptr(new Configuration); + QSet<QString> seenModules; + ResolvedProduct::Ptr dummyProduct(new ResolvedProduct); + foreach (const ProductData &pd, products) { + foreach (Module::Ptr module, pd.product->modules) { + if (seenModules.contains(module->name)) + continue; + seenModules.insert(module->name); + QVariantMap projectConfigValue = rproject->configuration->value(); + projectConfigValue.insert(module->name, evaluateAll(dummyProduct, module->object->scope)); + rproject->configuration->setValue(projectConfigValue); + } + } + } + + return rproject; +} + +static bool checkCondition(EvaluationObject *object) +{ + QScriptValue scriptValue = object->scope->property("condition"); + if (scriptValue.isBool()) { + return scriptValue.toBool(); + } else if (scriptValue.isValid() && !scriptValue.isUndefined()) { + const QScriptProgram &scriptProgram = object->objects.first()->bindings.value(QStringList("condition")).valueSource; + throw Error(QString("Invalid condition."), CodeLocation(scriptProgram.fileName(), scriptProgram.firstLineNumber())); + } + // no 'condition' property means 'the condition is true' + return true; +} + +void Loader::resolveModule(ResolvedProduct::Ptr rproduct, const QString &moduleName, EvaluationObject *module) +{ + ResolvedModule::Ptr rmodule(new ResolvedModule); + rmodule->name = moduleName; + rmodule->jsImports = module->instantiatingObject()->file->jsImports; + rmodule->setupBuildEnvironmentScript = module->scope->verbatimValue("setupBuildEnvironment"); + rmodule->setupRunEnvironmentScript = module->scope->verbatimValue("setupRunEnvironment"); + QStringList additionalProductFileTags = module->scope->stringListValue("additionalProductFileTags"); + if (!additionalProductFileTags.isEmpty()) { + rproduct->fileTags.append(additionalProductFileTags); + rproduct->fileTags = rproduct->fileTags.toSet().toList(); + } + foreach (Module::Ptr m, module->modules) + rmodule->moduleDependencies.append(m->name); + rproduct->modules.append(rmodule); + QList<EvaluationObject *> unhandledChildren = resolveCommonItems(module->children, rproduct, rmodule); + foreach (EvaluationObject *child, unhandledChildren) { + if (child->prototype == name_Artifact) { + if (!checkCondition(child)) + continue; + QString fileName = child->scope->stringValue("fileName"); + if (fileName.isEmpty()) + throw Error(QString("Source file %0 does not exist.").arg(fileName)); + SourceArtifact::Ptr artifact; + foreach (SourceArtifact::Ptr a, rproduct->sources) { + if (a->absoluteFilePath == fileName) { + artifact = a; + break; + } + } + if (!artifact) { + artifact = SourceArtifact::Ptr(new SourceArtifact); + artifact->absoluteFilePath = FileInfo::resolvePath(rproduct->sourceDirectory, fileName); + rproduct->sources += artifact; + } + artifact->fileTags += child->scope->stringListValue("fileTags").toSet(); + } else { + QString msg = "Items of type %0 not allowed in a Module."; + throw Error(msg.arg(child->prototype)); + } + } +} + +static QVariantMap evaluateModuleValues(ResolvedProduct::Ptr rproduct, EvaluationObject *product, Scope::Ptr objectScope) +{ + QVariantMap values; + QVariantMap modules; + for (QHash<QString, Module::Ptr>::const_iterator it = product->modules.begin(); + it != product->modules.end(); ++it) + { + Module::Ptr module = it.value(); + const QString name = module->name; + const QString id = module->id; + if (!id.isEmpty()) { + Scope::Ptr moduleScope = objectScope->properties.value(id).scope; + if (!moduleScope) + moduleScope = product->scope->properties.value(id).scope; + if (!moduleScope) + continue; + modules.insert(name, evaluateAll(rproduct, moduleScope)); + } else { + modules.insert(name, evaluateAll(rproduct, module->object->scope)); + } + } + values.insert(QLatin1String("modules"), modules); + return values; +} + +/** + * Resolve Group {} and the files part of Product {}. + */ +void Loader::resolveGroup(ResolvedProduct::Ptr rproduct, EvaluationObject *product, EvaluationObject *group) +{ + const bool isGroup = product != group; + + Configuration::Ptr configuration; + + if (isGroup) { + clearCachedValues(); + + if (!checkCondition(group)) + return; + + // build configuration for this group + configuration = Configuration::Ptr(new Configuration); + configuration->setValue(evaluateModuleValues(rproduct, product, group->scope)); + } else { + configuration = rproduct->configuration; + } + + // Products can have 'files' but not 'fileTags' + QStringList files = group->scope->stringListValue("files"); + if (isGroup) { + QString prefix = group->scope->stringValue("prefix"); + if (!prefix.isEmpty()) + for (int i=files.count(); --i >= 0;) + files[i].prepend(prefix); + } + QSet<QString> fileTags; + if (isGroup) + fileTags = group->scope->stringListValue("fileTags").toSet(); + foreach (const QString &fileName, files) { + SourceArtifact::Ptr artifact(new SourceArtifact); + artifact->configuration = configuration; + artifact->absoluteFilePath = FileInfo::resolvePath(rproduct->sourceDirectory, fileName); + artifact->fileTags = fileTags; + rproduct->sources.insert(artifact); + } +} + +void Loader::resolveProductModule(ResolvedProduct::Ptr rproduct, EvaluationObject *product, EvaluationObject *productModule) +{ + Q_ASSERT(!rproduct->name.isEmpty()); + + QVariantMap userProperties; // ### dummy + ScopeChain::Ptr localScopeChain(new ScopeChain(&m_engine, productModule->scope)); + ScopeChain::Ptr moduleScopeChain(new ScopeChain(&m_engine, productModule->scope)); + evaluateDependencies(productModule->instantiatingObject(), productModule, localScopeChain, moduleScopeChain, userProperties); + + clearCachedValues(); + QVariantMap moduleValues = evaluateModuleValues(rproduct, product, productModule->scope); + m_productModules.insert(rproduct->name.toLower(), moduleValues); +} + +void Loader::resolveTransformer(ResolvedProduct::Ptr rproduct, EvaluationObject *trafo, ResolvedModule::Ptr module) +{ + if (!checkCondition(trafo)) + return; + + ResolvedTransformer::Ptr rtrafo(new ResolvedTransformer); + rtrafo->module = module; + rtrafo->jsImports = trafo->instantiatingObject()->file->jsImports; + rtrafo->inputs = trafo->scope->stringListValue("inputs"); + for (int i=0; i < rtrafo->inputs.count(); ++i) + rtrafo->inputs[i] = FileInfo::resolvePath(rproduct->sourceDirectory, rtrafo->inputs[i]); + rtrafo->transform = RuleScript::Ptr(new RuleScript); + rtrafo->transform->script = trafo->scope->verbatimValue("prepare"); + rtrafo->transform->location.fileName = trafo->instantiatingObject()->file->fileName; + rtrafo->transform->location.column = 1; + Configuration::Ptr outputConfiguration(new Configuration); + foreach (EvaluationObject *child, trafo->children) { + if (child->prototype != name_Artifact) + throw Error(QString("Transformer: wrong child type '%0'.").arg(child->prototype)); + SourceArtifact::Ptr artifact(new SourceArtifact); + artifact->configuration = outputConfiguration; + QString fileName = child->scope->stringValue("fileName"); + if (fileName.isEmpty()) + throw Error("Artifact fileName must not be empty."); + artifact->absoluteFilePath = FileInfo::resolvePath(rproduct->buildDirectory, + fileName); + artifact->fileTags = child->scope->stringListValue("fileTags").toSet(); + rtrafo->outputs += artifact; + } + rproduct->transformers += rtrafo; +} + +static void addTransformPropertiesToRule(Rule::Ptr rule, LanguageObject *obj) +{ + foreach (const Binding &binding, obj->bindings) { + if (binding.name.length() != 1) { + throw Error("Binding with dots are prohibited in TransformProperties.", + CodeLocation(binding.valueSource.fileName(), + binding.valueSource.firstLineNumber())); + continue; + } + + rule->transformProperties.insert(binding.name.first(), binding.valueSource); + } +} + +/** + *Resolve stuff that Module and Product have in common. + */ +QList<EvaluationObject *> Loader::resolveCommonItems(const QList<EvaluationObject *> &objects, + ResolvedProduct::Ptr rproduct, ResolvedModule::Ptr module) +{ + QList<LanguageObject *> outerTransformProperties; // ### do we really want to allow these? + QList<Rule::Ptr> rules; + + QList<EvaluationObject *> unhandledObjects; + foreach (EvaluationObject *object, objects) { + const uint hashPrototypeName = qHash(object->prototype); + if (hashPrototypeName == hashName_FileTagger) { + FileTagger::Ptr fileTagger(new FileTagger); + fileTagger->artifactExpression = object->scope->stringValue("pattern"); + fileTagger->fileTags = object->scope->stringListValue("fileTags"); + rproduct->fileTaggers.insert(fileTagger); + } else if (hashPrototypeName == hashName_Rule) { + Rule::Ptr rule = resolveRule(object, module); + rproduct->rules.insert(rule); + rules.append(rule); + } else if (hashPrototypeName == hashName_Transformer) { + resolveTransformer(rproduct, object, module); + } else if (hashPrototypeName == hashName_TransformProperties) { + outerTransformProperties.append(object->instantiatingObject()); + } else if (hashPrototypeName == hashName_PropertyOptions) { + // Just ignore this type to allow it. It is handled elsewhere. + } else { + unhandledObjects.append(object); + } + } + + // attach the outer TransformProperties to the rules + foreach (Rule::Ptr rule, rules) { + foreach (LanguageObject *tp, outerTransformProperties) + addTransformPropertiesToRule(rule, tp); + } + + return unhandledObjects; +} + +Rule::Ptr Loader::resolveRule(EvaluationObject *object, ResolvedModule::Ptr module) +{ + Rule::Ptr rule(new Rule); + + LanguageObject *origObj = object->instantiatingObject(); + Q_CHECK_PTR(origObj); + + // read artifacts and TransformProperties + QList<RuleArtifact::Ptr> artifacts; + foreach (EvaluationObject *child, object->children) { + const uint hashChildPrototypeName = qHash(child->prototype); + if (hashChildPrototypeName == hashName_Artifact) { + RuleArtifact::Ptr artifact(new RuleArtifact); + artifacts.append(artifact); + artifact->fileScript = child->scope->verbatimValue("fileName"); + artifact->fileTags = child->scope->stringListValue("fileTags"); + LanguageObject *origArtifactObj = child->instantiatingObject(); + foreach (const Binding &binding, origArtifactObj->bindings) { + if (binding.name.length() <= 1) + continue; + artifact->bindings.append(qMakePair(binding.name, binding.valueSource.sourceCode())); + } + } else if (hashChildPrototypeName == hashName_TransformProperties) { + addTransformPropertiesToRule(rule, child->instantiatingObject()); + } else { + throw Error("'Rule' can only have children of type 'Artifact' or 'TransformProperties'.", + child->instantiatingObject()->prototypeLocation); + } + } + + RuleScript::Ptr ruleScript(new RuleScript); + ruleScript->script = object->scope->verbatimValue("prepare"); + ruleScript->location.fileName = object->instantiatingObject()->file->fileName; + ruleScript->location.column = 1; + { + Binding binding = object->instantiatingObject()->bindings.value(QStringList("prepare")); + ruleScript->location.line = binding.valueSource.firstLineNumber(); + } + + rule->objectId = origObj->id; + rule->jsImports = object->instantiatingObject()->file->jsImports; + rule->script = ruleScript; + rule->artifacts = artifacts; + rule->multiplex = object->scope->boolValue("multiplex", false); + rule->inputs = object->scope->stringListValue("inputs"); + rule->usings = object->scope->stringListValue("usings"); + rule->explicitlyDependsOn = object->scope->stringListValue("explicitlyDependsOn"); + rule->module = module; + + m_ruleMap.insert(rule, object); + return rule; +} + +/// -------------------------------------------------------------------------- + + +template <typename T> +static QString textOf(const QString &source, T *node) +{ + if (!node) + return QString(); + return source.mid(node->firstSourceLocation().begin(), node->lastSourceLocation().end() - node->firstSourceLocation().begin()); +} + +static void bindFunction(LanguageObject *result, const QString &source, FunctionDeclaration *ast) +{ + Function f; + if (!ast->name) + throw Error("function decl without name"); + f.name = ast->name->asString(); + + // remove the name + QString funcNoName = textOf(source, ast); + funcNoName.replace(QRegExp("^(\\s*function\\s*)\\w*"), "(\\1"); + funcNoName.append(")"); + f.source = QScriptProgram(funcNoName, result->file->fileName, ast->firstSourceLocation().startLine); + result->functions.insert(f.name, f); +} + +static QStringList toStringList(UiQualifiedId *qid) +{ + QStringList result; + for (; qid; qid = qid->next) { + if (qid->name) + result.append(qid->name->asString()); + else + result.append(QString()); // throw error instead? + } + return result; +} + +static CodeLocation location(const QString &fileName, SourceLocation location) +{ + return CodeLocation(fileName, location.startLine, location.startColumn); +} + +static QScriptProgram bindingProgram(const QString &fileName, const QString &source, Statement *statement) +{ + QString sourceCode = textOf(source, statement); + if (cast<Block *>(statement)) { + // rewrite blocks to be able to use return statements in property assignments + sourceCode.prepend("(function()"); + sourceCode.append(")()"); + } + return QScriptProgram(sourceCode, fileName, + statement->firstSourceLocation().startLine); +} + +static void checkDuplicateBinding(LanguageObject *object, const QStringList &bindingName, const SourceLocation &sourceLocation) +{ + if (object->bindings.contains(bindingName)) { + QString msg("Duplicate binding for '%1'"); + throw Error(msg.arg(bindingName.join(".")), + location(object->file->fileName, sourceLocation)); + } +} + +static void bindBinding(LanguageObject *result, const QString &source, UiScriptBinding *ast) +{ + Binding p; + if (!ast->qualifiedId || !ast->qualifiedId->name) + throw Error("script binding without name"); + p.name = toStringList(ast->qualifiedId); + checkDuplicateBinding(result, p.name, ast->qualifiedId->identifierToken); + + if (p.name == QStringList("id")) { + ExpressionStatement *expStmt = cast<ExpressionStatement *>(ast->statement); + if (!expStmt) + throw Error("id: must be followed by identifier"); + IdentifierExpression *idExp = cast<IdentifierExpression *>(expStmt->expression); + if (!idExp || !idExp->name) + throw Error("id: must be followed by identifier"); + result->id = idExp->name->asString(); + return; + } + + p.valueSource = bindingProgram(result->file->fileName, source, ast->statement); + + result->bindings.insert(p.name, p); +} + +static void bindBinding(LanguageObject *result, const QString &source, UiPublicMember *ast) +{ + Binding p; + if (!ast->name) + throw Error("public member without name"); + p.name = QStringList(ast->name->asString()); + checkDuplicateBinding(result, p.name, ast->identifierToken); + + if (ast->statement) + p.valueSource = bindingProgram(result->file->fileName, source, ast->statement); + else + p.valueSource = QScriptProgram("undefined"); + + result->bindings.insert(p.name, p); +} + +static PropertyDeclaration::Type propertyTypeFromString(const QString &typeName) +{ + if (typeName == "bool") + return PropertyDeclaration::Boolean; + if (typeName == "paths") + return PropertyDeclaration::Paths; + if (typeName == "string") + return PropertyDeclaration::String; + if (typeName == "var" || typeName == "variant") + return PropertyDeclaration::Variant; + return PropertyDeclaration::UnknownType; +} + +static void bindPropertyDeclaration(LanguageObject *result, UiPublicMember *ast) +{ + PropertyDeclaration p; + if (!ast->name) + throw Error("public member without name"); + if (!ast->memberType) + throw Error("public member without type"); + if (ast->typeModifier && ast->typeModifier->asString() != QLatin1String("list")) + throw Error("public member with type modifier that is not 'list'"); + if (ast->type == UiPublicMember::Signal) + throw Error("public member with signal type not supported"); + p.name = ast->name->asString(); + p.type = propertyTypeFromString(ast->memberType->asString()); + if (ast->typeModifier && ast->typeModifier->asString() == QLatin1String("list")) + p.flags |= PropertyDeclaration::ListProperty; + + result->propertyDeclarations.insert(p.name, p); +} + +static LanguageObject *bindObject(ProjectFile::Ptr file, const QString &source, UiObjectDefinition *ast, + const QHash<QStringList, QString> prototypeToFile) +{ + LanguageObject *result = new LanguageObject(file.data()); + result->file = file.data(); + +// result->location = CodeLocation( +// currentSourceFileName, +// ast->firstSourceLocation().startLine, +// ast->firstSourceLocation().startColumn +// ); + + if (!ast->qualifiedTypeNameId || !ast->qualifiedTypeNameId->name) + throw Error("no prototype"); + result->prototype = toStringList(ast->qualifiedTypeNameId); + result->prototypeLocation = location(file->fileName, ast->qualifiedTypeNameId->identifierToken); + + // resolve prototype if possible + result->prototypeFileName = prototypeToFile.value(result->prototype); + + if (!ast->initializer) + return result; + + for (UiObjectMemberList *it = ast->initializer->members; it; it = it->next) { + if (UiPublicMember *publicMember = cast<UiPublicMember *>(it->member)) { + bindPropertyDeclaration(result, publicMember); + bindBinding(result, source, publicMember); + } else if (UiScriptBinding *scriptBinding = cast<UiScriptBinding *>(it->member)) { + bindBinding(result, source, scriptBinding); + } else if (UiSourceElement *sourceElement = cast<UiSourceElement *>(it->member)) { + FunctionDeclaration *fdecl = cast<FunctionDeclaration *>(sourceElement->sourceElement); + if (!fdecl) + continue; + bindFunction(result, source, fdecl); + } else if (UiObjectDefinition *objDef = cast<UiObjectDefinition *>(it->member)) { + result->children += bindObject(file, source, objDef, prototypeToFile); + } + } + + return result; +} + +static void collectPrototypes(const QString &path, const QString &as, + QHash<QStringList, QString> *prototypeToFile) +{ + QDirIterator dirIter(path, QStringList("*.qbs")); + while (dirIter.hasNext()) { + const QString filePath = dirIter.next(); + const QString fileName = dirIter.fileName(); + + if (fileName.size() <= 4) + continue; + + const QString componentName = fileName.left(fileName.size() - 4); + // ### validate componentName + + if (!componentName.at(0).isUpper()) + continue; + + QStringList prototypeName; + if (!as.isEmpty()) + prototypeName.append(as); + prototypeName.append(componentName); + + prototypeToFile->insert(prototypeName, filePath); + } +} + +static ProjectFile::Ptr bindFile(const QString &source, const QString &fileName, UiProgram *ast, QStringList searchPaths) +{ + ProjectFile::Ptr file(new ProjectFile); + file->fileName = fileName; + const QString path = FileInfo::path(fileName); + + // maps names of known prototypes to absolute file names + QHash<QStringList, QString> prototypeToFile; + + // files in the same directory are available as prototypes + collectPrototypes(path, QString(), &prototypeToFile); + + QSet<QString> importAsNames; + + for (QmlJS::AST::UiImportList *it = ast->imports; it; it = it->next) { + QmlJS::AST::UiImport *import = it->import; + QmlJS::NameId *iFileName = import->fileName; + QmlJS::NameId *importId = import->importId; + + QStringList importUri; + bool isBase = false; + if (import->importUri) { + importUri = toStringList(import->importUri); + if (importUri.size() == 2 + && importUri.first() == "qbs" + && importUri.last() == "base") + isBase = true; // ### check version! + } + + QString as; + if (isBase) { + if (importId) { + // ### location + throw Error("Import of qbs.base must have no 'as <Name>'"); + } + } else { + if (!importId) { + // ### location + throw Error("Imports require 'as <Name>'"); + } + + as = importId->asString(); + if (importAsNames.contains(as)) { + // ### location + throw Error("Can't import into the same name more than once"); + } + importAsNames.insert(as); + } + + if (iFileName) { + QString name = FileInfo::resolvePath(path, iFileName->asString()); + + QFileInfo fi(name); + if (!fi.exists()) + throw Error(QString("Can't find imported file %0.").arg(name), + CodeLocation(fileName, import->fileNameToken.startLine, import->fileNameToken.startColumn)); + name = fi.canonicalFilePath(); + if (fi.isDir()) { + collectPrototypes(name, as, &prototypeToFile); + } else { + if (name.endsWith(".js", Qt::CaseInsensitive)) { + file->jsImports[as].append(name); + } else if (name.endsWith(".qbs", Qt::CaseInsensitive)) { + prototypeToFile.insert(QStringList(as), name); + } else { + throw Error("Can only import .qbs and .js files", + CodeLocation(fileName, import->fileNameToken.startLine, import->fileNameToken.startColumn)); + } + } + } else if (!importUri.isEmpty()) { + const QString importPath = importUri.join(QDir::separator()); + bool found = false; + foreach (const QString &searchPath, searchPaths) { + const QFileInfo fi(FileInfo::resolvePath(FileInfo::resolvePath(searchPath, "imports"), importPath)); + if (fi.isDir()) { + // ### versioning, qbsdir file, etc. + const QString &resultPath = fi.absoluteFilePath(); + collectPrototypes(resultPath, as, &prototypeToFile); + + QDirIterator dirIter(resultPath, QStringList("*.js")); + while (dirIter.hasNext()) { + dirIter.next(); + file->jsImports[as].append(dirIter.filePath()); + } + found = true; + break; + } + } + if (!found) { + // ### location + throw Error(QString("import %1 not found").arg(importUri.join("."))); + } + } + } + + UiObjectDefinition *rootDef = cast<UiObjectDefinition *>(ast->members->member); + if (rootDef) + file->root = bindObject(file, source, rootDef, prototypeToFile); + + return file; +} + +ProjectFile::Ptr Loader::parseFile(const QString &fileName) +{ + ProjectFile::Ptr result = m_parsedFiles.value(fileName); + if (result) + return result; + + QFile file(fileName); + if (!file.open(QFile::ReadOnly)) + throw Error(QString("Couldn't open '%1'.").arg(fileName)); + + const QString code = QTextStream(&file).readAll(); + QScopedPointer<QmlJS::Engine> engine(new QmlJS::Engine); + QScopedPointer<QmlJS::NodePool> nodePool(new QmlJS::NodePool(fileName, engine.data())); + QmlJS::Lexer lexer(engine.data()); + lexer.setCode(code, 1); + QmlJS::Parser parser(engine.data()); + parser.parse(); + + QList<QmlJS::DiagnosticMessage> parserMessages = parser.diagnosticMessages(); + if (!parserMessages.isEmpty()) { + QString msgstr = "Parsing errors:\n"; + foreach (const QmlJS::DiagnosticMessage &msg, parserMessages) + msgstr += Error(msg.message, fileName, msg.loc.startLine, msg.loc.startColumn).toString() + + QLatin1String("\n"); + throw Error(msgstr); + } + + result = bindFile(code, fileName, parser.ast(), m_searchPaths); + if (result) { + Q_ASSERT(!m_parsedFiles.contains(result->fileName)); + m_parsedFiles.insert(result->fileName, result); + } + return result; +} + +Loader *Loader::get(QScriptEngine *engine) +{ + QVariant v = engine->property(szLoaderPropertyName); + return static_cast<Loader*>(v.value<void*>()); +} + +QScriptValue Loader::js_getHostOS(QScriptContext *context, QScriptEngine *engine) +{ + Q_UNUSED(context); + QString hostSystem; +#if defined(Q_OS_WIN) + hostSystem = "windows"; +#elif defined(Q_OS_MAC) + hostSystem = "mac"; +#elif defined(Q_OS_LINUX) + hostSystem = "linux"; +#else +# error unknown host platform +#endif + return engine->toScriptValue(hostSystem); +} + +QScriptValue Loader::js_getHostDefaultArchitecture(QScriptContext *context, QScriptEngine *engine) +{ + // ### TODO implement properly, do not hard-code + Q_UNUSED(context); + QString architecture; +#if defined(Q_OS_WIN) + architecture = "x86"; +#elif defined(Q_OS_MAC) + architecture = "x86_64"; +#elif defined(Q_OS_LINUX) + architecture = "x86_64"; +#else +# error unknown host platform +#endif + return engine->toScriptValue(architecture); +} + +QScriptValue Loader::js_configurationValue(QScriptContext *context, QScriptEngine *engine) +{ + if (context->argumentCount() < 1 || context->argumentCount() > 2) { + return context->throwError(QScriptContext::SyntaxError, + QString("configurationValue expects 1 or 2 arguments")); + } + + Settings::Ptr settings = Loader::get(engine)->m_settings; + const bool defaultValueProvided = context->argumentCount() > 1; + const QString key = context->argument(0).toString(); + const QString defaultValue = (defaultValueProvided ? QString() : (context->argument(1).isUndefined() ? QString() : context->argument(1).toString())); + QVariant v = settings->value(key, defaultValue); + if (!defaultValueProvided && v.isNull()) + return context->throwError(QScriptContext::SyntaxError, + QString("configuration value '%1' does not exist").arg(context->argument(0).toString())); + return engine->toScriptValue(v); +} + +EvaluationObject *Loader::resolveTopLevel(const ResolvedProject::Ptr &rproject, + LanguageObject *object, + const QString &projectFileName, + ProjectData *projectData, + QList<Rule::Ptr> *globalRules, + const Configuration::Ptr &userProperties, + const ScopeChain::Ptr &scope, + const ResolvedModule::Ptr &dummyModule, + QFutureInterface<bool> &futureInterface) +{ + if (qbsLogLevel(LoggerTrace)) + qbsTrace() << "[LDR] resolve top-level " << object->file->fileName; + EvaluationObject *evaluationObject = new EvaluationObject(object); + + const QString propertiesName = object->prototype.join("."); + evaluationObject->scope = Scope::create(&m_engine, propertiesName, object->file); + + Scope::Ptr productProperty = Scope::create(&m_engine, name_productPropertyScope, object->file); + Scope::Ptr projectProperty = Scope::create(&m_engine, name_projectPropertyScope, object->file); + Scope::Ptr oldProductProperty(scope->findNonEmpty(name_productPropertyScope)); + Scope::Ptr oldProjectProperty(scope->findNonEmpty(name_projectPropertyScope)); + + // for the 'product' and 'project' property available to the modules + ScopeChain::Ptr moduleScope(new ScopeChain(&m_engine)); + moduleScope->prepend(oldProductProperty); + moduleScope->prepend(oldProjectProperty); + moduleScope->prepend(productProperty); + moduleScope->prepend(projectProperty); + + ScopeChain::Ptr localScope(scope->clone()); + localScope->prepend(productProperty); + localScope->prepend(projectProperty); + localScope->prepend(evaluationObject->scope); + + evaluationObject->scope->properties.insert("configurationValue", Property(m_jsFunction_configurationValue)); + + resolveInheritance(object, evaluationObject, moduleScope, userProperties->value()); + + if (evaluationObject->prototype == name_Project) { + // if this is a nested project, set a fallback scope + Property outerProperty = scope->lookupProperty("project"); + if (outerProperty.isValid() && outerProperty.scope) { + evaluationObject->scope->fallbackScope = outerProperty.scope; + } else { + // set the 'path' and 'filePath' properties + setPathAndFilePath(evaluationObject->scope, object->file->fileName); + } + + fillEvaluationObjectBasics(localScope, object, evaluationObject); + + // load referenced files + foreach (const QString &reference, evaluationObject->scope->stringListValue("references")) { + + QString projectFileDir = FileInfo::path(projectFileName); + QString absReferencePath = FileInfo::resolvePath(projectFileDir, reference); + ProjectFile::Ptr referencedFile = parseFile(absReferencePath); + ScopeChain::Ptr referencedFileScope(new ScopeChain(&m_engine, buildFileContext(referencedFile.data()))); + referencedFileScope->prepend(projectProperty); + resolveTopLevel(rproject, + referencedFile->root, + referencedFile->fileName, + projectData, + globalRules, + userProperties, + referencedFileScope, + dummyModule, futureInterface); + } + + // load children + ScopeChain::Ptr childScope(scope->clone()->prepend(projectProperty)); + foreach (LanguageObject *child, object->children) + resolveTopLevel(rproject, + child, + projectFileName, + projectData, + globalRules, + userProperties, + childScope, + dummyModule, + futureInterface); + + return evaluationObject; + } else if (evaluationObject->prototype == name_Rule) { + fillEvaluationObject(localScope, object, evaluationObject->scope, evaluationObject, userProperties->value()); + Rule::Ptr rule = resolveRule(evaluationObject, dummyModule); + globalRules->append(rule); + + return evaluationObject; + } else if (evaluationObject->prototype != name_Product) { + QString msg("unknown prototype '%1' - expected Product"); + // ### location + throw Error(msg.arg(evaluationObject->prototype)); + } + + // set the 'path' and 'filePath' properties + setPathAndFilePath(evaluationObject->scope, object->file->fileName); + + ResolvedProduct::Ptr rproduct(new ResolvedProduct); + rproduct->qbsFile = projectFileName; + rproduct->sourceDirectory = QFileInfo(projectFileName).absolutePath(); + rproduct->project = rproject.data(); + + ProductData productData; + productData.product = evaluationObject; + + evaluateDependencies(object, evaluationObject, localScope, moduleScope, userProperties->value()); + productData.usedProducts = evaluationObject->unknownModules; + fillEvaluationObject(localScope, object, evaluationObject->scope, evaluationObject, userProperties->value()); + + // check if product's name is empty and set a default value + Property &nameProperty = evaluationObject->scope->properties["name"]; + if (nameProperty.value.isUndefined()) { + QString projectName = FileInfo::fileName(projectFileName); + projectName.chop(4); + nameProperty.value = m_engine.toScriptValue(projectName); + } + + if (!object->id.isEmpty()) { + Scope::Ptr context = scope->last(); + context->properties.insert(object->id, Property(evaluationObject)); + } + + // collect all dependent modules and put them into the product + QHash<QString, ProjectFile *> moduleDependencies = findModuleDependencies(evaluationObject); + QHashIterator<QString, ProjectFile *> it(moduleDependencies); + while (it.hasNext()) { + it.next(); + if (productData.product->modules.contains(it.key())) + continue; // ### check if files equal + Module::Ptr module = loadModule(it.value(), QString(), it.key(), moduleScope, userProperties->value(), + CodeLocation(object->file->fileName)); + if (!module) { + throw Error(QString("could not load module '%1' from file '%2' into product even though it was loaded into a submodule").arg( + it.key(), it.value()->fileName)); + } + evaluationObject->modules.insert(module->name, module); + } + buildModulesProperty(evaluationObject); + + // get the build variant / determine the project id + QString buildVariant = userProperties->value().value("qbs").toMap().value("buildVariant").toString(); + if (rproject->id.isEmpty()) { + EvaluationObject *baseModule = evaluationObject->modules.value("qbs")->object; + if (!baseModule) + throw Error("base module not loaded"); + const QString hostName = baseModule->scope->stringValue("hostOS"); + const QString targetOS = baseModule->scope->stringValue("targetOS"); + const QString targetName = baseModule->scope->stringValue("targetName"); + rproject->id = buildVariant; + if (!targetName.isEmpty()) + rproject->id.prepend(targetName + "-"); + if (hostName != targetOS) { + QString platformName = targetOS; + const QString hostArchitecture = baseModule->scope->stringValue("hostArchitecture"); + const QString targetArchitecture = baseModule->scope->stringValue("architecture"); + if (hostArchitecture != targetArchitecture) { + platformName += "-"; + platformName += targetArchitecture; + } + rproject->id.prepend(platformName + "-"); + } + } + projectData->insert(rproduct, productData); + return evaluationObject; +} + +ProjectFile::ProjectFile() + : m_destructing(false) +{ +} + +ProjectFile::~ProjectFile() +{ + m_destructing = true; + qDeleteAll(m_evaluationObjects); + qDeleteAll(m_languageObjects); +} + +void Loader::ProductData::addUsedProducts(const QList<UnknownModule> &additionalUsedProducts, bool *productsAdded) +{ + int oldCount = usedProducts.count(); + QSet<QString> usedProductNames; + foreach (const UnknownModule &usedProduct, usedProducts) + usedProductNames += usedProduct.name; + foreach (const UnknownModule &usedProduct, additionalUsedProducts) + if (!usedProductNames.contains(usedProduct.name)) + usedProducts += usedProduct; + *productsAdded = (oldCount != usedProducts.count()); +} + +} // namespace qbs |