diff options
| -rw-r--r-- | tools/cross_compile_android/main.py | 106 | ||||
| -rw-r--r-- | tools/cross_compile_android/templates/toolchain_default.tmpl.cmake | 56 |
2 files changed, 144 insertions, 18 deletions
diff --git a/tools/cross_compile_android/main.py b/tools/cross_compile_android/main.py index 5fc0fad0f..a3cad74ec 100644 --- a/tools/cross_compile_android/main.py +++ b/tools/cross_compile_android/main.py @@ -8,7 +8,7 @@ import tempfile import subprocess import stat import warnings - +from dataclasses import dataclass from typing import List from pathlib import Path @@ -16,9 +16,20 @@ from git import Repo, RemoteProgress from tqdm import tqdm from jinja2 import Environment, FileSystemLoader +# Note: Does not work with PyEnv. Your Host Python should contain openssl. PYTHON_VERSION = "3.10" +@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 @@ -37,8 +48,12 @@ class CloneProgress(RemoteProgress): self.pbar.refresh() -def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False): - ex = subprocess.call(command, cwd=cwd, shell=True) +def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False, + dry_run: bool = False): + if dry_run: + print(" ".join(command)) + return + ex = subprocess.call(command, cwd=cwd) if ex != 0 and not ignore_fail: sys.exit(ex) @@ -56,10 +71,11 @@ if __name__ == "__main__": 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="27", help="Android API level to use") - parser.add_argument( - "--ndk-path", type=str, required=True, help="Path to Android NDK (Preferred 23b)" - ) + parser.add_argument("--api-level", type=str, default="31", help="Android API level to use") + parser.add_argument("--ndk-path", type=str, required=True, + help="Path to Android NDK (Preferred 25b)") + 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") @@ -77,13 +93,23 @@ if __name__ == "__main__": android wheels are created. ''') + parser.add_argument("--dry-run", action="store_true", help="show the commands to be run") + args = parser.parse_args() logging.basicConfig(level=args.loglevel) - current_dir = Path.cwd() + pyside_setup_dir = Path(__file__).parents[2].resolve() + qt_install_path = args.qt_install_path ndk_path = args.ndk_path only_py_cross_compile = args.only_cross_compile_python python_path = args.android_python_install_path + # the same android platforms are named differently in CMake, Cpython and Qt. + # Hence, we need to distinguish them + qt_plat_name = None + android_abi = None + gcc_march = None + plat_bits = None + dry_run = args.dry_run # python path is valid, if Python for android installation exists in python_path valid_python_path = True @@ -106,11 +132,20 @@ if __name__ == "__main__": # 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": - api_level = f"eabi{api_level}" + platform_data = PlatformData("armv7a", f"eabi{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") # clone cpython and checkout 3.10 with tempfile.TemporaryDirectory() as temp_dir: + environment = Environment(loader=FileSystemLoader(templates_path)) temp_dir = Path(temp_dir) logging.info(f"temp dir created at {temp_dir}") if not python_path or not valid_python_path: @@ -131,12 +166,11 @@ if __name__ == "__main__": android_py_install_path_prefix = python_path # use jinja2 to create cross_compile.sh script - environment = Environment(loader=FileSystemLoader(templates_path)) template = environment.get_template("cross_compile.tmpl.sh") content = template.render( - plat_name=plat_name, + plat_name=platform_data.plat_name, ndk_path=ndk_path, - api_level=api_level, + api_level=platform_data.api_level, android_py_install_path_prefix=android_py_install_path_prefix, ) @@ -148,23 +182,59 @@ if __name__ == "__main__": python_ccompile_script.chmod(python_ccompile_script.stat().st_mode | stat.S_IEXEC) # run the cross compile script - logging.info(f"Running Python cross-compile for platform {plat_name}") - run_command(["./cross_compile.sh"], cwd=cpython_dir) + logging.info(f"Running Python cross-compile for platform {platform_data.plat_name}") + run_command(["./cross_compile.sh"], cwd=cpython_dir, dry_run=dry_run) - python_path = (f"{android_py_install_path_prefix}/Python-{plat_name}-linux-android/" + python_path = (f"{android_py_install_path_prefix}/Python-{platform_data.plat_name}-linux-android/" "_install") # run patchelf to change the SONAME of libpython from libpython3.x.so.1.0 to # libpython3.x.so, to match with python_for_android's Python library. Otherwise, # the Qfp binaries won't be able to link to Python - run_command(["patchelf", "--set-soname", f"libpython{PYTHON_VERSION}.so.1.0", - f"libpython{PYTHON_VERSION}.so"], cwd=Path(python_path) / "lib") + run_command(["patchelf", "--set-soname", f"libpython{PYTHON_VERSION}.so", + f"libpython{PYTHON_VERSION}.so.1.0"], cwd=Path(python_path) / "lib") logging.info( - f"Cross compile Python for Android platform {plat_name}." + f"Cross compile Python for Android platform {platform_data.plat_name}. " f"Final installation in " f"{python_path}" ) if only_py_cross_compile: sys.exit(0) + + qfp_toolchain = temp_dir / f"toolchain_{platform_data.plat_name}.cmake" + template = environment.get_template("toolchain_default.tmpl.cmake") + content = template.render( + ndk_path=ndk_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" + f"{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) + + # 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", + "--ignore-git", "--standalone", "--limited-api=yes", + f"--cmake-toolchain-file={str(qfp_toolchain.resolve())}", + f"--qt-host-path={qt_install_path}/gcc_64", + 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", "--skip-docs", "--no-examples"] + run_command(qfp_ccompile_cmd, cwd=pyside_setup_dir, dry_run=dry_run) 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..cc73cf485 --- /dev/null +++ b/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake @@ -0,0 +1,56 @@ +# 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.18) +include_guard(GLOBAL) +set(CMAKE_SYSTEM_NAME Android) +set(CMAKE_SYSTEM_PROCESSOR {{ plat_name }}) +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(QT_COMPILER_FLAGS "--target={{ plat_name }}-linux-android{{ api_level }} \ + -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") +set(QT_LINKER_FLAGS "-Wl,-O1 -Wl,--hash-style=gnu -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() |
