diff --git a/AddQtAndroidApk.cmake b/AddQtAndroidApk.cmake index e9cfe4e..78b66f0 100644 --- a/AddQtAndroidApk.cmake +++ b/AddQtAndroidApk.cmake @@ -1,5 +1,4 @@ -cmake_minimum_required(VERSION 3.0) -cmake_policy(SET CMP0026 OLD) # allow use of the LOCATION target property +cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR) # store the current source directory for future use set(QT_ANDROID_SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}) @@ -39,7 +38,7 @@ if(NOT QT_ANDROID_NDK_ROOT) if(NOT QT_ANDROID_NDK_ROOT) set(QT_ANDROID_NDK_ROOT ${ANDROID_NDK}) if(NOT QT_ANDROID_NDK_ROOT) - message(FATAL_ERROR "Could not find the Android NDK. Please set either the ANDROID_NDK environment or CMake variable, or the QT_ANDROID_NDK_ROOT CMake variable to the root directory of the Android NDK") + message(FATAL_ERROR "Could not find the Android NDK. Please set either the ANDROID_NDK environment or CMake variable, or the QT_ANDROID_NDK_ROOT CMake variable to the root directory of the Android NDK") endif() endif() endif() @@ -61,18 +60,14 @@ include(CMakeParseArguments) # DEPENDS a_linked_target "path/to/a_linked_library.so" ... # INSTALL #) -# +# macro(add_qt_android_apk TARGET SOURCE_TARGET) # parse the macro arguments cmake_parse_arguments(ARG "INSTALL" "NAME;VERSION_CODE;PACKAGE_NAME;PACKAGE_SOURCES;KEYSTORE_PASSWORD" "DEPENDS;KEYSTORE" ${ARGN}) # extract the full path of the source target binary - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - get_property(QT_ANDROID_APP_PATH TARGET ${SOURCE_TARGET} PROPERTY DEBUG_LOCATION) - else() - get_property(QT_ANDROID_APP_PATH TARGET ${SOURCE_TARGET} PROPERTY LOCATION) - endif() + set(QT_ANDROID_APP_PATH "$") # full file path to the app's main shared library # define the application name if(ARG_NAME) @@ -97,41 +92,82 @@ macro(add_qt_android_apk TARGET SOURCE_TARGET) set(QT_ANDROID_SDK_BUILDTOOLS_REVISION ${BUILD_TOOLS_VERSION}) endif() endforeach() - message("Detected Android SDK build tools version ${QT_ANDROID_SDK_BUILDTOOLS_REVISION}") + message(STATUS "Found Android SDK build tools version: ${QT_ANDROID_SDK_BUILDTOOLS_REVISION}") - # define the application source package directory - if(ARG_PACKAGE_SOURCES) - set(QT_ANDROID_APP_PACKAGE_SOURCE_ROOT ${ARG_PACKAGE_SOURCES}) - else() - # get version code from arguments, or generate a fixed one if not provided - set(QT_ANDROID_APP_VERSION_CODE ${ARG_VERSION_CODE}) - if(NOT QT_ANDROID_APP_VERSION_CODE) - set(QT_ANDROID_APP_VERSION_CODE 1) - endif() + # get version code from arguments, or generate a fixed one if not provided + set(QT_ANDROID_APP_VERSION_CODE ${ARG_VERSION_CODE}) + if(NOT QT_ANDROID_APP_VERSION_CODE) + set(QT_ANDROID_APP_VERSION_CODE 1) + endif() - # try to extract the app version from the target properties, or use the version code if not provided - get_property(QT_ANDROID_APP_VERSION TARGET ${SOURCE_TARGET} PROPERTY VERSION) - if(NOT QT_ANDROID_APP_VERSION) + # try to extract the app version from the target properties, or use the version code if not provided + get_property(QT_ANDROID_APP_VERSION TARGET ${SOURCE_TARGET} PROPERTY VERSION) + if(NOT QT_ANDROID_APP_VERSION) + if(${PROJECT_VERSION} STREQUAL "") set(QT_ANDROID_APP_VERSION ${QT_ANDROID_APP_VERSION_CODE}) + else() + set(QT_ANDROID_APP_VERSION ${PROJECT_VERSION}) endif() + endif() - # create a subdirectory for the extra package sources + # check if the user provides a custom source package and its own manifest file + if(ARG_PACKAGE_SOURCES) + if(EXISTS "${ARG_PACKAGE_SOURCES}/AndroidManifest.xml") + # custom manifest provided, use the provided source package directly + set(QT_ANDROID_APP_PACKAGE_SOURCE_ROOT ${ARG_PACKAGE_SOURCES}) + elseif(EXISTS "${ARG_PACKAGE_SOURCES}/AndroidManifest.xml.in") + # custom manifest template provided + set(QT_ANDROID_MANIFEST_TEMPLATE "${ARG_PACKAGE_SOURCES}/AndroidManifest.xml.in") + endif() + endif() + + # generate a source package directory if none was provided, or if we need to configure a manifest file + if(NOT QT_ANDROID_APP_PACKAGE_SOURCE_ROOT) + # create our own configured package directory in build dir set(QT_ANDROID_APP_PACKAGE_SOURCE_ROOT "${CMAKE_CURRENT_BINARY_DIR}/package") - # generate a manifest from the template - configure_file(${QT_ANDROID_SOURCE_DIR}/AndroidManifest.xml.in ${QT_ANDROID_APP_PACKAGE_SOURCE_ROOT}/AndroidManifest.xml @ONLY) + # create the manifest from the template file + if(NOT QT_ANDROID_MANIFEST_TEMPLATE) + set(QT_ANDROID_MANIFEST_TEMPLATE "${QT_ANDROID_SOURCE_DIR}/AndroidManifest.xml.in") + endif() + configure_file(${QT_ANDROID_MANIFEST_TEMPLATE} ${CMAKE_CURRENT_BINARY_DIR}/AndroidManifest.xml @ONLY) + + # define commands that will be added before the APK target build commands, to refresh the source package directory + set(QT_ANDROID_PRE_COMMANDS ${QT_ANDROID_PRE_COMMANDS} COMMAND ${CMAKE_COMMAND} -E remove_directory ${QT_ANDROID_APP_PACKAGE_SOURCE_ROOT}) # clean the destination directory + set(QT_ANDROID_PRE_COMMANDS ${QT_ANDROID_PRE_COMMANDS} COMMAND ${CMAKE_COMMAND} -E make_directory ${QT_ANDROID_APP_PACKAGE_SOURCE_ROOT}) # re-create it + if(ARG_PACKAGE_SOURCES) + set(QT_ANDROID_PRE_COMMANDS ${QT_ANDROID_PRE_COMMANDS} COMMAND ${CMAKE_COMMAND} -E copy_directory ${ARG_PACKAGE_SOURCES} ${QT_ANDROID_APP_PACKAGE_SOURCE_ROOT}) # copy the user package + endif() + set(QT_ANDROID_PRE_COMMANDS ${QT_ANDROID_PRE_COMMANDS} COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/AndroidManifest.xml ${QT_ANDROID_APP_PACKAGE_SOURCE_ROOT}/AndroidManifest.xml) # copy the generated manifest + endif() + + # newer NDK toolchains don't define ANDROID_STL_PREFIX anymore, + # so this is a fallback to the only supported value in recent versions + if(NOT ANDROID_STL_PREFIX) + if(ANDROID_STL MATCHES "^c\\+\\+_") + set(ANDROID_STL_PREFIX llvm-libc++) + endif() + endif() + if(NOT ANDROID_STL_PREFIX) + message(WARNING "Failed to determine ANDROID_STL_PREFIX value for ANDROID_STL=${ANDROID_STL}") endif() # define the STL shared library path + # up until NDK r18, ANDROID_STL_SHARED_LIBRARIES is populated by the NDK's toolchain file + # since NDK r19, the only option for a shared STL library is libc++_shared if(ANDROID_STL_SHARED_LIBRARIES) list(GET ANDROID_STL_SHARED_LIBRARIES 0 STL_LIBRARY_NAME) # we can only give one to androiddeployqt if(ANDROID_STL_PATH) - set(QT_ANDROID_STL_PATH "${ANDROID_STL_PATH}/libs/${ANDROID_ABI}/lib${STL_LIBRARY_NAME}.so") + set(QT_ANDROID_STL_PATH "${ANDROID_STL_PATH}/libs/${ANDROID_ABI}/lib${ANDROID_STL}.so") else() - set(QT_ANDROID_STL_PATH "${ANDROID_NDK}/sources/cxx-stl/${ANDROID_STL_PREFIX}/libs/${ANDROID_ABI}/lib${STL_LIBRARY_NAME}.so") + set(QT_ANDROID_STL_PATH "${QT_ANDROID_NDK_ROOT}/sources/cxx-stl/${ANDROID_STL_PREFIX}/libs/${ANDROID_ABI}/lib${ANDROID_STL}.so") endif() + elseif(ANDROID_STL STREQUAL c++_shared) + set(QT_ANDROID_STL_PATH "${QT_ANDROID_NDK_ROOT}/sources/cxx-stl/${ANDROID_STL_PREFIX}/libs/${ANDROID_ABI}/libc++_shared.so") else() - set(QT_ANDROID_STL_PATH) + message(WARNING "ANDROID_STL (${ANDROID_STL}) isn't a known shared stl library." + "You should consider setting ANDROID_STL to c++_shared (like Qt).") + set(QT_ANDROID_STL_PATH "${QT_ANDROID_NDK_ROOT}/sources/cxx-stl/${ANDROID_STL_PREFIX}/libs/${ANDROID_ABI}/libc++_shared.so") endif() # set the list of dependant libraries @@ -139,42 +175,66 @@ macro(add_qt_android_apk TARGET SOURCE_TARGET) foreach(LIB ${ARG_DEPENDS}) if(TARGET ${LIB}) # item is a CMake target, extract the library path - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - get_property(LIB_PATH TARGET ${LIB} PROPERTY DEBUG_LOCATION) - else() - get_property(LIB_PATH TARGET ${LIB} PROPERTY LOCATION) - endif() - set(LIB ${LIB_PATH}) + set(LIB "$") + endif() + if(EXTRA_LIBS) + set(EXTRA_LIBS "${EXTRA_LIBS},${LIB}") + else() + set(EXTRA_LIBS "${LIB}") endif() - if(EXTRA_LIBS) - set(EXTRA_LIBS "${EXTRA_LIBS},${LIB}") - else() - set(EXTRA_LIBS "${LIB}") - endif() endforeach() set(QT_ANDROID_APP_EXTRA_LIBS "\"android-extra-libs\": \"${EXTRA_LIBS}\",") endif() + # determine whether to use the gcc- or llvm/clang- toolchain; + # if ANDROID_USE_LLVM was explicitly set, use its value directly, + # otherwise ANDROID_TOOLCHAIN value (set by the NDK's toolchain file) + # says whether llvm/clang or gcc is used + if(DEFINED ANDROID_USE_LLVM) + string(TOLOWER "${ANDROID_USE_LLVM}" QT_ANDROID_USE_LLVM) + elseif(ANDROID_TOOLCHAIN STREQUAL clang) + set(QT_ANDROID_USE_LLVM "true") + else() + set(QT_ANDROID_USE_LLVM "false") + endif() + # set some toolchain variables used by androiddeployqt; # unfortunately, Qt tries to build paths from these variables although these full paths - # are already available in the toochain file, so we have to parse them - string(REGEX MATCH "${ANDROID_NDK}/toolchains/(.*)-(.*)/prebuilt/.*" ANDROID_TOOLCHAIN_PARSED ${ANDROID_TOOLCHAIN_ROOT}) - if(ANDROID_TOOLCHAIN_PARSED) - set(QT_ANDROID_TOOLCHAIN_PREFIX ${CMAKE_MATCH_1}) - set(QT_ANDROID_TOOLCHAIN_VERSION ${CMAKE_MATCH_2}) + # are already available in the toochain file, so we have to parse them if using gcc + if(QT_ANDROID_USE_LLVM STREQUAL "true") + set(QT_ANDROID_TOOLCHAIN_PREFIX "llvm") + set(QT_ANDROID_TOOLCHAIN_VERSION) + set(QT_ANDROID_TOOL_PREFIX "llvm") else() - message(FATAL_ERROR "Failed to parse ANDROID_TOOLCHAIN_ROOT to get toolchain prefix and version") + string(REGEX MATCH "${QT_ANDROID_NDK_ROOT}/toolchains/(.*)-(.*)/prebuilt/.*/bin/(.*)-" ANDROID_TOOLCHAIN_PARSED ${ANDROID_TOOLCHAIN_PREFIX}) + if(ANDROID_TOOLCHAIN_PARSED) + set(QT_ANDROID_TOOLCHAIN_PREFIX ${CMAKE_MATCH_1}) + set(QT_ANDROID_TOOLCHAIN_VERSION ${CMAKE_MATCH_2}) + set(QT_ANDROID_TOOL_PREFIX ${CMAKE_MATCH_3}) + else() + message(FATAL_ERROR "Failed to parse ANDROID_TOOLCHAIN_PREFIX to get toolchain prefix and version and tool prefix") + endif() endif() # make sure that the output directory for the Android package exists - file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/libs/${ANDROID_ABI}) + set(QT_ANDROID_APP_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/${SOURCE_TARGET}-${ANDROID_ABI}) + file(MAKE_DIRECTORY ${QT_ANDROID_APP_BINARY_DIR}/libs/${ANDROID_ABI}) # create the configuration file that will feed androiddeployqt - configure_file(${QT_ANDROID_SOURCE_DIR}/qtdeploy.json.in ${CMAKE_CURRENT_BINARY_DIR}/qtdeploy.json @ONLY) + # 1. replace placeholder variables at generation time + configure_file(${QT_ANDROID_SOURCE_DIR}/qtdeploy.json.in ${CMAKE_CURRENT_BINARY_DIR}/qtdeploy.json.in @ONLY) + # 2. evaluate generator expressions at build time + file(GENERATE + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/qtdeploy.json + INPUT ${CMAKE_CURRENT_BINARY_DIR}/qtdeploy.json.in + ) + # 3. Configure build.gradle to properly work with Android Studio import + set(QT_ANDROID_NATIVE_API_LEVEL ${ANDROID_NATIVE_API_LEVEL}) + configure_file(${QT_ANDROID_SOURCE_DIR}/build.gradle.in ${QT_ANDROID_APP_BINARY_DIR}/build.gradle @ONLY) # check if the apk must be signed if(ARG_KEYSTORE) - set(SIGN_OPTIONS --release --sign ${ARG_KEYSTORE} --tsa http://timestamp.digicert.com) + set(SIGN_OPTIONS --sign ${ARG_KEYSTORE}) if(ARG_KEYSTORE_PASSWORD) set(SIGN_OPTIONS ${SIGN_OPTIONS} --storepass ${ARG_KEYSTORE_PASSWORD}) endif() @@ -190,15 +250,32 @@ macro(add_qt_android_apk TARGET SOURCE_TARGET) set(TARGET_LEVEL_OPTIONS --android-platform android-${ANDROID_PLATFORM_LEVEL}) endif() + # determine the build type to pass to androiddeployqt + if(CMAKE_BUILD_TYPE AND ${CMAKE_BUILD_TYPE} STREQUAL "Debug" AND NOT ARG_KEYSTORE) + set(QT_ANDROID_BUILD_TYPE --debug) + elseif() + set(QT_ANDROID_BUILD_TYPE --release) + endif() + # create a custom command that will run the androiddeployqt utility to prepare the Android package add_custom_target( ${TARGET} ALL DEPENDS ${SOURCE_TARGET} - COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_CURRENT_BINARY_DIR}/libs/${ANDROID_ABI} # it seems that recompiled libraries are not copied if we don't remove them first - COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/libs/${ANDROID_ABI} - COMMAND ${CMAKE_COMMAND} -E copy ${QT_ANDROID_APP_PATH} ${CMAKE_CURRENT_BINARY_DIR}/libs/${ANDROID_ABI} - COMMAND ${QT_ANDROID_QT_ROOT}/bin/androiddeployqt --verbose --output ${CMAKE_CURRENT_BINARY_DIR} --input ${CMAKE_CURRENT_BINARY_DIR}/qtdeploy.json --gradle ${TARGET_LEVEL_OPTIONS} ${INSTALL_OPTIONS} ${SIGN_OPTIONS} + ${QT_ANDROID_PRE_COMMANDS} + # it seems that recompiled libraries are not copied if we don't remove them first + COMMAND ${CMAKE_COMMAND} -E remove_directory ${QT_ANDROID_APP_BINARY_DIR}/libs/${ANDROID_ABI} + COMMAND ${CMAKE_COMMAND} -E make_directory ${QT_ANDROID_APP_BINARY_DIR}/libs/${ANDROID_ABI} + COMMAND ${CMAKE_COMMAND} -E copy ${QT_ANDROID_APP_PATH} ${QT_ANDROID_APP_BINARY_DIR}/libs/${ANDROID_ABI} + COMMAND ${QT_ANDROID_QT_ROOT}/bin/androiddeployqt + --verbose + --output ${QT_ANDROID_APP_BINARY_DIR} + --input ${CMAKE_CURRENT_BINARY_DIR}/qtdeploy.json + --gradle + ${QT_ANDROID_BUILD_TYPE} + ${TARGET_LEVEL_OPTIONS} + ${INSTALL_OPTIONS} + ${SIGN_OPTIONS} ) endmacro() diff --git a/AndroidManifest.xml.in b/AndroidManifest.xml.in index e4fb78d..02ba232 100644 --- a/AndroidManifest.xml.in +++ b/AndroidManifest.xml.in @@ -1,35 +1,81 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5c082aa --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +# +# This CMakeLists.txt goal is to be used with CMake's FetchContent function +# In your CMake script you should do : +# +# FetchContent_Declare( +# QtAndroidCMake +# GIT_REPOSITORY https://github.com/LaurentGomila/qt-android-cmake +# GIT_TAG master +# ) +# FetchContent_MakeAvailable(QtAndroidCMake) +# +# Then you can call add_qt_android_apk when you want. +# This code snippet will download the repository if not already done and include the CMakeLists +# (that will include the script). +# Everything will be working out of the box. +# + +cmake_minimum_required(VERSION 3.0.0 FATAL_ERROR) + +include(${CMAKE_CURRENT_LIST_DIR}/AddQtAndroidApk.cmake) + +set(QT_ANDROID_SOURCE_DIR ${QT_ANDROID_SOURCE_DIR} CACHE STRING "Source directory of AddQtAndroidApk.cmake") +set(QT_ANDROID_QT_ROOT ${QT_ANDROID_QT_ROOT} CACHE STRING "Qt SDK root folder") +set(QT_ANDROID_SDK_ROOT ${QT_ANDROID_SDK_ROOT} CACHE STRING "" FORCE) +set(QT_ANDROID_NDK_ROOT ${QT_ANDROID_NDK_ROOT} CACHE STRING "" FORCE) diff --git a/build.gradle.in b/build.gradle.in new file mode 100644 index 0000000..c3c7250 --- /dev/null +++ b/build.gradle.in @@ -0,0 +1,61 @@ +buildscript { + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.2.0' + } +} + +repositories { + google() + jcenter() +} + +apply plugin: 'com.android.application' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qt5AndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + compileSdkVersion androidCompileSdkVersion.toInteger() + + buildToolsVersion androidBuildToolsVersion + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qt5AndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qt5AndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qt5AndroidDir + '/res', 'res'] + resources.srcDirs = ['src'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + lintOptions { + abortOnError false + } + defaultConfig { + minSdkVersion 16 + targetSdkVersion @QT_ANDROID_NATIVE_API_LEVEL@ + } +} diff --git a/qtdeploy.json.in b/qtdeploy.json.in index 3cdab02..3a1f9b6 100644 --- a/qtdeploy.json.in +++ b/qtdeploy.json.in @@ -5,7 +5,7 @@ "ndk": "@QT_ANDROID_NDK_ROOT@", "sdkBuildToolsRevision": "@QT_ANDROID_SDK_BUILDTOOLS_REVISION@", "toolchain-prefix": "@QT_ANDROID_TOOLCHAIN_PREFIX@", - "tool-prefix": "@QT_ANDROID_TOOLCHAIN_PREFIX@", + "tool-prefix": "@QT_ANDROID_TOOL_PREFIX@", "toolchain-version": "@QT_ANDROID_TOOLCHAIN_VERSION@", "ndk-host": "@ANDROID_NDK_HOST_SYSTEM_NAME@", "target-architecture": "@ANDROID_ABI@", @@ -14,6 +14,7 @@ "android-app-name": "@QT_ANDROID_APP_NAME@", "qml-root-path": "@CMAKE_SOURCE_DIR@", "stdcpp-path": "@QT_ANDROID_STL_PATH@", + "useLLVM": @QT_ANDROID_USE_LLVM@, @QT_ANDROID_APP_EXTRA_LIBS@ "android-package-source-directory": "@QT_ANDROID_APP_PACKAGE_SOURCE_ROOT@" } diff --git a/readme.md b/readme.md index c3e43b6..0c56e40 100644 --- a/readme.md +++ b/readme.md @@ -133,6 +133,8 @@ The path to a directory containing additional files for the package (custom mani If you don't provide this argument, a default manifest is generated from the ```AndroidManifest.xml.in``` template and automatically used for building the APK. +If your PACKAGE_SOURCES directory contains a ```AndroidManifest.xml.in``` template file rather than a direct ```AndroidManifest.xml``` , it is automatically detected by the tool, configured and outputted as ```AndroidManifest.xml```, so that you can still use the provided CMake variables in your custom manifest. + Example: ```cmake @@ -189,6 +191,7 @@ add_qt_android_apk(my_app_apk my_app INSTALL ) ``` + ## Troubleshooting In case of