# SPDX-FileCopyrightText: 2014 Aleix Pol i Gonzalez <aleixpol@kde.org>
#
# SPDX-License-Identifier: BSD-3-Clause

#[=======================================================================[.rst:
AndroidToolchain
----------------

Enable easy compilation of cmake projects on Android.

By using this android toolchain, the projects will be set up to compile the
specified project targeting an Android platform, depending on its input.
Furthermore, if desired, an APK can be directly generated by using the
`androiddeployqt <https://doc.qt.io/qt-5/deployment-android.html>`_ tool.

CMake upstream has Android support now. This module will still give us some
useful features offering androiddeployqt integration and adequate executables
format for our Android applications.

Since we are using CMake Android support, any information from CMake documentation
still applies:
https://cmake.org/cmake/help/v3.7/manual/cmake-toolchains.7.html#cross-compiling-for-android

.. note::

  This module requires CMake 3.18.

Since 1.7.0.

Usage
=====

To use this file, you need to set the ``CMAKE_TOOLCHAIN_FILE`` to point to
``Android.cmake`` on the command line::

  cmake -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake

You will also need to provide the locations of the Android NDK and SDK. This
can be done on the commandline or with environment variables; in either case
the variable names are:

``CMAKE_ANDROID_NDK``
    The NDK root path.
``ANDROID_SDK_ROOT``
    The SDK root path.

Additional options are specified as cache variables (eg: on the command line):

``ANDROID_ABI``
    The ABI to use. See the ``sources/cxx-stl/gnu-libstdc++/*/libs``
    directories in the NDK. Default: ``armeabi-v7a``.
``ANDROID_SDK_COMPILE_API``
    The platform API level to compile against. May be different from the NDK
    target. Default: newest installed version (e.g. android-30).
``ANDROID_SDK_BUILD_TOOLS_REVISION``
    The build tools version to use.
    Default: newest installed version (e.g. ``30.0.2``).
``ANDROID_EXTRA_LIBS``
    The ";"-separated list of full paths to libs to include in resulting APK (Qt 5 only).

For integrating other libraries which are not part of the Android toolchain,
like Qt5, and installed to a separate prefix on the host system, the install
prefixes of those libraries would be passed as alternative roots as list via
``ECM_ADDITIONAL_FIND_ROOT_PATH``. Since 5.30.0.

For example, for integrating a Qt5 for Android present at
``~/Qt/5.14.2/android/`` and some other libraries installed to
the prefix ``/opt/android/foo``, you would use::

  cmake \
    -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \
    -DECM_ADDITIONAL_FIND_ROOT_PATH="~/Qt/5.14.2/android/;/opt/android/foo"

If your project uses ``find_package()`` to locate build tools on the host
system, make sure to pass ``CMAKE_FIND_ROOT_PATH_BOTH`` or
``NO_CMAKE_FIND_ROOT_PATH`` as argument in the call. See the
``find_package()`` documentation for more details.

Deploying Qt 5 Applications
===========================

After building the application, you will need to generate an APK that can be
deployed to an Android device. This module integrates androiddeployqt support
to help with this for Qt-based projects. To enable this, set the
``QTANDROID_EXPORTED_TARGET`` variable to the targets you wish to export as an
APK (in a ;-separed list), as well as ``ANDROID_APK_DIR`` to a directory
containing some basic information. This will create a ``create-apk-<target>``
target that will generate the APK file.  See the `Qt on Android deployment
documentation <https://doc.qt.io/qt-5/deployment-android.html>`_ for more
information.

For example, you could do::

  cmake \
    -DCMAKE_TOOLCHAIN_FILE=/usr/share/ECM/toolchain/Android.cmake \
    -DQTANDROID_EXPORTED_TARGET=myapp \
    -DANDROID_APK_DIR=myapp-apk
  make
  make create-apk-myapp

You can specify the APK output directory by setting ``ANDROID_APK_OUTPUT_DIR``.
Otherwise the APK can be found in ``myapp_build_apk/`` in the build directory.

The create-apk-myapp target will be able to take an ARGS parameter with further
arguments for androiddeployqt. For example, one can use::

  make create-apk-myapp ARGS="--install"

To install the apk to test. To generate a signed apk, one can do it with the
following syntax::

  make create-apk-myapp ARGS="--sign ~/my.keystore alias_name"

In case it's needed for your application to set the APK directory from cmake
scripting you can also set the directory as the ANDROID_APK_DIR property of
the create-apk-myapp target.

See Android documentation on how to create a keystore to use

Advanced Options
================

The following packaging options are mainly interesting for automation or integration
with CI/CD pipelines:

``ANDROID_APK_OUTPUT_DIR``
    Specifies a folder where the generated APK files should be placed.
``ANDROID_FASTLANE_METADATA_OUTPUT_DIR``
    Specifies a folder where the generated metadata for the F-Droid store
    should be placed.
``ANDROIDDEPLOYQT_EXTRA_ARGS``
    Allows to pass additional arguments to `androiddeployqt`. This is an alternative to
    the `ARGS=` argument for `make` and unlike that works with all CMake generators.
#]=======================================================================]

cmake_minimum_required(VERSION "3.18")

macro(set_deprecated_variable actual_variable deprecated_variable default_value)
    set(${deprecated_variable} "${default_value}" CACHE STRING "Deprecated. Use ${actual_variable}")
    if (NOT DEFINED ${actual_variable})
        set(${actual_variable} ${${deprecated_variable}})
    endif()
endmacro()

set_deprecated_variable(CMAKE_ANDROID_NDK ANDROID_NDK "$ENV{ANDROID_NDK}")
set_deprecated_variable(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION ANDROID_GCC_VERSION "clang")

# try to determine default API level from environment
# we unfortunately can't make this dependent on the Qt version as we don't know that here yet
set(_default_api_level "21")
if($ENV{ANDROID_NDK_PLATFORM} MATCHES "^android-([0-9]+)$")
    set(_default_api_level ${CMAKE_MATCH_1})
endif()

set_deprecated_variable(CMAKE_ANDROID_API ANDROID_API_LEVEL ${_default_api_level})
if(NOT DEFINED ENV{ANDROID_ARCH})
    set(ENV{ANDROID_ARCH} "arm")
endif()
set_deprecated_variable(CMAKE_ANDROID_ARCH ANDROID_ARCHITECTURE $ENV{ANDROID_ARCH})
if(NOT DEFINED ENV{ANDROID_ARCH_ABI})
    set(ENV{ANDROID_ARCH_ABI} "armeabi-v7a")
endif()
set_deprecated_variable(CMAKE_ANDROID_ARCH_ABI ANDROID_ABI "$ENV{ANDROID_ARCH_ABI}")

set(ANDROID_SDK_ROOT "$ENV{ANDROID_SDK_ROOT}" CACHE PATH "Android SDK path")

file(GLOB platforms LIST_DIRECTORIES TRUE RELATIVE ${ANDROID_SDK_ROOT}/platforms ${ANDROID_SDK_ROOT}/platforms/*)
list(SORT platforms COMPARE NATURAL)
list(GET platforms -1 _default_platform)
set(ANDROID_SDK_COMPILE_API "${_default_platform}" CACHE STRING "Android API Level")
if(ANDROID_SDK_COMPILE_API MATCHES "^android-([0-9]+)$")
    set(ANDROID_SDK_COMPILE_API ${CMAKE_MATCH_1})
endif()

file(GLOB build-tools LIST_DIRECTORIES TRUE RELATIVE ${ANDROID_SDK_ROOT}/build-tools ${ANDROID_SDK_ROOT}/build-tools/*)
list(SORT build-tools COMPARE NATURAL)
list(GET build-tools -1 _default_sdk)
set(ANDROID_SDK_BUILD_TOOLS_REVISION "${_default_sdk}" CACHE STRING "Android Build Tools version")

set(CMAKE_SYSTEM_VERSION ${CMAKE_ANDROID_API})
set(CMAKE_SYSTEM_NAME Android)
if (NOT CMAKE_ANDROID_STL_TYPE)
    set(CMAKE_ANDROID_STL_TYPE c++_shared)
endif()

# Workaround link failure at FindThreads in CXX-only mode,
# armv7 really doesn't like mixing PIC/PIE code.
# Since we only have to care about a single compiler,
# hard-code the values here.
# Qt6 fixes this and breaks in nested CMake calls (e.g. try_compile) if we
# define Threads::Threads here, at least if the "C" language is enabled. In
# CXX-only projects we still need to do this unconditionally...
#
# We cannot use our usual Qt version check at this point though yet,
# so check whether we are chainloaded by the Qt toolchain as an indicator
# for Qt6.
# When building Qt6Base itself the check does not work, hence we have
# ECM_THREADS_WORKAROUND for that case which set to OFF in the Craft blueprints.
# As that doesn't propagate to nested CMake calls (e.g. tye_compile), we also check
# whether that is set via an environment variable (necessary with Qt >= 6.8.1).
if (NOT DEFINED ECM_THREADS_WORKAROUND AND NOT DEFINED ENV{ECM_THREADS_WORKAROUND})
    set(ECM_THREADS_WORKAROUND TRUE)
endif()
get_property(_languages GLOBAL PROPERTY ENABLED_LANGUAGES)
if (ECM_THREADS_WORKAROUND AND NOT TARGET Threads::Threads AND (NOT DEFINED __qt_chainload_toolchain_file OR NOT "C" IN_LIST _languages))
    set(Threads_FOUND TRUE)
    set(CMAKE_THREAD_LIBS_INIT "-pthread")
    add_library(Threads::Threads INTERFACE IMPORTED)
    set_property(TARGET Threads::Threads PROPERTY INTERFACE_COMPILE_OPTIONS "-pthread")
    set_property(TARGET Threads::Threads PROPERTY INTERFACE_LINK_LIBRARIES "-pthread")
endif()

# let the Android NDK toolchain file do the actual work
set(ANDROID_PLATFORM "android-${CMAKE_ANDROID_API}")
set(ANDROID_STL ${CMAKE_ANDROID_STL_TYPE})
include(${CMAKE_ANDROID_NDK}/build/cmake/android.toolchain.cmake REQUIRED)

# Export configurable variables for the try_compile() command.
list(APPEND CMAKE_TRY_COMPILE_PLATFORM_VARIABLES
  CMAKE_ANDROID_NDK
  CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION
  CMAKE_ANDROID_API
  CMAKE_ANDROID_ARCH
  CMAKE_ANDROID_ARCH_ABI
  ANDROID_SDK_ROOT
  ANDROID_SDK_COMPILE_API
)

# needed for androiddeployqt's stdcpp-path setting
if (ANDROID_TOOLCHAIN_ROOT)
    set(ANDROID_SYSROOT_PREFIX "${ANDROID_TOOLCHAIN_ROOT}/sysroot/usr")
else()
    set(ANDROID_SYSROOT_PREFIX "${CMAKE_NDK_ROOT}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr")
endif()

## HACK: Remove when we can depend on NDK r23
# Workaround issue https://github.com/android/ndk/issues/929
if(ANDROID_NDK_MAJOR VERSION_LESS 23)
    unset(CMAKE_SYSROOT)

    list(APPEND CMAKE_SYSTEM_INCLUDE_PATH
      "${ANDROID_SYSROOT_PREFIX}/include/${CMAKE_LIBRARY_ARCHITECTURE}")
    list(APPEND CMAKE_SYSTEM_INCLUDE_PATH "${ANDROID_SYSROOT_PREFIX}/include")

    # Prepend in reverse order
    list(PREPEND CMAKE_SYSTEM_LIBRARY_PATH
      "${ANDROID_SYSROOT_PREFIX}/lib/${CMAKE_LIBRARY_ARCHITECTURE}")
    list(PREPEND CMAKE_SYSTEM_LIBRARY_PATH
      "${ANDROID_SYSROOT_PREFIX}/lib/${CMAKE_LIBRARY_ARCHITECTURE}/${ANDROID_PLATFORM_LEVEL}")
endif()

# Qt6 still expects ABI suffixes but only applies them in its own qt_add_[executable|library] macros
# as we typically don't use those we need another way to get those
if (DEFINED __qt_chainload_toolchain_file) # indicator for Qt6, see above
    set(CMAKE_MODULE_LIBRARY_SUFFIX_C "_${CMAKE_ANDROID_ARCH_ABI}.so")
    set(CMAKE_MODULE_LIBRARY_SUFFIX_CXX "_${CMAKE_ANDROID_ARCH_ABI}.so")
    set(CMAKE_SHARED_LIBRARY_SUFFIX_C "_${CMAKE_ANDROID_ARCH_ABI}.so")
    set(CMAKE_SHARED_LIBRARY_SUFFIX_CXX "_${CMAKE_ANDROID_ARCH_ABI}.so")
endif()

# these aren't set yet at this point by the Android toolchain, but without
# those the find_package() call in ECMAndroidDeployQt5 will fail
set(CMAKE_FIND_LIBRARY_PREFIXES "lib")
set(CMAKE_FIND_LIBRARY_SUFFIXES "_${CMAKE_ANDROID_ARCH_ABI}.so" ".so" ".a")

# Work around Qt messing with CMAKE_SHARED_LIBRARY_SUFFIX and thus breaking find_library()
# Unfortunately, just setting CMAKE_FIND_LIBRARY_SUFFIXES here won't help, as this will
# be subsequently overwritten.
macro(addAbiSuffix _var _access)
     if (${_access} STREQUAL "MODIFIED_ACCESS")
         list(PREPEND CMAKE_FIND_LIBRARY_SUFFIXES "_${CMAKE_ANDROID_ARCH_ABI}.so")
     endif()
endmacro()
variable_watch(CMAKE_FIND_LIBRARY_SUFFIXES addAbiSuffix)

# determine STL architecture, which is using a different format than ANDROID_ARCH_ABI
string(REGEX REPLACE "-(clang)?([0-9].[0-9])?$" "" ECM_ANDROID_STL_ARCH "${ANDROID_TOOLCHAIN_NAME}")

if (NOT DEFINED ECM_ADDITIONAL_FIND_ROOT_PATH)
    SET(ECM_ADDITIONAL_FIND_ROOT_PATH ${CMAKE_PREFIX_PATH})
endif()

LIST(APPEND CMAKE_FIND_ROOT_PATH ${ECM_ADDITIONAL_FIND_ROOT_PATH})

#we want executables to be shared libraries, hooks will invoke the exported cmake function
set(CMAKE_CXX_LINK_EXECUTABLE
    "<CMAKE_CXX_COMPILER> <CMAKE_SHARED_LIBRARY_CXX_FLAGS> <LINK_FLAGS> <CMAKE_SHARED_LIBRARY_CREATE_CXX_FLAGS> <SONAME_FLAG><TARGET_SONAME> -o <TARGET> <OBJECTS> <LINK_LIBRARIES>"
)

# As our executables are shared libraries, we also need them build with position independent code (PIC).
# Qt 5 forces that anyway, but in Qt 6 that is no longer the case for executables (which we pretend to build here),
# and so we end up with just PIE (coming from CMake).
# And as subsequent steps overwrite that setting again, we have to watch for that and redo our change.
set(CMAKE_CXX_COMPILE_OPTIONS_PIE "-fPIC")
macro(resetPieOption _var _access)
    if (${_access} STREQUAL "MODIFIED_ACCESS")
        set(CMAKE_CXX_COMPILE_OPTIONS_PIE "-fPIC")
    endif()
endmacro()
variable_watch(CMAKE_CXX_COMPILE_OPTIONS_PIE resetPieOption)

set(ECM_DIR "${CMAKE_CURRENT_LIST_DIR}/../cmake" CACHE STRING "")

######### generation (Qt 5 only)

# Need to ensure we only get in here once, as this file is included twice:
# from CMakeDetermineSystem.cmake and from CMakeSystem.cmake generated within the
# build directory.
if(DEFINED QTANDROID_EXPORTED_TARGET AND NOT TARGET "create-apk" AND NOT __qt_chainload_toolchain_file)
    get_filename_component(_CMAKE_ANDROID_DIR "${CMAKE_TOOLCHAIN_FILE}" DIRECTORY)
    list(LENGTH QTANDROID_EXPORTED_TARGET targetsCount)
    include(${_CMAKE_ANDROID_DIR}/ECMAndroidDeployQt5.cmake)

    math(EXPR last "${targetsCount}-1")
    foreach(idx RANGE 0 ${last})
        list(GET QTANDROID_EXPORTED_TARGET ${idx} exportedTarget)
        list(GET ANDROID_APK_DIR ${idx} APK_DIR)
        if(APK_DIR AND NOT EXISTS "${ANDROID_APK_DIR}/AndroidManifest.xml" AND IS_ABSOLUTE ANDROID_APK_DIR)
            message(FATAL_ERROR "Cannot find ${APK_DIR}/AndroidManifest.xml according to ANDROID_APK_DIR. ${ANDROID_APK_DIR} ${exportedTarget}")
        elseif(NOT APK_DIR)
            get_filename_component(_qt5Core_install_prefix "${Qt5Core_DIR}/../../../" ABSOLUTE)
            set(APK_DIR "${_qt5Core_install_prefix}/src/android/templates/")
        endif()

        ecm_androiddeployqt5("${exportedTarget}" "${ECM_ADDITIONAL_FIND_ROOT_PATH}")
        set_target_properties(create-apk-${exportedTarget} PROPERTIES ANDROID_APK_DIR "${APK_DIR}")
    endforeach()
elseif (NOT __qt_chainload_toolchain_file)
    message(STATUS "You can export a target by specifying -DQTANDROID_EXPORTED_TARGET=<targetname> and -DANDROID_APK_DIR=<paths>")
endif()
