diff options
Diffstat (limited to 'src/corelib/Qt6AndroidMacros.cmake')
-rw-r--r-- | src/corelib/Qt6AndroidMacros.cmake | 1432 |
1 files changed, 1300 insertions, 132 deletions
diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index e37ce287bd..3e96130b85 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + # Generate deployment tool json # Locate newest Android sdk build tools revision @@ -17,13 +20,69 @@ function(_qt_internal_android_get_sdk_build_tools_revision out_var) set(${out_var} "${android_build_tools_latest}" PARENT_SCOPE) endfunction() +# The function appends to the 'out_var' a 'json_property' that contains the 'tool' path. If 'tool' +# target or its IMPORTED_LOCATION are not found the function displays warning, but is not failing +# at the project configuring phase. +function(_qt_internal_add_tool_to_android_deployment_settings out_var tool json_property target) + unset(tool_binary_path) + __qt_internal_get_tool_imported_location(tool_binary_path ${tool}) + if("${tool_binary_path}" STREQUAL "") + # Fallback search for the tool in host bin and host libexec directories + find_program(tool_binary_path + NAMES ${tool} ${tool}.exe + PATHS + "${QT_HOST_PATH}/${QT6_HOST_INFO_BINDIR}" + "${QT_HOST_PATH}/${QT6_HOST_INFO_LIBEXECDIR}" + NO_DEFAULT_PATH + ) + if(NOT tool_binary_path) + message(WARNING "Unable to locate ${tool}. Android package deployment of ${target}" + " target can be incomplete. Make sure the host Qt has ${tool} installed.") + return() + endif() + endif() + + file(TO_CMAKE_PATH "${tool_binary_path}" tool_binary_path) + string(APPEND ${out_var} + " \"${json_property}\" : \"${tool_binary_path}\",\n") + + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() + # Generate the deployment settings json file for a cmake target. function(qt6_android_generate_deployment_settings target) # Information extracted from mkspecs/features/android/android_deployment_settings.prf if (NOT TARGET ${target}) - message(SEND_ERROR "${target} is not a cmake target") + message(FATAL_ERROR "${target} is not a cmake target") + endif() + + # When parsing JSON file format backslashes and follow up symbols are regarded as special + # characters. This puts Windows path format into a trouble. + # _qt_internal_android_format_deployment_paths converts sensitive paths to the CMake format + # that is supported by JSON as well. The function should be called as many times as + # qt6_android_generate_deployment_settings, because users may change properties that contain + # paths in between the calls. + _qt_internal_android_format_deployment_paths(${target}) + + # Avoid calling the function body twice because of 'file(GENERATE'. + get_target_property(is_called ${target} _qt_is_android_generate_deployment_settings_called) + if(is_called) return() endif() + set_target_properties(${target} PROPERTIES + _qt_is_android_generate_deployment_settings_called TRUE + ) + + get_target_property(android_executable_finalizer_called + ${target} _qt_android_executable_finalizer_called) + + if(android_executable_finalizer_called) + # Don't show deprecation when called by our own function implementations. + else() + message(DEPRECATION + "Calling qt_android_generate_deployment_settings directly is deprecated since Qt 6.5. " + "Use qt_add_executable instead.") + endif() get_target_property(target_type ${target} TYPE) @@ -38,7 +97,17 @@ function(qt6_android_generate_deployment_settings target) if (NOT target_output_name) set(target_output_name ${target}) endif() - set(deploy_file "${target_binary_dir}/android-${target_output_name}-deployment-settings.json") + + # QtCreator requires the file name of deployment settings has no config related suffixes + # to run androiddeployqt correctly. If we use multi-config generator for the first config + # in a list avoid adding any configuration-specific suffixes. + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + list(GET CMAKE_CONFIGURATION_TYPES 0 first_config_type) + set(config_suffix "$<$<NOT:$<CONFIG:${first_config_type}>>:-$<CONFIG>>") + endif() + set(deploy_file + "${target_binary_dir}/android-${target}-deployment-settings${config_suffix}.json") set(file_contents "{\n") # content begin @@ -53,9 +122,9 @@ function(qt6_android_generate_deployment_settings target) file(GLOB plugin_dir_files LIST_DIRECTORIES FALSE "${glob_expression}") if (NOT plugin_dir_files) message(SEND_ERROR - "Detected Qt installation does not contain qtforandroid_${CMAKE_ANDROID_ARCH_ABI}.so in the following dir:" - "${android_plugin_dir_path}" - "This is most likely due to the installation not being a Qt for Android build." + "Detected Qt installation does not contain qtforandroid_${CMAKE_ANDROID_ARCH_ABI}.so in the following dir:\n" + "${android_plugin_dir_path}\n" + "This is most likely due to the installation not being a Qt for Android build. " "Please recheck your build configuration.") return() else() @@ -64,10 +133,7 @@ function(qt6_android_generate_deployment_settings target) endif() endif() - set(qt_android_install_dir "${QT6_INSTALL_PREFIX}") - file(TO_CMAKE_PATH "${qt_android_install_dir}" qt_android_install_dir_native) - string(APPEND file_contents - " \"qt\": \"${qt_android_install_dir_native}\",\n") + _qt_internal_collect_qt_for_android_paths(file_contents) # Android SDK path file(TO_CMAKE_PATH "${ANDROID_SDK_ROOT}" android_sdk_root_native) @@ -75,9 +141,16 @@ function(qt6_android_generate_deployment_settings target) " \"sdk\": \"${android_sdk_root_native}\",\n") # Android SDK Build Tools Revision - _qt_internal_android_get_sdk_build_tools_revision(QT_ANDROID_SDK_BUILD_TOOLS_REVISION) + _qt_internal_android_get_sdk_build_tools_revision(android_sdk_build_tools) + set(android_sdk_build_tools_genex "") + string(APPEND android_sdk_build_tools_genex + "$<IF:$<BOOL:$<TARGET_PROPERTY:${target},QT_ANDROID_SDK_BUILD_TOOLS_REVISION>>," + "$<TARGET_PROPERTY:${target},QT_ANDROID_SDK_BUILD_TOOLS_REVISION>," + "${android_sdk_build_tools}" + ">" + ) string(APPEND file_contents - " \"sdkBuildToolsRevision\": \"${QT_ANDROID_SDK_BUILD_TOOLS_REVISION}\",\n") + " \"sdkBuildToolsRevision\": \"${android_sdk_build_tools_genex}\",\n") # Android NDK file(TO_CMAKE_PATH "${CMAKE_ANDROID_NDK}" android_ndk_root_native) @@ -100,100 +173,76 @@ function(qt6_android_generate_deployment_settings target) string(APPEND file_contents " \"ndk-host\": \"${ANDROID_NDK_HOST_SYSTEM_NAME}\",\n") - if (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86") - set(arch_value "i686-linux-android") - elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "x86_64") - set(arch_value "x86_64-linux-android") - elseif (CMAKE_ANDROID_ARCH_ABI STREQUAL "arm64-v8a") - set(arch_value "aarch64-linux-android") - else() - set(arch_value "arm-linux-androideabi") + get_target_property(qt_android_abis ${target} _qt_android_abis) + if(NOT qt_android_abis) + set(qt_android_abis "") endif() + set(architecture_record_list "") + foreach(abi IN LISTS qt_android_abis CMAKE_ANDROID_ARCH_ABI) + if(abi STREQUAL "x86") + set(arch_value "i686-linux-android") + elseif(abi STREQUAL "x86_64") + set(arch_value "x86_64-linux-android") + elseif(abi STREQUAL "arm64-v8a") + set(arch_value "aarch64-linux-android") + elseif(abi) + set(arch_value "arm-linux-androideabi") + endif() + list(APPEND architecture_record_list "\"${abi}\":\"${arch_value}\"") + endforeach() + list(JOIN architecture_record_list "," architecture_records) # Architecture string(APPEND file_contents - " \"architectures\": { \"${CMAKE_ANDROID_ARCH_ABI}\" : \"${arch_value}\" },\n") + " \"architectures\": { ${architecture_records} },\n") # deployment dependencies - get_target_property(android_deployment_dependencies - ${target} QT_ANDROID_DEPLOYMENT_DEPENDENCIES) - if (android_deployment_dependencies) - list(JOIN android_deployment_dependencies "," android_deployment_dependencies) - string(APPEND file_contents - " \"deployment-dependencies\": \"${android_deployment_dependencies}\",\n") - endif() + _qt_internal_add_android_deployment_multi_value_property(file_contents "deployment-dependencies" + ${target} "QT_ANDROID_DEPLOYMENT_DEPENDENCIES" ) # Extra plugins - get_target_property(android_extra_plugins ${target} QT_ANDROID_EXTRA_PLUGINS) - if (android_extra_plugins) - list(JOIN android_extra_plugins "," android_extra_plugins) - string(APPEND file_contents - " \"android-extra-plugins\": \"${android_extra_plugins}\",\n") - endif() + _qt_internal_add_android_deployment_multi_value_property(file_contents "android-extra-plugins" + ${target} "_qt_android_native_extra_plugins" ) # Extra libs - get_target_property(android_extra_libs ${target} QT_ANDROID_EXTRA_LIBS) - if (android_extra_libs) - list(JOIN android_extra_libs "," android_extra_libs) - string(APPEND file_contents - " \"android-extra-libs\": \"${android_extra_libs}\",\n") - endif() + _qt_internal_add_android_deployment_multi_value_property(file_contents "android-extra-libs" + ${target} "_qt_android_native_extra_libs" ) + + # Alternative path to Qt libraries on target device + _qt_internal_add_android_deployment_property(file_contents "android-system-libs-prefix" + ${target} "QT_ANDROID_SYSTEM_LIBS_PREFIX") # package source dir - get_target_property(android_package_source_dir ${target} QT_ANDROID_PACKAGE_SOURCE_DIR) - if (android_package_source_dir) - file(TO_CMAKE_PATH "${android_package_source_dir}" android_package_source_dir_native) - string(APPEND file_contents - " \"android-package-source-directory\": \"${android_package_source_dir_native}\",\n") - endif() + _qt_internal_add_android_deployment_property(file_contents "android-package-source-directory" + ${target} "_qt_android_native_package_source_dir") # version code - get_target_property(android_version_code ${target} QT_ANDROID_VERSION_CODE) - if (android_version_code) - string(APPEND file_contents - " \"android-version-code\": \"${android_version_code}\",\n") - endif() + _qt_internal_add_android_deployment_property(file_contents "android-package-name" + ${target} "QT_ANDROID_PACKAGE_NAME") + + # version code + _qt_internal_add_android_deployment_property(file_contents "android-version-code" + ${target} "QT_ANDROID_VERSION_CODE") # version name - get_target_property(android_version_name ${target} QT_ANDROID_VERSION_NAME) - if (android_version_name) - string(APPEND file_contents - " \"android-version-name\": \"${android_version_name}\",\n") - endif() + _qt_internal_add_android_deployment_property(file_contents "android-version-name" + ${target} "QT_ANDROID_VERSION_NAME") # minimum SDK version - get_target_property(android_min_sdk_version ${target} QT_ANDROID_MIN_SDK_VERSION) - if(android_min_sdk_version) - string(APPEND file_contents - " \"android-min-sdk-version\": \"${android_min_sdk_version}\",\n") - endif() + _qt_internal_add_android_deployment_property(file_contents "android-min-sdk-version" + ${target} "QT_ANDROID_MIN_SDK_VERSION") # target SDK version - get_target_property(android_target_sdk_version ${target} QT_ANDROID_TARGET_SDK_VERSION) - if(android_target_sdk_version) - string(APPEND file_contents - " \"android-target-sdk-version\": \"${android_target_sdk_version}\",\n") - endif() + _qt_internal_add_android_deployment_property(file_contents "android-target-sdk-version" + ${target} "QT_ANDROID_TARGET_SDK_VERSION") - get_target_property(qml_import_path ${target} QT_QML_IMPORT_PATH) - if (qml_import_path) - set(_import_paths "") - foreach(_path IN LISTS qml_import_path) - file(TO_CMAKE_PATH "${_path}" _path) - list(APPEND _import_paths ${_path}) - endforeach() - list(JOIN _import_paths "," _import_paths) - string(APPEND file_contents - " \"qml-import-paths\": \"${_import_paths}\",\n") - endif() + # should Qt shared libs be excluded from deployment + _qt_internal_add_android_deployment_property(file_contents "android-no-deploy-qt-libs" + ${target} "QT_ANDROID_NO_DEPLOY_QT_LIBS") - get_target_property(qml_root_path ${target} QT_QML_ROOT_PATH) - if(NOT qml_root_path) - set(qml_root_path "${target_source_dir}") - endif() - file(TO_CMAKE_PATH "${qml_root_path}" qml_root_path_native) - string(APPEND file_contents - " \"qml-root-path\": \"${qml_root_path_native}\",\n") + __qt_internal_collect_plugin_targets_from_dependencies("${target}" plugin_targets) + __qt_internal_collect_plugin_library_files("${target}" "${plugin_targets}" plugin_targets) + string(APPEND file_contents " \"android-deploy-plugins\":\"${plugin_targets}\",\n") # App binary string(APPEND file_contents @@ -205,35 +254,52 @@ function(qt6_android_generate_deployment_settings target) " \"android-application-arguments\": \"${QT_ANDROID_APPLICATION_ARGUMENTS}\",\n") endif() - # Override qmlimportscanner binary path - set(qml_importscanner_binary_path "${QT_HOST_PATH}/${QT6_HOST_INFO_LIBEXECDIR}/qmlimportscanner") - if (WIN32) - string(APPEND qml_importscanner_binary_path ".exe") + if(COMMAND _qt_internal_generate_android_qml_deployment_settings) + _qt_internal_generate_android_qml_deployment_settings(file_contents ${target}) + else() + string(APPEND file_contents + " \"qml-skip-import-scanning\": true,\n") endif() - file(TO_CMAKE_PATH "${qml_importscanner_binary_path}" qml_importscanner_binary_path_native) - string(APPEND file_contents - " \"qml-importscanner-binary\" : \"${qml_importscanner_binary_path_native}\",\n") # Override rcc binary path - set(rcc_binary_path "${QT_HOST_PATH}/${QT6_HOST_INFO_LIBEXECDIR}/rcc") - if (WIN32) - string(APPEND rcc_binary_path ".exe") - endif() - file(TO_CMAKE_PATH "${rcc_binary_path}" rcc_binary_path_native) - string(APPEND file_contents - " \"rcc-binary\" : \"${rcc_binary_path_native}\",\n") + _qt_internal_add_tool_to_android_deployment_settings(file_contents rcc "rcc-binary" "${target}") # Extra prefix paths foreach(prefix IN LISTS CMAKE_FIND_ROOT_PATH) if (NOT "${prefix}" STREQUAL "${qt_android_install_dir_native}" AND NOT "${prefix}" STREQUAL "${android_ndk_root_native}") - list(APPEND extra_prefix_list \"${prefix}\") + file(TO_CMAKE_PATH "${prefix}" prefix) + list(APPEND extra_prefix_list "\"${prefix}\"") endif() endforeach() string (REPLACE ";" "," extra_prefix_list "${extra_prefix_list}") string(APPEND file_contents " \"extraPrefixDirs\" : [ ${extra_prefix_list} ],\n") + # Create an empty target for the cases when we need to generate deployment setting but + # qt_finalize_project is never called. + if(NOT TARGET _qt_internal_apk_dependencies AND NOT QT_NO_COLLECT_BUILD_TREE_APK_DEPS) + add_custom_target(_qt_internal_apk_dependencies) + endif() + + # Extra library paths that could be used as a dependency lookup path by androiddeployqt. + # + # Unlike 'extraPrefixDirs', the 'extraLibraryDirs' key doesn't expect the 'lib' subfolder + # when looking for dependencies. + # TODO: add a public target property accessible from user space + _qt_internal_add_android_deployment_list_property(file_contents "extraLibraryDirs" + ${target} "_qt_android_extra_library_dirs" + _qt_internal_apk_dependencies "_qt_android_extra_library_dirs" + ) + + if(QT_FEATURE_zstd) + set(is_zstd_enabled "true") + else() + set(is_zstd_enabled "false") + endif() + string(APPEND file_contents + " \"zstdCompression\": ${is_zstd_enabled},\n") + # Last item in json file # base location of stdlibc++, will be suffixed by androiddeploy qt @@ -245,7 +311,7 @@ function(qt6_android_generate_deployment_settings target) # content end string(APPEND file_contents "}\n") - file(WRITE ${deploy_file} ${file_contents}) + file(GENERATE OUTPUT ${deploy_file} CONTENT "${file_contents}") set_target_properties(${target} PROPERTIES @@ -260,6 +326,16 @@ if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) endif() function(qt6_android_apply_arch_suffix target) + get_target_property(called_from_qt_impl + ${target} _qt_android_apply_arch_suffix_called_from_qt_impl) + if(called_from_qt_impl) + # Don't show deprecation when called by our own function implementations. + else() + message(DEPRECATION + "Calling qt_android_apply_arch_suffix directly is deprecated since Qt 6.5. " + "Use qt_add_executable or qt_add_library instead.") + endif() + get_target_property(target_type ${target} TYPE) if (target_type STREQUAL "SHARED_LIBRARY" OR target_type STREQUAL "MODULE_LIBRARY") set_property(TARGET "${target}" PROPERTY SUFFIX "_${CMAKE_ANDROID_ARCH_ABI}.so") @@ -276,62 +352,213 @@ endif() # Add custom target to package the APK function(qt6_android_add_apk_target target) + # Avoid calling qt6_android_add_apk_target twice + get_property(apk_targets GLOBAL PROPERTY _qt_apk_targets) + if("${target}" IN_LIST apk_targets) + return() + endif() + + get_target_property(android_executable_finalizer_called + ${target} _qt_android_executable_finalizer_called) + + if(android_executable_finalizer_called) + # Don't show deprecation when called by our own function implementations. + else() + message(DEPRECATION + "Calling qt_android_add_apk_target directly is deprecated since Qt 6.5. " + "Use qt_add_executable instead.") + endif() + get_target_property(deployment_file ${target} QT_ANDROID_DEPLOYMENT_SETTINGS_FILE) if (NOT deployment_file) message(FATAL_ERROR "Target ${target} is not a valid android executable target\n") endif() + # Use genex to get path to the deployment settings, the above check only to confirm that + # qt6_android_add_apk_target is called on an android executable target. + string(JOIN "" deployment_file + "$<GENEX_EVAL:" + "$<TARGET_PROPERTY:${target},QT_ANDROID_DEPLOYMENT_SETTINGS_FILE>" + ">" + ) - # Make global apk target depend on the current apk target. + # Make global apk and aab targets depend on the current apk target. + if(TARGET aab) + add_dependencies(aab ${target}_make_aab) + endif() + if(TARGET aar) + add_dependencies(aar ${target}_make_aar) + endif() if(TARGET apk) add_dependencies(apk ${target}_make_apk) _qt_internal_create_global_apk_all_target_if_needed() endif() set(deployment_tool "${QT_HOST_PATH}/${QT6_HOST_INFO_BINDIR}/androiddeployqt") - set(apk_final_dir "$<TARGET_PROPERTY:${target},BINARY_DIR>/android-build") - set(apk_intermediate_dir "${CMAKE_CURRENT_BINARY_DIR}/android-build") + # No need to use genex for the BINARY_DIR since it's read-only. + get_target_property(target_binary_dir ${target} BINARY_DIR) + + if($CACHE{QT_USE_TARGET_ANDROID_BUILD_DIR}) + set(apk_final_dir "${target_binary_dir}/android-build-${target}") + else() + if(QT_USE_TARGET_ANDROID_BUILD_DIR) + message(WARNING "QT_USE_TARGET_ANDROID_BUILD_DIR needs to be set in CACHE") + endif() + + get_property(known_android_build GLOBAL PROPERTY _qt_internal_known_android_build_dir) + get_property(already_warned GLOBAL PROPERTY _qt_internal_already_warned_android_build_dir) + set(apk_final_dir "${target_binary_dir}/android-build") + if(NOT QT_SKIP_ANDROID_BUILD_DIR_CHECK AND "${apk_final_dir}" IN_LIST known_android_build + AND NOT "${apk_final_dir}" IN_LIST already_warned) + message(WARNING "${CMAKE_CURRENT_SOURCE_DIR}/CMakeLists.txt contains multiple" + " Qt Android executable targets. This can lead to mixing of deployment artifacts" + " of targets defined there. Setting QT_USE_TARGET_ANDROID_BUILD_DIR=TRUE" + " allows building multiple executable targets within a single CMakeLists.txt." + " Note: This option is not supported by Qt Creator versions older than 13." + " Set QT_SKIP_ANDROID_BUILD_DIR_CHECK=TRUE to suppress this warning." + ) + set_property(GLOBAL APPEND PROPERTY _qt_internal_already_warned_android_build_dir + "${apk_final_dir}") + else() + set_property(GLOBAL APPEND PROPERTY + _qt_internal_known_android_build_dir "${apk_final_dir}") + endif() + endif() + set(apk_file_name "${target}.apk") + set(aar_file_name "${target}.aar") set(dep_file_name "${target}.d") set(apk_final_file_path "${apk_final_dir}/${apk_file_name}") - set(apk_intermediate_file_path "${apk_intermediate_dir}/${apk_file_name}") - set(dep_intermediate_file_path "${apk_intermediate_dir}/${dep_file_name}") + set(aar_final_file_path "${apk_final_dir}/${aar_file_name}") + set(dep_file_path "${apk_final_dir}/${dep_file_name}") + set(target_file_copy_relative_path + "libs/${CMAKE_ANDROID_ARCH_ABI}/$<TARGET_FILE_NAME:${target}>") + + set(extra_deps "") + + if(QT_ENABLE_VERBOSE_DEPLOYMENT) + set(uses_terminal USES_TERMINAL) + endif() + + # Plugins still might be added after creating the deployment targets. + if(NOT TARGET qt_internal_plugins) + add_custom_target(qt_internal_plugins) + endif() + # Before running androiddeployqt, we need to make sure all plugins are built. + list(APPEND extra_deps qt_internal_plugins) # This target is used by Qt Creator's Android support and by the ${target}_make_apk target # in case DEPFILEs are not supported. + # Also the target is used to copy the library that belongs to ${target} when building multi-abi + # apk to the abi-specific directory. + _qt_internal_copy_file_if_different_command(copy_command + "$<TARGET_FILE:${target}>" + "${apk_final_dir}/${target_file_copy_relative_path}" + ) add_custom_target(${target}_prepare_apk_dir ALL - DEPENDS ${target} - COMMAND ${CMAKE_COMMAND} - -E copy_if_different $<TARGET_FILE:${target}> - "${apk_final_dir}/libs/${CMAKE_ANDROID_ARCH_ABI}/$<TARGET_FILE_NAME:${target}>" + DEPENDS ${target} ${extra_deps} + COMMAND ${copy_command} COMMENT "Copying ${target} binary to apk folder" + ${uses_terminal} ) - # The DEPFILE argument to add_custom_command is only available with Ninja or CMake>=3.20 and make. - if (CMAKE_GENERATOR MATCHES "Ninja" OR - (CMAKE_VERSION VERSION_GREATER_EQUAL 3.20 AND CMAKE_GENERATOR MATCHES "Makefiles")) - # Add custom command that creates the apk in an intermediate location. - # We need the intermediate location, because we cannot have target-dependent generator - # expressions in OUTPUT. - add_custom_command(OUTPUT "${apk_intermediate_file_path}" + set(sign_apk "") + if(QT_ANDROID_SIGN_APK) + set(sign_apk "--sign") + endif() + set(sign_aab "") + if(QT_ANDROID_SIGN_AAB) + set(sign_aab "--sign") + endif() + + set(extra_args "") + if(QT_INTERNAL_NO_ANDROID_RCC_BUNDLE_CLEANUP) + list(APPEND extra_args "--no-rcc-bundle-cleanup") + endif() + if(QT_ENABLE_VERBOSE_DEPLOYMENT) + list(APPEND extra_args "--verbose") + endif() + + if(QT_ANDROID_DEPLOY_RELEASE) + message(WARNING "QT_ANDROID_DEPLOY_RELEASE is not a valid Qt variable." + " Please set QT_ANDROID_DEPLOYMENT_TYPE to RELEASE instead.") + endif() + # Setting QT_ANDROID_DEPLOYMENT_TYPE to a value other than Release disables + # release package signing regardless of the build type. + if(QT_ANDROID_DEPLOYMENT_TYPE) + string(TOUPPER "${QT_ANDROID_DEPLOYMENT_TYPE}" deployment_type_upper) + if("${deployment_type_upper}" STREQUAL "RELEASE") + list(APPEND extra_args "--release") + endif() + elseif(NOT QT_BUILD_TESTS) + # Workaround for tests: do not set automatically --release flag if QT_BUILD_TESTS is set. + # Release package need to be signed. Signing is currently not supported by CI. + # What is more, also androidtestrunner is not working on release APKs, + # For example running "adb shell run-as" on release APK will finish with the error: + # run-as: Package '[PACKAGE-NAME]' is not debuggable + list(APPEND extra_args $<$<OR:$<CONFIG:Release>,$<CONFIG:RelWithDebInfo>,$<CONFIG:MinSizeRel>>:--release>) + endif() + + _qt_internal_check_depfile_support(has_depfile_support) + + if(has_depfile_support) + cmake_policy(PUSH) + if(POLICY CMP0116) + # Without explicitly setting this policy to NEW, we get a warning + # even though we ensure there's actually no problem here. + # See https://gitlab.kitware.com/cmake/cmake/-/issues/21959 + cmake_policy(SET CMP0116 NEW) + set(relative_to_dir ${CMAKE_CURRENT_BINARY_DIR}) + else() + set(relative_to_dir ${CMAKE_BINARY_DIR}) + endif() + + # Add custom command that creates the apk and triggers rebuild if files listed in + # ${dep_file_path} are changed. + add_custom_command(OUTPUT "${apk_final_file_path}" COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:${target}>" - "${apk_intermediate_dir}/libs/${CMAKE_ANDROID_ARCH_ABI}/$<TARGET_FILE_NAME:${target}>" + "${apk_final_dir}/${target_file_copy_relative_path}" COMMAND "${deployment_tool}" --input "${deployment_file}" - --output "${apk_intermediate_dir}" - --apk "${apk_intermediate_file_path}" - --depfile "${dep_intermediate_file_path}" - --builddir "${CMAKE_BINARY_DIR}" + --output "${apk_final_dir}" + --apk "${apk_final_file_path}" + --depfile "${dep_file_path}" + --builddir "${relative_to_dir}" + ${extra_args} + ${sign_apk} COMMENT "Creating APK for ${target}" - DEPENDS "${target}" "${deployment_file}" - DEPFILE "${dep_intermediate_file_path}") + DEPENDS "${target}" "${deployment_file}" ${extra_deps} + DEPFILE "${dep_file_path}" + VERBATIM + ${uses_terminal} + ) - # Create a ${target}_make_apk target to copy the apk from the intermediate to its final - # location. If the final and intermediate locations are identical, this is a no-op. - add_custom_target(${target}_make_apk - COMMAND "${CMAKE_COMMAND}" - -E copy_if_different "${apk_intermediate_file_path}" "${apk_final_file_path}" - DEPENDS "${apk_intermediate_file_path}") + # Add custom command that creates the aar and triggers rebuild if files listed in + # ${dep_file_path} are changed. + add_custom_command(OUTPUT "${aar_final_file_path}" + COMMAND ${CMAKE_COMMAND} + -E copy "$<TARGET_FILE:${target}>" + "${apk_final_dir}/${target_file_copy_relative_path}" + COMMAND "${deployment_tool}" + --input "${deployment_file}" + --output "${apk_final_dir}" + --apk "${aar_final_file_path}" + --depfile "${dep_file_path}" + --builddir "${relative_to_dir}" + --build-aar + ${extra_args} + COMMENT "Creating AAR for ${target}" + DEPENDS "${target}" "${deployment_file}" ${extra_deps} + DEPFILE "${dep_file_path}" + VERBATIM + ${uses_terminal} + ) + cmake_policy(POP) + + # Create a ${target}_make_apk target to trigger the apk build. + add_custom_target(${target}_make_apk DEPENDS "${apk_final_file_path}") + # Create a ${target}_make_aar target to trigger the aar build. + add_custom_target(${target}_make_aar DEPENDS "${aar_final_file_path}") else() add_custom_target(${target}_make_apk DEPENDS ${target}_prepare_apk_dir @@ -339,20 +566,284 @@ function(qt6_android_add_apk_target target) --input ${deployment_file} --output ${apk_final_dir} --apk ${apk_final_file_path} + ${extra_args} + ${sign_apk} COMMENT "Creating APK for ${target}" + VERBATIM + ${uses_terminal} + ) + + add_custom_target(${target}_make_aar + DEPENDS ${target}_prepare_apk_dir + COMMAND ${deployment_tool} + --input ${deployment_file} + --output ${apk_final_dir} + --apk ${aar_final_file_path} + --build-aar + ${extra_args} + COMMENT "Creating AAR for ${target}" + VERBATIM + ${uses_terminal} + ) + endif() + + # Add target triggering AAB creation. Since the _make_aab target is not added to the ALL + # set, we may avoid dependency check for it and admit that the target is "always out + # of date". + add_custom_target(${target}_make_aab + DEPENDS ${target}_prepare_apk_dir + COMMAND ${deployment_tool} + --input ${deployment_file} + --output ${apk_final_dir} + --apk ${apk_final_file_path} + --aab + ${sign_aab} + ${extra_args} + COMMENT "Creating AAB for ${target}" + ${uses_terminal} + ) + + if(QT_IS_ANDROID_MULTI_ABI_EXTERNAL_PROJECT) + # When building per-ABI external projects we only need to copy ABI-specific libraries and + # resources to the "main" ABI android build folder. + + if("${QT_INTERNAL_ANDROID_MULTI_ABI_BINARY_DIR}" STREQUAL "") + message(FATAL_ERROR "QT_INTERNAL_ANDROID_MULTI_ABI_BINARY_DIR is not set when building" + " ABI specific external project. This should not happen and might mean an issue" + " in Qt. Please report a bug with CMake traces attached.") + endif() + # Assume that external project mirrors build structure of the top-level ABI project and + # replace the build root when specifying the output directory of androiddeployqt. + file(RELATIVE_PATH androiddeployqt_output_path "${CMAKE_BINARY_DIR}" "${apk_final_dir}") + set(androiddeployqt_output_path + "${QT_INTERNAL_ANDROID_MULTI_ABI_BINARY_DIR}/${androiddeployqt_output_path}") + _qt_internal_copy_file_if_different_command(copy_command + "$<TARGET_FILE:${target}>" + "${androiddeployqt_output_path}/${target_file_copy_relative_path}" ) + if(has_depfile_support) + set(deploy_android_deps_dir "${apk_final_dir}/${target}_deploy_android") + set(timestamp_file "${deploy_android_deps_dir}/timestamp") + set(dep_file "${deploy_android_deps_dir}/${target}.d") + add_custom_command(OUTPUT "${timestamp_file}" + DEPENDS ${target} ${extra_deps} + COMMAND ${CMAKE_COMMAND} -E make_directory "${deploy_android_deps_dir}" + COMMAND ${CMAKE_COMMAND} -E touch "${timestamp_file}" + COMMAND ${copy_command} + COMMAND ${deployment_tool} + --input ${deployment_file} + --output ${androiddeployqt_output_path} + --copy-dependencies-only + ${extra_args} + --depfile "${dep_file}" + --builddir "${CMAKE_BINARY_DIR}" + COMMENT "Resolving ${CMAKE_ANDROID_ARCH_ABI} dependencies for the ${target} APK" + DEPFILE "${dep_file}" + VERBATIM + ${uses_terminal} + ) + add_custom_target(qt_internal_${target}_copy_apk_dependencies + DEPENDS "${timestamp_file}") + else() + add_custom_target(qt_internal_${target}_copy_apk_dependencies + DEPENDS ${target} ${extra_deps} + COMMAND ${copy_command} + COMMAND ${deployment_tool} + --input ${deployment_file} + --output ${androiddeployqt_output_path} + --copy-dependencies-only + ${extra_args} + COMMENT "Resolving ${CMAKE_ANDROID_ARCH_ABI} dependencies for the ${target} APK" + ${uses_terminal} + ) + endif() endif() + + set_property(GLOBAL APPEND PROPERTY _qt_apk_targets ${target}) + _qt_internal_collect_apk_dependencies_defer() + _qt_internal_collect_apk_imported_dependencies_defer("${target}") endfunction() -function(_qt_internal_create_global_apk_target) +function(_qt_internal_create_global_android_targets) + macro(_qt_internal_create_global_android_targets_impl target) + string(TOUPPER "${target}" target_upper) + if(NOT QT_NO_GLOBAL_${target_upper}_TARGET) + if(NOT TARGET ${target}) + add_custom_target(${target} COMMENT "Building all apks") + endif() + endif() + endmacro() + # Create a top-level "apk" target for convenience, so that users can call 'ninja apk'. # It will trigger building all the apk build targets that are added as part of the project. # Allow opting out. - if(NOT QT_NO_GLOBAL_APK_TARGET) - if(NOT TARGET apk) - add_custom_target(apk COMMENT "Building all apks") + _qt_internal_create_global_android_targets_impl(apk) + + # Create a top-level "aab" target for convenience, so that users can call 'ninja aab'. + # It will trigger building all the apk build targets that are added as part of the project. + # Allow opting out. + _qt_internal_create_global_android_targets_impl(aab) + + # Create a top-level "aar" target for convenience, so that users can call 'ninja aar'. + # It will trigger building all the aar build targets that are added as part of the project. + # Allow opting out. + _qt_internal_create_global_android_targets_impl(aar) +endfunction() + +# The function collects all known non-imported shared libraries that are created in the build tree. +# It uses the CMake DEFER CALL feature if the CMAKE_VERSION is greater +# than or equal to 3.19. +# Note: Users that use cmake version less that 3.19 need to call qt_finalize_project +# in the end of a project's top-level CMakeLists.txt. +function(_qt_internal_collect_apk_dependencies_defer) + # User opted-out the functionality + if(QT_NO_COLLECT_BUILD_TREE_APK_DEPS) + return() + endif() + + get_property(is_called GLOBAL PROPERTY _qt_is_collect_apk_dependencies_defer_called) + if(is_called) # Already scheduled + return() + endif() + set_property(GLOBAL PROPERTY _qt_is_collect_apk_dependencies_defer_called TRUE) + + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.19") + cmake_language(EVAL CODE "cmake_language(DEFER DIRECTORY \"${CMAKE_SOURCE_DIR}\" + CALL _qt_internal_collect_apk_dependencies)") + else() + # User don't want to see the warning + if(NOT QT_NO_WARN_BUILD_TREE_APK_DEPS) + message(WARNING + "The CMake version you use is less than 3.19. APK dependencies, that are a" + " part of the project tree, might not be collected correctly." + " Please call qt_finalize_project in the end of a project's top-level" + " CMakeLists.txt file to make sure that all the APK dependencies are" + " collected correctly." + " You can pass -DQT_NO_WARN_BUILD_TREE_APK_DEPS=ON when configuring the project" + " to silence the warning.") + endif() + endif() +endfunction() + +# The function collects project-built shared libraries that might be dependencies for +# the main apk targets. It stores their locations in a global custom target property. +function(_qt_internal_collect_apk_dependencies) + # User opted-out the functionality + if(QT_NO_COLLECT_BUILD_TREE_APK_DEPS) + return() + endif() + + get_property(is_called GLOBAL PROPERTY _qt_is_collect_apk_dependencies_called) + if(is_called) + return() + endif() + set_property(GLOBAL PROPERTY _qt_is_collect_apk_dependencies_called TRUE) + + get_property(apk_targets GLOBAL PROPERTY _qt_apk_targets) + + _qt_internal_collect_buildsystem_targets(libs + "${CMAKE_SOURCE_DIR}" INCLUDE SHARED_LIBRARY MODULE_LIBRARY) + list(REMOVE_DUPLICATES libs) + + if(NOT TARGET qt_internal_plugins) + add_custom_target(qt_internal_plugins) + endif() + + foreach(lib IN LISTS libs) + if(NOT lib IN_LIST apk_targets) + list(APPEND extra_library_dirs "$<TARGET_FILE_DIR:${lib}>") + get_target_property(target_type ${lib} TYPE) + # We collect all MODULE_LIBRARY targets since target APK may have implicit dependency + # to the plugin that will cause the runtime issue. Plugins that were added using + # qt6_add_plugin should be already added to the qt_internal_plugins dependency list, + # but it's ok to re-add them. + if(target_type STREQUAL "MODULE_LIBRARY") + add_dependencies(qt_internal_plugins ${lib}) + endif() + endif() + endforeach() + + if(NOT TARGET _qt_internal_apk_dependencies) + add_custom_target(_qt_internal_apk_dependencies) + endif() + set_target_properties(_qt_internal_apk_dependencies PROPERTIES + _qt_android_extra_library_dirs "${extra_library_dirs}" + ) +endfunction() + +# This function collects all imported shared libraries that might be dependencies for +# the main apk targets. The actual collection is deferred until the target's directory scope +# is processed. +# The function requires CMake 3.21 or later. +function(_qt_internal_collect_apk_imported_dependencies_defer target) + # User opted-out of the functionality. + if(QT_NO_COLLECT_IMPORTED_TARGET_APK_DEPS) + return() + endif() + + get_target_property(target_source_dir "${target}" SOURCE_DIR) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.21") + cmake_language(EVAL CODE "cmake_language(DEFER DIRECTORY \"${target_source_dir}\" + CALL _qt_internal_collect_apk_imported_dependencies \"${target}\")") + endif() +endfunction() + +# This function collects imported shared libraries that might be dependencies for +# the main apk targets. It stores their locations on a custom target property for the given target. +# The function requires CMake 3.21 or later. +function(_qt_internal_collect_apk_imported_dependencies target) + # User opted-out the functionality + if(QT_NO_COLLECT_IMPORTED_TARGET_APK_DEPS) + return() + endif() + + get_target_property(target_source_dir "${target}" SOURCE_DIR) + _qt_internal_collect_imported_shared_libraries_recursive(libs "${target_source_dir}") + list(REMOVE_DUPLICATES libs) + + foreach(lib IN LISTS libs) + list(APPEND extra_library_dirs "$<TARGET_FILE_DIR:${lib}>") + endforeach() + + set_property(TARGET "${target}" APPEND PROPERTY + _qt_android_extra_library_dirs "${extra_library_dirs}" + ) +endfunction() + +# This function recursively walks the current directory and its parent directories to collect +# imported shared library targets. +# The recursion goes upwards instead of downwards because imported targets are usually not global, +# and we can't call get_target_property() on a target which is not available in the current +# directory or parent scopes. +# We also can't cache parent directories because the imported targets in a parent directory +# might change in-between collection calls. +# The function requires CMake 3.21 or later. +function(_qt_internal_collect_imported_shared_libraries_recursive out_var subdir) + set(result "") + + get_directory_property(imported_targets DIRECTORY "${subdir}" IMPORTED_TARGETS) + foreach(imported_target IN LISTS imported_targets) + get_target_property(target_type "${imported_target}" TYPE) + if(target_type STREQUAL "SHARED_LIBRARY" OR target_type STREQUAL "MODULE_LIBRARY") + # If the target has the _qt_package_version property set, it means it's an + # 'official' qt target like a module or plugin, so we don't want to add it + # to the list of extra paths to scan for in androiddeployqt, because they are + # already handled via the regular 'qt' code path in the androiddeployqt. + # Thus this will pick up only non-qt 3rd party targets. + get_target_property(qt_package_version "${imported_target}" _qt_package_version) + if(NOT qt_package_version) + list(APPEND result "${imported_target}") + endif() endif() + endforeach() + + get_directory_property(parent_dir DIRECTORY "${subdir}" PARENT_DIRECTORY) + if(parent_dir) + _qt_internal_collect_imported_shared_libraries_recursive(result_inner "${parent_dir}") endif() + + list(APPEND result ${result_inner}) + set(${out_var} "${result}" PARENT_SCOPE) endfunction() # This function allows deciding whether apks should be built as part of the ALL target at first @@ -388,8 +879,685 @@ function(_qt_internal_create_global_apk_all_target_if_needed) endif() endfunction() +# The function converts the target property to a json record and appends it to the output +# variable. +function(_qt_internal_add_android_deployment_property out_var json_key target property) + set(property_genex "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},${property}>>") + string(APPEND ${out_var} + "$<$<BOOL:${property_genex}>:" + " \"${json_key}\": \"${property_genex}\"\,\n" + ">" + ) + + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() + +# The function converts the list properties of the targets to a json list record and appends it +# to the output variable. +# _qt_internal_add_android_deployment_list_property(out_var json_key [<target> <property>]...) +# The generated JSON object is the normal JSON array, e.g.: +# "qml-root-path": ["qml/root/path1","qml/root/path2"], +function(_qt_internal_add_android_deployment_list_property out_var json_key) + list(LENGTH ARGN argn_count) + math(EXPR is_odd "${argn_count} % 2") + if(is_odd) + message(FATAL_ERROR "Invalid argument count") + endif() + + set(skip_next FALSE) + set(property_genex "") + math(EXPR last_index "${argn_count} - 1") + foreach(idx RANGE ${last_index}) + if(skip_next) + set(skip_next FALSE) + continue() + endif() + set(skip_next TRUE) + + math(EXPR property_idx "${idx} + 1") + list(GET ARGN ${idx} target) + list(GET ARGN ${property_idx} property) + + # Add comma if we have at least one element from the previous iteration + if(property_genex) + set(add_comma_genex + "$<$<BOOL:${property_genex}>:$<COMMA>>" + ) + endif() + + set(property_genex + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},${property}>>" + ) + set(add_quote_genex + "$<$<BOOL:${property_genex}>:\">" + ) + + # Add comma only if next property genex contains non-empty value. + set(add_comma_genex "$<$<BOOL:${property_genex}>:${add_comma_genex}>") + string(JOIN "" list_join_genex + "${list_join_genex}" + "${add_comma_genex}${add_quote_genex}" + "$<JOIN:" + "${property_genex}," + "\"$<COMMA>\"" + ">" + "${add_quote_genex}" + ) + endforeach() + + string(APPEND ${out_var} + " \"${json_key}\" : [ ${list_join_genex} ],\n") + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() + +# The function converts the target list property to a json multi-value string record and appends it +# to the output variable. +# The generated JSON object is a simple string with the list property items separated by commas, +# e.g: +# "android-extra-plugins": "plugin1,plugin2", +function(_qt_internal_add_android_deployment_multi_value_property out_var json_key target property) + set(property_genex + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},${property}>>" + ) + string(JOIN "" list_join_genex + "$<JOIN:" + "${property_genex}," + "$<COMMA>" + ">" + ) + string(APPEND ${out_var} + "$<$<BOOL:${property_genex}>:" + " \"${json_key}\" : \"${list_join_genex}\",\n" + ">" + ) + + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() + +# The function converts paths to the CMake format to make them acceptable for JSON. +# It doesn't overwrite public properties, but instead writes formatted values to internal +# properties. +function(_qt_internal_android_format_deployment_paths target) + if(QT_BUILD_STANDALONE_TESTS OR QT_BUILDING_QT OR QT_INTERNAL_IS_STANDALONE_TEST) + set(android_deployment_paths_policy NEW) + else() + set(policy_path_properties + QT_QML_IMPORT_PATH + QT_QML_ROOT_PATH + QT_ANDROID_PACKAGE_SOURCE_DIR + QT_ANDROID_EXTRA_PLUGINS + QT_ANDROID_EXTRA_LIBS + ) + + # Check if any of paths contains the value and stop the evaluation if all properties are + # empty or -NOTFOUND + set(has_android_paths FALSE) + foreach(prop_name IN LISTS policy_path_properties) + get_target_property(prop_value ${target} ${prop_name}) + if(prop_value) + set(has_android_paths TRUE) + break() + endif() + endforeach() + if(has_android_paths) + __qt_internal_setup_policy(QTP0002 "6.6.0" + "Target properties that specify android-specific paths may contain generator\ + expressions but they must evaluate to valid JSON strings.\ + Check https://doc.qt.io/qt-6/qt-cmake-policy-qtp0002.html for policy details." + ) + qt6_policy(GET QTP0002 android_deployment_paths_policy) + endif() + endif() + if(android_deployment_paths_policy STREQUAL "NEW") + # When building standalone tests or Qt itself we obligate developers to not use + # windows paths when setting QT_* properties below, so their values are used as is when + # generating deployment settings. + string(JOIN "" qml_root_path_genex + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},QT_QML_ROOT_PATH>>" + "$<" + "$<AND:" + "$<BOOL:$<GENEX_EVAL:$<TARGET_PROPERTY:${target},QT_QML_ROOT_PATH>>>," + "$<BOOL:$<GENEX_EVAL:$<TARGET_PROPERTY:${target},_qt_internal_qml_root_path>>>" + ">:;" + ">" + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},_qt_internal_qml_root_path>>" + ) + set_target_properties(${target} PROPERTIES + _qt_native_qml_import_paths + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},QT_QML_IMPORT_PATH>>" + _qt_android_native_qml_root_paths + "${qml_root_path_genex}" + _qt_android_native_package_source_dir + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},QT_ANDROID_PACKAGE_SOURCE_DIR>>" + _qt_android_native_extra_plugins + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},QT_ANDROID_EXTRA_PLUGINS>>" + _qt_android_native_extra_libs + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},QT_ANDROID_EXTRA_LIBS>>" + ) + else() + # User projects still may use windows paths inside the QT_* properties below, with + # obligation to run the finalizer code. + _qt_internal_android_format_deployment_path_property(${target} + QT_QML_IMPORT_PATH _qt_native_qml_import_paths) + + _qt_internal_android_format_deployment_path_property(${target} + QT_QML_ROOT_PATH _qt_android_native_qml_root_paths) + + _qt_internal_android_format_deployment_path_property(${target} + _qt_internal_qml_root_path _qt_android_native_qml_root_paths APPEND) + + _qt_internal_android_format_deployment_path_property(${target} + QT_ANDROID_PACKAGE_SOURCE_DIR _qt_android_native_package_source_dir) + + _qt_internal_android_format_deployment_path_property(${target} + QT_ANDROID_EXTRA_PLUGINS _qt_android_native_extra_plugins) + + _qt_internal_android_format_deployment_path_property(${target} + QT_ANDROID_EXTRA_LIBS _qt_android_native_extra_libs) + endif() +endfunction() + +# The function converts the value of target property to JSON compatible path and writes the +# result to out_property. Property might be either single value, semicolon separated list or system +# path spec. +# The APPEND argument controls the property is set. The argument should be added after all +# the required arguments. +function(_qt_internal_android_format_deployment_path_property target property out_property) + set(should_append "") + if(ARGC EQUAL 4) + if("${ARGV3}" STREQUAL "APPEND") + set(should_append APPEND) + else() + message(FATAL_ERROR "Unexpected argument ${ARGV3}") + endif() + elseif(ARGC GREATER 4) + message(FATAL_ERROR "Unexpected arguments ${ARGN}") + endif() + + get_target_property(_paths ${target} ${property}) + if(_paths) + set(native_paths "") + foreach(_path IN LISTS _paths) + file(TO_CMAKE_PATH "${_path}" _path) + list(APPEND native_paths "${_path}") + endforeach() + set_property(TARGET ${target} ${should_append} PROPERTY + ${out_property} "${native_paths}") + endif() +endfunction() + if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) function(qt_android_add_apk_target) qt6_android_add_apk_target(${ARGV}) endfunction() endif() + +# The function returns the installation path to Qt for Android for the specified ${abi}. +# By default function expects to find a layout as is installed by the Qt online installer: +# Qt_install_dir/Version/ +# |__ gcc_64 +# |__ android_arm64_v8a +# |__ android_armv7 +# |__ android_x86 +# |__ android_x86_64 +function(_qt_internal_get_android_abi_prefix_path out_path abi) + if(CMAKE_ANDROID_ARCH_ABI STREQUAL abi) + # Required to build unit tests in developer build + if(QT_BUILD_INTERNALS_RELOCATABLE_INSTALL_PREFIX) + set(${out_path} "${QT_BUILD_INTERNALS_RELOCATABLE_INSTALL_PREFIX}") + else() + set(${out_path} "${QT6_INSTALL_PREFIX}") + endif() + elseif(DEFINED QT_PATH_ANDROID_ABI_${abi}) + get_filename_component(${out_path} "${QT_PATH_ANDROID_ABI_${abi}}" ABSOLUTE) + else() + # Map the ABI value to the Qt for Android folder. + if (abi STREQUAL "x86") + set(abi_directory_suffix "${abi}") + elseif (abi STREQUAL "x86_64") + set(abi_directory_suffix "${abi}") + elseif (abi STREQUAL "arm64-v8a") + set(abi_directory_suffix "arm64_v8a") + else() + set(abi_directory_suffix "armv7") + endif() + + get_filename_component(${out_path} + "${_qt_cmake_dir}/../../../android_${abi_directory_suffix}" ABSOLUTE) + endif() + set(${out_path} "${${out_path}}" PARENT_SCOPE) +endfunction() + +function(_qt_internal_get_android_abi_cmake_dir_path out_path abi) + if(DEFINED QT_ANDROID_PATH_CMAKE_DIR_${abi}) + set(cmake_dir "${QT_ANDROID_PATH_CMAKE_DIR_${abi}}") + else() + _qt_internal_get_android_abi_prefix_path(prefix_path ${abi}) + if((PROJECT_NAME STREQUAL "QtBase" OR QT_SUPERBUILD) AND QT_BUILDING_QT AND + NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_INTERNAL_IS_STANDALONE_TEST) + set(cmake_dir "${QT_CONFIG_BUILD_DIR}") + else() + string(TOUPPER "${QT_CMAKE_EXPORT_NAMESPACE}" export_namespace_upper) + set(cmake_dir "${prefix_path}/${${export_namespace_upper}_INSTALL_LIBS}/cmake") + endif() + endif() + + set(${out_path} "${cmake_dir}" PARENT_SCOPE) +endfunction() + +function(_qt_internal_get_android_abi_toolchain_path out_path abi) + set(toolchain_path "${QT_CMAKE_EXPORT_NAMESPACE}/qt.toolchain.cmake") + _qt_internal_get_android_abi_cmake_dir_path(cmake_dir ${abi}) + get_filename_component(toolchain_path + "${cmake_dir}/${toolchain_path}" ABSOLUTE) + set(${out_path} "${toolchain_path}" PARENT_SCOPE) +endfunction() + +function(_qt_internal_get_android_abi_subdir_path out_path subdir abi) + set(install_paths_path "${QT_CMAKE_EXPORT_NAMESPACE}/QtInstallPaths.cmake") + _qt_internal_get_android_abi_cmake_dir_path(cmake_dir ${abi}) + include("${cmake_dir}/${install_paths_path}") + set(${out_path} "${${subdir}}" PARENT_SCOPE) +endfunction() + +function(_qt_internal_collect_qt_for_android_paths out_var) + get_target_property(qt_android_abis ${target} _qt_android_abis) + if(NOT qt_android_abis) + set(qt_android_abis "") + endif() + + set(custom_qt_paths data libexecs libs plugins qml) + foreach(type IN ITEMS prefix ${custom_qt_paths}) + set(${type}_records "") + endforeach() + + foreach(abi IN LISTS qt_android_abis CMAKE_ANDROID_ARCH_ABI) + _qt_internal_get_android_abi_prefix_path(qt_abi_prefix_path ${abi}) + file(TO_CMAKE_PATH "${qt_abi_prefix_path}" qt_abi_prefix_path) + get_filename_component(qt_abi_prefix_path "${qt_abi_prefix_path}" ABSOLUTE) + list(APPEND prefix_records " \"${abi}\": \"${qt_abi_prefix_path}\"") + foreach(type IN ITEMS ${custom_qt_paths}) + string(TOUPPER "${type}" upper_case_type) + _qt_internal_get_android_abi_subdir_path(qt_abi_path + QT6_INSTALL_${upper_case_type} ${abi}) + list(APPEND ${type}_records + " \"${abi}\": \"${qt_abi_path}\"") + endforeach() + endforeach() + + foreach(type IN ITEMS prefix ${custom_qt_paths}) + list(JOIN ${type}_records ",\n" ${type}_records_string) + set(${type}_records_string "{\n${${type}_records_string}\n }") + endforeach() + + string(APPEND ${out_var} + " \"qt\": ${prefix_records_string},\n") + string(APPEND ${out_var} + " \"qtDataDirectory\": ${data_records_string},\n") + string(APPEND ${out_var} + " \"qtLibExecsDirectory\": ${libexecs_records_string},\n") + string(APPEND ${out_var} + " \"qtLibsDirectory\": ${libs_records_string},\n") + string(APPEND ${out_var} + " \"qtPluginsDirectory\": ${plugins_records_string},\n") + string(APPEND ${out_var} + " \"qtQmlDirectory\": ${qml_records_string},\n") + + set(${out_var} "${${out_var}}" PARENT_SCOPE) +endfunction() + +# The function collects list of existing Qt for Android using +# _qt_internal_get_android_abi_prefix_path and pre-defined set of known Android ABIs. The result is +# written to QT_DEFAULT_ANDROID_ABIS cache variable. +# Note that QT_DEFAULT_ANDROID_ABIS is not intended to be set outside the function and will be +# rewritten. +function(_qt_internal_collect_default_android_abis) + set(known_android_abis armeabi-v7a arm64-v8a x86 x86_64) + + set(default_abis) + foreach(abi IN LISTS known_android_abis) + _qt_internal_get_android_abi_toolchain_path(qt_abi_toolchain_path ${abi}) + # It's expected that Qt for Android contains ABI specific toolchain file. + if(EXISTS "${qt_abi_toolchain_path}" + OR CMAKE_ANDROID_ARCH_ABI STREQUAL abi) + list(APPEND default_abis ${abi}) + endif() + endforeach() + set(QT_DEFAULT_ANDROID_ABIS "${default_abis}" CACHE STRING + "The list of autodetected Qt for Android ABIs" FORCE + ) + set(QT_ANDROID_ABIS "${CMAKE_ANDROID_ARCH_ABI}" CACHE STRING + "The list of Qt for Android ABIs used to build the project apk" + ) + set(QT_ANDROID_BUILD_ALL_ABIS FALSE CACHE BOOL + "Build project using the list of autodetected Qt for Android ABIs" + ) +endfunction() + +# Returns a path to the timestamp file for the specific step of the multi-ABI Android project +function(_qt_internal_get_android_abi_step_stampfile out project abi step) + get_target_property(build_dir ${project} _qt_android_build_directory) + get_property(is_multi GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(is_multi) + set(${out} "${build_dir}/$<CONFIG>/${project}_${step}_stamp" PARENT_SCOPE) + else() + set(${out} "${build_dir}/${project}_${step}_stamp" PARENT_SCOPE) + endif() +endfunction() + +# Creates the multi-ABI Android projects and assigns the JOB_POOL to them if it's possible +function(_qt_internal_add_android_abi_project project abi) + add_custom_target(${project}) + + set(build_dir "${CMAKE_BINARY_DIR}/android_abi_builds/${abi}") + set_target_properties(${project} PROPERTIES + _qt_android_build_directory "${build_dir}" + ) + + file(MAKE_DIRECTORY "${build_dir}") + if(CMAKE_GENERATOR MATCHES "^Ninja") + set_property(GLOBAL APPEND PROPERTY JOB_POOLS _qt_android_${project}_pool=1) + endif() +endfunction() + +# Adds the custom build step to the multi-ABI Android project +function(_qt_internal_add_android_abi_step project abi step) + cmake_parse_arguments(arg "" "" "COMMAND;DEPENDS" ${ARGV}) + + if(NOT arg_COMMAND) + message(FATAL_ERROR "COMMAND is not set for ${project} step ${step} Android ABI ${abi}.") + endif() + + set(dep_stamps "") + foreach(dep ${arg_DEPENDS}) + _qt_internal_get_android_abi_step_stampfile(stamp ${project} ${abi} ${dep}) + list(APPEND dep_stamps "${stamp}") + endforeach() + + get_target_property(build_dir ${project} _qt_android_build_directory) + + if(CMAKE_GENERATOR MATCHES "^Ninja") + set(add_to_pool JOB_POOL _qt_android_${project}_pool) + else() + set(add_to_pool "") + endif() + + _qt_internal_get_android_abi_step_stampfile(stamp ${project} ${abi} ${step}) + add_custom_command(OUTPUT "${stamp}" + COMMAND ${arg_COMMAND} + COMMAND "${CMAKE_COMMAND}" -E touch "${stamp}" + ${add_to_pool} + DEPENDS + ${dep_stamps} + WORKING_DIRECTORY + "${build_dir}" + VERBATIM + ) + add_custom_target("${project}_${step}" DEPENDS "${stamp}") + + get_target_property(known_steps ${project} _qt_android_abi_steps) + if(NOT CMAKE_GENERATOR MATCHES "^Ninja") + if(NOT QT_NO_WARN_ANDROID_MULTI_ABI_GENERATOR) + get_property(is_warned GLOBAL PROPERTY _qt_internal_warn_android_multi_abi_generator) + if(NOT is_warned) + set_property(GLOBAL PROPERTY _qt_internal_warn_android_multi_abi_generator TRUE) + message(WARNING "Building Multi-ABI Qt projects with the '${CMAKE_GENERATOR}'" + " generator has limitations. All targets from non-main ABI will be built" + " unconditionally. Please use the 'Ninja' or 'Ninja Multi-config' generators" + " with ninja build instead. Set QT_NO_WARN_ANDROID_MULTI_ABI_GENERATOR to" + " 'TRUE' to suppress this warning." + ) + endif() + endif() + if(known_steps) + list(GET known_steps 0 first) + add_dependencies(${first} ${project}_${step}) + endif() + endif() + + list(PREPEND known_steps ${project}_${step}) + set_target_properties(${project} PROPERTIES _qt_android_abi_steps "${known_steps}") +endfunction() + +# The function configures external projects for ABIs that target packages need to build with. +# Each target adds build step to the external project that is linked to the +# qt_internal_android_${abi}-${target}_build target in the primary ABI build tree. +function(_qt_internal_configure_android_multiabi_target target) + # Functionality is only applicable for the primary ABI + if(QT_IS_ANDROID_MULTI_ABI_EXTERNAL_PROJECT) + return() + endif() + + get_target_property(target_abis ${target} QT_ANDROID_ABIS) + if(target_abis) + # Use target-specific Qt for Android ABIs. + set(android_abis ${target_abis}) + elseif(QT_ANDROID_BUILD_ALL_ABIS) + # Use autodetected Qt for Android ABIs. + set(android_abis ${QT_DEFAULT_ANDROID_ABIS}) + elseif(QT_ANDROID_ABIS) + # Use project-wide Qt for Android ABIs. + set(android_abis ${QT_ANDROID_ABIS}) + else() + # User have an empty list of Qt for Android ABIs. + message(FATAL_ERROR + "The list of Android ABIs is empty, when building ${target}.\n" + "You have the following options to select ABIs for a target:\n" + " - Set the QT_ANDROID_ABIS variable before calling qt6_add_executable\n" + " - Set the ANDROID_ABIS property for ${target}\n" + " - Set QT_ANDROID_BUILD_ALL_ABIS flag to try building with\n" + " the list of autodetected Qt for Android:\n ${QT_DEFAULT_ANDROID_ABIS}" + ) + endif() + + get_cmake_property(is_multi_config GENERATOR_IS_MULTI_CONFIG) + if(is_multi_config) + list(JOIN CMAKE_CONFIGURATION_TYPES "$<SEMICOLON>" escaped_configuration_types) + set(config_arg "-DCMAKE_CONFIGURATION_TYPES=${escaped_configuration_types}") + else() + set(config_arg "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}") + endif() + + unset(extra_cmake_args) + + # The flag is needed when building qt standalone tests only to avoid building + # qt repo itself + if(QT_BUILD_STANDALONE_TESTS) + list(APPEND extra_cmake_args "-DQT_BUILD_STANDALONE_TESTS=ON") + endif() + + if(NOT QT_ADDITIONAL_PACKAGES_PREFIX_PATH STREQUAL "") + list(JOIN QT_ADDITIONAL_PACKAGES_PREFIX_PATH "$<SEMICOLON>" escaped_packages_prefix_path) + list(APPEND extra_cmake_args + "-DQT_ADDITIONAL_PACKAGES_PREFIX_PATH=${escaped_packages_prefix_path}") + endif() + + if(NOT QT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH STREQUAL "") + list(JOIN QT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH "$<SEMICOLON>" + escaped_host_packages_prefix_path) + list(APPEND extra_cmake_args + "-DQT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH=${escaped_host_packages_prefix_path}") + endif() + + if(ANDROID_SDK_ROOT) + list(APPEND extra_cmake_args "-DANDROID_SDK_ROOT=${ANDROID_SDK_ROOT}") + endif() + + # ANDROID_NDK_ROOT is invented by Qt and is what the qt toolchain file expects + if(ANDROID_NDK_ROOT) + list(APPEND extra_cmake_args "-DANDROID_NDK_ROOT=${ANDROID_NDK_ROOT}") + + # ANDROID_NDK is passed by Qt Creator and is also present in the android toolchain file. + elseif(ANDROID_NDK) + list(APPEND extra_cmake_args "-DANDROID_NDK_ROOT=${ANDROID_NDK}") + endif() + + if(DEFINED QT_NO_PACKAGE_VERSION_CHECK) + list(APPEND extra_cmake_args "-DQT_NO_PACKAGE_VERSION_CHECK=${QT_NO_PACKAGE_VERSION_CHECK}") + endif() + + if(DEFINED QT_HOST_PATH_CMAKE_DIR) + list(APPEND extra_cmake_args "-DQT_HOST_PATH_CMAKE_DIR=${QT_HOST_PATH_CMAKE_DIR}") + endif() + + if(CMAKE_MAKE_PROGRAM) + list(APPEND extra_cmake_args "-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}") + endif() + + if(CMAKE_C_COMPILER_LAUNCHER) + list(JOIN CMAKE_C_COMPILER_LAUNCHER "$<SEMICOLON>" + compiler_launcher) + list(APPEND extra_cmake_args + "-DCMAKE_C_COMPILER_LAUNCHER=${compiler_launcher}") + endif() + + if(CMAKE_CXX_COMPILER_LAUNCHER) + list(JOIN CMAKE_CXX_COMPILER_LAUNCHER "$<SEMICOLON>" + compiler_launcher) + list(APPEND extra_cmake_args + "-DCMAKE_CXX_COMPILER_LAUNCHER=${compiler_launcher}") + endif() + + unset(user_cmake_args) + foreach(var IN LISTS QT_ANDROID_MULTI_ABI_FORWARD_VARS) + string(REPLACE ";" "$<SEMICOLON>" var_value "${${var}}") + list(APPEND user_cmake_args "-D${var}=${var_value}") + endforeach() + + set(missing_qt_abi_toolchains "") + set(previous_copy_apk_dependencies_target ${target}) + # Create external projects for each android ABI except the main one. + list(REMOVE_ITEM android_abis "${CMAKE_ANDROID_ARCH_ABI}") + foreach(abi IN ITEMS ${android_abis}) + if(NOT "${abi}" IN_LIST QT_DEFAULT_ANDROID_ABIS) + list(APPEND missing_qt_abi_toolchains ${abi}) + list(REMOVE_ITEM android_abis "${abi}") + continue() + endif() + + get_property(abi_external_projects GLOBAL + PROPERTY _qt_internal_abi_external_projects) + if(NOT abi_external_projects + OR NOT "qt_internal_android_${abi}" IN_LIST abi_external_projects) + _qt_internal_add_android_abi_project(qt_internal_android_${abi} ${abi}) + + get_target_property(android_abi_build_dir qt_internal_android_${abi} + _qt_android_build_directory) + _qt_internal_get_android_abi_toolchain_path(qt_abi_toolchain_path ${abi}) + _qt_internal_add_android_abi_step(qt_internal_android_${abi} ${abi} configure + COMMAND + "${CMAKE_COMMAND}" + "-G${CMAKE_GENERATOR}" + "-DCMAKE_TOOLCHAIN_FILE=${qt_abi_toolchain_path}" + "-DQT_HOST_PATH=${QT_HOST_PATH}" + "-DQT_IS_ANDROID_MULTI_ABI_EXTERNAL_PROJECT=ON" + "-DQT_INTERNAL_ANDROID_MULTI_ABI_BINARY_DIR=${CMAKE_BINARY_DIR}" + "${config_arg}" + "${extra_cmake_args}" + "${user_cmake_args}" + "-B" "${android_abi_build_dir}" + "-S" "${CMAKE_SOURCE_DIR}" + ) + set_property(GLOBAL APPEND PROPERTY + _qt_internal_abi_external_projects "qt_internal_android_${abi}") + endif() + + get_target_property(android_abi_build_dir qt_internal_android_${abi} + _qt_android_build_directory) + _qt_internal_add_android_abi_step(qt_internal_android_${abi} ${abi} ${target}_build + DEPENDS + configure + COMMAND + "${CMAKE_COMMAND}" + --build "${android_abi_build_dir}" + --config $<CONFIG> + --target ${target} + ) + add_dependencies(${target} "qt_internal_android_${abi}_${target}_build") + + _qt_internal_add_android_abi_step(qt_internal_android_${abi} ${abi} + ${target}_copy_apk_dependencies + DEPENDS + ${target}_build + COMMAND + "${CMAKE_COMMAND}" + --build "${android_abi_build_dir}" + --config $<CONFIG> + --target qt_internal_${target}_copy_apk_dependencies + ) + set(external_project_copy_target + "qt_internal_android_${abi}_${target}_copy_apk_dependencies") + + # Need to build dependency chain between the + # qt_internal_android_${abi}-${target}_copy_apk_dependencies targets for all ABI's, to + # prevent parallel execution of androiddeployqt processes. We cannot use Ninja job pools + # here because it's not possible to define job pool for the step target in ExternalProject. + # All tricks with interlayer targets don't work, because we only can bind interlayer target + # to the job pool, but its dependencies can still be built in parallel. + add_dependencies(${previous_copy_apk_dependencies_target} + "${external_project_copy_target}") + set(previous_copy_apk_dependencies_target "${external_project_copy_target}") + endforeach() + + if(missing_qt_abi_toolchains) + list(JOIN missing_qt_abi_toolchains ", " missing_qt_abi_toolchains_string) + message(FATAL_ERROR "Cannot find toolchain files for the manually specified Android" + " ABIs: ${missing_qt_abi_toolchains_string}" + "\nNote that you also may manually specify the path to the required Qt for" + " Android ABI using QT_PATH_ANDROID_ABI_<abi> CMake variable with the value" + " of the installation prefix, and QT_ANDROID_PATH_CMAKE_DIR_<abi> with" + " the location of the cmake directory for that ABI.\n") + endif() + + list(JOIN android_abis ", " android_abis_string) + if(android_abis_string) + set(android_abis_string "${CMAKE_ANDROID_ARCH_ABI} (default), ${android_abis_string}") + else() + set(android_abis_string "${CMAKE_ANDROID_ARCH_ABI} (default)") + endif() + if(NOT QT_NO_ANDROID_ABI_STATUS_MESSAGE) + message(STATUS "Configuring '${target}' for the following Android ABIs:" + " ${android_abis_string}") + endif() + set_target_properties(${target} PROPERTIES _qt_android_abis "${android_abis}") +endfunction() + +# The wrapper function that contains routines that need to be called to produce a valid Android +# package for the executable 'target'. The function is added to the finalizer list of the Core +# module and is executed implicitly when configuring user projects. +function(_qt_internal_android_executable_finalizer target) + set_property(TARGET ${target} PROPERTY _qt_android_executable_finalizer_called TRUE) + + _qt_internal_expose_android_package_source_dir_to_ide(${target}) + + _qt_internal_configure_android_multiabi_target("${target}") + qt6_android_generate_deployment_settings("${target}") + qt6_android_add_apk_target("${target}") +endfunction() + +function(_qt_internal_expose_android_package_source_dir_to_ide target) + get_target_property(android_package_source_dir ${target} QT_ANDROID_PACKAGE_SOURCE_DIR) + if(android_package_source_dir) + get_target_property(target_source_dir ${target} SOURCE_DIR) + if(NOT IS_ABSOLUTE "${android_package_source_dir}") + string(JOIN "/" android_package_source_dir + "${target_source_dir}" + "${android_package_source_dir}" + ) + endif() + + if(EXISTS "${android_package_source_dir}") + file(GLOB_RECURSE android_package_sources + RELATIVE "${target_source_dir}" + "${android_package_source_dir}/*" + ) + endif() + + foreach(f IN LISTS android_package_sources) + _qt_internal_expose_source_file_to_ide(${target} "${f}") + endforeach() + endif() +endfunction() + +set(QT_INTERNAL_ANDROID_TARGET_BUILD_DIR_SUPPORT ON CACHE INTERNAL + "Indicates that Qt supports per-target Android build directories") |