aboutsummaryrefslogtreecommitdiffstats
path: root/src/qml/Qt6QmlDeploySupport.cmake
blob: a230e71409d86a866ccaa16e0ed8e3a3031deee6 (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
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

# NOTE: This code should only ever be executed in script mode. It expects to be
#       used either as part of an install(CODE) call or called by a script
#       invoked via cmake -P as a POST_BUILD step. It would not normally be
#       included directly, it should be pulled in automatically by the deploy
#       support set up by qtbase.

cmake_minimum_required(VERSION 3.16...3.21)

function(qt6_deploy_qml_imports)
    set(no_value_options
        NO_QT_IMPORTS
    )
    set(single_value_options
        TARGET
        PLUGINS_DIR     # Internal option, only used for macOS app bundle targets
        QML_DIR
        PLUGINS_FOUND   # Name of an output variable
    )
    set(multi_value_options "")
    cmake_parse_arguments(PARSE_ARGV 0 arg
        "${no_value_options}" "${single_value_options}" "${multi_value_options}"
    )

    if(arg_UNPARSED_ARGUMENTS)
        message(FATAL_ERROR "Unparsed arguments: ${arg_UNPARSED_ARGUMENTS}")
    endif()

    if(NOT arg_TARGET)
        message(FATAL_ERROR "TARGET must be specified")
    endif()

    if(NOT arg_QML_DIR)
        set(arg_QML_DIR "${QT_DEPLOY_QML_DIR}")
    endif()

    if(NOT arg_PLUGINS_DIR)
        set(arg_PLUGINS_DIR "${QT_DEPLOY_PLUGINS_DIR}")
    endif()

    # The target's finalizer should have written out this file
    string(MAKE_C_IDENTIFIER "${arg_TARGET}" target_id)
    set(filename "${__QT_DEPLOY_IMPL_DIR}/deploy_qml_imports/${target_id}")
    if(__QT_DEPLOY_GENERATOR_IS_MULTI_CONFIG)
        string(APPEND filename "-${__QT_DEPLOY_ACTIVE_CONFIG}")
    endif()
    string(APPEND filename ".cmake")
    if(NOT EXISTS "${filename}")
        message(FATAL_ERROR
            "No QML imports information recorded for target ${arg_TARGET}. "
            "The target must be an executable and qt_add_qml_module() must "
            "have been called with it. If using a CMake version lower than 3.19, ensure "
            "that the executable is manually finalized with qt_finalize_target(). "
            "Missing file:\n    ${filename}"
        )
    endif()
    include(${filename})

endfunction()

if(NOT __QT_NO_CREATE_VERSIONLESS_FUNCTIONS)
    function(qt_deploy_qml_imports)
        if(__QT_DEFAULT_MAJOR_VERSION EQUAL 6)
            qt6_deploy_qml_imports(${ARGV})

            cmake_parse_arguments(PARSE_ARGV 0 arg "" "PLUGINS_FOUND" "")
            if(arg_PLUGINS_FOUND)
                set(${arg_PLUGINS_FOUND} ${${arg_PLUGINS_FOUND}} PARENT_SCOPE)
            endif()
        else()
            message(FATAL_ERROR "qt_deploy_qml_imports() is only available in Qt 6.")
        endif()
    endfunction()
endif()

function(_qt_internal_deploy_qml_imports_for_target)
    set(no_value_options
        BUNDLE
        NO_QT_IMPORTS
    )
    set(single_value_options
        IMPORTS_FILE
        PLUGINS_FOUND
        QML_DIR
        PLUGINS_DIR
    )
    set(multi_value_options "")

    cmake_parse_arguments(PARSE_ARGV 0 arg
        "${no_value_options}" "${single_value_options}" "${multi_value_options}"
    )

    if(arg_UNPARSED_ARGUMENTS)
        message(FATAL_ERROR "Unexpected arguments: ${arg_UNPARSED_ARGUMENTS}")
    endif()
    foreach(opt IN LISTS single_value_options)
        if(NOT arg_${opt})
            message(FATAL_ERROR "Required argument not provided: ${opt}")
        endif()
    endforeach()

    include("${arg_IMPORTS_FILE}")

    macro(_qt_internal_parse_qml_imports_entry prefix index)
        cmake_parse_arguments("${prefix}"
            ""
            "CLASSNAME;NAME;PATH;PLUGIN;RELATIVEPATH;TYPE;VERSION;LINKTARGET"
            ""
            ${qml_import_scanner_import_${index}}
        )
    endmacro()

    get_filename_component(install_prefix_abs "${QT_DEPLOY_PREFIX}" ABSOLUTE)
    set(plugins_found "")

    if(__QT_DEPLOY_POST_BUILD)
        message(STATUS "Running macOS bundle QML support POST_BUILD routine.")
    endif()

    # Parse the generated cmake file. It is possible for the scanner to find no
    # usage of QML, in which case the import count is 0.
    if(qml_import_scanner_imports_count GREATER 0)
        set(processed_names "")
        math(EXPR last_index "${qml_import_scanner_imports_count} - 1")
        foreach(index RANGE 0 ${last_index})
            _qt_internal_parse_qml_imports_entry(entry ${index})

            if("${entry_NAME}" STREQUAL "")
                message(WARNING "No NAME at scan index ${index}: ${imports_file}")
                continue()
            endif()

            # A plugin might have multiple entries (e.g. for different versions).
            # Only copy it once.
            if("${entry_NAME}" IN_LIST processed_names)
                continue()
            endif()
            # Even if we skip this one, we don't need to process it again
            list(APPEND processed_names ${entry_NAME})

            if("${entry_PATH}" STREQUAL "" OR
                "${entry_PLUGIN}" STREQUAL "" OR
                "${entry_RELATIVEPATH}" STREQUAL "")
                # These might be a valid QML module, but not have a plugin library.
                # We only care about modules that have a plugin we need to copy.
                continue()
            endif()

            if(arg_NO_QT_IMPORTS AND
                    "${entry_LINKTARGET}" MATCHES "${__QT_CMAKE_EXPORT_NAMESPACE}::")
                continue()
            endif()

            # For installation, we want the qmldir file and its plugin. If the
            # CMake project generating the plugin sets version details on its
            # CMake target, we might have symlinks and version numbers in the
            # file names, so account for those. There should never be plugin
            # libraries for more than one QML module in the directory, so we
            # shouldn't need to worry about matching plugins we don't want.
            set(relative_qmldir "${arg_QML_DIR}/${entry_RELATIVEPATH}")
            if("${CMAKE_INSTALL_PREFIX}" STREQUAL "")
                set(install_qmldir "./${relative_qmldir}")
            else()
                set(install_qmldir "${CMAKE_INSTALL_PREFIX}/${relative_qmldir}")
            endif()
            set(dest_qmldir "${QT_DEPLOY_PREFIX}/${relative_qmldir}")
            if(arg_BUNDLE)
                if("${CMAKE_INSTALL_PREFIX}" STREQUAL "")
                    set(install_plugin "./${arg_PLUGINS_DIR}")
                else()
                    set(install_plugin "${CMAKE_INSTALL_PREFIX}/${arg_PLUGINS_DIR}")
                endif()
                set(dest_plugin "${QT_DEPLOY_PREFIX}/${arg_PLUGINS_DIR}")
            else()
                set(install_plugin "${install_qmldir}")
                set(dest_plugin "${dest_qmldir}")
            endif()

            file(INSTALL "${entry_PATH}/qmldir" DESTINATION "${install_qmldir}")

            if(__QT_DEPLOY_POST_BUILD)
                # We are being invoked as a post-build step. The plugin might
                # not exist yet, so we can't even glob for it, let alone copy
                # it. We know what its name should be though, so we can create
                # a symlink to where it will eventually be, which will be enough
                # to allow it to run from the build tree. It won't matter if
                # the plugin gets updated later in the build, the symlink will
                # still be pointing at the right location.
                # In theory, this could be possible for any platform that
                # supports symlinks (which all do in some form now, even
                # Windows if the right permissions are set), but we only really
                # expect to need this for macOS app bundles.
                if(DEFINED __QT_DEPLOY_TARGET_${entry_LINKTARGET}_FILE)
                    set(source_file "${__QT_DEPLOY_TARGET_${entry_LINKTARGET}_FILE}")
                    get_filename_component(source_file_name "${source_file}" NAME)
                    set(final_destination "${dest_qmldir}/${source_file_name}")
                else()
                    # TODO: This is inconsistent with what we do further down below for the
                    # installation case. There we file(GLOB) any files we find, whereas here we
                    # build the path manually, because the file might not exist yet.
                    # Ideally both cases should use neither file(GLOB) nor manual path building,
                    # and instead rely on available target information or qmldir information.
                    # Currently that is not possible because we don't have all targets exposed
                    # via the __QT_DEPLOY_TARGET_{target} mechanism, only those that are built as
                    # part of the current project, and the qmldir -> qmlimportscanner does print
                    # the full file path, because there is one qmldir, but possibly 2+ plugins
                    # (debug and release).
                    set(plugin_suffix "")
                    if(__QT_DEPLOY_ACTIVE_CONFIG STREQUAL "Debug")
                        string(APPEND plugin_suffix "${__QT_DEPLOY_QT_DEBUG_POSTFIX}")
                    endif()

                    set(source_file "${entry_PATH}/lib${entry_PLUGIN}${plugin_suffix}.dylib")
                    set(final_destination "${dest_qmldir}/lib${entry_PLUGIN}${plugin_suffix}.dylib")
                endif()

                message(STATUS "Symlinking: ${final_destination}")
                file(CREATE_LINK
                    "${source_file}"
                    "${final_destination}"
                    SYMBOLIC
                )

                # We don't add this plugin to plugins_found because we don't
                # actually make a copy of the plugin. We don't want the caller
                # thinking they should further process what would still be the
                # original plugin in the build tree.
                continue()
            endif()

            # Deploy the plugin.
            if(DEFINED __QT_DEPLOY_TARGET_${entry_LINKTARGET}_FILE)
                set(files "${__QT_DEPLOY_TARGET_${entry_LINKTARGET}_FILE}")
            else()
                # We don't know the exact target file name. Fall back to file name heuristics.

                # Construct a regular expression that matches the plugin's file name.
                set(plugin_regex "^(.*/)?(lib)?${entry_PLUGIN}")
                if(__QT_DEPLOY_QT_IS_MULTI_CONFIG_BUILD_WITH_DEBUG)
                    # If our application is a release build, do not match any debug suffix.
                    # If our application is a debug build, match exactly a debug suffix.
                    if(__QT_DEPLOY_ACTIVE_CONFIG STREQUAL "Debug")
                        string(APPEND plugin_regex "${__QT_DEPLOY_QT_DEBUG_POSTFIX}")
                    endif()
                else()
                    # The Qt installation does only contain one build of the plugin. We match any
                    # possible debug suffix, or none.
                    string(APPEND plugin_regex ".*")
                endif()
                string(APPEND plugin_regex "\\.(so|dylib|dll)(\\.[0-9]+)*$")

                file(GLOB files LIST_DIRECTORIES false "${entry_PATH}/*${entry_PLUGIN}*")
                list(FILTER files INCLUDE REGEX "${plugin_regex}")
            endif()
            file(INSTALL ${files} DESTINATION "${install_plugin}" USE_SOURCE_PERMISSIONS)

            get_filename_component(dest_plugin_abs "${dest_plugin}" ABSOLUTE)
            if(__QT_DEPLOY_TOOL STREQUAL "GRD")
                # Use the full plugin path for deployment. This is necessary for file(GRD) to
                # resolve the dependencies of the plugins.
                list(APPEND plugins_found ${files})
            else()
                # Use relative paths for the plugins. If we used full paths here, macdeployqt would
                # modify the RPATHS of plugins in the Qt installation.
                file(RELATIVE_PATH rel_path "${install_prefix_abs}" "${dest_plugin_abs}")
                foreach(file IN LISTS files)
                    get_filename_component(filename "${file}" NAME)
                    list(APPEND plugins_found "${rel_path}/${filename}")
                endforeach()
            endif()

            # Install runtime dependencies on Windows.
            if(__QT_DEPLOY_SYSTEM_NAME STREQUAL "Windows")
                foreach(file IN LISTS __QT_DEPLOY_TARGET_${entry_LINKTARGET}_RUNTIME_DLLS)
                    if(__QT_DEPLOY_VERBOSE)
                        message(STATUS "runtime dependency for QML plugin '${entry_PLUGIN}':")
                    endif()
                    file(INSTALL ${file} DESTINATION "${QT_DEPLOY_PREFIX}/${QT_DEPLOY_BIN_DIR}")
                endforeach()
            endif()

            if(__QT_DEPLOY_TOOL STREQUAL "GRD" AND __QT_DEPLOY_MUST_ADJUST_PLUGINS_RPATH)
                # The RPATHs of the installed plugins do not match Qt's original lib directory.
                # We must set the RPATH to point to QT_DEPLOY_LIBDIR.
                _qt_internal_get_rpath_origin(rpath_origin)
                foreach(file_path IN LISTS files)
                    get_filename_component(file_name ${file_path} NAME)
                    file(RELATIVE_PATH rel_lib_dir "${dest_plugin}"
                        "${QT_DEPLOY_PREFIX}/${QT_DEPLOY_LIB_DIR}"
                    )
                    _qt_internal_set_rpath(
                        FILE "${dest_plugin}/${file_name}"
                        NEW_RPATH "${rpath_origin}/${rel_lib_dir}"
                    )
                endforeach()
            endif()

            if(arg_BUNDLE)
                # Actual plugin binaries will be in PlugIns, but qmldir files
                # expect them to be in the same directory as themselves
                # (i.e. under Resources/qml/...). Add a symlink at the place
                # the qmldir expects the binary to be. This arrangement keeps
                # binaries under PlugIns and non-binaries under Resources,
                # which is required for code signing to work properly.
                get_filename_component(dest_qmldir_abs "${dest_qmldir}" ABSOLUTE)
                file(RELATIVE_PATH rel_path "${dest_qmldir_abs}" "${dest_plugin_abs}")
                foreach(plugin_file IN LISTS files)
                    get_filename_component(filename "${plugin_file}" NAME)

                    set(final_destination "${dest_qmldir}/${filename}")
                    message(STATUS "Symlinking: ${final_destination}")
                    file(CREATE_LINK "${rel_path}/${filename}" "${final_destination}" SYMBOLIC)
                endforeach()
            endif()
        endforeach()
    endif()

    set(${arg_PLUGINS_FOUND} ${plugins_found} PARENT_SCOPE)

endfunction()

function(_qt_internal_show_skip_qml_runtime_deploy_message)
    # Don't show the message in static Qt builds, it can be misleading, because we still
    # run qmlimportscanner / link the static qml plguins into the binary despite not having
    # a qml deployment step.
    if(__QT_DEPLOY_IS_SHARED_LIBS_BUILD)
        message(STATUS "Skipping QML module deployment steps.")
    endif()
endfunction()