diff options
| author | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2023-02-08 16:58:39 +0100 |
|---|---|---|
| committer | Shyamnath Premnadh <shyamnath.premnadh@qt.io> | 2023-03-31 13:29:32 +0200 |
| commit | 95abfa776411b6d7cd4296adf63bc7abce2270b6 (patch) | |
| tree | 74e51c8c6d28044a60011158215919ab223d6fa5 /sources/pyside-tools/deploy_lib/android | |
| parent | 94b30c7207bea2467f0d7e41e99af27ecc749ed5 (diff) | |
Deployment: New pyside6-android-deploy tool
- Preliminary support for PySide6 Android deployment
- Uses jinja2 to create PySide6 and shiboken6 recipes, to be used
by buildozer when python_for_android builds the app distribution
- Classes for Buildozer config interaction
- Run deployment to android. Typical command looks like:
"""
pyside6-android-deploy
--wheel-pyside=./PySide6-6.5.0a1-6.5.0-cp37-abi3-android_x86_64.whl
--wheel-shiboken=./shiboken6-6.5.0a1-6.5.0-cp37-abi3-android_x86_64.whl
--name=stringlistmodel
"""
- New entrypoint for pyside6-android-deploy
- Helper functinos for Android Deployment
- Remove unused function main_py_exists()
- Added the new files to deploy.pyproject
- Remove dry_run argument from install_python_dependencies()
- new Python packages added in requirements.txt to enable the
deploy and cross compile tool
Note: python-for-android uses my local fork. This will be changed
once it is merged into python-for-android dev.
Task-number: PYSIDE-1612
Pick-to: 6.5
Change-Id: I7eb96fa5507a476b4e86ec0195a5e9869f0f85fd
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Diffstat (limited to 'sources/pyside-tools/deploy_lib/android')
5 files changed, 220 insertions, 0 deletions
diff --git a/sources/pyside-tools/deploy_lib/android/__init__.py b/sources/pyside-tools/deploy_lib/android/__init__.py new file mode 100644 index 000000000..12eb6830c --- /dev/null +++ b/sources/pyside-tools/deploy_lib/android/__init__.py @@ -0,0 +1,10 @@ +# 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 + +WIDGET_APPLICATION_MODULES = ["Core", "Gui", "Widgets"] +QUICK_APPLICATION_MODULES = ["Core", "Gui", "Widgets", "Network", "OpenGL", "Qml", "Quick", + "QuickControls2"] + +from .android_helper import (create_recipe, extract_and_copy_jar, get_wheel_android_arch, + AndroidData) +from .buildozer import Buildozer 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..b0c8254ab --- /dev/null +++ b/sources/pyside-tools/deploy_lib/android/android_helper.py @@ -0,0 +1,59 @@ +# 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 +from pathlib import Path +from jinja2 import Environment, FileSystemLoader +from zipfile import ZipFile +from dataclasses import dataclass + + +@dataclass +class AndroidData: + 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): + ''' + Create python_for_android recipe for PySide6 and shiboken6 + ''' + 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, + ) + + recipe_path = generated_files_path / "recipes" / f"{component}" + recipe_path.mkdir(parents=True, exist_ok=True) + logging.info(f"Writing {component} recipe into {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 later use 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 + + +def get_wheel_android_arch(wheel: str): + wheel = Path(wheel) + supported_archs = ["aarch64", "armv7a", "i686", "x86_64"] + for arch in supported_archs: + if arch in wheel.stem: + return arch + + return None diff --git a/sources/pyside-tools/deploy_lib/android/buildozer.py b/sources/pyside-tools/deploy_lib/android/buildozer.py new file mode 100644 index 000000000..91d55f4ca --- /dev/null +++ b/sources/pyside-tools/deploy_lib/android/buildozer.py @@ -0,0 +1,84 @@ +# 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 +from pathlib import Path +from .. import run_command, BaseConfig, Config + + +class BuildozerConfig(BaseConfig): + def __init__(self, buildozer_spec_file: Path, pysidedeploy_config: Config, dry_run: bool): + super().__init__(buildozer_spec_file, dry_run, comment_prefixes="#") + self.set_value("app", "title", pysidedeploy_config.title) + self.set_value("app", "package.name", pysidedeploy_config.title) + self.set_value("app", "package.domain", + f"org.{pysidedeploy_config.title}") + + include_exts = self.get_value("app", "source.include_exts") + include_exts = f"{include_exts},qml" + self.set_value("app", "source.include_exts", include_exts) + + self.set_value("app", "requirements", "python3,shiboken6,PySide6") + + if pysidedeploy_config.ndk_path: + self.set_value("app", "android.ndk_path", str(pysidedeploy_config.ndk_path)) + + if pysidedeploy_config.sdk_path: + self.set_value("app", "android.sdk_path", str(pysidedeploy_config.sdk_path)) + + self.set_value("app", "android.add_jars", f"{str(pysidedeploy_config.jars_dir)}/*.jar") + + platform_map = {"aarch64": "arm64-v8a", + "armv7a": "armeabi-v7a", + "i686": "x86", + "x86_64": "x86_64"} + arch = platform_map[pysidedeploy_config.arch] + self.set_value("app", "android.archs", arch) + + # p4a changes + logging.info("[DEPLOY] Using custom fork of python-for-android: " + "https://github.com/shyamnathp/python-for-android/tree/pyside_support") + self.set_value("app", "p4a.fork", "shyamnathp") + self.set_value("app", "p4a.branch", "pyside_support") + self.set_value('app', "p4a.local_recipes", str(pysidedeploy_config.recipe_dir)) + self.set_value("app", "p4a.bootstrap", "qt") + + modules = ",".join(pysidedeploy_config.modules) + local_libs = ",".join(pysidedeploy_config.local_libs) + extra_args = (f"--qt-libs={modules} --load-local-libs={local_libs}") + self.set_value("app", "p4a.extra_args", extra_args) + + # TODO: does not work atm. Seems like a bug with buildozer + # change buildozer build_dir + # self.set_value("buildozer", "build_dir", str(build_dir.relative_to(Path.cwd()))) + + # change final apk/aab path + self.set_value("buildozer", "bin_dir", str(pysidedeploy_config.exe_dir.resolve())) + + self.update_config() + + +class Buildozer: + dry_run = False + + @staticmethod + def initialize(pysidedeploy_config: Config): + project_dir = Path(pysidedeploy_config.project_dir) + buildozer_spec = project_dir / "buildozer.spec" + if buildozer_spec.exists(): + logging.warning(f"[DEPLOY] buildozer.spec already present in {str(project_dir)}." + "Using it") + return + + # creates buildozer.spec config file + command = ["buildozer", "init"] + run_command(command=command, dry_run=Buildozer.dry_run) + if not Buildozer.dry_run and not buildozer_spec.exists(): + raise RuntimeError(f"buildozer.spec not found in {Path.cwd()}") + BuildozerConfig(buildozer_spec, pysidedeploy_config, Buildozer.dry_run) + + @staticmethod + def create_executable(mode: str): + # build the application in release mode + command = ["buildozer", "android", mode] + run_command(command=command, dry_run=Buildozer.dry_run) diff --git a/sources/pyside-tools/deploy_lib/android/recipes/PySide6/__init__.tmpl.py b/sources/pyside-tools/deploy_lib/android/recipes/PySide6/__init__.tmpl.py new file mode 100644 index 000000000..289f2d62b --- /dev/null +++ b/sources/pyside-tools/deploy_lib/android/recipes/PySide6/__init__.tmpl.py @@ -0,0 +1,43 @@ +# 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 pythonforandroid.recipe import PythonRecipe +from pythonforandroid.logger import info +import zipfile +import shutil +from pathlib import Path + + +class PySideRecipe(PythonRecipe): + version = '{{ version }}' + wheel_path = '{{ wheel_path }}' + depends = ["shiboken6"] + call_hostpython_via_targetpython = False + install_in_hostpython = False + + def build_arch(self, arch): + """Unzip the wheel and copy into site-packages of target""" + + info("Installing {} into site-packages".format(self.name)) + with zipfile.ZipFile(self.wheel_path, "r") as zip_ref: + info("Unzip wheels and copy into {}".format(self.ctx.get_python_install_dir(arch.arch))) + zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch)) + + lib_dir = Path(f"{self.ctx.get_python_install_dir(arch.arch)}/PySide6/Qt/lib") + info("Copying Qt libraries to be loaded on startup") + shutil.copytree(lib_dir, self.ctx.get_libs_dir(arch.arch), dirs_exist_ok=True) + + info("Copying libc++_shared.so from SDK to be loaded on startup") + libcpp_path = f"{self.ctx.ndk.sysroot_lib_dir}/{arch.command_prefix}/libc++_shared.so" + shutil.copyfile(libcpp_path, Path(self.ctx.get_libs_dir(arch.arch)) / "libc++_shared.so") + + info("Copying Qt platform plugin to be loaded on startup from SDK to be loaded on startup") + shutil.copyfile( + Path(self.ctx.get_python_install_dir(arch.arch)) + / "PySide6" / "Qt" / "plugins" / "platforms" + / f"libplugins_platforms_qtforandroid_{arch.arch}.so", + Path(self.ctx.get_libs_dir(arch.arch)) / f"libplugins_platforms_qtforandroid_{arch.arch}.so", + ) + + +recipe = PySideRecipe() diff --git a/sources/pyside-tools/deploy_lib/android/recipes/shiboken6/__init__.tmpl.py b/sources/pyside-tools/deploy_lib/android/recipes/shiboken6/__init__.tmpl.py new file mode 100644 index 000000000..afe094cbd --- /dev/null +++ b/sources/pyside-tools/deploy_lib/android/recipes/shiboken6/__init__.tmpl.py @@ -0,0 +1,24 @@ +# 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 pythonforandroid.recipe import PythonRecipe +from pythonforandroid.logger import info +import zipfile + + +class ShibokenRecipe(PythonRecipe): + version = '{{ version }}' + wheel_path = '{{ wheel_path }}' + + call_hostpython_via_targetpython = False + install_in_hostpython = False + + def build_arch(self, arch): + ''' Unzip the wheel and copy into site-packages of target''' + info('Installing {} into site-packages'.format(self.name)) + with zipfile.ZipFile(self.wheel_path, 'r') as zip_ref: + info('Unzip wheels and copy into {}'.format(self.ctx.get_python_install_dir(arch.arch))) + zip_ref.extractall(self.ctx.get_python_install_dir(arch.arch)) + + +recipe = ShibokenRecipe() |
