aboutsummaryrefslogtreecommitdiffstats
path: root/tools/cross_compile_android
diff options
context:
space:
mode:
Diffstat (limited to 'tools/cross_compile_android')
-rw-r--r--tools/cross_compile_android/android_utilities.py297
-rw-r--r--tools/cross_compile_android/main.py310
-rw-r--r--tools/cross_compile_android/requirements.txt3
-rw-r--r--tools/cross_compile_android/templates/cross_compile.tmpl.sh29
-rw-r--r--tools/cross_compile_android/templates/toolchain_default.tmpl.cmake73
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()