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/android_deploy.py | |
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/android_deploy.py')
-rw-r--r-- | sources/pyside-tools/android_deploy.py | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/sources/pyside-tools/android_deploy.py b/sources/pyside-tools/android_deploy.py new file mode 100644 index 000000000..b3d09cd94 --- /dev/null +++ b/sources/pyside-tools/android_deploy.py @@ -0,0 +1,233 @@ +# 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 argparse +import sys +import logging +import shutil +import traceback +from pathlib import Path +from textwrap import dedent + +from pkginfo import Wheel + +from deploy_lib import (setup_python, get_config, cleanup, install_python_dependencies, + config_option_exists, MAJOR_VERSION) +from deploy_lib.android import (create_recipe, extract_and_copy_jar, get_wheel_android_arch, + Buildozer, AndroidData, WIDGET_APPLICATION_MODULES, + QUICK_APPLICATION_MODULES) + + +""" pyside6-android-deploy deployment tool + + Deployment tool that uses buildozer (https://buildozer.readthedocs.io/en/latest/) and + python-for-android (https://python-for-android.readthedocs.io/en/latest/) to deploy PySide6 + applications to Android + + How does it work? + + Command: pyside6-android-deploy --wheel-pyside=<pyside_wheel_path> + --wheel-shiboken=<shiboken_wheel_path> + --ndk-path=<optional_ndk_path> + --sdk-path=<optional_sdk_path> + pyside6-android-deploy android -c /path/to/pysidedeploy.spec + + + Note: If --ndk-path and --sdk-path are not specified, the defaults from buildozer are used + + Prerequisities: Python main entrypoint file should be named "main.py" + + Platforms Supported: aarch64, armv7a, i686, x86_64 + + Supported Modules: Core, Gui, Widgets, Network, OpenGL, Qml, Quick, QuickControls2 + + Config file: + On the first run of the tool, it creates a config file called pysidedeploy.spec which + controls the various characteristic of the deployment. Users can simply change the value + in this config file to achieve different properties ie. change the application name, + deployment platform etc. + + Note: This file is used by both pyside6-deploy and pyside6-android-deploy +""" + + +def main(name: str = None, pyside_wheel: Path = None, shiboken_wheel: Path = None, ndk_path: Path = None, + sdk_path: Path = None, config_file: Path = None, init: bool = False, + loglevel=logging.WARNING, dry_run: bool = False, keep_deployment_files: bool = False, + force: bool = False): + + logging.basicConfig(level=loglevel) + main_file = Path.cwd() / "main.py" + generated_files_path = None + if not main_file.exists(): + print(dedent(""" + [DEPLOY] For android deployment to work, the main entrypoint Python file should be named + 'main.py' + """)) + return + + # check if ndk and sdk path given, else use default + if ndk_path and sdk_path: + logging.warning("[DEPLOY] May not work with custom Ndk and Sdk versions." + "Use the default by leaving out --ndk-path and --sdk-path cl" + "arguments") + + android_data = AndroidData(wheel_pyside=pyside_wheel, wheel_shiboken=shiboken_wheel, + ndk_path=ndk_path, sdk_path=sdk_path) + + if config_file and Path(config_file).exists(): + config_file = Path(config_file).resolve() + + python = setup_python(dry_run=dry_run, force=force, init=init) + config = get_config(python_exe=python.exe, dry_run=dry_run, config_file=config_file, + main_file=main_file, android_data=android_data, is_android=True) + + if config.project_dir != Path.cwd(): + raise RuntimeError("[DEPLOY] For Android deployment, pyside6-deploy should be run from" + f"{config.project_dir}") + + if not config.wheel_pyside and not config.wheel_shiboken: + raise RuntimeError(f"[DEPLOY] No PySide{MAJOR_VERSION} and Shiboken{MAJOR_VERSION} wheels" + "found") + + source_file = config.project_dir / config.source_file + generated_files_path = source_file.parent / "deployment" + cleanup(generated_files_path=generated_files_path, config=config, is_android=True) + + install_python_dependencies(config=config, python=python, init=init, + packages="android_packages", is_android=True) + + # set application name + if name: + config.title = name + + try: + # create recipes + # https://python-for-android.readthedocs.io/en/latest/recipes/ + # These recipes are manually added through buildozer.spec file to be used by + # python_for_android while building the distribution + if not config.recipes_exist(): + logging.info("[DEPLOY] Creating p4a recipes for PySide6 and shiboken6") + version = Wheel(config.wheel_pyside).version + create_recipe(version=version, component=f"PySide{MAJOR_VERSION}", + wheel_path=config.wheel_pyside, + generated_files_path=generated_files_path) + create_recipe(version=version, component=f"shiboken{MAJOR_VERSION}", + wheel_path=config.wheel_shiboken, + generated_files_path=generated_files_path) + config.recipe_dir = (generated_files_path / "recipes").resolve() + + # extract out and copy .jar files to {generated_files_path} + if not config.jars_dir or not Path(config.jars_dir).exists(): + logging.info("[DEPLOY] Extract and copy jar files from PySide6 wheel to " + f"{generated_files_path}") + extract_and_copy_jar(wheel_path=config.wheel_pyside, + generated_files_path=generated_files_path) + config.jars_dir = (generated_files_path / "jar" / "PySide6" / "jar").resolve() + + # check which modules are needed + # TODO: Optimize this based on the modules needed + # check if other modules not supported by Android used and raise error + if not config.modules: + config.modules = (QUICK_APPLICATION_MODULES if config.qml_files else + WIDGET_APPLICATION_MODULES) + + # find architecture from wheel name + if not config.arch: + arch = get_wheel_android_arch(wheel=config.wheel_pyside) + if not arch: + logging.exception("[DEPLOY] PySide wheel corrupted. Wheel name should end with" + "platform name") + raise + config.arch = arch + + # writing config file + if not dry_run: + config.update_config() + + if init: + # config file created above. Exiting. + logging.info(f"[DEPLOY]: Config file {config.config_file} created") + return + + # TODO: include qml files from pysidedeploy.spec rather than from extensions + # buildozer currently includes all the files with .qml extension + + # init buildozer + Buildozer.dry_run = dry_run + logging.info("[DEPLOY] Creating buildozer.spec file") + Buildozer.initialize(pysidedeploy_config=config) + + # run buildozer + logging.info("[DEPLOY] Running buildozer deployment") + Buildozer.create_executable(config.mode) + + # move buildozer build files to {generated_files_path} + if not dry_run: + buildozer_build_dir = config.project_dir / ".buildozer" + if not buildozer_build_dir.exists(): + logging.info(f"[DEPLOY] Unable to copy {buildozer_build_dir} to {generated_files_path}" + f"{buildozer_build_dir} does not exist") + logging.info(f"[DEPLOY] copy {buildozer_build_dir} to {generated_files_path}") + shutil.move(buildozer_build_dir, generated_files_path) + + logging.info(f"[DEPLOY] apk created in {config.exe_dir}") + except Exception: + print(f"Exception occurred: {traceback.format_exc()}") + finally: + if generated_files_path and config and not keep_deployment_files: + cleanup(generated_files_path=generated_files_path, config=config, is_android=True) + + logging.info("[DEPLOY] End") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=dedent(f""" + This tool deploys PySide{MAJOR_VERSION} to Android platforms. + + Note: The main python entrypoint should be named main.py + """), + formatter_class=argparse.RawTextHelpFormatter, + ) + + parser.add_argument("-c", "--config-file", type=str, help="Path to the .spec config file") + + parser.add_argument( + "--init", action="store_true", + help="Create pysidedeploy.spec file, if it doesn't already exists") + + parser.add_argument( + "-v", "--verbose", help="run in verbose mode", action="store_const", + dest="loglevel", const=logging.INFO) + + parser.add_argument("--dry-run", action="store_true", help="show the commands to be run") + + parser.add_argument("--keep-deployment-files", action="store_true", + help="keep the generated deployment files generated") + + parser.add_argument("-f", "--force", action="store_true", help="force all input prompts") + + parser.add_argument("--name", type=str, help="Application name") + + parser.add_argument("--wheel-pyside", type=lambda p: Path(p).resolve(), + help=f"Path to PySide{MAJOR_VERSION} Android Wheel", + required=not config_option_exists()) + + parser.add_argument("--wheel-shiboken", type=lambda p: Path(p).resolve(), + help=f"Path to shiboken{MAJOR_VERSION} Android Wheel", + required=not config_option_exists()) + + 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="--sdk-path" in sys.argv) + + 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="--ndk-path" in sys.argv) + + args = parser.parse_args() + + main(args.name, args.wheel_pyside, args.wheel_shiboken, args.ndk_path, args.sdk_path, + args.config_file, args.init, args.loglevel, args.dry_run, args.keep_deployment_files, + args.force) |