summaryrefslogtreecommitdiff
path: root/src/lib/corelib/loader/moduleinstantiator.cpp
blob: 35f5332afe232ab951178f334b155eed92925a85 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/****************************************************************************
**
** Copyright (C) 2023 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qbs.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "moduleinstantiator.h"

#include "modulepropertymerger.h"

#include <language/item.h>
#include <language/itempool.h>
#include <language/qualifiedid.h>
#include <language/value.h>
#include <logging/logger.h>
#include <logging/translator.h>
#include <tools/profiling.h>
#include <tools/setupprojectparameters.h>
#include <tools/stringconstants.h>

#include <utility>

namespace qbs::Internal {
class ModuleInstantiator::Private
{
public:
    Private(const SetupProjectParameters &parameters, ItemPool &itemPool,
            ModulePropertyMerger &propertyMerger, Logger &logger)
        : parameters(parameters), itemPool(itemPool), propertyMerger(propertyMerger),
          logger(logger) {}

    void overrideProperties(const Context &context);
    void setupScope(const Context &context);
    void exchangePlaceholderItem(Item *product, Item *loadingItem, const QString &loadingName,
        Item *moduleItemForItemValues, const QualifiedId &moduleName, const QString &id,
        bool isProductDependency, bool alreadyLoaded);
    std::pair<const Item *, Item *>
    getOrSetModuleInstanceItem(Item *container, Item *moduleItem, const QualifiedId &moduleName,
                               const QString &id, bool replace);

    const SetupProjectParameters &parameters;
    ItemPool &itemPool;
    ModulePropertyMerger &propertyMerger;
    Logger &logger;
    qint64 elapsedTime = 0;
};

ModuleInstantiator::ModuleInstantiator(
    const SetupProjectParameters &parameters, ItemPool &itemPool,
    ModulePropertyMerger &propertyMerger, Logger &logger)
    : d(new Private(parameters, itemPool, propertyMerger, logger)) {}
ModuleInstantiator::~ModuleInstantiator() { delete d; }

void ModuleInstantiator::instantiate(const Context &context)
{
    AccumulatingTimer timer(d->parameters.logElapsedTime() ? &d->elapsedTime : nullptr);

    // This part needs to be done only once per module and product, and only if the module
    // was successfully loaded.
    if (context.module && !context.alreadyLoaded) {
        context.module->setType(ItemType::ModuleInstance);
        d->overrideProperties(context);
        d->setupScope(context);
    }

    // This strange-looking code deals with the fact that our syntax cannot properly handle
    // dependencies on several multiplexed variants of the same product.
    // See getOrSetModuleInstanceItem() below for details.
    Item * const moduleItemForItemValues
        = context.moduleWithSameName ? context.moduleWithSameName
                                     : context.module;

    // Now attach the module instance as an item value to the loading item, potentially
    // evicting a previously attached placeholder item and merging its values into the instance.
    // Note that we potentially do this twice, once for the actual loading item and once
    // for the product item, if the two are different. The reason is this:
    // For convenience, in the product item we allow attaching to properties from indirectly
    // loaded modules. For instance:
    // Product {
    //    Depends { name: "Qt.core" }
    //    cpp.cxxLanguageVersion: "c++20" // Works even though there is no Depends item for cpp
    //  }
    // It's debatable whether that's a good feature, but it has been working (accidentally?)
    // for a long time, and removing it now would break a lot of existing projects.
    d->exchangePlaceholderItem(
        context.product, context.loadingItem, context.loadingName, moduleItemForItemValues,
        context.moduleName, context.id, context.exportingProduct, context.alreadyLoaded);

    if (!context.alreadyLoaded && context.product && context.product != context.loadingItem) {
        d->exchangePlaceholderItem(
            context.product, context.product, context.productName, moduleItemForItemValues,
            context.moduleName, context.id, context.exportingProduct, context.alreadyLoaded);
    }
}

void ModuleInstantiator::Private::exchangePlaceholderItem(
    Item *product, Item *loadingItem, const QString &loadingName, Item *moduleItemForItemValues,
    const QualifiedId &moduleName, const QString &id, bool isProductModule, bool alreadyLoaded)
{
    // If we have a module item, set an item value pointing to it as a property on the loading item.
    // Evict a possibly existing placeholder item, and return it to us, so we can merge its values
    // into the instance.
    const auto &[oldItem, newItem] = getOrSetModuleInstanceItem(
        loadingItem, moduleItemForItemValues, moduleName, id, true);

    // The new item always exists, even if we don't have a module item. In that case, the
    // function created a placeholder item for us, which we then have to turn into a
    // non-present module.
    QBS_CHECK(newItem);
    if (!moduleItemForItemValues) {
        createNonPresentModule(itemPool, moduleName.toString(), QLatin1String("not found"),
                               newItem);
        return;
    }

    // If the old and the new items are the same, it means the existing item value already
    // pointed to a module instance (rather than a placeholder).
    // This can happen in two cases:
    //   a) Multiple identical Depends items on the same level (easily possible with inheritance).
    //   b) Dependencies to multiplexed variants of the same product
    //      (see getOrSetModuleInstanceItem() below for details).
    if (oldItem == newItem) {
        QBS_CHECK(oldItem->type() == ItemType::ModuleInstance);
        QBS_CHECK(alreadyLoaded || isProductModule);
        return;
    }

    // In all other cases, our request to set the module instance item must have been honored.
    QBS_CHECK(newItem == moduleItemForItemValues);

    // If there was no placeholder item, we don't have to merge any values and are done.
    if (!oldItem)
        return;

    // If an item was replaced, then it must have been a placeholder.
    QBS_CHECK(oldItem->type() == ItemType::ModuleInstancePlaceholder);

    // Prevent setting read-only properties.
    for (auto it = oldItem->properties().cbegin(); it != oldItem->properties().cend(); ++it) {
        const PropertyDeclaration &pd = moduleItemForItemValues->propertyDeclaration(it.key());
        if (pd.flags().testFlag(PropertyDeclaration::ReadOnlyFlag)) {
            throw ErrorInfo(Tr::tr("Cannot set read-only property '%1'.").arg(pd.name()),
                            it.value()->location());
        }
    }

    // Now merge the locally attached values into the actual module instance.
    propertyMerger.mergeFromLocalInstance(product, loadingItem, loadingName,
                                          oldItem, moduleItemForItemValues);

    // TODO: We'd like to delete the placeholder item here, because it's not
    //       being referenced anymore and there's a lot of them. However, this
    //       is not supported by ItemPool. Investigate the use of std::pmr.
}

Item *ModuleInstantiator::retrieveModuleInstanceItem(Item *containerItem, const QualifiedId &name)
{
    return d->getOrSetModuleInstanceItem(containerItem, nullptr, name, {}, false).second;
}

Item *ModuleInstantiator::retrieveQbsItem(Item *containerItem)
{
    return retrieveModuleInstanceItem(containerItem, StringConstants::qbsModule());
}

// This important function deals with retrieving and setting (pseudo-)module instances from/on
// items.
// Use case 1: Suppose we resolve the dependency cpp in module Qt.core, which also contains
//             property bindings for cpp such as "cpp.defines: [...]".
//             The "cpp" part of this binding is represented by an ItemValue whose
//             item is of type ModuleInstancePlaceholder, originally set up by ItemReaderASTVisitor.
//             This function will be called with the actual cpp module item and will
//             replace the placeholder item in the item value. It will also return
//             the placeholder item for subsequent merging of its properties with the
//             ones of the actual module (in ModulePropertyMerger::mergeFromLocalInstance()).
//             If there were no cpp property bindings defined in Qt.core, then we'd still
//             have to replace the placeholder item, because references to "cpp" on the
//             right-hand-side of other properties must refer to the module item.
//             This is the common use of this function as employed by resolveProdsucDependencies().
//             Note that if a product has dependencies on more than one variant of a multiplexed
//             product, these dependencies are competing for the item value property name,
//             i.e. this case is not properly supported by the syntax. You must therefore not
//             export properties from multiplexed products that will be different between the
//             variants. In this function, the condition manifests itself by a module instance
//             being encountered instead of a module instance placeholder, in which case
//             nothing is done at all.
// Use case 2: We inject a fake qbs module into a project item, so qbs properties set in profiles
//             can be accessed in the project level. Doing this is discouraged, and the
//             functionality is kept mostly for backwards compatibility. The moduleItem
//             parameter is null in this case, and the item will be created by the function itself.
// Use case 3: A temporary qbs module is attached to a product during low-level setup, namely
//             in product multiplexing and setting qbs.profile.
// Use case 4: Module propagation to the the Group level.
// In all cases, the first returned item is the existing one, and the second returned item
// is the new one. Depending on the use case, they might be null and might also be the same item.
std::pair<const Item *, Item *> ModuleInstantiator::Private::getOrSetModuleInstanceItem(
    Item *container, Item *moduleItem, const QualifiedId &moduleName, const QString &id,
    bool replace)
{
    Item *instance = container;
    const QualifiedId itemValueName
        = !id.isEmpty() ? QualifiedId::fromString(id) : moduleName;
    for (int i = 0; i < itemValueName.size(); ++i) {
        const QString &moduleNameSegment = itemValueName.at(i);
        const ValuePtr v = instance->ownProperty(itemValueName.at(i));
        if (v && v->type() == Value::ItemValueType) {
            ItemValue * const itemValue = std::static_pointer_cast<ItemValue>(v).get();
            instance = itemValue->item();
            if (i == itemValueName.size() - 1) {
                if (replace && instance != moduleItem
                    && instance->type() == ItemType::ModuleInstancePlaceholder) {
                    if (!moduleItem)
                        moduleItem = Item::create(&itemPool, ItemType::ModuleInstancePlaceholder);
                    itemValue->setItem(moduleItem);
                }
                return {instance, itemValue->item()};
            }
        } else {
            Item *newItem = i < itemValueName.size() - 1
                                ? Item::create(&itemPool, ItemType::ModulePrefix) : moduleItem
                                      ? moduleItem : Item::create(&itemPool, ItemType::ModuleInstancePlaceholder);
            instance->setProperty(moduleNameSegment, ItemValue::create(newItem));
            instance = newItem;
        }
    }
    return {nullptr, instance};
}

void ModuleInstantiator::printProfilingInfo(int indent)
{
    if (!d->parameters.logElapsedTime())
        return;
    d->logger.qbsLog(LoggerInfo, true) << QByteArray(indent, ' ')
                                       << Tr::tr("Instantiating modules took %1.")
                                              .arg(elapsedTimeString(d->elapsedTime));
}

void ModuleInstantiator::Private::overrideProperties(const ModuleInstantiator::Context &context)
{
    // Users can override module properties on the command line with the
    // modules.<module-name>.<property-name>:<value> syntax.
    // For simplicity and backwards compatibility, qbs properties can also be given without
    // the "modules." prefix, i.e. just qbs.<property-name>:<value>.
    // In addition, users can override module properties just for certain products
    // using the products.<product-name>.<module-name>.<property-name>:<value> syntax.
    // Such product-specific overrides have higher precedence.
    const QString fullName = context.moduleName.toString();
    const QString generalOverrideKey = QStringLiteral("modules.") + fullName;
    const QString perProductOverrideKey = StringConstants::productsOverridePrefix()
                                          + context.productName + QLatin1Char('.') + fullName;
    context.module->overrideProperties(parameters.overriddenValuesTree(), generalOverrideKey,
                                       parameters, logger);
    if (fullName == StringConstants::qbsModule()) {
        context.module->overrideProperties(parameters.overriddenValuesTree(), fullName, parameters,
                                           logger);
    }
    context.module->overrideProperties(parameters.overriddenValuesTree(), perProductOverrideKey,
                                       parameters, logger);
}

void ModuleInstantiator::Private::setupScope(const ModuleInstantiator::Context &context)
{
    Item * const scope = Item::create(&itemPool, ItemType::Scope);
    QBS_CHECK(context.module->file());
    scope->setFile(context.module->file());
    QBS_CHECK(context.projectScope);
    context.projectScope->copyProperty(StringConstants::projectVar(), scope);
    if (context.productScope)
        context.productScope->copyProperty(StringConstants::productVar(), scope);
    else
        QBS_CHECK(context.moduleName.toString() == StringConstants::qbsModule()); // Dummy product.

    if (!context.module->id().isEmpty())
        scope->setProperty(context.module->id(), ItemValue::create(context.module));
    for (Item * const child : context.module->children()) {
        child->setScope(scope);
        if (!child->id().isEmpty())
            scope->setProperty(child->id(), ItemValue::create(child));
    }
    context.module->setScope(scope);

    if (context.exportingProduct) {
        QBS_CHECK(context.exportingProduct->type() == ItemType::Product);

        const auto exportingProductItemValue = ItemValue::create(context.exportingProduct);
        scope->setProperty(QStringLiteral("exportingProduct"), exportingProductItemValue);

        const auto importingProductItemValue = ItemValue::create(context.product);
        scope->setProperty(QStringLiteral("importingProduct"), importingProductItemValue);

        // FIXME: This looks wrong. Introduce exportingProject variable?
        scope->setProperty(StringConstants::projectVar(),
                           ItemValue::create(context.exportingProduct->parent()));

        PropertyDeclaration pd(StringConstants::qbsSourceDirPropertyInternal(),
                               PropertyDeclaration::String, QString(),
                               PropertyDeclaration::PropertyNotAvailableInConfig);
        context.module->setPropertyDeclaration(pd.name(), pd);
        context.module->setProperty(pd.name(), context.exportingProduct->property(
                                                   StringConstants::sourceDirectoryProperty()));
    }
}

} // namespace qbs::Internal