diff options
| author | Alexey Edelev <alexey.edelev@qt.io> | 2025-07-01 18:34:01 +0200 |
|---|---|---|
| committer | Alexey Edelev <alexey.edelev@qt.io> | 2025-07-24 17:33:35 +0200 |
| commit | e34914d5bfcecac4c2e242d4653391a62d7fbc81 (patch) | |
| tree | 5ea2f0fc8ed044d17b76601c28fddb4043c03daa | |
| parent | b8f6af3b43890156cb200d6ad4a15e8e3c9ef1d5 (diff) | |
Add signing support for modern android bundles
Use gradle based signing mechanism for the modern Android bundles.
From the interface perspective signing remains unchanged:
QT_ANDROID_SIGN_<AAB|APK> flags control if the package should be
signed. The credentials are taken from the QT_ANDROID_KEYSTORE_*
environment variables.
Signing is done in separate CMake script to avoid storing passwords
inside build scripts. Scripts reads passwords from the environment
when the respective signing rule is running.
Change-Id: Id1097b2b6d011a63c58e5a441c5360a1a5d97e8f
Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io>
Reviewed-by: Assam Boudjelthia <assam.boudjelthia@qt.io>
| -rw-r--r-- | cmake/QtAndroidSignPackage.cmake | 98 | ||||
| -rw-r--r-- | cmake/QtBuildHelpers.cmake | 1 | ||||
| -rw-r--r-- | src/corelib/Qt6AndroidGradleHelpers.cmake | 85 | ||||
| -rw-r--r-- | tests/auto/cmake/CMakeLists.txt | 35 | ||||
| -rw-r--r-- | tests/auto/cmake/test_android_signing/CMakeLists.txt | 16 |
5 files changed, 207 insertions, 28 deletions
diff --git a/cmake/QtAndroidSignPackage.cmake b/cmake/QtAndroidSignPackage.cmake new file mode 100644 index 00000000000..c4afa7dadcf --- /dev/null +++ b/cmake/QtAndroidSignPackage.cmake @@ -0,0 +1,98 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# The script allows signing of Android packages using either jarsigner or +# apksigner program. +# +# Usage: +# cmake -DPROGRAM=/path/to/signer [-DZIPALIGN_PATH=/path/to/zipalign] +# -DUNSIGNED_PACKAGE=path/to/input -DSIGNED_PACKAGE=path/to/output +# -P QtAndroidSignPackage.cmake + +if(NOT DEFINED PROGRAM) + message(FATAL_ERROR "PROGRAM variable is not defined. Please" + " specify the path to the apksigner or jarsigner program.") +endif() + +if(NOT DEFINED UNSIGNED_PACKAGE) + message(FATAL_ERROR "UNSIGNED_PACKAGE variable is not defined. Please" + " specify the path to the input file.") +endif() + +if(NOT DEFINED SIGNED_PACKAGE) + message(FATAL_ERROR "SIGNED_PACKAGE variable is not defined. Please" + " specify the path to the output file.") +endif() + +get_filename_component(program_name "${PROGRAM}" NAME_WE) +get_filename_component(type "${UNSIGNED_PACKAGE}" LAST_EXT) +if(type STREQUAL ".aab") + if(NOT program_name STREQUAL "jarsigner") + message(WARNING "Unexpected signer program for .aab files: ${program_name}. " + "Expected 'jarsigner'.") + endif() + + set(digestalg "SHA-256") + set(sigalg "SHA256withRSA") + + if(DEFINED ENV{QT_ANDROID_KEYSTORE_KEY_PASS}) + set(keypass_arg -keypass "$ENV{QT_ANDROID_KEYSTORE_KEY_PASS}") + else() + set(keypass_arg "") + endif() + + if(DEFINED ENV{QT_ANDROID_KEYSTORE_STORE_PASS}) + set(storepass_arg -storepass "$ENV{QT_ANDROID_KEYSTORE_STORE_PASS}") + else() + set(storepass_arg "") + endif() + + execute_process( + COMMAND ${PROGRAM} + -sigalg ${sigalg} -digestalg ${digestalg} + -keystore "$ENV{QT_ANDROID_KEYSTORE_PATH}" + ${storepass_arg} + ${keypass_arg} + -signedjar "${SIGNED_PACKAGE}" + "${UNSIGNED_PACKAGE}" + "$ENV{QT_ANDROID_KEYSTORE_ALIAS}" + RESULT_VARIABLE sign_result + OUTPUT_VARIABLE sign_output + ERROR_VARIABLE sign_error + ) + + if(NOT sign_result EQUAL 0) + message(FATAL_ERROR "Unable to sign ${UNSIGNED_PACKAGE}: ${sign_output} ${sign_error}") + endif() +elseif(type STREQUAL ".apk") + if(NOT DEFINED ZIPALIGN_PATH) + message(FATAL_ERROR "ZIPALIGN_PATH variable is not defined. Please" + " specify the path to the zipalign program.") + endif() + + if(NOT program_name STREQUAL "apksigner") + message(WARNING "Unexpected signer program for .apk files: ${program_name}. " + "Expected 'apksigner'.") + endif() + + execute_process( + COMMAND "${ZIPALIGN_PATH}" -c 4 "${UNSIGNED_PACKAGE}") + + execute_process( + COMMAND "${PROGRAM}" sign + --ks "$ENV{QT_ANDROID_KEYSTORE_PATH}" + --ks-pass "pass:$ENV{QT_ANDROID_KEYSTORE_STORE_PASS}" + --key-pass "pass:$ENV{QT_ANDROID_KEYSTORE_KEY_PASS}" + --ks-key-alias "$ENV{QT_ANDROID_KEYSTORE_ALIAS}" + --out "${SIGNED_PACKAGE}" "${UNSIGNED_PACKAGE}" + RESULT_VARIABLE sign_result + OUTPUT_VARIABLE sign_output + ERROR_VARIABLE sign_error + ) + + if(NOT sign_result EQUAL 0) + message(FATAL_ERROR "Unable to sign ${UNSIGNED_PACKAGE}: ${sign_output} ${sign_error}") + endif() +else() + message(FATAL_ERROR "Unsupported file type: ${type}. Only .apk and .aab files are supported.") +endif() diff --git a/cmake/QtBuildHelpers.cmake b/cmake/QtBuildHelpers.cmake index df0aa33cda2..b6132ddae7e 100644 --- a/cmake/QtBuildHelpers.cmake +++ b/cmake/QtBuildHelpers.cmake @@ -320,6 +320,7 @@ endfunction() # The files are expected to exist under the qtbase/cmake sub-directory. function(qt_internal_get_qt_build_public_files_to_install out_var) set(${out_var} + QtAndroidSignPackage.cmake QtCopyFileIfDifferent.cmake QtInitProject.cmake QtPublicCMakeEarlyPolicyHelpers.cmake diff --git a/src/corelib/Qt6AndroidGradleHelpers.cmake b/src/corelib/Qt6AndroidGradleHelpers.cmake index 933ecce2950..f71bac4e08a 100644 --- a/src/corelib/Qt6AndroidGradleHelpers.cmake +++ b/src/corelib/Qt6AndroidGradleHelpers.cmake @@ -263,6 +263,21 @@ function(_qt_internal_android_prepare_gradle_build target) _qt_internal_create_global_apk_all_target_if_needed() endfunction() +# Returns the path to the output package file for the target. +function(_qt_internal_android_get_output_package_name out_var target) + _qt_internal_android_package_path(package_build_dir ${target} ${type}) + _qt_internal_android_get_deployment_type_option(deployment_type_suffix "release" "debug") + if(NOT deployment_type_suffix STREQUAL "release" AND type STREQUAL "apk") + set(extra_suffix "-unsigned") + else() + set(extra_suffix "") + endif() + + set(output_dir "${package_build_dir}/${deployment_type_suffix}") + set(${out_var} "${output_dir}/app-${deployment_type_suffix}${extra_suffix}.${type}" + PARENT_SCOPE) +endfunction() + # Adds the modern gradle build targets. # These targets use the settings.gradle based build directory structure. function(_qt_internal_android_add_gradle_build target type) @@ -280,11 +295,7 @@ function(_qt_internal_android_add_gradle_build target type) set(package_file_path "${android_build_dir}/${target}.${type}") - _qt_internal_android_package_path(package_build_dir ${target} ${type}) - _qt_internal_android_get_deployment_type_option(deployment_type_suffix - "release" "debug") - set(package_build_file_path - "${package_build_dir}/${deployment_type_suffix}/app-${deployment_type_suffix}.${type}") + _qt_internal_android_get_output_package_name(package_build_file_path ${target} ${type}) set(extra_deps "") if(TARGET ${target}_copy_feature_names) @@ -314,7 +325,69 @@ function(_qt_internal_android_add_gradle_build target type) VERBATIM ) - add_custom_target(${target}_make_${type} DEPENDS "${package_file_path}") + _qt_internal_android_sign_package(signed_package ${target} ${type}) + + add_custom_target(${target}_make_${type} DEPENDS "${package_file_path}" "${signed_package}") +endfunction() + +function(_qt_internal_android_sign_package out_file target type) + string(TOUPPER "${type}" type_upper) + if(NOT QT_ANDROID_SIGN_${type_upper}) + set(${out_file} "" PARENT_SCOPE) + return() + endif() + + if(type STREQUAL "aab") + find_program(jarsigner NAMES jarsigner) + if(NOT jarsigner) + message(FATAL_ERROR "jarsigner is not found. Please install" + " a JDK version to sign '${target}'.") + endif() + + set(program ${jarsigner}) + set(extra_args "") + elseif(type STREQUAL "apk") + _qt_internal_android_get_target_sdk_build_tools_revision(build_tools_version ${target}) + if(CMAKE_HOST_WIN32) + set(suffix ".bat") + else() + set(suffix "") + endif() + set(build_tools_base_path "${ANDROID_SDK_ROOT}/build-tools/${build_tools_version}") + set(program "${build_tools_base_path}/apksigner${suffix}") + set(extra_args "-DZIPALIGN_PATH=${build_tools_base_path}/build-tools/zipalign${suffix}") + endif() + + _qt_internal_android_get_output_package_name(package_build_file_path ${target} ${type}) + + _qt_internal_android_package_path(package_build_dir ${target} ${type}) + _qt_internal_android_get_deployment_type_option(deployment_type_suffix + "release" "debug") + set(base_output_path "${package_build_dir}/${deployment_type_suffix}") + set(package_build_file_path_signed + "${base_output_path}/app-${deployment_type_suffix}-signed.${type}") + + set(package_file_path "${android_build_dir}/${target}-signed.${type}") + + set(sign_package_script "${_qt_6_config_cmake_dir}/QtAndroidSignPackage.cmake") + add_custom_command(OUTPUT "${package_file_path}" + BYPRODUCTS "${package_build_file_path_signed}" + COMMAND + ${CMAKE_COMMAND} "-DPROGRAM=${program}" "-DUNSIGNED_PACKAGE=${package_build_file_path}" + "-DSIGNED_PACKAGE=${package_build_file_path_signed}" ${extra_args} + -P "${sign_package_script}" + COMMAND + ${CMAKE_COMMAND} -E copy_if_different + "${package_build_file_path_signed}" "${package_file_path}" + DEPENDS + ${package_build_file_path} + ${sign_package_script} + WORKING_DIRECTORY + "${android_build_dir}" + VERBATIM + ) + + set(${out_file} "${package_file_path}" PARENT_SCOPE) endfunction() # Returns the path to the android executable package either apk or aab. diff --git a/tests/auto/cmake/CMakeLists.txt b/tests/auto/cmake/CMakeLists.txt index a228cb1a5a0..056bd6acfdf 100644 --- a/tests/auto/cmake/CMakeLists.txt +++ b/tests/auto/cmake/CMakeLists.txt @@ -147,25 +147,22 @@ if(ANDROID) "QT_ANDROID_KEYSTORE_KEY_PASS=qttest" ) - # Signing unimplemented for the modern Android bundles - if(NOT QT_USE_ANDROID_MODERN_BUNDLE) - foreach(package_type aab apk) - string(TOUPPER "${package_type}" package_type_upper) - set(test_name "test_android_signing_${package_type}") - _qt_internal_test_expect_pass(test_android_signing - TESTNAME ${test_name} - BUILD_OPTIONS ${common_android_vars} -DQT_ANDROID_SIGN_${package_type_upper}=ON - BUILD_DIR ${test_name} - BUILD_TARGET test_android_signing_make_${package_type} - BINARY ${CMAKE_CTEST_COMMAND} - BINARY_ARGS -V - ) - set_property(TEST ${test_name} APPEND PROPERTY - ENVIRONMENT ${signing_test_common_environment} - "QT_ANDROID_KEYSTORE_PATH=${CMAKE_CURRENT_BINARY_DIR}/${test_name}/qttest.jks" - ) - endforeach() - endif() + foreach(package_type aab apk) + string(TOUPPER "${package_type}" package_type_upper) + set(test_name "test_android_signing_${package_type}") + _qt_internal_test_expect_pass(test_android_signing + TESTNAME ${test_name} + BUILD_OPTIONS ${common_android_vars} -DQT_ANDROID_SIGN_${package_type_upper}=ON + BUILD_DIR ${test_name} + BUILD_TARGET test_android_signing_make_${package_type} + BINARY ${CMAKE_CTEST_COMMAND} + BINARY_ARGS -V + ) + set_property(TEST ${test_name} APPEND PROPERTY + ENVIRONMENT ${signing_test_common_environment} + "QT_ANDROID_KEYSTORE_PATH=${CMAKE_CURRENT_BINARY_DIR}/${test_name}/qttest.jks" + ) + endforeach() # Test only multi-abi specific functionality when QT_BUILD_MINIMAL_ANDROID_MULTI_ABI_TESTS is # ON. diff --git a/tests/auto/cmake/test_android_signing/CMakeLists.txt b/tests/auto/cmake/test_android_signing/CMakeLists.txt index 50bdf85041b..4df64873360 100644 --- a/tests/auto/cmake/test_android_signing/CMakeLists.txt +++ b/tests/auto/cmake/test_android_signing/CMakeLists.txt @@ -45,13 +45,23 @@ target_link_libraries(test_android_signing PRIVATE _qt_internal_android_get_target_deployment_dir(android_build test_android_signing) -get_filename_component(build_dir_name ${android_build} NAME) +if(QT_USE_ANDROID_MODERN_BUNDLE) + set(output_package_basename "app-release-signed") +else() + get_filename_component(build_dir_name ${android_build} NAME) + if(QT_ANDROID_SIGN_AAB) + set(output_package_basename "${build_dir_name}-release") + else() + set(output_package_basename "${build_dir_name}-release-signed") + endif() +endif() + if(QT_ANDROID_SIGN_AAB) set(expected_output - "${android_build}/build/outputs/bundle/release/${build_dir_name}-release.aab") + "${android_build}/build/outputs/bundle/release/${output_package_basename}.aab") else() set(expected_output - "${android_build}/build/outputs/apk/release/${build_dir_name}-release-signed.apk") + "${android_build}/build/outputs/apk/release/${output_package_basename}.apk") endif() add_test(NAME test_android_signing_verify_signature |
