diff options
Diffstat (limited to 'tools/cross_compile_android')
-rw-r--r-- | tools/cross_compile_android/android_utilities.py | 297 | ||||
-rw-r--r-- | tools/cross_compile_android/main.py | 310 | ||||
-rw-r--r-- | tools/cross_compile_android/requirements.txt | 3 | ||||
-rw-r--r-- | tools/cross_compile_android/templates/cross_compile.tmpl.sh | 29 | ||||
-rw-r--r-- | tools/cross_compile_android/templates/toolchain_default.tmpl.cmake | 73 |
5 files changed, 712 insertions, 0 deletions
diff --git a/tools/cross_compile_android/android_utilities.py b/tools/cross_compile_android/android_utilities.py new file mode 100644 index 000000000..7f2047a7e --- /dev/null +++ b/tools/cross_compile_android/android_utilities.py @@ -0,0 +1,297 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from __future__ import annotations + +import logging +import shutil +import re +import os +import stat +import sys +import subprocess + +from urllib import request +from pathlib import Path +from packaging import version +from tqdm import tqdm + +# the tag number does not matter much since we update the sdk later +DEFAULT_SDK_TAG = 6514223 +ANDROID_NDK_VERSION = "26b" +ANDROID_NDK_VERSION_NUMBER_SUFFIX = "10909125" + + +def run_command(command: list[str], cwd: str | None = None, ignore_fail: bool = False, + dry_run: bool = False, accept_prompts: bool = False, show_stdout: bool = False, + capture_stdout: bool = False): + + if capture_stdout and not show_stdout: + raise RuntimeError("capture_stdout should always be used together with show_stdout") + + if dry_run: + print(" ".join(command)) + return + + input = None + if accept_prompts: + input = str.encode("y") + + if show_stdout: + stdout = None + else: + stdout = subprocess.DEVNULL + + result = subprocess.run(command, cwd=cwd, input=input, stdout=stdout, + capture_output=capture_stdout) + + if result.returncode != 0 and not ignore_fail: + sys.exit(result.returncode) + + if capture_stdout and not result.returncode: + return result.stdout.decode("utf-8") + + return None + + +class DownloadProgressBar(tqdm): + def update_to(self, b=1, bsize=1, tsize=None): + if tsize is not None: + self.total = tsize + self.update(b * bsize - self.n) + + +class SdkManager: + def __init__(self, android_sdk_dir: Path, dry_run: bool = False): + self._sdk_manager = android_sdk_dir / "tools" / "bin" / "sdkmanager" + + if not self._sdk_manager.exists(): + raise RuntimeError(f"Unable to find SdkManager in {str(self._sdk_manager)}") + + if not os.access(self._sdk_manager, os.X_OK): + current_permissions = stat.S_IMODE(os.lstat(self._sdk_manager).st_mode) + os.chmod(self._sdk_manager, current_permissions | stat.S_IEXEC) + + self._android_sdk_dir = android_sdk_dir + self._dry_run = dry_run + + def list_packages(self): + command = [self._sdk_manager, f"--sdk_root={self._android_sdk_dir}", "--list"] + return run_command(command=command, dry_run=self._dry_run, show_stdout=True, + capture_stdout=True) + + def install(self, *args, accept_license: bool = False, show_stdout=False): + command = [str(self._sdk_manager), f"--sdk_root={self._android_sdk_dir}", *args] + run_command(command=command, dry_run=self._dry_run, + accept_prompts=accept_license, show_stdout=show_stdout) + + +def extract_zip(file: Path, destination: Path): + """ + Unpacks the zip file into destination preserving all permissions + + TODO: Try to use zipfile module. Currently we cannot use zipfile module here because + extractAll() does not preserve permissions. + + In case `unzip` is not available, the user is requested to install it manually + """ + unzip = shutil.which("unzip") + if not unzip: + raise RuntimeError("Unable to find program unzip. Use `sudo apt-get install unzip`" + "to install it") + + command = [unzip, file, "-d", destination] + run_command(command=command, show_stdout=True) + + +def extract_dmg(file: Path, destination: Path): + output = run_command(['hdiutil', 'attach', '-nobrowse', '-readonly', file], + show_stdout=True, capture_stdout=True) + + # find the mounted volume + mounted_vol_name = re.search(r'/Volumes/(.*)', output).group(1) + if not mounted_vol_name: + raise RuntimeError(f"Unable to find mounted volume for file {file}") + + # copy files + shutil.copytree(f'/Volumes/{mounted_vol_name}/', destination, dirs_exist_ok=True) + + # Detach mounted volume + run_command(['hdiutil', 'detach', f'/Volumes/{mounted_vol_name}']) + + +def _download(url: str, destination: Path): + """ + Download url to destination + """ + headers, download_path = None, None + # https://github.com/tqdm/tqdm#hooks-and-callbacks + with DownloadProgressBar(unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1]) as t: + download_path, headers = request.urlretrieve(url=url, filename=destination, + reporthook=t.update_to) + assert Path(download_path).resolve() == destination + + +def download_android_ndk(ndk_path: Path): + """ + Downloads the given ndk_version into ndk_path + """ + ndk_path = ndk_path / "android-ndk" + ndk_extension = "dmg" if sys.platform == "darwin" else "zip" + ndk_zip_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}" + ndk_version_path = "" + if sys.platform == "linux": + ndk_version_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}" + elif sys.platform == "darwin": + ndk_version_path = (ndk_path + / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK") + else: + raise RuntimeError(f"Unsupported platform {sys.platform}") + + if ndk_version_path.exists(): + print(f"NDK path found in {str(ndk_version_path)}") + else: + ndk_path.mkdir(parents=True, exist_ok=True) + url = (f"https://dl.google.com/android/repository" + f"/android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}") + + print(f"Downloading Android Ndk version r{ANDROID_NDK_VERSION}") + _download(url=url, destination=ndk_zip_path) + + print("Unpacking Android Ndk") + if sys.platform == "darwin": + extract_dmg(file=(ndk_path + / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}" + ), + destination=ndk_path) + ndk_version_path = (ndk_version_path + / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK") + else: + extract_zip(file=(ndk_path + / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}" + ), + destination=ndk_path) + + return ndk_version_path + + +def download_android_commandlinetools(android_sdk_dir: Path): + """ + Downloads Android commandline tools into cltools_path. + """ + sdk_platform = sys.platform if sys.platform != "darwin" else "mac" + android_sdk_dir = android_sdk_dir / "android-sdk" + url = ("https://dl.google.com/android/repository/" + f"commandlinetools-{sdk_platform}-{DEFAULT_SDK_TAG}_latest.zip") + cltools_zip_path = (android_sdk_dir + / f"commandlinetools-{sdk_platform}-{DEFAULT_SDK_TAG}_latest.zip") + cltools_path = android_sdk_dir / "tools" + + if cltools_path.exists(): + print(f"Command-line tools found in {str(cltools_path)}") + else: + android_sdk_dir.mkdir(parents=True, exist_ok=True) + + print("Download Android Command Line Tools: " + f"commandlinetools-{sys.platform}-{DEFAULT_SDK_TAG}_latest.zip") + _download(url=url, destination=cltools_zip_path) + + print("Unpacking Android Command Line Tools") + extract_zip(file=cltools_zip_path, destination=android_sdk_dir) + + return android_sdk_dir + + +def android_list_build_tools_versions(sdk_manager: SdkManager): + """ + List all the build-tools versions available for download + """ + available_packages = sdk_manager.list_packages() + build_tools_versions = [] + lines = available_packages.split('\n') + + for line in lines: + if not line.strip().startswith('build-tools;'): + continue + package_name = line.strip().split(' ')[0] + if package_name.count(';') != 1: + raise RuntimeError(f"Unable to parse build-tools version: {package_name}") + ver = package_name.split(';')[1] + + build_tools_versions.append(version.Version(ver)) + + return build_tools_versions + + +def find_installed_buildtools_version(build_tools_dir: Path): + """ + It is possible that the user has multiple build-tools installed. The newer version is generally + used. This function find the newest among the installed build-tools + """ + versions = [version.Version(bt_dir.name) for bt_dir in build_tools_dir.iterdir() + if bt_dir.is_dir()] + return max(versions) + + +def find_latest_buildtools_version(sdk_manager: SdkManager): + """ + Uses sdk manager to find the latest build-tools version + """ + available_build_tools_v = android_list_build_tools_versions(sdk_manager=sdk_manager) + + if not available_build_tools_v: + raise RuntimeError('Unable to find any build tools available for download') + + # find the latest build tools version that is not a release candidate + # release candidates end has rc in the version number + available_build_tools_v = [v for v in available_build_tools_v if "rc" not in str(v)] + + return max(available_build_tools_v) + + +def install_android_packages(android_sdk_dir: Path, android_api: str, dry_run: bool = False, + accept_license: bool = False, skip_update: bool = False): + """ + Use the sdk manager to install build-tools, platform-tools and platform API + """ + tools_dir = android_sdk_dir / "tools" + if not tools_dir.exists(): + raise RuntimeError("Unable to find Android command-line tools in " + f"{str(tools_dir)}") + + # incase of --verbose flag + show_output = (logging.getLogger().getEffectiveLevel() == logging.INFO) + + sdk_manager = SdkManager(android_sdk_dir=android_sdk_dir, dry_run=dry_run) + + # install/upgrade platform-tools + if not (android_sdk_dir / "platform-tools").exists(): + print("Installing/Updating Android platform-tools") + sdk_manager.install("platform-tools", accept_license=accept_license, + show_stdout=show_output) + # The --update command is only relevant for platform tools + if not skip_update: + sdk_manager.install("--update", show_stdout=show_output) + + # install/upgrade build-tools + buildtools_dir = android_sdk_dir / "build-tools" + + if not buildtools_dir.exists(): + latest_build_tools_v = find_latest_buildtools_version(sdk_manager=sdk_manager) + print(f"Installing Android build-tools version {latest_build_tools_v}") + sdk_manager.install(f"build-tools;{latest_build_tools_v}", show_stdout=show_output) + else: + if not skip_update: + latest_build_tools_v = find_latest_buildtools_version(sdk_manager=sdk_manager) + installed_build_tools_v = find_installed_buildtools_version(buildtools_dir) + if latest_build_tools_v > installed_build_tools_v: + print(f"Updating Android build-tools version to {latest_build_tools_v}") + sdk_manager.install(f"build-tools;{latest_build_tools_v}", show_stdout=show_output) + installed_build_tools_v = latest_build_tools_v + + # install the platform API + platform_api_dir = android_sdk_dir / "platforms" / f"android-{android_api}" + if not platform_api_dir.exists(): + print(f"Installing Android platform API {android_api}") + sdk_manager.install(f"platforms;android-{android_api}", show_stdout=show_output) + + print("Android packages installation done") diff --git a/tools/cross_compile_android/main.py b/tools/cross_compile_android/main.py new file mode 100644 index 000000000..3a4ef8f67 --- /dev/null +++ b/tools/cross_compile_android/main.py @@ -0,0 +1,310 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +from __future__ import annotations + +import sys +import logging +import argparse +import stat +import warnings +import shutil +from dataclasses import dataclass + +from pathlib import Path +from git import Repo, RemoteProgress +from tqdm import tqdm +from jinja2 import Environment, FileSystemLoader + +from android_utilities import (run_command, download_android_commandlinetools, + download_android_ndk, install_android_packages) + +# Note: Does not work with PyEnv. Your Host Python should contain openssl. +# also update the version in ShibokenHelpers.cmake if Python version changes. +PYTHON_VERSION = "3.11" + +SKIP_UPDATE_HELP = ("skip the updation of SDK packages build-tools, platform-tools to" + " latest version") + +ACCEPT_LICENSE_HELP = (''' +Accepts license automatically for Android SDK installation. Otherwise, +accept the license manually through command line. +''') + +CLEAN_CACHE_HELP = (''' +Cleans cache stored in $HOME/.pyside6_deploy_cache. +Options: + +1. all - all the cache including Android Ndk, Android Sdk and Cross-compiled Python are deleted. +2. ndk - Only the Android Ndk is deleted. +3. sdk - Only the Android Sdk is deleted. +4. python - The cross compiled Python for all platforms, the cloned CPython, the cross compilation + scripts for all platforms are deleted. +5. toolchain - The CMake toolchain file required for cross-compiling Qt for Python, for all + platforms are deleted. + +If --clean-cache is used and no explicit value is suppied, then `all` is used as default. +''') + + +@dataclass +class PlatformData: + plat_name: str + api_level: str + android_abi: str + qt_plat_name: str + gcc_march: str + plat_bits: str + + +def occp_exists(): + ''' + check if '--only-cross-compile-python' exists in command line arguments + ''' + return "-occp" in sys.argv or "--only-cross-compile-python" in sys.argv + + +class CloneProgress(RemoteProgress): + def __init__(self): + super().__init__() + self.pbar = tqdm() + + def update(self, op_code, cur_count, max_count=None, message=""): + self.pbar.total = max_count + self.pbar.n = cur_count + self.pbar.refresh() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="This tool cross builds CPython for Android and uses that Python to cross build" + "Android Qt for Python wheels", + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument("-p", "--plat-name", type=str, nargs="*", + choices=["aarch64", "armv7a", "i686", "x86_64"], + default=["aarch64", "armv7a", "i686", "x86_64"], dest="plat_names", + help="Android target platforms") + + parser.add_argument("-v", "--verbose", help="run in verbose mode", action="store_const", + dest="loglevel", const=logging.INFO) + parser.add_argument("--api-level", type=str, default="26", + help="Minimum Android API level to use") + parser.add_argument("--ndk-path", type=str, help="Path to Android NDK (Preferred r25c)") + # sdk path is needed to compile all the Qt Java Acitivity files into Qt6AndroidBindings.jar + parser.add_argument("--sdk-path", type=str, help="Path to Android SDK") + parser.add_argument("--qt-install-path", type=str, required=not occp_exists(), + help="Qt installation path eg: /home/Qt/6.5.0") + + parser.add_argument("-occp", "--only-cross-compile-python", action="store_true", + help="Only cross compiles Python for the specified Android platform") + + parser.add_argument("--dry-run", action="store_true", help="show the commands to be run") + + parser.add_argument("--skip-update", action="store_true", + help=SKIP_UPDATE_HELP) + + parser.add_argument("--auto-accept-license", action="store_true", + help=ACCEPT_LICENSE_HELP) + + parser.add_argument("--clean-cache", type=str, nargs="?", const="all", + choices=["all", "python", "ndk", "sdk", "toolchain"], + help=CLEAN_CACHE_HELP) + + args = parser.parse_args() + + logging.basicConfig(level=args.loglevel) + pyside_setup_dir = Path(__file__).parents[2].resolve() + qt_install_path = args.qt_install_path + ndk_path = args.ndk_path + sdk_path = args.sdk_path + only_py_cross_compile = args.only_cross_compile_python + android_abi = None + gcc_march = None + plat_bits = None + dry_run = args.dry_run + plat_names = args.plat_names + api_level = args.api_level + skip_update = args.skip_update + auto_accept_license = args.auto_accept_license + clean_cache = args.clean_cache + + # auto download Android NDK and SDK + pyside6_deploy_cache = Path.home() / ".pyside6_android_deploy" + logging.info(f"Cache created at {str(pyside6_deploy_cache.resolve())}") + pyside6_deploy_cache.mkdir(exist_ok=True) + + if pyside6_deploy_cache.exists() and clean_cache: + if clean_cache == "all": + shutil.rmtree(pyside6_deploy_cache) + elif clean_cache == "ndk": + cached_ndk_dir = pyside6_deploy_cache / "android-ndk" + if cached_ndk_dir.exists(): + shutil.rmtree(cached_ndk_dir) + elif clean_cache == "sdk": + cached_sdk_dir = pyside6_deploy_cache / "android-sdk" + if cached_sdk_dir.exists(): + shutil.rmtree(cached_sdk_dir) + elif clean_cache == "python": + cached_cpython_dir = pyside6_deploy_cache / "cpython" + if cached_cpython_dir.exists(): + shutil.rmtree(pyside6_deploy_cache / "cpython") + for cc_python_path in pyside6_deploy_cache.glob("Python-*"): + if cc_python_path.is_dir(): + shutil.rmtree(cc_python_path) + elif clean_cache == "toolchain": + for toolchain_path in pyside6_deploy_cache.glob("toolchain_*"): + if toolchain_path.is_file(): + toolchain_path.unlink() + + if not ndk_path: + # Download android ndk + ndk_path = download_android_ndk(pyside6_deploy_cache) + + if not sdk_path: + # download and unzip command-line tools + sdk_path = download_android_commandlinetools(pyside6_deploy_cache) + # install and update required android packages + install_android_packages(android_sdk_dir=sdk_path, android_api=api_level, dry_run=dry_run, + accept_license=auto_accept_license, skip_update=skip_update) + + templates_path = Path(__file__).parent / "templates" + + for plat_name in plat_names: + # for armv7a the API level dependent binaries like clang are named + # armv7a-linux-androideabi27-clang, as opposed to other platforms which + # are named like x86_64-linux-android27-clang + platform_data = None + if plat_name == "armv7a": + platform_data = PlatformData("armv7a", api_level, "armeabi-v7a", "armv7", + "armv7", "32") + elif plat_name == "aarch64": + platform_data = PlatformData("aarch64", api_level, "arm64-v8a", "arm64_v8a", "armv8-a", + "64") + elif plat_name == "i686": + platform_data = PlatformData("i686", api_level, "x86", "x86", "i686", "32") + else: # plat_name is x86_64 + platform_data = PlatformData("x86_64", api_level, "x86_64", "x86_64", "x86-64", "64") + + # python path is valid, if Python for android installation exists in python_path + python_path = (pyside6_deploy_cache + / f"Python-{platform_data.plat_name}-linux-android" / "_install") + valid_python_path = python_path.exists() + if Path(python_path).exists(): + expected_dirs = ["lib", "include"] + for expected_dir in expected_dirs: + if not (Path(python_path) / expected_dir).is_dir(): + valid_python_path = False + warnings.warn( + f"{str(python_path.resolve())} is corrupted. New Python for {plat_name} " + f"android will be cross-compiled into {str(pyside6_deploy_cache.resolve())}" + ) + break + + environment = Environment(loader=FileSystemLoader(templates_path)) + if not valid_python_path: + # clone cpython and checkout 3.10 + cpython_dir = pyside6_deploy_cache / "cpython" + python_ccompile_script = cpython_dir / f"cross_compile_{plat_name}.sh" + + if not cpython_dir.exists(): + logging.info(f"cloning cpython {PYTHON_VERSION}") + Repo.clone_from( + "https://github.com/python/cpython.git", + cpython_dir, + progress=CloneProgress(), + branch=PYTHON_VERSION, + ) + + if not python_ccompile_script.exists(): + host_system_config_name = run_command("./config.guess", cwd=cpython_dir, + dry_run=dry_run, show_stdout=True, + capture_stdout=True).strip() + + # use jinja2 to create cross_compile.sh script + template = environment.get_template("cross_compile.tmpl.sh") + content = template.render( + plat_name=platform_data.plat_name, + ndk_path=ndk_path, + api_level=platform_data.api_level, + android_py_install_path_prefix=pyside6_deploy_cache, + host_python_path=sys.executable, + python_version=PYTHON_VERSION, + host_system_name=host_system_config_name, + host_platform_name=sys.platform + ) + + logging.info(f"Writing Python cross compile script into {python_ccompile_script}") + with open(python_ccompile_script, mode="w", encoding="utf-8") as ccompile_script: + ccompile_script.write(content) + + # give run permission to cross compile script + python_ccompile_script.chmod(python_ccompile_script.stat().st_mode | stat.S_IEXEC) + + # clean built files + logging.info("Cleaning CPython built files") + run_command(["make", "distclean"], cwd=cpython_dir, dry_run=dry_run, ignore_fail=True) + + # run the cross compile script + logging.info(f"Running Python cross-compile for platform {platform_data.plat_name}") + run_command([f"./{python_ccompile_script.name}"], cwd=cpython_dir, dry_run=dry_run, + show_stdout=True) + + logging.info( + f"Cross compile Python for Android platform {platform_data.plat_name}. " + f"Final installation in {python_path}" + ) + + if only_py_cross_compile: + continue + + if only_py_cross_compile: + requested_platforms = ",".join(plat_names) + print(f"Python for Android platforms: {requested_platforms} cross compiled " + f"to {str(pyside6_deploy_cache)}") + sys.exit(0) + + qfp_toolchain = pyside6_deploy_cache / f"toolchain_{platform_data.plat_name}.cmake" + + if not qfp_toolchain.exists(): + template = environment.get_template("toolchain_default.tmpl.cmake") + content = template.render( + ndk_path=ndk_path, + sdk_path=sdk_path, + api_level=platform_data.api_level, + qt_install_path=qt_install_path, + plat_name=platform_data.plat_name, + android_abi=platform_data.android_abi, + qt_plat_name=platform_data.qt_plat_name, + gcc_march=platform_data.gcc_march, + plat_bits=platform_data.plat_bits, + python_version=PYTHON_VERSION, + target_python_path=python_path + ) + + logging.info(f"Writing Qt for Python toolchain file into {qfp_toolchain}") + with open(qfp_toolchain, mode="w", encoding="utf-8") as ccompile_script: + ccompile_script.write(content) + + # give run permission to cross compile script + qfp_toolchain.chmod(qfp_toolchain.stat().st_mode | stat.S_IEXEC) + + if sys.platform == "linux": + host_qt_install_suffix = "gcc_64" + elif sys.platform == "darwin": + host_qt_install_suffix = "macos" + else: + raise RuntimeError("Qt for Python cross compilation not supported on this platform") + + # run the cross compile script + logging.info(f"Running Qt for Python cross-compile for platform {platform_data.plat_name}") + qfp_ccompile_cmd = [sys.executable, "setup.py", "bdist_wheel", "--parallel=9", + "--standalone", + f"--cmake-toolchain-file={str(qfp_toolchain.resolve())}", + f"--qt-host-path={qt_install_path}/{host_qt_install_suffix}", + f"--plat-name=android_{platform_data.plat_name}", + f"--python-target-path={python_path}", + (f"--qt-target-path={qt_install_path}/" + f"android_{platform_data.qt_plat_name}"), + "--no-qt-tools"] + run_command(qfp_ccompile_cmd, cwd=pyside_setup_dir, dry_run=dry_run, show_stdout=True) diff --git a/tools/cross_compile_android/requirements.txt b/tools/cross_compile_android/requirements.txt new file mode 100644 index 000000000..62e8ee3b0 --- /dev/null +++ b/tools/cross_compile_android/requirements.txt @@ -0,0 +1,3 @@ +gitpython +Jinja2 +tqdm diff --git a/tools/cross_compile_android/templates/cross_compile.tmpl.sh b/tools/cross_compile_android/templates/cross_compile.tmpl.sh new file mode 100644 index 000000000..784e822ca --- /dev/null +++ b/tools/cross_compile_android/templates/cross_compile.tmpl.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +set -x -e +export HOST_ARCH={{ plat_name }}-linux-android +export TOOLCHAIN={{ ndk_path }}/toolchains/llvm/prebuilt/{{ host_platform_name }}-x86_64/bin +export TOOL_PREFIX=$TOOLCHAIN/$HOST_ARCH +export PLATFORM_API={{ api_level }} +{% if plat_name == "armv7a" -%} +export CXX=${TOOL_PREFIX}eabi${PLATFORM_API}-clang++ +export CPP="${TOOL_PREFIX}eabi${PLATFORM_API}-clang++ -E" +export CC=${TOOL_PREFIX}eabi${PLATFORM_API}-clang +{% else %} +export CXX=${TOOL_PREFIX}${PLATFORM_API}-clang++ +export CPP="${TOOL_PREFIX}${PLATFORM_API}-clang++ -E" +export CC=${TOOL_PREFIX}${PLATFORM_API}-clang +{% endif %} +export AR=$TOOLCHAIN/llvm-ar +export RANLIB=$TOOLCHAIN/llvm-ranlib +export LD=$TOOLCHAIN/ld +export READELF=$TOOLCHAIN/llvm-readelf +export CFLAGS='-fPIC -DANDROID' +./configure --host=$HOST_ARCH --target=$HOST_ARCH --build={{ host_system_name }} \ +--with-build-python={{ host_python_path }} --enable-shared \ +--enable-ipv6 ac_cv_file__dev_ptmx=yes ac_cv_file__dev_ptc=no --without-ensurepip \ +ac_cv_little_endian_double=yes +make BLDSHARED="$CC -shared" CROSS-COMPILE=$TOOL_PREFIX- CROSS_COMPILE_TARGET=yes \ +INSTSONAME=libpython{{ python_version }}.so +make install prefix={{ android_py_install_path_prefix }}/Python-$HOST_ARCH/_install diff --git a/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake b/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake new file mode 100644 index 000000000..3c9752f43 --- /dev/null +++ b/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake @@ -0,0 +1,73 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +# toolchain file to cross compile Qt for Python wheels for Android +cmake_minimum_required(VERSION 3.23) +include_guard(GLOBAL) +set(CMAKE_SYSTEM_NAME Android) +{% if plat_name == "armv7a" -%} +set(CMAKE_SYSTEM_PROCESSOR armv7-a) +{% else %} +set(CMAKE_SYSTEM_PROCESSOR {{ plat_name }}) +{% endif %} +set(CMAKE_ANDROID_API {{ api_level }}) +set(CMAKE_ANDROID_NDK {{ ndk_path }}) +set(CMAKE_ANDROID_ARCH_ABI {{ android_abi }}) +set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang) +set(CMAKE_ANDROID_STL_TYPE c++_shared) +if(NOT DEFINED ANDROID_PLATFORM AND NOT DEFINED ANDROID_NATIVE_API_LEVEL) + set(ANDROID_PLATFORM "android-{{ api_level }}" CACHE STRING "") +endif() +set(ANDROID_SDK_ROOT {{ sdk_path }}) +{% if plat_name == "armv7a" -%} +set(_TARGET_NAME_ENDING "eabi{{ api_level }}") +{% else %} +set(_TARGET_NAME_ENDING "{{ api_level }}") +{% endif %} +set(QT_COMPILER_FLAGS "--target={{ plat_name }}-linux-android${_TARGET_NAME_ENDING} \ + -fomit-frame-pointer \ + -march={{ gcc_march }} \ + -msse4.2 \ + -mpopcnt \ + -m{{ plat_bits }} \ + -fPIC \ + -I{{ target_python_path }}/include/python{{ python_version }} \ + -Wno-unused-command-line-argument") +set(QT_COMPILER_FLAGS_RELEASE "-O2 -pipe") + +# FIXME +# https://gitlab.kitware.com/cmake/cmake/-/issues/23670 +# The CMake Android toolchain does not allow RPATHS. Hence CMAKE_INSTALL_RPATH does not work. +# Currently the linker flags are set directly as -Wl,-rpath='$ORIGIN' -Wl,-rpath='$ORIGIN/Qt/lib' +# set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +# set(CMAKE_INSTALL_RPATH "$ORIGIN") + +set(QT_LINKER_FLAGS "-Wl,-O1 -Wl,--hash-style=gnu -Wl,-rpath='$ORIGIN' -Wl,-rpath='$ORIGIN/Qt/lib' \ + -Wl,--as-needed -L{{ qt_install_path }}/android_{{ qt_plat_name }}/lib \ + -L{{ qt_install_path }}/android_{{ qt_plat_name }}/plugins/platforms \ + -L{{ target_python_path }}/lib \ + -lpython{{ python_version }}") +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +add_compile_definitions(ANDROID) + +include(CMakeInitializeConfigs) +function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING) + if (_PREFIX MATCHES "CMAKE_(C|CXX|ASM)_FLAGS") + set(CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT "${QT_COMPILER_FLAGS}") + foreach (config DEBUG RELEASE MINSIZEREL RELWITHDEBINFO) + if (DEFINED QT_COMPILER_FLAGS_${config}) + set(CMAKE_${CMAKE_MATCH_1}_FLAGS_${config}_INIT "${QT_COMPILER_FLAGS_${config}}") + endif() + endforeach() + endif() + if (_PREFIX MATCHES "CMAKE_(SHARED|MODULE|EXE)_LINKER_FLAGS") + foreach (config SHARED MODULE EXE) + set(CMAKE_${config}_LINKER_FLAGS_INIT "${QT_LINKER_FLAGS}") + endforeach() + endif() + _cmake_initialize_per_config_variable(${ARGV}) +endfunction() |