180 lines
8.9 KiB
Plaintext
180 lines
8.9 KiB
Plaintext
|
qml-autoreqprov - Automatic generation of Provides and Requires for QML imports
|
||
|
======
|
||
|
|
||
|
QML is a user interface specification and programming language. Each QML file
|
||
|
can have import statements at the top, which can either reference files or
|
||
|
directories directly or modules by identifier and version (major.minor).
|
||
|
If any of those import statements can't be satisfied, loading fails. For imports
|
||
|
which are provided by other packages, this maps naturally to Requires statements
|
||
|
in RPM packages, which enforce that all imports are satisfied on package
|
||
|
installation.
|
||
|
|
||
|
Direct file/directory imports are ignored here, as those are usually contained
|
||
|
within a single package and thus not relevant for inter-package dependencies.
|
||
|
|
||
|
TLDR for packagers
|
||
|
------------------
|
||
|
|
||
|
Packages with system-wide QML modules get Provides like
|
||
|
`qt5qmlimport(QtQuick.Controls.2) = 15` automatically. Imports in .qml files
|
||
|
map to RPM requires like `qt5qmlimport(QtQuick.Controls.2) >= 15`. This can be
|
||
|
disabled with `%global %_disable_qml_requires 1` in .spec files. It's important
|
||
|
to check that all dependendencies are fulfilled, as in some cases a needed
|
||
|
`qmlimport` Provides is missing. See the "Internal and private exports" section
|
||
|
for how to deal with that.
|
||
|
|
||
|
How the QML engine imports modules
|
||
|
----------------------------------
|
||
|
|
||
|
For each module import, the QML engine looks into the import cache to find any
|
||
|
suitable export with identical identifier and major version and same/higher
|
||
|
minor version. If there is no match, it goes through the QML import path (with
|
||
|
a system-wide default) in order with the module identifier and version appended
|
||
|
in various ways and reads the qmldir file inside. If the qmldir file mentions
|
||
|
plugins, those are loaded and it can register the exported types. If the import
|
||
|
can't be satisfied (due to a version mismatch), the search continues.
|
||
|
|
||
|
Mapping to RPM capabilities
|
||
|
---------------------------
|
||
|
|
||
|
The goal is that all imports for .qml files within a package are satisfied by
|
||
|
RPM dependencies of that package. This is achieved by adding Provides to
|
||
|
packages which satisfy a specific module import and Requires to the packages
|
||
|
with QML files inside. The capability has to include both the full module
|
||
|
identifier and version. As modules with a different major version are pretty
|
||
|
much independent, the major version is part of the capabilities' name and the
|
||
|
minor version is used as the capabilities' version. Additionally, the system
|
||
|
import paths are specific to a Qt major version, this is also included. The
|
||
|
end result are QML modules providing capabilities like:
|
||
|
|
||
|
`Provides: qt5qmlimport(QtQuick.Controls.2) = 15`
|
||
|
|
||
|
On the import side, packages with QML files get requirements like:
|
||
|
|
||
|
`Requires: qt5qmlimport(QtQuick.Controls.2) >= 13`
|
||
|
for a statement like `import QtQuick.Controls 2.13`. The `>=` is there so that
|
||
|
modules with a higher minor version satisfy it as well.
|
||
|
|
||
|
With Qt 6, unversioned QML import statements got introduced which import the
|
||
|
highest available major version (which IMO does not make sense...).
|
||
|
Representing those in RPM requires using separate unversioned capabilities
|
||
|
alongside the versioned ones:
|
||
|
|
||
|
`Provides: qt6qmlimport(QtQuick.Controls)`
|
||
|
`Requires: qt6qmlimport(QtQuick.Controls)`
|
||
|
|
||
|
Generating Requires from .qml files
|
||
|
-----------------------------------
|
||
|
|
||
|
As can be seen, mapping QML import statements to RPM requires is
|
||
|
straightforward, and even made easier by using the `qmlimportscanner` tool from
|
||
|
qtdeclarative combined with `jq` to convert its JSON output with some filtering
|
||
|
directly into the capabilities format for RPM.
|
||
|
|
||
|
There is one tricky part though: QML files are (intentionally) not tied to any
|
||
|
specific version of Qt, while QML modules are (though the Qt version specific
|
||
|
import paths and binary plugins). So when generating the list of required
|
||
|
imports, those are tied to a specific Qt major version. Currently this is
|
||
|
automatically detected by looking at which versions of qmlimportscanner are
|
||
|
installed. If multiple versions are found, the requires scanner aborts. This
|
||
|
can be overwritten by setting a variable in the .spec file (unfortunately not
|
||
|
possible per subpackage):
|
||
|
|
||
|
`%global __qml_requires_opts --qtver 5`
|
||
|
|
||
|
Currently, only .qml files directly part of the package are handled, so if
|
||
|
those are part of a resources file embedded into an executable or library, they
|
||
|
will not be read. Making this possible needs more research and effort.
|
||
|
|
||
|
Generating Provides from qmldir files
|
||
|
-------------------------------------
|
||
|
|
||
|
Every module installed into the QML import path contains a `qmldir` file with
|
||
|
metainformation. They usually contain a `module` line which specifies the
|
||
|
identifier (those which don't are ignored) and a list of exports and plugins.
|
||
|
Just the module identifier is not enough to generate the capability, major and
|
||
|
corresponding minor version(s) are also needed. For each major version, only
|
||
|
the highest minor version is stored, as it also satisfies imports with a lower
|
||
|
minor version.
|
||
|
|
||
|
Handling direct exports are easy, as they mention the major and minor version
|
||
|
directly, which combined with the Qt version (derived from the location the
|
||
|
qmldir file is installed to) results in a capability. For plugins, it's not as
|
||
|
easy though, those actually have to be loaded to get their registrations.
|
||
|
While `qtdeclarative` provides a tool called `qmlplugindump`, which lists all
|
||
|
exported types in a QML format, it uses the QML engine for loading plugins,
|
||
|
which requires the module identifier and a version. In addition to that, it
|
||
|
just doesn't work at all sometimes and does not provide all necessary
|
||
|
information (like pure module exports, which just bump the available minor
|
||
|
version without exporting any new type revisions).
|
||
|
|
||
|
To get a list of all versioned exports made by a plugin, a new tool called
|
||
|
`qmlpluginexports` specifically for that was written. It uses private API to
|
||
|
load a plugin without specifying a version and then iterates through all known
|
||
|
types to get their versions. It also handles lazy registration using
|
||
|
QQmlModuleRegistration and qmlRegisterModule by implementing the underlying
|
||
|
modules, which overrides the symbols in the Qt libraries (symbols in
|
||
|
executables have higher priority than public symbols from shared objects) to
|
||
|
store the information and then forwarding the call to the Qt library.
|
||
|
|
||
|
As loading a plugin also triggers loading of all dependencies, it's possible
|
||
|
that those register their own exports as well. So only exports including the
|
||
|
module identifier from the `qmldir` file are used and others are ignored. For
|
||
|
instance, the plugin for `QtQuick.Controls.2` also registers
|
||
|
`QtQuick.Controls.impl.2`.
|
||
|
|
||
|
Internal and private exports
|
||
|
----------------------------
|
||
|
|
||
|
The automatic generation of import requirements uses every module import in
|
||
|
installed .qml files. In some cases, those imports are not for system-wide
|
||
|
modules, but for imports provided by code in shared libraries and executables,
|
||
|
usable only in .qml files loaded by those. Provides for those can't be
|
||
|
generated, but the requirement will be, which makes the package unsatisfiable
|
||
|
due to missing dependencies. How to deal with that depends on the export
|
||
|
itself.
|
||
|
|
||
|
RPM dependencies are inter-package, so if an export is only used within a
|
||
|
package (for instance, an application with its UI), it's fine to filter it
|
||
|
out from requires and provides like this:
|
||
|
|
||
|
```
|
||
|
%global __requires_exclude (org.kde.private.kcms.kwin.effects)|(org.kde.kcms.kwinrules)
|
||
|
%global __provides_exclude (org.kde.private.kcms.kwin.effects)|(org.kde.kcms.kwinrules)
|
||
|
```
|
||
|
|
||
|
Truly private exports are usually not found by the generation of provided
|
||
|
capabilities, so the latter is normally not necessary.
|
||
|
|
||
|
The opposite case is when an application or library registers exports for use
|
||
|
by other packages (e.g. Plasma applets) which aren't available as system wide
|
||
|
QML imports. Those shouldn't simply be filtered out, as other packages actually
|
||
|
make use of it. Instead, the capability has to be provided manually, e.g.
|
||
|
|
||
|
```
|
||
|
Provides: qt5qmlimport(org.kde.plasma.configuration.2) = 0
|
||
|
Provides: qt5qmlimport(org.kde.plasma.plasmoid.2) = 0
|
||
|
```
|
||
|
|
||
|
qtdeclarative-imports-provides
|
||
|
------------------------------
|
||
|
|
||
|
The qml-autoreqprov scripts need qmlimportscanner from qtdeclarative to
|
||
|
generate Requires and for `qmldir` files with `plugin` lines `qmlpluginexports`
|
||
|
is required. The latter needs qtdeclarative to be built. This is a problem,
|
||
|
because qtdeclarative provides important exports, which have to be available as
|
||
|
RPM capabilities as well. Making qml-autoreqprov and its dependencies available
|
||
|
during build of qtdeclarative would cause a build cycle. To work around that,
|
||
|
a "stub" package installs qtdeclarative during build and runs the provides
|
||
|
generator for each relevant qtdeclarative package, to map the exports to the
|
||
|
corresponding package.
|
||
|
|
||
|
TODO
|
||
|
----
|
||
|
|
||
|
Is special treatment for baselibs.conf needed?
|
||
|
How do .qmlc files relate to this?
|
||
|
RPM doesn't seem to merge "foo >= 10" and "foo >= 11", so there are redundant
|
||
|
requirements generated. As the generator is run for each file separately, it's
|
||
|
not directly possible to work around that.
|