diff options
Diffstat (limited to 'sources/pyside-tools/deploy_lib/android/android_helper.py')
-rw-r--r-- | sources/pyside-tools/deploy_lib/android/android_helper.py | 151 |
1 files changed, 151 insertions, 0 deletions
diff --git a/sources/pyside-tools/deploy_lib/android/android_helper.py b/sources/pyside-tools/deploy_lib/android/android_helper.py new file mode 100644 index 000000000..7d2f5d575 --- /dev/null +++ b/sources/pyside-tools/deploy_lib/android/android_helper.py @@ -0,0 +1,151 @@ +# 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 zipfile +from dataclasses import dataclass +from pathlib import Path +from typing import List, Set +from zipfile import ZipFile + +from jinja2 import Environment, FileSystemLoader + +from .. import run_command + + +@dataclass +class AndroidData: + """ + Dataclass to store all the Android data obtained through cli + """ + wheel_pyside: Path + wheel_shiboken: Path + ndk_path: Path + sdk_path: Path + + +def create_recipe(version: str, component: str, wheel_path: str, generated_files_path: Path, + qt_modules: List[str] = None, local_libs: List[str] = None, + plugins: List[str] = None): + ''' + Create python_for_android recipe for PySide6 and shiboken6 + ''' + qt_plugins = [] + if plugins: + # split plugins based on category + for plugin in plugins: + plugin_category, plugin_name = plugin.split('_', 1) + qt_plugins.append((plugin_category, plugin_name)) + + qt_local_libs = [] + if local_libs: + qt_local_libs = [local_lib for local_lib in local_libs if local_lib.startswith("Qt6")] + + rcp_tmpl_path = Path(__file__).parent / "recipes" / f"{component}" + environment = Environment(loader=FileSystemLoader(rcp_tmpl_path)) + template = environment.get_template("__init__.tmpl.py") + content = template.render( + version=version, + wheel_path=wheel_path, + qt_modules=qt_modules, + qt_local_libs=qt_local_libs, + qt_plugins=qt_plugins + ) + + recipe_path = generated_files_path / "recipes" / f"{component}" + recipe_path.mkdir(parents=True, exist_ok=True) + logging.info(f"[DEPLOY] Writing {component} recipe into {str(recipe_path)}") + with open(recipe_path / "__init__.py", mode="w", encoding="utf-8") as recipe: + recipe.write(content) + + +def extract_and_copy_jar(wheel_path: Path, generated_files_path: Path) -> str: + ''' + extracts the PySide6 wheel and copies the 'jar' folder to 'generated_files_path'. + These .jar files are added to the buildozer.spec file to be used later by buildozer + ''' + jar_path = generated_files_path / "jar" + jar_path.mkdir(parents=True, exist_ok=True) + archive = ZipFile(wheel_path) + jar_files = [file for file in archive.namelist() if file.startswith("PySide6/jar")] + for file in jar_files: + archive.extract(file, jar_path) + return (jar_path / "PySide6" / "jar").resolve() if jar_files else None + + +def get_wheel_android_arch(wheel: Path): + ''' + Get android architecture from wheel + ''' + supported_archs = ["aarch64", "armv7a", "i686", "x86_64"] + for arch in supported_archs: + if arch in wheel.stem: + return arch + + return None + + +def get_llvm_readobj(ndk_path: Path) -> Path: + ''' + Return the path to llvm_readobj from the Android Ndk + ''' + # TODO: Requires change if Windows platform supports Android Deployment or if we + # support host other than linux-x86_64 + return (ndk_path / "toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readobj") + + +def find_lib_dependencies(llvm_readobj: Path, lib_path: Path, used_dependencies: Set[str] = None, + dry_run: bool = False): + """ + Find all the Qt dependencies of a library using llvm_readobj + """ + if lib_path.name in used_dependencies: + return + + used_dependencies.add(lib_path.name) + + command = [str(llvm_readobj), "--needed-libs", str(lib_path)] + + # even if dry_run is given, we need to run the actual command to see all the dependencies + # for which llvm-readelf is run. + if dry_run: + _, output = run_command(command=command, dry_run=dry_run, fetch_output=True) + _, output = run_command(command=command, dry_run=False, fetch_output=True) + + dependencies = set() + neededlibraries_found = False + for line in output.splitlines(): + line = line.decode("utf-8").lstrip() + if line.startswith("NeededLibraries") and not neededlibraries_found: + neededlibraries_found = True + if neededlibraries_found and line.startswith("libQt"): + dependencies.add(line) + used_dependencies.add(line) + dependent_lib_path = lib_path.parent / line + find_lib_dependencies(llvm_readobj, dependent_lib_path, used_dependencies, dry_run) + + if dependencies: + logging.info(f"[DEPLOY] Following dependencies found for {lib_path.stem}: {dependencies}") + else: + logging.info(f"[DEPLOY] No Qt dependencies found for {lib_path.stem}") + + +def find_qtlibs_in_wheel(wheel_pyside: Path): + """ + Find the path to Qt/lib folder inside the wheel. + """ + archive = ZipFile(wheel_pyside) + qt_libs_path = wheel_pyside / "PySide6/Qt/lib" + qt_libs_path = zipfile.Path(archive, at=qt_libs_path) + if not qt_libs_path.exists(): + for file in archive.namelist(): + # the dependency files are inside the libs folder + if file.endswith("android-dependencies.xml"): + qt_libs_path = zipfile.Path(archive, at=file).parent + # all dependency files are in the same path + break + + if not qt_libs_path: + raise FileNotFoundError("[DEPLOY] Unable to find Qt libs folder inside the wheel") + + return qt_libs_path |