| # Distributed under the OSI-approved BSD 3-Clause License. See accompanying |
| # file LICENSE.rst or https://cmake.org/licensing for details. |
| |
| # Assemble an Android APK from the CPack staging tree by driving the Android |
| # SDK build tools (aapt2, d8, zipalign, apksigner) and the JDK directly. |
| # Gradle is deliberately not used. |
| |
| if(CMAKE_BINARY_DIR) |
| message(FATAL_ERROR "CPackAPK.cmake may only be used by CPack internally.") |
| endif() |
| |
| function(_cpack_apk_debug) |
| if(CPACK_APK_DEBUG) |
| message("CPackAPK:Debug: " ${ARGN}) |
| endif() |
| endfunction() |
| |
| # Run a command, failing the package generation with the captured output |
| # if it does not succeed. |
| function(_cpack_apk_run description) |
| set(_command ${ARGN}) |
| list(JOIN _command " " _command_text) |
| _cpack_apk_debug("Running ${description}: ${_command_text}") |
| execute_process( |
| COMMAND ${_command} |
| WORKING_DIRECTORY "${CPACK_APK_WORK_DIRECTORY}" |
| RESULT_VARIABLE _result |
| OUTPUT_VARIABLE _output |
| ERROR_VARIABLE _error |
| ) |
| if(NOT _result EQUAL 0) |
| message(FATAL_ERROR |
| "CPack APK: ${description} failed with exit code ${_result}.\n" |
| "Command: ${_command_text}\n" |
| "${_output}${_error}") |
| endif() |
| if(_output OR _error) |
| _cpack_apk_debug("Output of ${description}: ${_output}${_error}") |
| endif() |
| endfunction() |
| |
| #----------------------------------------------------------------------------- |
| # Locate the Android SDK |
| #----------------------------------------------------------------------------- |
| |
| function(_cpack_apk_find_sdk_root result) |
| # An explicitly given SDK is used or rejected, never silently replaced by |
| # one found elsewhere. |
| if(CPACK_APK_SDK_ROOT) |
| if(IS_DIRECTORY "${CPACK_APK_SDK_ROOT}/build-tools") |
| file(REAL_PATH "${CPACK_APK_SDK_ROOT}" _real) |
| set(${result} "${_real}" PARENT_SCOPE) |
| else() |
| set(${result} "" PARENT_SCOPE) |
| endif() |
| return() |
| endif() |
| set(_candidates) |
| foreach(_var IN ITEMS ANDROID_HOME ANDROID_SDK_ROOT) |
| if(DEFINED ENV{${_var}}) |
| list(APPEND _candidates "$ENV{${_var}}") |
| endif() |
| endforeach() |
| # The NDK usually lives inside the SDK, either as <sdk>/ndk/<version> |
| # or as <sdk>/ndk-bundle. |
| set(_ndk_roots "${CPACK_APK_NDK_ROOT}") |
| foreach(_var IN ITEMS ANDROID_NDK_ROOT ANDROID_NDK_HOME ANDROID_NDK) |
| if(DEFINED ENV{${_var}}) |
| list(APPEND _ndk_roots "$ENV{${_var}}") |
| endif() |
| endforeach() |
| foreach(_ndk IN LISTS _ndk_roots) |
| if(_ndk) |
| list(APPEND _candidates "${_ndk}/../.." "${_ndk}/..") |
| endif() |
| endforeach() |
| |
| foreach(_candidate IN LISTS _candidates) |
| if(IS_DIRECTORY "${_candidate}/build-tools") |
| file(REAL_PATH "${_candidate}" _real) |
| set(${result} "${_real}" PARENT_SCOPE) |
| return() |
| endif() |
| endforeach() |
| set(${result} "" PARENT_SCOPE) |
| endfunction() |
| |
| # Pick the highest version among the sub-directories of `directory` whose |
| # name matches `regex`, with the version captured in group 1. |
| function(_cpack_apk_newest_version directory regex result) |
| file(GLOB _entries RELATIVE "${directory}" "${directory}/*") |
| set(_versions) |
| foreach(_entry IN LISTS _entries) |
| if(IS_DIRECTORY "${directory}/${_entry}" AND _entry MATCHES "${regex}") |
| list(APPEND _versions "${CMAKE_MATCH_1}") |
| endif() |
| endforeach() |
| if(_versions) |
| list(SORT _versions COMPARE NATURAL ORDER DESCENDING) |
| list(GET _versions 0 _newest) |
| set(${result} "${_newest}" PARENT_SCOPE) |
| else() |
| set(${result} "" PARENT_SCOPE) |
| endif() |
| endfunction() |
| |
| _cpack_apk_find_sdk_root(_sdk_root) |
| if(NOT _sdk_root) |
| if(CPACK_APK_SDK_ROOT) |
| message(FATAL_ERROR |
| "CPack APK: CPACK_APK_SDK_ROOT is set to '${CPACK_APK_SDK_ROOT}', " |
| "which holds no 'build-tools' directory. Install the build tools " |
| "with the Android SDK manager.") |
| endif() |
| message(FATAL_ERROR |
| "CPack APK: Cannot find the Android SDK. Set CPACK_APK_SDK_ROOT to the " |
| "SDK root directory, or set the ANDROID_HOME environment variable.") |
| endif() |
| set(CPACK_APK_SDK_ROOT "${_sdk_root}") |
| _cpack_apk_debug("Android SDK: ${CPACK_APK_SDK_ROOT}") |
| |
| if(NOT CPACK_APK_BUILD_TOOLS_VERSION) |
| _cpack_apk_newest_version( |
| "${CPACK_APK_SDK_ROOT}/build-tools" "^([0-9][0-9.]*)$" |
| CPACK_APK_BUILD_TOOLS_VERSION) |
| if(NOT CPACK_APK_BUILD_TOOLS_VERSION) |
| message(FATAL_ERROR |
| "CPack APK: No build tools found in " |
| "'${CPACK_APK_SDK_ROOT}/build-tools'. Install them with the Android " |
| "SDK manager or set CPACK_APK_BUILD_TOOLS_VERSION.") |
| endif() |
| endif() |
| set(CPACK_APK_BUILD_TOOLS_DIRECTORY |
| "${CPACK_APK_SDK_ROOT}/build-tools/${CPACK_APK_BUILD_TOOLS_VERSION}") |
| if(NOT IS_DIRECTORY "${CPACK_APK_BUILD_TOOLS_DIRECTORY}") |
| message(FATAL_ERROR |
| "CPack APK: Android SDK build tools version " |
| "'${CPACK_APK_BUILD_TOOLS_VERSION}' not found at " |
| "'${CPACK_APK_BUILD_TOOLS_DIRECTORY}'.") |
| endif() |
| _cpack_apk_debug("Build tools: ${CPACK_APK_BUILD_TOOLS_DIRECTORY}") |
| |
| if(NOT CPACK_APK_COMPILE_SDK_VERSION) |
| _cpack_apk_newest_version( |
| "${CPACK_APK_SDK_ROOT}/platforms" "^android-([0-9]+)$" |
| CPACK_APK_COMPILE_SDK_VERSION) |
| if(NOT CPACK_APK_COMPILE_SDK_VERSION) |
| message(FATAL_ERROR |
| "CPack APK: No platform found in '${CPACK_APK_SDK_ROOT}/platforms'. " |
| "Install one with the Android SDK manager or set " |
| "CPACK_APK_COMPILE_SDK_VERSION.") |
| endif() |
| endif() |
| set(CPACK_APK_ANDROID_JAR |
| "${CPACK_APK_SDK_ROOT}/platforms/android-${CPACK_APK_COMPILE_SDK_VERSION}/android.jar") |
| if(NOT EXISTS "${CPACK_APK_ANDROID_JAR}") |
| message(FATAL_ERROR |
| "CPack APK: '${CPACK_APK_ANDROID_JAR}' not found. Install the " |
| "'platforms;android-${CPACK_APK_COMPILE_SDK_VERSION}' SDK package or set " |
| "CPACK_APK_COMPILE_SDK_VERSION to an installed platform.") |
| endif() |
| _cpack_apk_debug("android.jar: ${CPACK_APK_ANDROID_JAR}") |
| |
| #----------------------------------------------------------------------------- |
| # Locate the tools |
| #----------------------------------------------------------------------------- |
| |
| # `apksigner` and `d8` are wrapper scripts, hence the explicit `.bat` names. |
| foreach(_tool_spec IN ITEMS |
| "AAPT2|aapt2" "D8|d8;d8.bat" |
| "ZIPALIGN|zipalign" "APKSIGNER|apksigner;apksigner.bat") |
| string(REPLACE "|" ";" _tool_spec "${_tool_spec}") |
| list(POP_FRONT _tool_spec _tool_var) |
| if(NOT CPACK_APK_${_tool_var}_EXECUTABLE) |
| find_program(CPACK_APK_${_tool_var}_EXECUTABLE |
| NAMES ${_tool_spec} |
| HINTS "${CPACK_APK_BUILD_TOOLS_DIRECTORY}" |
| NO_DEFAULT_PATH |
| ) |
| endif() |
| if(NOT CPACK_APK_${_tool_var}_EXECUTABLE) |
| list(GET _tool_spec 0 _tool_name) |
| message(FATAL_ERROR |
| "CPack APK: '${_tool_name}' not found in " |
| "'${CPACK_APK_BUILD_TOOLS_DIRECTORY}'. Set " |
| "CPACK_APK_${_tool_var}_EXECUTABLE to its location.") |
| endif() |
| endforeach() |
| |
| # The NDK provides the `llvm-strip`, the `llvm-readelf` and the shared C++ |
| # runtime. |
| set(_ndk_roots "${CPACK_APK_NDK_ROOT}") |
| foreach(_var IN ITEMS ANDROID_NDK_ROOT ANDROID_NDK_HOME ANDROID_NDK) |
| if(DEFINED ENV{${_var}}) |
| file(TO_CMAKE_PATH "$ENV{${_var}}" _ndk) |
| list(APPEND _ndk_roots "${_ndk}") |
| endif() |
| endforeach() |
| _cpack_apk_newest_version("${CPACK_APK_SDK_ROOT}/ndk" "^([0-9][0-9.]*)$" |
| _ndk_version) |
| if(_ndk_version) |
| list(APPEND _ndk_roots "${CPACK_APK_SDK_ROOT}/ndk/${_ndk_version}") |
| endif() |
| list(APPEND _ndk_roots "${CPACK_APK_SDK_ROOT}/ndk-bundle") |
| |
| # A tool set in `var`, possibly with arguments of its own, is used or |
| # rejected. Otherwise, `var` is set to the `tool` of the NDK, if any. |
| function(_cpack_apk_find_ndk_tool var tool) |
| if(${var}) |
| list(GET ${var} 0 _tool) |
| if(NOT IS_ABSOLUTE "${_tool}") |
| find_program(_tool_found NAMES "${_tool}" NO_CACHE) |
| set(_tool "${_tool_found}") |
| endif() |
| if(NOT EXISTS "${_tool}" OR IS_DIRECTORY "${_tool}") |
| list(GET ${var} 0 _tool) |
| message(FATAL_ERROR |
| "CPack APK: ${var} is set to '${_tool}', which does not exist.") |
| endif() |
| return() |
| endif() |
| foreach(_ndk IN LISTS _ndk_roots) |
| if(NOT _ndk) |
| continue() |
| endif() |
| file(GLOB _tools |
| "${_ndk}/toolchains/llvm/prebuilt/*/bin/${tool}" |
| "${_ndk}/toolchains/llvm/prebuilt/*/bin/${tool}.exe") |
| if(_tools) |
| list(GET _tools 0 ${var}) |
| return(PROPAGATE ${var}) |
| endif() |
| endforeach() |
| endfunction() |
| |
| # `llvm-strip` of the NDK strips the native libraries, as with the Android |
| # Gradle plugin. Without one, they are packaged as they are. |
| _cpack_apk_find_ndk_tool(CPACK_APK_STRIP_EXECUTABLE llvm-strip) |
| _cpack_apk_debug("Strip: ${CPACK_APK_STRIP_EXECUTABLE}") |
| |
| # `llvm-readelf` of the NDK tells which native libraries are linked to the |
| # shared C++ runtime. CPack sets CPACK_READELF_EXECUTABLE to CMAKE_READELF, |
| # which is the one of the NDK when cross-compiling for Android. |
| _cpack_apk_find_ndk_tool(CPACK_READELF_EXECUTABLE llvm-readelf) |
| _cpack_apk_debug("Readelf: ${CPACK_READELF_EXECUTABLE}") |
| |
| # The libraries of the NDK sysroot, `usr/lib/<triple>/`, provide the shared |
| # C++ runtime. The `llvm-strip` and `llvm-readelf` of an NDK, such as those |
| # of a toolchain file, sit next to the sysroot. |
| set(_sysroots) |
| foreach(_ndk IN LISTS _ndk_roots) |
| if(_ndk) |
| file(GLOB _ndk_sysroots "${_ndk}/toolchains/llvm/prebuilt/*/sysroot") |
| list(APPEND _sysroots ${_ndk_sysroots}) |
| endif() |
| endforeach() |
| foreach(_tool_var IN ITEMS CPACK_APK_STRIP_EXECUTABLE CPACK_READELF_EXECUTABLE) |
| if(${_tool_var}) |
| list(GET ${_tool_var} 0 _tool) |
| get_filename_component(_tool_directory "${_tool}" DIRECTORY) |
| list(APPEND _sysroots "${_tool_directory}/../sysroot") |
| endif() |
| endforeach() |
| set(_sysroot_libraries) |
| foreach(_sysroot IN LISTS _sysroots) |
| if(IS_DIRECTORY "${_sysroot}/usr/lib") |
| set(_sysroot_libraries "${_sysroot}/usr/lib") |
| break() |
| endif() |
| endforeach() |
| _cpack_apk_debug("NDK sysroot libraries: ${_sysroot_libraries}") |
| |
| # The names of the native libraries to keep the debug symbols of. |
| set(_keep_debug_symbols_regexes) |
| foreach(_pattern IN LISTS CPACK_APK_KEEP_DEBUG_SYMBOLS) |
| string(REGEX REPLACE "([][.+^$()|\\\\{}])" "\\\\\\1" _regex "${_pattern}") |
| string(REPLACE "*" ".*" _regex "${_regex}") |
| string(REPLACE "?" "." _regex "${_regex}") |
| list(APPEND _keep_debug_symbols_regexes "^${_regex}$") |
| endforeach() |
| |
| # Settle on the JDK first and have FindJava look in there, so that all the |
| # Java tools come from the same JDK. FindJava asks `/usr/libexec/java_home` |
| # for it only when `APPLE` is set, which it is not when cpack runs. |
| if(CPACK_APK_JAVA_HOME) |
| # The tools run in the work directory. |
| file(REAL_PATH "${CPACK_APK_JAVA_HOME}" CPACK_APK_JAVA_HOME) |
| else() |
| if(DEFINED ENV{JAVA_HOME}) |
| file(TO_CMAKE_PATH "$ENV{JAVA_HOME}" CPACK_APK_JAVA_HOME) |
| elseif(CMAKE_HOST_APPLE AND EXISTS "/usr/libexec/java_home") |
| execute_process(COMMAND /usr/libexec/java_home |
| OUTPUT_VARIABLE CPACK_APK_JAVA_HOME |
| OUTPUT_STRIP_TRAILING_WHITESPACE |
| ERROR_QUIET |
| ) |
| endif() |
| if(NOT IS_DIRECTORY "${CPACK_APK_JAVA_HOME}") |
| set(CPACK_APK_JAVA_HOME) |
| endif() |
| if(NOT CPACK_APK_JAVA_HOME) |
| # The `javac` found on the `PATH` may well be a link into the JDK, such |
| # as the `/usr/bin/javac` of the Debian alternatives system. Unlike |
| # `java`, it cannot belong to a JRE lacking the other tools of a JDK. |
| find_program(_cpack_apk_javac NAMES javac NO_CACHE) |
| if(_cpack_apk_javac) |
| file(REAL_PATH "${_cpack_apk_javac}" CPACK_APK_JAVA_HOME) |
| get_filename_component(CPACK_APK_JAVA_HOME "${CPACK_APK_JAVA_HOME}" |
| DIRECTORY) |
| get_filename_component(CPACK_APK_JAVA_HOME "${CPACK_APK_JAVA_HOME}" |
| DIRECTORY) |
| endif() |
| endif() |
| endif() |
| if(CPACK_APK_JAVA_HOME) |
| set(JAVA_HOME "${CPACK_APK_JAVA_HOME}") |
| endif() |
| |
| find_package(Java QUIET COMPONENTS Development) |
| if(NOT Java_JAVA_EXECUTABLE) |
| message(FATAL_ERROR |
| "CPack APK: No Java runtime found. The Android build tools 'd8' and " |
| "'apksigner' require a JDK. Set the JAVA_HOME environment variable or " |
| "CPACK_APK_JAVA_HOME.") |
| endif() |
| if(NOT CPACK_APK_JAVA_HOME) |
| # No JDK then, which the checks below report. |
| file(REAL_PATH "${Java_JAVA_EXECUTABLE}" CPACK_APK_JAVA_HOME) |
| get_filename_component(CPACK_APK_JAVA_HOME "${CPACK_APK_JAVA_HOME}" DIRECTORY) |
| get_filename_component(CPACK_APK_JAVA_HOME "${CPACK_APK_JAVA_HOME}" DIRECTORY) |
| endif() |
| _cpack_apk_debug("JAVA_HOME: ${CPACK_APK_JAVA_HOME}") |
| |
| foreach(_java_tool IN ITEMS jar keytool) |
| string(TOUPPER "${_java_tool}" _java_tool_var) |
| if(NOT CPACK_APK_${_java_tool_var}_EXECUTABLE) |
| find_program(CPACK_APK_${_java_tool_var}_EXECUTABLE |
| NAMES ${_java_tool} |
| HINTS "${CPACK_APK_JAVA_HOME}/bin" |
| NO_DEFAULT_PATH |
| ) |
| endif() |
| if(NOT CPACK_APK_${_java_tool_var}_EXECUTABLE) |
| message(FATAL_ERROR |
| "CPack APK: '${_java_tool}' not found in " |
| "'${CPACK_APK_JAVA_HOME}/bin'. A JDK is required to build an APK.") |
| endif() |
| endforeach() |
| |
| # The build tool wrapper scripts look at JAVA_HOME to find the JVM. |
| set(CPACK_APK_ENV "${CMAKE_COMMAND}" -E env "JAVA_HOME=${CPACK_APK_JAVA_HOME}") |
| |
| #----------------------------------------------------------------------------- |
| # Collect the inputs |
| #----------------------------------------------------------------------------- |
| |
| # Make the relative paths listed in the variable named `var` absolute. They |
| # are relative to the directory cpack runs in, but the tools run in the work |
| # directory. An unset variable stays unset, so that its default applies. |
| function(_cpack_apk_absolute_paths var) |
| if(NOT DEFINED ${var}) |
| return() |
| endif() |
| set(_paths) |
| foreach(_path IN LISTS ${var}) |
| if(NOT IS_ABSOLUTE "${_path}") |
| file(REAL_PATH "${_path}" _path) |
| endif() |
| list(APPEND _paths "${_path}") |
| endforeach() |
| set(${var} "${_paths}" PARENT_SCOPE) |
| endfunction() |
| |
| foreach(_path_var IN ITEMS |
| MANIFEST RESOURCE_DIRECTORIES ASSET_DIRECTORIES JAVA_SOURCE_DIRECTORIES |
| JARS KEYSTORE MAVEN_CACHE_DIRECTORY) |
| _cpack_apk_absolute_paths(CPACK_APK_${_path_var}) |
| endforeach() |
| |
| set(CPACK_APK_STAGING_DIRECTORY "${CPACK_TEMPORARY_DIRECTORY}") |
| # Named after the staging tree, so that no package file name can make the two |
| # the same directory and have the staged files removed below. |
| set(CPACK_APK_WORK_DIRECTORY "${CPACK_APK_STAGING_DIRECTORY}.apkbuild") |
| file(REMOVE_RECURSE "${CPACK_APK_WORK_DIRECTORY}") |
| file(MAKE_DIRECTORY "${CPACK_APK_WORK_DIRECTORY}") |
| |
| # Paths of the staging tree that are consumed by the build tools instead of |
| # being copied into the APK verbatim. |
| set(_consumed_paths) |
| |
| # Whether `path`, relative to the staging tree, is one of them. |
| function(_cpack_apk_is_consumed path result) |
| foreach(_consumed IN LISTS _consumed_paths) |
| if(_consumed MATCHES "/$") |
| # A consumed directory, recorded with a trailing slash. |
| string(FIND "${path}" "${_consumed}" _position) |
| if(_position EQUAL 0) |
| set(${result} TRUE PARENT_SCOPE) |
| return() |
| endif() |
| elseif("${path}" STREQUAL "${_consumed}") |
| set(${result} TRUE PARENT_SCOPE) |
| return() |
| endif() |
| endforeach() |
| set(${result} FALSE PARENT_SCOPE) |
| endfunction() |
| |
| # These are always handed to the build tools, never copied into the APK |
| # as they are: `aapt2` puts its own compiled versions of them there. |
| if(EXISTS "${CPACK_APK_STAGING_DIRECTORY}/AndroidManifest.xml") |
| list(APPEND _consumed_paths "AndroidManifest.xml") |
| if(NOT CPACK_APK_MANIFEST) |
| set(CPACK_APK_MANIFEST "${CPACK_APK_STAGING_DIRECTORY}/AndroidManifest.xml") |
| endif() |
| endif() |
| if(NOT CPACK_APK_MANIFEST) |
| message(FATAL_ERROR |
| "CPack APK: No AndroidManifest.xml. Set CPACK_APK_MANIFEST to its " |
| "location, or install it to the root of the packaging tree.") |
| endif() |
| if(NOT EXISTS "${CPACK_APK_MANIFEST}") |
| message(FATAL_ERROR |
| "CPack APK: CPACK_APK_MANIFEST is set to '${CPACK_APK_MANIFEST}', " |
| "which does not exist.") |
| endif() |
| |
| if(IS_DIRECTORY "${CPACK_APK_STAGING_DIRECTORY}/res") |
| list(APPEND _consumed_paths "res/") |
| if(NOT DEFINED CPACK_APK_RESOURCE_DIRECTORIES) |
| set(CPACK_APK_RESOURCE_DIRECTORIES "${CPACK_APK_STAGING_DIRECTORY}/res") |
| endif() |
| endif() |
| |
| if(IS_DIRECTORY "${CPACK_APK_STAGING_DIRECTORY}/assets") |
| list(APPEND _consumed_paths "assets/") |
| if(NOT DEFINED CPACK_APK_ASSET_DIRECTORIES) |
| set(CPACK_APK_ASSET_DIRECTORIES "${CPACK_APK_STAGING_DIRECTORY}/assets") |
| endif() |
| endif() |
| |
| # Java archives staged for installation are compiled into the APK's bytecode |
| # rather than packaged as-is. |
| file(GLOB_RECURSE _staged_jars |
| RELATIVE "${CPACK_APK_STAGING_DIRECTORY}" |
| "${CPACK_APK_STAGING_DIRECTORY}/*.jar" "${CPACK_APK_STAGING_DIRECTORY}/*.aar") |
| foreach(_staged_jar IN LISTS _staged_jars) |
| # Unless they are resources: an archive below `res/` or `assets/` is data |
| # the application reads at runtime, not code to compile into it. |
| _cpack_apk_is_consumed("${_staged_jar}" _is_resource) |
| if(_is_resource) |
| continue() |
| endif() |
| list(APPEND _consumed_paths "${_staged_jar}") |
| list(APPEND CPACK_APK_JARS "${CPACK_APK_STAGING_DIRECTORY}/${_staged_jar}") |
| endforeach() |
| |
| if(CPACK_APK_MAVEN_DEPENDENCIES) |
| include("${CMAKE_CURRENT_LIST_DIR}/CPackAPKMaven.cmake") |
| _cpack_apk_maven_resolve(_maven_archives) |
| list(APPEND CPACK_APK_JARS ${_maven_archives}) |
| endif() |
| # A staged archive may be listed explicitly as well, and `d8` rejects the |
| # classes it would then see twice. |
| list(REMOVE_DUPLICATES CPACK_APK_JARS) |
| |
| # An Android archive is a zip holding `classes.jar` and, optionally, its own |
| # manifest, resources, assets and native libraries. Unpack the parts we know |
| # how to consume. |
| set(_jars) |
| set(_aar_index 0) |
| set(_aar_manifests) |
| set(_aar_resource_directories) |
| set(_aar_asset_directories) |
| set(_aar_native_libraries) |
| foreach(_jar IN LISTS CPACK_APK_JARS) |
| if(NOT EXISTS "${_jar}") |
| message(FATAL_ERROR |
| "CPack APK: '${_jar}' listed in CPACK_APK_JARS does not exist.") |
| endif() |
| if(NOT _jar MATCHES "\\.aar$") |
| list(APPEND _jars "${_jar}") |
| continue() |
| endif() |
| # Archives from different directories may well share a file name. |
| get_filename_component(_aar_name "${_jar}" NAME_WLE) |
| set(_aar_dir "${CPACK_APK_WORK_DIRECTORY}/aar/${_aar_index}-${_aar_name}") |
| math(EXPR _aar_index "${_aar_index} + 1") |
| file(ARCHIVE_EXTRACT INPUT "${_jar}" DESTINATION "${_aar_dir}") |
| if(EXISTS "${_aar_dir}/classes.jar") |
| list(APPEND _jars "${_aar_dir}/classes.jar") |
| endif() |
| file(GLOB _aar_libs "${_aar_dir}/libs/*.jar") |
| list(APPEND _jars ${_aar_libs}) |
| if(EXISTS "${_aar_dir}/AndroidManifest.xml") |
| list(APPEND _aar_manifests "${_aar_dir}/AndroidManifest.xml") |
| endif() |
| if(IS_DIRECTORY "${_aar_dir}/res") |
| list(APPEND _aar_resource_directories "${_aar_dir}/res") |
| endif() |
| if(IS_DIRECTORY "${_aar_dir}/assets") |
| list(APPEND _aar_asset_directories "${_aar_dir}/assets") |
| endif() |
| file(GLOB _aar_jni RELATIVE "${_aar_dir}/jni" "${_aar_dir}/jni/*/*.so") |
| foreach(_aar_library IN LISTS _aar_jni) |
| list(APPEND _aar_native_libraries "${_aar_dir}/jni/${_aar_library}") |
| endforeach() |
| endforeach() |
| set(CPACK_APK_JARS "${_jars}") |
| |
| # The application overrides the resources and assets of the archives it |
| # bundles, so theirs come first. |
| list(PREPEND CPACK_APK_RESOURCE_DIRECTORIES ${_aar_resource_directories}) |
| list(PREPEND CPACK_APK_ASSET_DIRECTORIES ${_aar_asset_directories}) |
| |
| set(_java_sources) |
| foreach(_java_dir IN LISTS CPACK_APK_JAVA_SOURCE_DIRECTORIES) |
| if(NOT IS_DIRECTORY "${_java_dir}") |
| message(FATAL_ERROR |
| "CPack APK: '${_java_dir}' listed in " |
| "CPACK_APK_JAVA_SOURCE_DIRECTORIES is not a directory.") |
| endif() |
| file(GLOB_RECURSE _dir_sources "${_java_dir}/*.java") |
| list(APPEND _java_sources ${_dir_sources}) |
| endforeach() |
| |
| if(NOT DEFINED CPACK_APK_NO_COMPRESS_EXTENSIONS) |
| # Android can map uncompressed native libraries straight out of the APK, |
| # which is required when the manifest sets `extractNativeLibs` to false. |
| set(CPACK_APK_NO_COMPRESS_EXTENSIONS ".so") |
| endif() |
| |
| #----------------------------------------------------------------------------- |
| # Preprocess the manifest |
| #----------------------------------------------------------------------------- |
| |
| # The manifest is edited as text rather than parsed as XML. That is enough |
| # for the attributes and elements touched below, which are all well known. |
| set(_xml_space "[ \t\r\n]") |
| set(_xml_value "(\"([^\"]*)\"|'([^']*)')") |
| set(_xml_assignment "${_xml_space}*=${_xml_space}*${_xml_value}") |
| # What follows the element name of a start tag, attributes included. A `>` |
| # may appear in a quoted attribute value without ending the tag. |
| set(_xml_tag_rest |
| "(${_xml_space}[^\"'>]*((\"[^\"]*\"|'[^']*')[^\"'>]*)*)?/?>") |
| |
| # Escape `value` for an attribute. |
| function(_cpack_apk_xml_escape value result) |
| string(REPLACE "&" "&" value "${value}") |
| string(REPLACE "<" "<" value "${value}") |
| string(REPLACE ">" ">" value "${value}") |
| string(REPLACE "\"" """ value "${value}") |
| set(${result} "${value}" PARENT_SCOPE) |
| endfunction() |
| |
| # Replace the first occurrence of `old` in the variable named `content_var`. |
| function(_cpack_apk_replace_first content_var old new) |
| string(FIND "${${content_var}}" "${old}" _position) |
| if(_position EQUAL -1) |
| return() |
| endif() |
| string(LENGTH "${old}" _length) |
| math(EXPR _after "${_position} + ${_length}") |
| string(SUBSTRING "${${content_var}}" 0 ${_position} _head) |
| string(SUBSTRING "${${content_var}}" ${_after} -1 _tail) |
| set(${content_var} "${_head}${new}${_tail}" PARENT_SCOPE) |
| endfunction() |
| |
| # Insert `text` into the variable named `content_var` in front of the first, |
| # or with REVERSE the last, occurrence of `marker`. |
| function(_cpack_apk_insert_before content_var marker text) |
| string(FIND "${${content_var}}" "${marker}" _position ${ARGN}) |
| if(_position EQUAL -1) |
| message(FATAL_ERROR "CPack APK: No '${marker}' in the manifest.") |
| endif() |
| string(SUBSTRING "${${content_var}}" 0 ${_position} _head) |
| string(SUBSTRING "${${content_var}}" ${_position} -1 _tail) |
| set(${content_var} "${_head}${text}${_tail}" PARENT_SCOPE) |
| endfunction() |
| |
| # Get the value of `attribute` from the start tag `tag`. |
| function(_cpack_apk_tag_attribute tag attribute result) |
| if(tag MATCHES "${_xml_space}${attribute}${_xml_assignment}") |
| set(${result} "${CMAKE_MATCH_2}${CMAKE_MATCH_3}" PARENT_SCOPE) |
| else() |
| set(${result} "" PARENT_SCOPE) |
| endif() |
| endfunction() |
| |
| # Set `attribute` of the start tag held by the variable named `tag_var`. |
| function(_cpack_apk_set_tag_attribute tag_var attribute value) |
| _cpack_apk_xml_escape("${value}" value) |
| set(_tag "${${tag_var}}") |
| set(_assignment " ${attribute}=\"${value}\"") |
| if(_tag MATCHES "${_xml_space}${attribute}${_xml_assignment}") |
| _cpack_apk_replace_first(_tag "${CMAKE_MATCH_0}" "${_assignment}") |
| else() |
| string(REGEX MATCH "^<[^ \t\r\n/>]+" _element "${_tag}") |
| string(LENGTH "${_element}" _length) |
| string(SUBSTRING "${_tag}" ${_length} -1 _tail) |
| set(_tag "${_element}${_assignment}${_tail}") |
| endif() |
| set(${tag_var} "${_tag}" PARENT_SCOPE) |
| endfunction() |
| |
| # Set `attribute` of the first `element` of the manifest held by the |
| # variable named `content_var`. A missing `<uses-sdk>` is added. |
| function(_cpack_apk_set_manifest_attribute content_var element attribute value) |
| set(_content "${${content_var}}") |
| if(NOT _content MATCHES "<${element}${_xml_tag_rest}") |
| if(NOT element STREQUAL "uses-sdk" |
| OR NOT _content MATCHES "<manifest${_xml_tag_rest}") |
| message(FATAL_ERROR "CPack APK: No <${element}> in the manifest.") |
| endif() |
| _cpack_apk_replace_first(_content "${CMAKE_MATCH_0}" |
| "${CMAKE_MATCH_0}\n <uses-sdk/>") |
| string(REGEX MATCH "<${element}${_xml_tag_rest}" _tag "${_content}") |
| else() |
| set(_tag "${CMAKE_MATCH_0}") |
| endif() |
| set(_new_tag "${_tag}") |
| _cpack_apk_set_tag_attribute(_new_tag "${attribute}" "${value}") |
| _cpack_apk_replace_first(_content "${_tag}" "${_new_tag}") |
| set(${content_var} "${_content}" PARENT_SCOPE) |
| endfunction() |
| |
| # Resolve the Gradle-style `${<name>}` placeholders in the variable named |
| # `content_var`. |
| function(_cpack_apk_apply_placeholders content_var) |
| set(_content "${${content_var}}") |
| foreach(_placeholder IN LISTS CPACK_APK_MANIFEST_PLACEHOLDERS) |
| if(NOT _placeholder MATCHES "^([^=]+)=(.*)$") |
| message(FATAL_ERROR |
| "CPack APK: CPACK_APK_MANIFEST_PLACEHOLDERS entry '${_placeholder}' " |
| "is not of the form <name>=<value>.") |
| endif() |
| string(REPLACE "\${${CMAKE_MATCH_1}}" "${CMAKE_MATCH_2}" |
| _content "${_content}") |
| endforeach() |
| set(${content_var} "${_content}" PARENT_SCOPE) |
| endfunction() |
| |
| function(_cpack_apk_strip_xml_comments content_var) |
| set(_content "${${content_var}}") |
| while(TRUE) |
| string(FIND "${_content}" "<!--" _start) |
| if(_start EQUAL -1) |
| break() |
| endif() |
| string(SUBSTRING "${_content}" ${_start} -1 _comment) |
| string(FIND "${_comment}" "-->" _length) |
| if(_length EQUAL -1) |
| message(FATAL_ERROR "CPack APK: Unterminated comment in a manifest.") |
| endif() |
| math(EXPR _length "${_length} + 3") |
| string(SUBSTRING "${_comment}" 0 ${_length} _comment) |
| _cpack_apk_replace_first(_content "${_comment}" "") |
| endwhile() |
| set(${content_var} "${_content}" PARENT_SCOPE) |
| endfunction() |
| |
| # The elements of a library manifest merged into the application manifest, |
| # either at the top level or into `<application>`. A `<queries>` element |
| # is taken as a whole, so that the `<provider>` it may hold is not taken for |
| # a component. |
| set(_manifest_top_level_elements |
| permission uses-permission uses-permission-sdk-23 uses-feature queries) |
| set(_manifest_component_elements |
| activity activity-alias service receiver provider) |
| set(_manifest_application_elements |
| ${_manifest_component_elements} |
| meta-data uses-library uses-native-library) |
| |
| # Qualify the class name `name` found in a manifest of `package`, the way |
| # Android does: a name starting with a dot, or holding none, is relative. |
| function(_cpack_apk_qualify_class name package result) |
| if(name MATCHES "^\\.") |
| set(name "${package}${name}") |
| elseif(name AND NOT name MATCHES "\\.") |
| set(name "${package}.${name}") |
| endif() |
| set(${result} "${name}" PARENT_SCOPE) |
| endfunction() |
| |
| # Drop the elements marked `tools:node="remove"` from inside the element held |
| # by the variable named `snippet_var`. |
| function(_cpack_apk_drop_removed_children snippet_var) |
| set(_rest "${${snippet_var}}") |
| set(_kept) |
| # The element itself is looked at by the caller. |
| if(_rest MATCHES "^<[^ \t\r\n/>]+${_xml_tag_rest}") |
| string(LENGTH "${CMAKE_MATCH_0}" _length) |
| string(SUBSTRING "${_rest}" 0 ${_length} _kept) |
| string(SUBSTRING "${_rest}" ${_length} -1 _rest) |
| endif() |
| while(_rest MATCHES "<([A-Za-z][^ \t\r\n/>]*)${_xml_tag_rest}") |
| set(_element "${CMAKE_MATCH_1}") |
| set(_tag "${CMAKE_MATCH_0}") |
| string(FIND "${_rest}" "${_tag}" _start) |
| string(LENGTH "${_tag}" _length) |
| math(EXPR _end "${_start} + ${_length}") |
| string(SUBSTRING "${_rest}" 0 ${_start} _before) |
| string(SUBSTRING "${_rest}" ${_end} -1 _rest) |
| _cpack_apk_tag_attribute("${_tag}" "tools:node" _node) |
| if(NOT _node MATCHES "^remove") |
| string(APPEND _kept "${_before}${_tag}") |
| continue() |
| endif() |
| # Drop the indentation of the removed element along with it. |
| string(REGEX REPLACE "\n[ \t]*$" "" _before "${_before}") |
| string(APPEND _kept "${_before}") |
| if(NOT _tag MATCHES "/>$") |
| string(FIND "${_rest}" "</${_element}>" _end) |
| if(_end EQUAL -1) |
| message(FATAL_ERROR |
| "CPack APK: No </${_element}> for '${_tag}' in a manifest.") |
| endif() |
| string(LENGTH "</${_element}>" _length) |
| math(EXPR _end "${_end} + ${_length}") |
| string(SUBSTRING "${_rest}" ${_end} -1 _rest) |
| endif() |
| endwhile() |
| set(${snippet_var} "${_kept}${_rest}" PARENT_SCOPE) |
| endfunction() |
| |
| # Set `result` to the `<offset>:<length>` in `snippet` of each child element |
| # of the element it holds. |
| function(_cpack_apk_child_elements snippet result) |
| set(_children) |
| if(NOT snippet MATCHES "^<[^ \t\r\n/>]+${_xml_tag_rest}") |
| set(${result} "" PARENT_SCOPE) |
| return() |
| endif() |
| set(_tag "${CMAKE_MATCH_0}") |
| if(_tag MATCHES "/>$") |
| set(${result} "" PARENT_SCOPE) |
| return() |
| endif() |
| string(LENGTH "${_tag}" _offset) |
| string(SUBSTRING "${snippet}" ${_offset} -1 _rest) |
| # The end tag of the element itself ends the walk. |
| while(_rest MATCHES "^([^<]*)<([A-Za-z][^ \t\r\n/>]*)${_xml_tag_rest}") |
| set(_element "${CMAKE_MATCH_2}") |
| string(LENGTH "${CMAKE_MATCH_1}" _space) |
| string(LENGTH "${CMAKE_MATCH_0}" _length) |
| if(CMAKE_MATCH_0 MATCHES "/>$") |
| math(EXPR _length "${_length} - ${_space}") |
| else() |
| # Elements of the same kind do not nest, so the first end tag is theirs. |
| string(FIND "${_rest}" "</${_element}>" _end) |
| if(_end EQUAL -1) |
| message(FATAL_ERROR |
| "CPack APK: No </${_element}> for <${_element}> in a manifest.") |
| endif() |
| string(LENGTH "</${_element}>" _length) |
| math(EXPR _length "${_end} + ${_length} - ${_space}") |
| endif() |
| math(EXPR _start "${_offset} + ${_space}") |
| list(APPEND _children "${_start}:${_length}") |
| math(EXPR _consumed "${_space} + ${_length}") |
| math(EXPR _offset "${_offset} + ${_consumed}") |
| string(SUBSTRING "${_rest}" ${_consumed} -1 _rest) |
| endwhile() |
| set(${result} "${_children}" PARENT_SCOPE) |
| endfunction() |
| |
| # Set `result` to the key under which the child element `child` of a |
| # component is merged: its `android:name`, or else its content. |
| function(_cpack_apk_child_key child result) |
| string(REGEX MATCH "^<([^ \t\r\n/>]+)${_xml_tag_rest}" _tag "${child}") |
| set(_element "${CMAKE_MATCH_1}") |
| _cpack_apk_tag_attribute("${_tag}" "android:name" _name) |
| if(_name) |
| string(MD5 _key "${_element}:${_name}") |
| else() |
| # Such as an `<intent-filter>`, which archives may indent differently. |
| string(REGEX REPLACE |
| "${_xml_space}tools:[A-Za-z]+${_xml_assignment}" "" child "${child}") |
| string(REGEX REPLACE ">${_xml_space}+" ">" child "${child}") |
| string(REGEX REPLACE "${_xml_space}+<" "<" child "${child}") |
| string(REGEX REPLACE "${_xml_space}+" " " child "${child}") |
| string(MD5 _key "${child}") |
| endif() |
| set(${result} "${_key}" PARENT_SCOPE) |
| endfunction() |
| |
| # Append `child` to the content of the element held by the variable named |
| # `snippet_var`. |
| function(_cpack_apk_append_child snippet_var child) |
| set(_snippet "${${snippet_var}}") |
| string(REGEX MATCH "^<([^ \t\r\n/>]+)${_xml_tag_rest}" _tag "${_snippet}") |
| set(_element "${CMAKE_MATCH_1}") |
| if(_tag MATCHES "/>$") |
| string(REGEX REPLACE "${_xml_space}*/>$" ">" _start_tag "${_tag}") |
| _cpack_apk_replace_first(_snippet "${_tag}" |
| "${_start_tag}\n ${child}\n</${_element}>") |
| else() |
| string(FIND "${_snippet}" "</${_element}>" _end REVERSE) |
| string(SUBSTRING "${_snippet}" 0 ${_end} _head) |
| string(SUBSTRING "${_snippet}" ${_end} -1 _tail) |
| set(_indent) |
| if(_head MATCHES "\n([ \t]*)$") |
| set(_indent "${CMAKE_MATCH_1}") |
| endif() |
| set(_snippet "${_head} ${child}\n${_indent}${_tail}") |
| endif() |
| set(${snippet_var} "${_snippet}" PARENT_SCOPE) |
| endfunction() |
| |
| # Merge the child elements of the component `snippet` into the component |
| # already declared under `key`, as the Gradle manifest merger does, unless |
| # that one was removed or replaces the others. The children declared under |
| # the same `android:name` are left out. |
| function(_cpack_apk_merge_children key snippet) |
| get_property(_target GLOBAL PROPERTY _cpack_apk_manifest_${key}) |
| get_property(_closed GLOBAL PROPERTY _cpack_apk_manifest_closed_${key}) |
| if(_target STREQUAL "" OR _closed) |
| return() |
| endif() |
| get_property(_child_keys GLOBAL PROPERTY _cpack_apk_manifest_children_${key}) |
| _cpack_apk_child_elements("${snippet}" _children) |
| foreach(_child IN LISTS _children) |
| string(REPLACE ":" ";" _child "${_child}") |
| list(GET _child 0 _offset) |
| list(GET _child 1 _length) |
| string(SUBSTRING "${snippet}" ${_offset} ${_length} _child) |
| _cpack_apk_child_key("${_child}" _child_key) |
| if(_child_key IN_LIST _child_keys) |
| continue() |
| endif() |
| # A child marked for removal still keeps those of the same name out. |
| list(APPEND _child_keys "${_child_key}") |
| string(REGEX MATCH "^<[^ \t\r\n/>]+${_xml_tag_rest}" _tag "${_child}") |
| _cpack_apk_tag_attribute("${_tag}" "tools:node" _node) |
| if(_node MATCHES "^remove") |
| continue() |
| endif() |
| _cpack_apk_drop_removed_children(_child) |
| string(REGEX REPLACE |
| "${_xml_space}tools:[A-Za-z]+${_xml_assignment}" |
| "" _child "${_child}") |
| _cpack_apk_append_child(_target "${_child}") |
| endforeach() |
| set_property(GLOBAL PROPERTY _cpack_apk_manifest_${key} "${_target}") |
| set_property(GLOBAL PROPERTY |
| _cpack_apk_manifest_children_${key} "${_child_keys}") |
| endfunction() |
| |
| # Walk the mergeable elements of `content`, a manifest of `package`. |
| # Each one not in `_manifest_keys` yet is added to it and, unless |
| # `collect_only` is set, to `_manifest_top_level_keys` or |
| # `_manifest_application_keys`, in the calling scope, with its text in the |
| # global property `_cpack_apk_manifest_<key>`. The child elements of a |
| # component already in `_manifest_keys` are merged into it instead. With |
| # `collect_only`, `content` without the elements marked |
| # `tools:node="remove"` is stored in `_manifest_kept`, and the components |
| # it keeps are added to `_manifest_kept_keys`, so that the children merged |
| # into them can be written back. |
| function(_cpack_apk_manifest_elements content package collect_only) |
| set(_keys "${_manifest_keys}") |
| set(_kept) |
| set(_kept_keys "${_manifest_kept_keys}") |
| set(_top_level "${_manifest_top_level_keys}") |
| set(_application "${_manifest_application_keys}") |
| list(JOIN _manifest_top_level_elements "|" _names) |
| list(JOIN _manifest_application_elements "|" _application_names) |
| string(APPEND _names "|${_application_names}") |
| |
| set(_rest "${content}") |
| while(_rest MATCHES "<(${_names})[ \t\r\n/>]") |
| set(_element "${CMAKE_MATCH_1}") |
| string(FIND "${_rest}" "${CMAKE_MATCH_0}" _start) |
| string(SUBSTRING "${_rest}" 0 ${_start} _before) |
| string(APPEND _kept "${_before}") |
| string(SUBSTRING "${_rest}" ${_start} -1 _rest) |
| if(NOT _rest MATCHES "^<${_element}${_xml_tag_rest}") |
| message(FATAL_ERROR "CPack APK: Unterminated <${_element}> in a manifest.") |
| endif() |
| set(_tag "${CMAKE_MATCH_0}") |
| string(LENGTH "${_tag}" _tag_length) |
| if(_tag MATCHES "/>$") |
| set(_length ${_tag_length}) |
| else() |
| # Elements of the same kind do not nest, so the first end tag is theirs. |
| string(FIND "${_rest}" "</${_element}>" _length) |
| if(_length EQUAL -1) |
| message(FATAL_ERROR |
| "CPack APK: No </${_element}> for '${_tag}' in a manifest.") |
| endif() |
| string(LENGTH "</${_element}>" _end_tag_length) |
| math(EXPR _length "${_length} + ${_end_tag_length}") |
| endif() |
| string(SUBSTRING "${_rest}" 0 ${_length} _snippet) |
| string(SUBSTRING "${_rest}" ${_length} -1 _rest) |
| set(_full_snippet "${_snippet}") |
| _cpack_apk_drop_removed_children(_snippet) |
| |
| _cpack_apk_tag_attribute("${_tag}" "android:name" _name) |
| # Component class names may be relative to the package of the manifest. |
| set(_component FALSE) |
| if(_element IN_LIST _manifest_component_elements) |
| set(_component TRUE) |
| if(package) |
| _cpack_apk_qualify_class("${_name}" "${package}" _name) |
| endif() |
| endif() |
| _cpack_apk_tag_attribute("${_tag}" "tools:node" _node) |
| if(collect_only) |
| if(_node MATCHES "^remove") |
| # Drop the indentation of the removed element along with it. |
| string(REGEX REPLACE "\n[ \t]*$" "" _kept "${_kept}") |
| else() |
| string(APPEND _kept "${_snippet}") |
| endif() |
| endif() |
| if(_name) |
| string(MD5 _key "${_element}:${_name}") |
| else() |
| string(MD5 _key "${_snippet}") |
| endif() |
| if(_key IN_LIST _keys) |
| # Components declared again, such as the `InitializationProvider` of |
| # AndroidX Startup, contribute their children. |
| if(_component AND _name AND NOT collect_only |
| AND NOT _node MATCHES "^remove") |
| _cpack_apk_merge_children("${_key}" "${_full_snippet}") |
| endif() |
| continue() |
| endif() |
| # An element marked for removal still keeps those of the same name out. |
| list(APPEND _keys "${_key}") |
| if(_component AND _name) |
| # The children the element declares, or removes, keep those of the |
| # same name out. |
| set(_child_keys) |
| _cpack_apk_child_elements("${_full_snippet}" _children) |
| foreach(_child IN LISTS _children) |
| string(REPLACE ":" ";" _child "${_child}") |
| list(GET _child 0 _offset) |
| list(GET _child 1 _length) |
| string(SUBSTRING "${_full_snippet}" ${_offset} ${_length} _child) |
| _cpack_apk_child_key("${_child}" _child_key) |
| list(APPEND _child_keys "${_child_key}") |
| endforeach() |
| set_property(GLOBAL PROPERTY |
| _cpack_apk_manifest_children_${_key} "${_child_keys}") |
| set(_closed FALSE) |
| if(_node MATCHES "^(remove|replace)") |
| set(_closed TRUE) |
| endif() |
| set_property(GLOBAL PROPERTY _cpack_apk_manifest_closed_${_key} ${_closed}) |
| endif() |
| if(_node MATCHES "^remove") |
| continue() |
| endif() |
| if(collect_only) |
| if(_component AND _name) |
| set_property(GLOBAL PROPERTY _cpack_apk_manifest_${_key} "${_snippet}") |
| set_property(GLOBAL PROPERTY |
| _cpack_apk_manifest_kept_${_key} "${_snippet}") |
| list(APPEND _kept_keys "${_key}") |
| endif() |
| continue() |
| endif() |
| |
| # Merged into the application manifest, relative class names would |
| # resolve against the package of the application. |
| if(_component AND package) |
| set(_new_tag "${_tag}") |
| foreach(_class_attribute IN ITEMS |
| android:name android:targetActivity android:parentActivityName) |
| _cpack_apk_tag_attribute("${_tag}" "${_class_attribute}" _class) |
| _cpack_apk_qualify_class("${_class}" "${package}" _qualified_class) |
| if(NOT _qualified_class STREQUAL _class) |
| _cpack_apk_set_tag_attribute(_new_tag |
| "${_class_attribute}" "${_qualified_class}") |
| endif() |
| endforeach() |
| _cpack_apk_replace_first(_snippet "${_tag}" "${_new_tag}") |
| endif() |
| # The merge rules the Gradle manifest merger follows are not applied, and |
| # the application manifest need not declare their namespace. |
| string(REGEX REPLACE |
| "${_xml_space}tools:[A-Za-z]+${_xml_assignment}" |
| "" _snippet "${_snippet}") |
| set_property(GLOBAL PROPERTY _cpack_apk_manifest_${_key} "${_snippet}") |
| if(_element IN_LIST _manifest_top_level_elements) |
| list(APPEND _top_level "${_key}") |
| else() |
| list(APPEND _application "${_key}") |
| endif() |
| endwhile() |
| |
| string(APPEND _kept "${_rest}") |
| set(_manifest_kept "${_kept}" PARENT_SCOPE) |
| set(_manifest_kept_keys "${_kept_keys}" PARENT_SCOPE) |
| set(_manifest_keys "${_keys}" PARENT_SCOPE) |
| set(_manifest_top_level_keys "${_top_level}" PARENT_SCOPE) |
| set(_manifest_application_keys "${_application}" PARENT_SCOPE) |
| endfunction() |
| |
| file(READ "${CPACK_APK_MANIFEST}" _manifest_content) |
| _cpack_apk_apply_placeholders(_manifest_content) |
| _cpack_apk_strip_xml_comments(_manifest_content) |
| if(NOT _manifest_content MATCHES "<manifest${_xml_tag_rest}") |
| message(FATAL_ERROR |
| "CPack APK: '${CPACK_APK_MANIFEST}' holds no <manifest> element.") |
| endif() |
| # Manifests of projects built with Gradle leave the package to the build |
| # script, as its `namespace`. |
| if(NOT "${CPACK_APK_PACKAGE}" STREQUAL "") |
| _cpack_apk_set_manifest_attribute(_manifest_content |
| manifest package "${CPACK_APK_PACKAGE}") |
| set(_package "${CPACK_APK_PACKAGE}") |
| else() |
| string(REGEX MATCH "<manifest${_xml_tag_rest}" _manifest_tag |
| "${_manifest_content}") |
| _cpack_apk_tag_attribute("${_manifest_tag}" "package" _package) |
| if(_package STREQUAL "") |
| message(FATAL_ERROR |
| "CPack APK: The <manifest> of '${CPACK_APK_MANIFEST}' has no 'package' " |
| "attribute. Set CPACK_APK_PACKAGE to the package of the application.") |
| endif() |
| endif() |
| |
| # Unless set explicitly, `${applicationId}` is the package of the application, |
| # as with Gradle. |
| if(_package AND NOT CPACK_APK_MANIFEST_PLACEHOLDERS MATCHES "(^|;)applicationId=") |
| list(APPEND CPACK_APK_MANIFEST_PLACEHOLDERS "applicationId=${_package}") |
| string(REPLACE "\${applicationId}" "${_package}" |
| _manifest_content "${_manifest_content}") |
| endif() |
| |
| # The elements the application manifest marks `tools:node="remove"` are |
| # not packaged, and keep those of the same name in the archives out. |
| set(_manifest_keys) |
| set(_manifest_kept_keys) |
| set(_manifest_top_level_keys) |
| set(_manifest_application_keys) |
| _cpack_apk_manifest_elements("${_manifest_content}" "${_package}" TRUE) |
| set(_manifest_content "${_manifest_kept}") |
| |
| # Merge the manifests of the Android archives. Their code, resources and |
| # native libraries are packaged, so the components, permissions and features |
| # they declare must be as well. |
| set(_aar_packages) |
| if(_aar_manifests) |
| foreach(_aar_manifest IN LISTS _aar_manifests) |
| file(READ "${_aar_manifest}" _aar_manifest_content) |
| _cpack_apk_apply_placeholders(_aar_manifest_content) |
| _cpack_apk_strip_xml_comments(_aar_manifest_content) |
| set(_aar_package) |
| if(_aar_manifest_content MATCHES "<manifest${_xml_tag_rest}") |
| _cpack_apk_tag_attribute("${CMAKE_MATCH_0}" "package" _aar_package) |
| endif() |
| if(_aar_package AND NOT _aar_package STREQUAL _package) |
| list(APPEND _aar_packages "${_aar_package}") |
| endif() |
| _cpack_apk_manifest_elements( |
| "${_aar_manifest_content}" "${_aar_package}" FALSE) |
| endforeach() |
| list(REMOVE_DUPLICATES _aar_packages) |
| |
| # The components of the application manifest the archives contributed |
| # children to. |
| foreach(_key IN LISTS _manifest_kept_keys) |
| get_property(_kept GLOBAL PROPERTY _cpack_apk_manifest_kept_${_key}) |
| get_property(_merged GLOBAL PROPERTY _cpack_apk_manifest_${_key}) |
| if(NOT _merged STREQUAL _kept) |
| _cpack_apk_replace_first(_manifest_content "${_kept}" "${_merged}") |
| endif() |
| endforeach() |
| set(_manifest_top_level) |
| foreach(_key IN LISTS _manifest_top_level_keys) |
| get_property(_snippet GLOBAL PROPERTY _cpack_apk_manifest_${_key}) |
| string(APPEND _manifest_top_level " ${_snippet}\n") |
| endforeach() |
| set(_manifest_application) |
| foreach(_key IN LISTS _manifest_application_keys) |
| get_property(_snippet GLOBAL PROPERTY _cpack_apk_manifest_${_key}) |
| string(APPEND _manifest_application " ${_snippet}\n") |
| endforeach() |
| |
| if(_manifest_top_level) |
| if(_manifest_content MATCHES "<application[ \t\r\n/>]") |
| # Keep the indentation of `<application>` for the first element. |
| string(STRIP "${_manifest_top_level}" _manifest_top_level) |
| _cpack_apk_insert_before(_manifest_content "${CMAKE_MATCH_0}" |
| "${_manifest_top_level}\n ") |
| else() |
| _cpack_apk_insert_before(_manifest_content "</manifest>" |
| "${_manifest_top_level}" REVERSE) |
| endif() |
| endif() |
| if(_manifest_application) |
| if(_manifest_content MATCHES "<application${_xml_tag_rest}") |
| set(_application_tag "${CMAKE_MATCH_0}") |
| if(_application_tag MATCHES "^(.*)/>$") |
| _cpack_apk_replace_first(_manifest_content "${_application_tag}" |
| "${CMAKE_MATCH_1}>\n${_manifest_application} </application>") |
| else() |
| _cpack_apk_insert_before(_manifest_content "</application>" |
| "\n${_manifest_application} ") |
| endif() |
| else() |
| _cpack_apk_insert_before(_manifest_content "</manifest>" |
| " <application>\n${_manifest_application} </application>\n" |
| REVERSE) |
| endif() |
| endif() |
| endif() |
| foreach(_key IN LISTS _manifest_keys) |
| foreach(_property IN ITEMS |
| _cpack_apk_manifest_${_key} |
| _cpack_apk_manifest_kept_${_key} |
| _cpack_apk_manifest_children_${_key} |
| _cpack_apk_manifest_closed_${_key}) |
| set_property(GLOBAL PROPERTY ${_property}) |
| endforeach() |
| endforeach() |
| |
| # `aapt2` only fills in the versions and API levels the manifest leaves out, |
| # so the ones set explicitly are written into the manifest. |
| foreach(_attribute_spec IN ITEMS |
| "VERSION_CODE|manifest|android:versionCode" |
| "VERSION_NAME|manifest|android:versionName" |
| "MIN_SDK_VERSION|uses-sdk|android:minSdkVersion" |
| "TARGET_SDK_VERSION|uses-sdk|android:targetSdkVersion") |
| string(REPLACE "|" ";" _attribute_spec "${_attribute_spec}") |
| list(GET _attribute_spec 0 _attribute_var) |
| list(GET _attribute_spec 1 _attribute_element) |
| list(GET _attribute_spec 2 _attribute) |
| if(NOT "${CPACK_APK_${_attribute_var}}" STREQUAL "") |
| _cpack_apk_set_manifest_attribute(_manifest_content |
| "${_attribute_element}" "${_attribute}" "${CPACK_APK_${_attribute_var}}") |
| endif() |
| endforeach() |
| |
| # `d8` needs to know the minimum API level as well, wherever it comes from. |
| set(_min_sdk_version) |
| if(_manifest_content MATCHES "<uses-sdk${_xml_tag_rest}") |
| _cpack_apk_tag_attribute("${CMAKE_MATCH_0}" "android:minSdkVersion" |
| _min_sdk_version) |
| if(NOT _min_sdk_version MATCHES "^[0-9]+$") |
| set(_min_sdk_version) |
| endif() |
| endif() |
| |
| set(CPACK_APK_MANIFEST "${CPACK_APK_WORK_DIRECTORY}/AndroidManifest.xml") |
| file(WRITE "${CPACK_APK_MANIFEST}" "${_manifest_content}") |
| _cpack_apk_debug("Manifest: ${CPACK_APK_MANIFEST}") |
| |
| #----------------------------------------------------------------------------- |
| # Compile and link the resources into the base APK |
| #----------------------------------------------------------------------------- |
| |
| set(_compiled_resources) |
| set(_resource_index 0) |
| foreach(_resource_dir IN LISTS CPACK_APK_RESOURCE_DIRECTORIES) |
| if(NOT IS_DIRECTORY "${_resource_dir}") |
| message(FATAL_ERROR |
| "CPack APK: '${_resource_dir}' listed in " |
| "CPACK_APK_RESOURCE_DIRECTORIES is not a directory.") |
| endif() |
| set(_compiled "${CPACK_APK_WORK_DIRECTORY}/resources-${_resource_index}.zip") |
| _cpack_apk_run("aapt2 compile of '${_resource_dir}'" |
| "${CPACK_APK_AAPT2_EXECUTABLE}" compile --dir "${_resource_dir}" |
| -o "${_compiled}") |
| # Each directory after the first overlays the ones before it: a resource |
| # they share takes its value from the later one instead of conflicting. |
| if(_compiled_resources) |
| list(APPEND _compiled_resources -R) |
| endif() |
| list(APPEND _compiled_resources "${_compiled}") |
| math(EXPR _resource_index "${_resource_index} + 1") |
| endforeach() |
| |
| set(_link_options) |
| foreach(_extension IN LISTS CPACK_APK_NO_COMPRESS_EXTENSIONS) |
| list(APPEND _link_options -0 "${_extension}") |
| endforeach() |
| # Of the asset directories given to aapt2 link, the first one holding a file |
| # wins, so the later directories, which override the earlier ones, go first. |
| set(_asset_directories ${CPACK_APK_ASSET_DIRECTORIES}) |
| list(REVERSE _asset_directories) |
| foreach(_asset_directory IN LISTS _asset_directories) |
| if(NOT IS_DIRECTORY "${_asset_directory}") |
| message(FATAL_ERROR |
| "CPack APK: '${_asset_directory}' listed in " |
| "CPACK_APK_ASSET_DIRECTORIES is not a directory.") |
| endif() |
| list(APPEND _link_options -A "${_asset_directory}") |
| endforeach() |
| # These only apply when the manifest leaves the versions out. |
| if("${CPACK_APK_VERSION_CODE}" STREQUAL "") |
| list(APPEND _link_options --version-code 1) |
| endif() |
| if("${CPACK_APK_VERSION_NAME}" STREQUAL "" AND CPACK_PACKAGE_VERSION) |
| list(APPEND _link_options --version-name "${CPACK_PACKAGE_VERSION}") |
| endif() |
| |
| # Generate R.java so that the application sources can refer to the resources. |
| # The code of an Android archive refers to them through an R class of its own |
| # package, which the archive does not ship, so generate those as well. |
| set(_generated_java_directory "${CPACK_APK_WORK_DIRECTORY}/gen") |
| if(_compiled_resources AND (_java_sources OR _aar_packages)) |
| file(MAKE_DIRECTORY "${_generated_java_directory}") |
| list(APPEND _link_options --java "${_generated_java_directory}") |
| if(_aar_packages) |
| list(JOIN _aar_packages ":" _extra_packages) |
| list(APPEND _link_options --extra-packages "${_extra_packages}") |
| endif() |
| endif() |
| |
| # Without a target API level, Android takes the minimum one, and recent |
| # versions refuse to install applications targeting old API levels. Like |
| # Gradle, default it to the compile API level. `aapt2` only uses this for |
| # manifests leaving it out. |
| if(CPACK_APK_COMPILE_SDK_VERSION MATCHES "^([0-9]+)") |
| list(APPEND _link_options --target-sdk-version "${CMAKE_MATCH_1}") |
| endif() |
| |
| set(_apk "${CPACK_APK_WORK_DIRECTORY}/unaligned.apk") |
| _cpack_apk_run("aapt2 link" |
| "${CPACK_APK_AAPT2_EXECUTABLE}" link |
| -o "${_apk}" |
| -I "${CPACK_APK_ANDROID_JAR}" |
| --manifest "${CPACK_APK_MANIFEST}" |
| --auto-add-overlay |
| ${_link_options} |
| ${CPACK_APK_AAPT2_LINK_EXTRA_OPTIONS} |
| ${_compiled_resources} |
| ) |
| |
| #----------------------------------------------------------------------------- |
| # Compile the Java sources and convert the bytecode to Dalvik executables |
| #----------------------------------------------------------------------------- |
| |
| if(CMAKE_HOST_WIN32) |
| set(_class_path_separator ";") |
| else() |
| set(_class_path_separator ":") |
| endif() |
| |
| # `javac` checks the Java library calls against the JDK it belongs to, which |
| # is not the library Android provides. Build a system image out of the one |
| # of the platform instead, the way the Android Gradle plugin does, so that |
| # the calls are checked against what is available on a device. |
| function(_cpack_apk_java_system_image result) |
| set(${result} "" PARENT_SCOPE) |
| get_filename_component(_platform_directory "${CPACK_APK_ANDROID_JAR}" |
| DIRECTORY) |
| set(_core_jar "${_platform_directory}/core-for-system-modules.jar") |
| # `javac` reads the image through the file system provider of the JDK. |
| set(_jrt_fs_jar "${CPACK_APK_JAVA_HOME}/lib/jrt-fs.jar") |
| if(NOT EXISTS "${_core_jar}" OR NOT EXISTS "${_jrt_fs_jar}" |
| OR NOT Java_VERSION_MAJOR) |
| _cpack_apk_debug("No Java system image for the platform, compiling " |
| "against the Java library of the JDK.") |
| return() |
| endif() |
| foreach(_java_tool IN ITEMS jmod jlink) |
| string(TOUPPER "${_java_tool}" _java_tool_var) |
| if(NOT CPACK_APK_${_java_tool_var}_EXECUTABLE) |
| find_program(CPACK_APK_${_java_tool_var}_EXECUTABLE |
| NAMES ${_java_tool} |
| HINTS "${CPACK_APK_JAVA_HOME}/bin" |
| NO_DEFAULT_PATH |
| ) |
| endif() |
| if(NOT CPACK_APK_${_java_tool_var}_EXECUTABLE) |
| _cpack_apk_debug("No '${_java_tool}', compiling against the Java " |
| "library of the JDK.") |
| return() |
| endif() |
| endforeach() |
| |
| set(_system_directory "${CPACK_APK_WORK_DIRECTORY}/java-system") |
| set(_classes "${_system_directory}/classes") |
| file(ARCHIVE_EXTRACT INPUT "${_core_jar}" DESTINATION "${_classes}") |
| |
| # The image holds a single `java.base` module exporting every package. |
| set(_module_info "module java.base {\n") |
| file(GLOB_RECURSE _entries LIST_DIRECTORIES TRUE |
| RELATIVE "${_classes}" "${_classes}/*") |
| foreach(_entry IN LISTS _entries) |
| if(IS_DIRECTORY "${_classes}/${_entry}") |
| file(GLOB _package_classes "${_classes}/${_entry}/*.class") |
| if(_package_classes) |
| string(REPLACE "/" "." _package "${_entry}") |
| string(APPEND _module_info " exports ${_package};\n") |
| endif() |
| endif() |
| endforeach() |
| string(APPEND _module_info "}\n") |
| file(WRITE "${_system_directory}/module-info.java" "${_module_info}") |
| _cpack_apk_run("javac of the Java system module" |
| ${CPACK_APK_ENV} "${Java_JAVAC_EXECUTABLE}" |
| --system none --patch-module "java.base=${_classes}" |
| -d "${_classes}" "${_system_directory}/module-info.java" |
| ) |
| |
| # `jlink` insists on the module version matching its own, and on a target |
| # platform being recorded, although nothing platform specific is in there. |
| file(MAKE_DIRECTORY "${_system_directory}/jmods") |
| _cpack_apk_run("jmod of the Java system module" |
| ${CPACK_APK_ENV} "${CPACK_APK_JMOD_EXECUTABLE}" create |
| --module-version "${Java_VERSION_MAJOR}" |
| --target-platform linux-amd64 |
| --class-path "${_classes}" |
| "${_system_directory}/jmods/java.base.jmod" |
| ) |
| set(_image "${_system_directory}/image") |
| _cpack_apk_run("jlink of the Java system image" |
| ${CPACK_APK_ENV} "${CPACK_APK_JLINK_EXECUTABLE}" |
| --module-path "${_system_directory}/jmods" |
| --add-modules java.base |
| --disable-plugin system-modules |
| --output "${_image}" |
| ) |
| file(COPY "${_jrt_fs_jar}" DESTINATION "${_image}/lib") |
| set(${result} "${_image}" PARENT_SCOPE) |
| endfunction() |
| |
| file(GLOB_RECURSE _generated_java_sources "${_generated_java_directory}/*.java") |
| list(APPEND _java_sources ${_generated_java_sources}) |
| |
| if(_java_sources) |
| if(NOT Java_JAVAC_EXECUTABLE) |
| message(FATAL_ERROR |
| "CPack APK: 'javac' not found, but there are Java sources to compile. " |
| "A JDK is required.") |
| endif() |
| |
| set(_classes_directory "${CPACK_APK_WORK_DIRECTORY}/classes") |
| file(MAKE_DIRECTORY "${_classes_directory}") |
| |
| set(_class_path "${CPACK_APK_ANDROID_JAR}") |
| foreach(_jar IN LISTS CPACK_APK_JARS) |
| string(APPEND _class_path "${_class_path_separator}${_jar}") |
| endforeach() |
| |
| # Write the classpath and the sources to an argument file: the source list |
| # easily grows past the command line length limit, and on Windows the |
| # classpath separator is the very character a CMake list is split on, so a |
| # classpath does not survive being passed along as a single argument. |
| # Arguments in an argument file are separated by whitespace, hence the |
| # quoting, and a backslash between the quotes is an escape character, hence |
| # the forward slashes `javac` accepts on every platform. |
| set(_javac_argument_file "${CPACK_APK_WORK_DIRECTORY}/javac-arguments.txt") |
| string(REPLACE "\\" "/" _class_path "${_class_path}") |
| set(_javac_arguments "-classpath \"${_class_path}\"\n") |
| |
| # A system image cannot be combined with `--release`, nor used for a |
| # target release older than 9, where `android.jar` becomes the boot class |
| # path instead. |
| set(_javac_release) |
| set(_javac_source) |
| set(_javac_target) |
| set(_javac_options ${CPACK_APK_JAVAC_EXTRA_OPTIONS}) |
| while(_javac_options) |
| list(POP_FRONT _javac_options _javac_option) |
| if(_javac_option MATCHES "^--?(release|source|target)=(.*)$") |
| set(_javac_${CMAKE_MATCH_1} "${CMAKE_MATCH_2}") |
| elseif(_javac_option MATCHES "^(--release|--?source|--?target)$" |
| AND _javac_options) |
| string(REGEX REPLACE "^-+" "" _javac_option "${_javac_option}") |
| list(POP_FRONT _javac_options _javac_${_javac_option}) |
| endif() |
| endwhile() |
| # Left to itself, `javac` targets the Java release of the JDK, whose class |
| # files the `d8` of older build tools cannot read. |
| if(NOT _javac_release AND NOT _javac_source AND NOT _javac_target) |
| if(NOT DEFINED CPACK_APK_JAVA_VERSION) |
| set(CPACK_APK_JAVA_VERSION 11) |
| endif() |
| if(NOT "${CPACK_APK_JAVA_VERSION}" STREQUAL "") |
| set(_javac_source "${CPACK_APK_JAVA_VERSION}") |
| set(_javac_target "${CPACK_APK_JAVA_VERSION}") |
| string(APPEND _javac_arguments |
| "-source ${_javac_source}\n-target ${_javac_target}\n") |
| endif() |
| endif() |
| if(NOT _javac_target) |
| set(_javac_target "${_javac_source}") |
| endif() |
| if(_javac_release) |
| _cpack_apk_debug("javac targets release ${_javac_release}, compiling " |
| "against the Java library of the JDK.") |
| elseif(_javac_target MATCHES "^(1\\.)?[0-8]$") |
| string(REPLACE "\\" "/" _boot_class_path "${CPACK_APK_ANDROID_JAR}") |
| string(APPEND _javac_arguments "-bootclasspath \"${_boot_class_path}\"\n") |
| else() |
| _cpack_apk_java_system_image(_java_system_image) |
| if(_java_system_image) |
| string(REPLACE "\\" "/" _java_system_image "${_java_system_image}") |
| string(APPEND _javac_arguments "--system \"${_java_system_image}\"\n") |
| endif() |
| endif() |
| foreach(_java_source IN LISTS _java_sources) |
| string(REPLACE "\\" "/" _java_source "${_java_source}") |
| string(APPEND _javac_arguments "\"${_java_source}\"\n") |
| endforeach() |
| file(WRITE "${_javac_argument_file}" "${_javac_arguments}") |
| |
| _cpack_apk_run("javac" |
| ${CPACK_APK_ENV} "${Java_JAVAC_EXECUTABLE}" |
| -d "${_classes_directory}" |
| ${CPACK_APK_JAVAC_EXTRA_OPTIONS} |
| "@${_javac_argument_file}" |
| ) |
| |
| set(_classes_jar "${CPACK_APK_WORK_DIRECTORY}/classes.jar") |
| _cpack_apk_run("jar of the compiled Java sources" |
| ${CPACK_APK_ENV} "${CPACK_APK_JAR_EXECUTABLE}" |
| cfM "${_classes_jar}" -C "${_classes_directory}" . |
| ) |
| list(APPEND CPACK_APK_JARS "${_classes_jar}") |
| endif() |
| |
| set(_dex_directory "${CPACK_APK_WORK_DIRECTORY}/dex") |
| if(CPACK_APK_JARS) |
| file(MAKE_DIRECTORY "${_dex_directory}") |
| set(_d8_options) |
| if(_min_sdk_version) |
| list(APPEND _d8_options --min-api "${_min_sdk_version}") |
| endif() |
| # `d8` compiles with debugging information unless told otherwise, and |
| # rejects being told both. |
| if(NOT "--debug" IN_LIST CPACK_APK_D8_EXTRA_OPTIONS |
| AND NOT "--release" IN_LIST CPACK_APK_D8_EXTRA_OPTIONS) |
| list(APPEND _d8_options --release) |
| endif() |
| # On Windows, `d8` is a batch script, and its command line is subject to |
| # the length limit of `cmd.exe`. An argument file of `d8` holds one |
| # argument per line, taken as is. |
| set(_d8_argument_file "${CPACK_APK_WORK_DIRECTORY}/d8-arguments.txt") |
| list(JOIN CPACK_APK_JARS "\n" _d8_arguments) |
| file(WRITE "${_d8_argument_file}" "${_d8_arguments}\n") |
| _cpack_apk_run("d8" |
| ${CPACK_APK_ENV} "${CPACK_APK_D8_EXECUTABLE}" |
| --lib "${CPACK_APK_ANDROID_JAR}" |
| --output "${_dex_directory}" |
| ${_d8_options} |
| ${CPACK_APK_D8_EXTRA_OPTIONS} |
| "@${_d8_argument_file}" |
| ) |
| endif() |
| |
| #----------------------------------------------------------------------------- |
| # Lay out everything else that goes into the APK |
| #----------------------------------------------------------------------------- |
| |
| # Entries added with `jar -0` are stored uncompressed so that `zipalign` can |
| # page-align them; the rest are deflated. |
| set(_deflated_directory "${CPACK_APK_WORK_DIRECTORY}/deflated") |
| set(_stored_directory "${CPACK_APK_WORK_DIRECTORY}/stored") |
| file(MAKE_DIRECTORY "${_deflated_directory}" "${_stored_directory}") |
| |
| file(GLOB _dex_files RELATIVE "${_dex_directory}" "${_dex_directory}/*.dex") |
| foreach(_dex_file IN LISTS _dex_files) |
| file(COPY "${_dex_directory}/${_dex_file}" |
| DESTINATION "${_deflated_directory}") |
| endforeach() |
| |
| file(GLOB_RECURSE _staged_files |
| RELATIVE "${CPACK_APK_STAGING_DIRECTORY}" "${CPACK_APK_STAGING_DIRECTORY}/*") |
| |
| # Copy the native library `source` to `destination`, stripped of its debug |
| # symbols the way the Android Gradle plugin does, unless its name matches |
| # CPACK_APK_KEEP_DEBUG_SYMBOLS. Files that are not ELF, such as those of a |
| # host build, are copied as they are. Unlike file(COPY), which skips files |
| # whose time stamps match, as those of libraries built reproducibly may, |
| # this replaces a library of an archive. |
| function(_cpack_apk_copy_native_library source destination) |
| get_filename_component(_destination_directory "${destination}" DIRECTORY) |
| file(MAKE_DIRECTORY "${_destination_directory}") |
| get_filename_component(_name "${destination}" NAME) |
| set(_strip TRUE) |
| foreach(_regex IN LISTS _keep_debug_symbols_regexes) |
| if(_name MATCHES "${_regex}") |
| set(_strip FALSE) |
| break() |
| endif() |
| endforeach() |
| file(READ "${source}" _magic LIMIT 4 HEX) |
| if(NOT _magic STREQUAL "7f454c46") |
| set(_strip FALSE) |
| elseif(NOT _name STREQUAL "libc++_shared.so" AND CPACK_READELF_EXECUTABLE) |
| # Note the ABI of a library linked to the shared C++ runtime. |
| set(_command ${CPACK_READELF_EXECUTABLE} -d "${source}") |
| list(JOIN _command " " _command_text) |
| _cpack_apk_debug("Running readelf: ${_command_text}") |
| execute_process( |
| COMMAND ${_command} |
| RESULT_VARIABLE _result |
| OUTPUT_VARIABLE _output |
| ERROR_VARIABLE _error |
| ) |
| if(NOT _result EQUAL 0) |
| message(WARNING |
| "CPack APK: Unable to read the dynamic section of '${source}'.\n" |
| "Command: ${_command_text}\n${_error}") |
| elseif(_output MATCHES "\\(NEEDED\\)[^\n]*\\[libc\\+\\+_shared\\.so\\]") |
| get_filename_component(_abi "${_destination_directory}" NAME) |
| set_property(GLOBAL APPEND PROPERTY _cpack_apk_cxx_shared_abis "${_abi}") |
| endif() |
| elseif(NOT _name STREQUAL "libc++_shared.so") |
| set_property(GLOBAL APPEND PROPERTY _cpack_apk_unchecked "${source}") |
| endif() |
| if(_strip AND CPACK_APK_STRIP_EXECUTABLE) |
| set(_command ${CPACK_APK_STRIP_EXECUTABLE} |
| --strip-unneeded -o "${destination}" "${source}") |
| list(JOIN _command " " _command_text) |
| _cpack_apk_debug("Running strip: ${_command_text}") |
| execute_process( |
| COMMAND ${_command} |
| RESULT_VARIABLE _result |
| OUTPUT_VARIABLE _output |
| ERROR_VARIABLE _output |
| ) |
| if(_result EQUAL 0) |
| return() |
| endif() |
| message(WARNING |
| "CPack APK: Unable to strip '${source}', packaging it as it is.\n" |
| "Command: ${_command_text}\n${_output}") |
| elseif(_strip) |
| set_property(GLOBAL APPEND PROPERTY _cpack_apk_unstripped "${source}") |
| endif() |
| file(COPY_FILE "${source}" "${destination}") |
| endfunction() |
| set_property(GLOBAL PROPERTY _cpack_apk_unstripped) |
| set_property(GLOBAL PROPERTY _cpack_apk_cxx_shared_abis) |
| set_property(GLOBAL PROPERTY _cpack_apk_unchecked) |
| |
| # Native libraries of the Android archives go to `lib/<abi>/`. Android |
| # picks the ABI to load libraries for by what is in the APK, so only the ABIs |
| # the application itself ships code for are packaged. The application's own |
| # libraries, copied after these, take precedence. |
| set(_abis) |
| if(CPACK_APK_ABI) |
| list(APPEND _abis "${CPACK_APK_ABI}") |
| endif() |
| foreach(_staged_file IN LISTS _staged_files) |
| if(_staged_file MATCHES "^lib/([^/]+)/[^/]+\\.so$") |
| list(APPEND _abis "${CMAKE_MATCH_1}") |
| endif() |
| endforeach() |
| list(REMOVE_DUPLICATES _abis) |
| foreach(_aar_library IN LISTS _aar_native_libraries) |
| get_filename_component(_abi_directory "${_aar_library}" DIRECTORY) |
| get_filename_component(_abi "${_abi_directory}" NAME) |
| if(_abis AND NOT _abi IN_LIST _abis) |
| continue() |
| endif() |
| if(".so" IN_LIST CPACK_APK_NO_COMPRESS_EXTENSIONS) |
| set(_destination "${_stored_directory}") |
| else() |
| set(_destination "${_deflated_directory}") |
| endif() |
| get_filename_component(_aar_library_name "${_aar_library}" NAME) |
| _cpack_apk_copy_native_library("${_aar_library}" |
| "${_destination}/lib/${_abi}/${_aar_library_name}") |
| endforeach() |
| |
| foreach(_staged_file IN LISTS _staged_files) |
| _cpack_apk_is_consumed("${_staged_file}" _skip) |
| if(_skip) |
| continue() |
| endif() |
| |
| # A shared library installed straight into `lib/` carries no ABI |
| # sub-directory, which Android requires. |
| set(_entry "${_staged_file}") |
| if(CPACK_APK_ABI AND _entry MATCHES "^lib/[^/]+\\.so$") |
| string(REPLACE "lib/" "lib/${CPACK_APK_ABI}/" _entry "${_entry}") |
| endif() |
| |
| get_filename_component(_extension "${_staged_file}" LAST_EXT) |
| if(_extension IN_LIST CPACK_APK_NO_COMPRESS_EXTENSIONS) |
| set(_destination "${_stored_directory}") |
| else() |
| set(_destination "${_deflated_directory}") |
| endif() |
| |
| if(_entry MATCHES "^lib/[^/]+/[^/]+\\.so$") |
| _cpack_apk_copy_native_library( |
| "${CPACK_APK_STAGING_DIRECTORY}/${_staged_file}" |
| "${_destination}/${_entry}") |
| continue() |
| endif() |
| # Unlike file(COPY), which skips files whose time stamps match, as those of |
| # libraries built reproducibly may, replace a library of an archive. |
| get_filename_component(_entry_directory "${_destination}/${_entry}" |
| DIRECTORY) |
| file(MAKE_DIRECTORY "${_entry_directory}") |
| file(COPY_FILE "${CPACK_APK_STAGING_DIRECTORY}/${_staged_file}" |
| "${_destination}/${_entry}") |
| endforeach() |
| |
| # Like the Android Gradle plugin, package the shared C++ runtime of the NDK |
| # for the ABIs whose libraries are linked to it, unless it is packaged |
| # already. |
| get_property(_cxx_shared_abis GLOBAL PROPERTY _cpack_apk_cxx_shared_abis) |
| list(REMOVE_DUPLICATES _cxx_shared_abis) |
| set(_cxx_shared_missing) |
| foreach(_abi IN LISTS _cxx_shared_abis) |
| if(EXISTS "${_stored_directory}/lib/${_abi}/libc++_shared.so" OR |
| EXISTS "${_deflated_directory}/lib/${_abi}/libc++_shared.so") |
| continue() |
| endif() |
| set(_triple) |
| if(_abi STREQUAL "arm64-v8a") |
| set(_triple aarch64-linux-android) |
| elseif(_abi STREQUAL "armeabi-v7a") |
| set(_triple arm-linux-androideabi) |
| elseif(_abi STREQUAL "x86") |
| set(_triple i686-linux-android) |
| elseif(_abi STREQUAL "x86_64") |
| set(_triple x86_64-linux-android) |
| elseif(_abi STREQUAL "riscv64") |
| set(_triple riscv64-linux-android) |
| endif() |
| set(_cxx_shared "${_sysroot_libraries}/${_triple}/libc++_shared.so") |
| if(NOT _sysroot_libraries OR NOT _triple OR NOT EXISTS "${_cxx_shared}") |
| list(APPEND _cxx_shared_missing "${_abi}") |
| continue() |
| endif() |
| if(".so" IN_LIST CPACK_APK_NO_COMPRESS_EXTENSIONS) |
| set(_destination "${_stored_directory}") |
| else() |
| set(_destination "${_deflated_directory}") |
| endif() |
| _cpack_apk_copy_native_library("${_cxx_shared}" |
| "${_destination}/lib/${_abi}/libc++_shared.so") |
| endforeach() |
| if(_cxx_shared_missing) |
| list(JOIN _cxx_shared_missing ", " _cxx_shared_missing) |
| message(WARNING |
| "CPack APK: Native libraries for ${_cxx_shared_missing} are linked to " |
| "libc++_shared.so, but no Android NDK providing it was found. Set " |
| "CPACK_APK_NDK_ROOT, or install libc++_shared.so into lib/<abi>/.") |
| endif() |
| get_property(_unchecked GLOBAL PROPERTY _cpack_apk_unchecked) |
| if(_unchecked) |
| list(JOIN _unchecked "\n " _unchecked) |
| message(WARNING |
| "CPack APK: No readelf found, unable to tell whether these native " |
| "libraries are linked to the shared C++ runtime, libc++_shared.so:\n" |
| " ${_unchecked}\n" |
| "Install the NDK with the Android SDK manager, or set " |
| "CPACK_APK_NDK_ROOT or CPACK_READELF_EXECUTABLE.") |
| endif() |
| |
| # Like the Android Gradle plugin, package the libraries as they are when |
| # there is nothing to strip them with. |
| get_property(_unstripped GLOBAL PROPERTY _cpack_apk_unstripped) |
| if(_unstripped) |
| list(JOIN _unstripped "\n " _unstripped) |
| message(WARNING |
| "CPack APK: No llvm-strip found in the Android NDK, packaging these " |
| "native libraries without stripping their debug symbols:\n" |
| " ${_unstripped}\n" |
| "Install the NDK with the Android SDK manager, or set " |
| "CPACK_APK_NDK_ROOT or CPACK_APK_STRIP_EXECUTABLE.") |
| endif() |
| |
| foreach(_mode IN ITEMS deflated stored) |
| if(_mode STREQUAL "stored") |
| set(_jar_options u0fM) |
| set(_directory "${_stored_directory}") |
| else() |
| set(_jar_options ufM) |
| set(_directory "${_deflated_directory}") |
| endif() |
| file(GLOB _entries "${_directory}/*") |
| if(NOT _entries) |
| continue() |
| endif() |
| _cpack_apk_run("jar update of the ${_mode} APK entries" |
| ${CPACK_APK_ENV} "${CPACK_APK_JAR_EXECUTABLE}" |
| ${_jar_options} "${_apk}" -C "${_directory}" . |
| ) |
| endforeach() |
| |
| #----------------------------------------------------------------------------- |
| # Align and sign |
| #----------------------------------------------------------------------------- |
| |
| set(_aligned_apk "${CPACK_APK_WORK_DIRECTORY}/aligned.apk") |
| if(NOT DEFINED CPACK_APK_ZIPALIGN_OPTIONS) |
| # Devices with 16 KB memory pages can only map uncompressed native |
| # libraries that are aligned to them. Older build tools only know 4 KB. |
| if(CPACK_APK_BUILD_TOOLS_VERSION VERSION_LESS 35) |
| set(CPACK_APK_ZIPALIGN_OPTIONS -p -f 4) |
| else() |
| set(CPACK_APK_ZIPALIGN_OPTIONS -P 16 -f 4) |
| endif() |
| endif() |
| _cpack_apk_run("zipalign" |
| ${CPACK_APK_ENV} "${CPACK_APK_ZIPALIGN_EXECUTABLE}" |
| ${CPACK_APK_ZIPALIGN_OPTIONS} "${_apk}" "${_aligned_apk}" |
| ) |
| |
| if(NOT DEFINED CPACK_APK_SIGN) |
| set(CPACK_APK_SIGN TRUE) |
| endif() |
| |
| if(NOT CPACK_APK_SIGN) |
| file(RENAME "${_aligned_apk}" "${CPACK_TEMPORARY_PACKAGE_FILE_NAME}") |
| message(WARNING |
| "CPack APK: '${CPACK_TEMPORARY_PACKAGE_FILE_NAME}' is not signed and " |
| "cannot be installed on a device as-is.") |
| return() |
| endif() |
| |
| # The passwords reach the signing tools through the environment, which the |
| # tools inherit from this process. On the command line, other users could |
| # read them off the process list. |
| function(_cpack_apk_export_passwords) |
| set(ENV{CPACK_APK_KEYSTORE_PASSWORD} "${CPACK_APK_KEYSTORE_PASSWORD}") |
| set(ENV{CPACK_APK_KEY_PASSWORD} "${CPACK_APK_KEY_PASSWORD}") |
| endfunction() |
| |
| if(NOT CPACK_APK_KEYSTORE) |
| # Match the debug keystore the Android tooling uses, so that incremental |
| # installs onto a device keep working. |
| # That is `ANDROID_USER_HOME`, or else the `.android` directory of the |
| # user's home, as the JVM knows it: on Windows, that is the profile |
| # directory, whatever `HOME` may say. |
| set(_android_user_home) |
| if(NOT "$ENV{ANDROID_USER_HOME}" STREQUAL "") |
| file(TO_CMAKE_PATH "$ENV{ANDROID_USER_HOME}" _android_user_home) |
| else() |
| if(CMAKE_HOST_WIN32) |
| set(_home_vars ANDROID_SDK_HOME USERPROFILE HOME) |
| else() |
| set(_home_vars ANDROID_SDK_HOME HOME) |
| endif() |
| foreach(_var IN LISTS _home_vars) |
| if(DEFINED ENV{${_var}} AND IS_DIRECTORY "$ENV{${_var}}") |
| file(TO_CMAKE_PATH "$ENV{${_var}}/.android" _android_user_home) |
| break() |
| endif() |
| endforeach() |
| endif() |
| if(NOT _android_user_home) |
| message(FATAL_ERROR |
| "CPack APK: Cannot determine the home directory to hold the default " |
| "debug keystore. Set CPACK_APK_KEYSTORE to a keystore of your own.") |
| endif() |
| set(CPACK_APK_KEYSTORE "${_android_user_home}/debug.keystore") |
| set(CPACK_APK_KEYSTORE_PASSWORD "android") |
| set(CPACK_APK_KEY_ALIAS "androiddebugkey") |
| set(CPACK_APK_KEY_PASSWORD "android") |
| |
| if(NOT EXISTS "${CPACK_APK_KEYSTORE}") |
| message(STATUS |
| "CPack APK: Generating a debug keystore at '${CPACK_APK_KEYSTORE}'.") |
| get_filename_component(_keystore_directory |
| "${CPACK_APK_KEYSTORE}" DIRECTORY) |
| file(MAKE_DIRECTORY "${_keystore_directory}") |
| _cpack_apk_export_passwords() |
| _cpack_apk_run("keytool" |
| ${CPACK_APK_ENV} "${CPACK_APK_KEYTOOL_EXECUTABLE}" |
| -genkeypair |
| -keystore "${CPACK_APK_KEYSTORE}" |
| -storepass:env CPACK_APK_KEYSTORE_PASSWORD |
| -alias "${CPACK_APK_KEY_ALIAS}" |
| -keypass:env CPACK_APK_KEY_PASSWORD |
| -keyalg RSA -keysize 2048 -validity 10000 |
| -dname "CN=Android Debug,O=Android,C=US" |
| ) |
| endif() |
| elseif(NOT EXISTS "${CPACK_APK_KEYSTORE}") |
| message(FATAL_ERROR |
| "CPack APK: CPACK_APK_KEYSTORE is set to '${CPACK_APK_KEYSTORE}', which " |
| "does not exist.") |
| endif() |
| |
| if(NOT CPACK_APK_KEY_ALIAS) |
| message(FATAL_ERROR |
| "CPack APK: CPACK_APK_KEY_ALIAS must name the key to sign with inside " |
| "'${CPACK_APK_KEYSTORE}'.") |
| endif() |
| if(NOT DEFINED CPACK_APK_KEY_PASSWORD) |
| set(CPACK_APK_KEY_PASSWORD "${CPACK_APK_KEYSTORE_PASSWORD}") |
| endif() |
| |
| # `apksigner` reads the minimum API level from the manifest of the APK. |
| _cpack_apk_export_passwords() |
| _cpack_apk_run("apksigner" |
| ${CPACK_APK_ENV} "${CPACK_APK_APKSIGNER_EXECUTABLE}" sign |
| --ks "${CPACK_APK_KEYSTORE}" |
| --ks-pass env:CPACK_APK_KEYSTORE_PASSWORD |
| --ks-key-alias "${CPACK_APK_KEY_ALIAS}" |
| --key-pass env:CPACK_APK_KEY_PASSWORD |
| ${CPACK_APK_APKSIGNER_EXTRA_OPTIONS} |
| --out "${CPACK_TEMPORARY_PACKAGE_FILE_NAME}" |
| "${_aligned_apk}" |
| ) |
| unset(ENV{CPACK_APK_KEYSTORE_PASSWORD}) |
| unset(ENV{CPACK_APK_KEY_PASSWORD}) |
| |
| _cpack_apk_run("apksigner verify" |
| ${CPACK_APK_ENV} "${CPACK_APK_APKSIGNER_EXECUTABLE}" verify |
| "${CPACK_TEMPORARY_PACKAGE_FILE_NAME}" |
| ) |