aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--sources/pyside-tools/android_deploy.py5
-rw-r--r--sources/pyside-tools/deploy_lib/config.py24
-rw-r--r--tools/cross_compile_android/android_utilities.py256
-rw-r--r--tools/cross_compile_android/main.py86
4 files changed, 335 insertions, 36 deletions
diff --git a/sources/pyside-tools/android_deploy.py b/sources/pyside-tools/android_deploy.py
index f10834c12..fbc613069 100644
--- a/sources/pyside-tools/android_deploy.py
+++ b/sources/pyside-tools/android_deploy.py
@@ -237,14 +237,13 @@ if __name__ == "__main__":
help=f"Path to shiboken{MAJOR_VERSION} Android Wheel",
required=not config_option_exists())
- #TODO: --ndk-path and --sdk-path will be removed when automatic download of sdk and ndk is added
parser.add_argument("--ndk-path", type=lambda p: Path(p).resolve(),
help=("Path to Android Ndk. If omitted, the default from buildozer is used")
- , required=True)
+ )
parser.add_argument("--sdk-path", type=lambda p: Path(p).resolve(),
help=("Path to Android Sdk. If omitted, the default from buildozer is used")
- , required=True)
+ )
parser.add_argument("--extra-ignore-dirs", type=str, help=HELP_EXTRA_IGNORE_DIRS)
diff --git a/sources/pyside-tools/deploy_lib/config.py b/sources/pyside-tools/deploy_lib/config.py
index af019a093..8af341bad 100644
--- a/sources/pyside-tools/deploy_lib/config.py
+++ b/sources/pyside-tools/deploy_lib/config.py
@@ -12,6 +12,9 @@ from .commands import run_qmlimportscanner
# Some QML plugins like QtCore are excluded from this list as they don't contribute much to
# executable size. Excluding them saves the extra processing of checking for them in files
EXCLUDED_QML_PLUGINS = {"QtQuick", "QtQuick3D", "QtCharts", "QtWebEngine", "QtTest", "QtSensors"}
+# TODO: Move this to android module. Fix circular import.
+ANDROID_NDK_VERSION = "25c"
+ANDROID_DEPLOY_CACHE = Path.home() / ".pyside6_android_deploy"
class BaseConfig:
@@ -128,13 +131,30 @@ class Config(BaseConfig):
self.ndk_path = android_data.ndk_path
else:
ndk_path_temp = self.get_value("buildozer", "ndk_path")
- self.ndk_path = Path(ndk_path_temp) if ndk_path_temp else None
+ if ndk_path_temp:
+ self.ndk_path = Path(ndk_path_temp)
+ else:
+ self.ndk_path = (ANDROID_DEPLOY_CACHE / "android-ndk"
+ / f"android-ndk-r{ANDROID_NDK_VERSION}")
+ if not self.ndk_path.exists():
+ logging.info("[DEPLOY] Use default NDK from buildoer")
+
+ if self.ndk_path:
+ print(f"Using Android NDK: {str(self.ndk_path)}")
if android_data.sdk_path:
self.sdk_path = android_data.sdk_path
else:
sdk_path_temp = self.get_value("buildozer", "sdk_path")
- self.sdk_path = Path(sdk_path_temp) if sdk_path_temp else None
+ if sdk_path_temp:
+ self.sdk_path = Path(sdk_path_temp)
+ else:
+ self.sdk_path = ANDROID_DEPLOY_CACHE / "android-sdk"
+ if not self.sdk_path.exists():
+ logging.info("[DEPLOY] Use default SDK from buildozer")
+
+ if self.sdk_path:
+ print(f"Using Android SDK: {str(self.sdk_path)}")
recipe_dir_temp = self.get_value("buildozer", "recipe_dir")
self.recipe_dir = Path(recipe_dir_temp) if recipe_dir_temp else None
diff --git a/tools/cross_compile_android/android_utilities.py b/tools/cross_compile_android/android_utilities.py
new file mode 100644
index 000000000..e93631173
--- /dev/null
+++ b/tools/cross_compile_android/android_utilities.py
@@ -0,0 +1,256 @@
+# 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
+
+import logging
+import shutil
+import os
+import stat
+import sys
+import subprocess
+
+from urllib import request
+from pathlib import Path
+from typing import List
+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 = "25c"
+
+
+def run_command(command: List[str], cwd: str = 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 _unpack(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, zip_file, "-d", destination]
+ run_command(command=command, show_stdout=True)
+
+
+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 headers["Content-Type"] == "application/zip"
+ 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_zip_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}-linux.zip"
+ ndk_version_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}"
+
+ 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/android-ndk-r{ANDROID_NDK_VERSION}-linux.zip"
+
+ print(f"Downloading Android Ndk version r{ANDROID_NDK_VERSION}")
+ _download(url=url, destination=ndk_zip_path)
+
+ print("Unpacking Android Ndk")
+ _unpack(zip_file=(ndk_path /
+ f"android-ndk-r{ANDROID_NDK_VERSION}-linux.zip"),
+ destination=ndk_path)
+
+ return ndk_version_path
+
+
+def download_android_commandlinetools(android_sdk_dir: Path):
+ """
+ Downloads Android commandline tools into cltools_path.
+ """
+ android_sdk_dir = android_sdk_dir / "android-sdk"
+ url = ("https://dl.google.com/android/repository/"
+ f"commandlinetools-linux-{DEFAULT_SDK_TAG}_latest.zip")
+ cltools_zip_path = android_sdk_dir / f"commandlinetools-linux-{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-linux-{DEFAULT_SDK_TAG}_latest.zip")
+ _download(url=url, destination=cltools_zip_path)
+
+ print("Unpacking Android Command Line Tools")
+ _unpack(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')
+
+ 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
index 073fe74e8..97f4030ac 100644
--- a/tools/cross_compile_android/main.py
+++ b/tools/cross_compile_android/main.py
@@ -16,9 +16,31 @@ 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.
PYTHON_VERSION = "3.10"
+APIC_HELP = ('''
+Points to the installation path of Python for the specific Android
+platform. If the path given does not exist, then Python for Android
+is cross compiled for the specific platform and installed into this
+path as <path>/Python-'plat_name'/_install.
+
+If this path is not given, then Python for Android is cross-compiled
+into a temportary directory, which is deleted when the Qt for Python
+Android wheels are created.
+''')
+
+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.
+''')
+
@dataclass
class PlatformData:
@@ -48,20 +70,10 @@ class CloneProgress(RemoteProgress):
self.pbar.refresh()
-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)
-
-
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",
+ description="This tool cross builds CPython for Android and uses that Python to cross build"
+ "Android Qt for Python wheels",
formatter_class=argparse.RawTextHelpFormatter,
)
@@ -72,11 +84,9 @@ 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="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("--ndk-path", type=str, help="Path to Android NDK (Preferred 25b)")
# sdk path is needed to compile all the Qt Java Acitivity files into Qt6AndroidBindings.jar
- parser.add_argument("--sdk-path", type=str, required=True,
- help="Path to Android SDK")
+ 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")
@@ -85,19 +95,16 @@ if __name__ == "__main__":
parser.add_argument("-apic", "--android-python-install-path", type=str, default=None,
required=occp_exists(),
- help='''
- Points to the installation path of Python for the specific Android
- platform. If the path given does not exist, then Python for android
- is cross compiled for the specific platform and installed into this
- path as <path>/Python-'plat_name'/_install.
-
- If this path is not given, then Python for android is cross-compiled
- into a temportary directory, which is deleted when the Qt for Python
- android wheels are created.
- ''')
+ help=APIC_HELP)
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)
+
args = parser.parse_args()
logging.basicConfig(level=args.loglevel)
@@ -114,6 +121,24 @@ if __name__ == "__main__":
gcc_march = None
plat_bits = None
dry_run = args.dry_run
+ plat_name = args.plat_name
+ api_level = args.api_level
+ skip_update = args.skip_update
+ auto_accept_license = args.auto_accept_license
+
+ # auto download Android NDK and SDK
+ pyside6_deploy_cache = Path.home() / ".pyside6_android_deploy"
+
+ 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)
# python path is valid, if Python for android installation exists in python_path
valid_python_path = True
@@ -130,8 +155,6 @@ if __name__ == "__main__":
break
templates_path = Path(__file__).parent / "templates"
- plat_name = args.plat_name
- api_level = args.api_level
# for armv7a the API level dependent binaries like clang are named
# armv7a-linux-androideabi27-clang, as opposed to other platforms which
@@ -187,7 +210,7 @@ if __name__ == "__main__":
# run the cross compile script
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)
+ run_command(["./cross_compile.sh"], cwd=cpython_dir, dry_run=dry_run, show_stdout=True)
python_path = (f"{android_py_install_path_prefix}/Python-{platform_data.plat_name}-linux-android/"
"_install")
@@ -196,7 +219,8 @@ if __name__ == "__main__":
# 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",
- f"libpython{PYTHON_VERSION}.so.1.0"], cwd=Path(python_path) / "lib")
+ f"libpython{PYTHON_VERSION}.so.1.0"], cwd=Path(python_path) / "lib",
+ dry_run=dry_run)
logging.info(
f"Cross compile Python for Android platform {platform_data.plat_name}. "
@@ -242,4 +266,4 @@ if __name__ == "__main__":
(f"--qt-target-path={qt_install_path}/"
f"android_{platform_data.qt_plat_name}"),
"--no-qt-tools", "--skip-docs", "--unity"]
- run_command(qfp_ccompile_cmd, cwd=pyside_setup_dir, dry_run=dry_run)
+ run_command(qfp_ccompile_cmd, cwd=pyside_setup_dir, dry_run=dry_run, show_stdout=True)