aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2023-02-08 16:58:39 +0100
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2023-03-31 14:21:05 +0000
commit2e458381e4847f46814518da9f7cfca097aa8bc4 (patch)
treebd31c54bb24c26b5ba6b5a3fb815c7b44f625a4f
parent22f35d990b5cb5b65a08823128ec42305822c21a (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 Change-Id: I7eb96fa5507a476b4e86ec0195a5e9869f0f85fd Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> (cherry picked from commit 95abfa776411b6d7cd4296adf63bc7abce2270b6) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--build_scripts/__init__.py1
-rw-r--r--build_scripts/config.py20
-rw-r--r--build_scripts/platforms/unix.py16
-rw-r--r--create_wheels.py13
-rw-r--r--requirements.txt5
-rw-r--r--sources/pyside-tools/CMakeLists.txt3
-rw-r--r--sources/pyside-tools/android_deploy.py233
-rw-r--r--sources/pyside-tools/android_deploy.pyproject9
-rw-r--r--sources/pyside-tools/deploy_lib/android/__init__.py10
-rw-r--r--sources/pyside-tools/deploy_lib/android/android_helper.py59
-rw-r--r--sources/pyside-tools/deploy_lib/android/buildozer.py84
-rw-r--r--sources/pyside-tools/deploy_lib/android/recipes/PySide6/__init__.tmpl.py43
-rw-r--r--sources/pyside-tools/deploy_lib/android/recipes/shiboken6/__init__.tmpl.py24
-rw-r--r--sources/pyside-tools/deploy_lib/config.py156
-rw-r--r--sources/pyside-tools/deploy_lib/default.spec38
-rw-r--r--sources/pyside-tools/deploy_lib/deploy_util.py22
-rw-r--r--sources/pyside-tools/deploy_lib/python_helper.py1
-rw-r--r--sources/pyside-tools/pyside_tool.py4
18 files changed, 719 insertions, 22 deletions
diff --git a/build_scripts/__init__.py b/build_scripts/__init__.py
index ac76724b0..ba7888bdb 100644
--- a/build_scripts/__init__.py
+++ b/build_scripts/__init__.py
@@ -7,6 +7,7 @@ SHIBOKEN = 'shiboken6'
PYSIDE_PYTHON_TOOLS = ["metaobjectdump",
"deploy",
+ "android_deploy",
"project",
"qml",
"qtpy2cpp",
diff --git a/build_scripts/config.py b/build_scripts/config.py
index 1f01ccdd4..1f2f0b25c 100644
--- a/build_scripts/config.py
+++ b/build_scripts/config.py
@@ -1,6 +1,7 @@
# Copyright (C) 2018 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 sys
from .log import log, LogLevel
from pathlib import Path
@@ -201,11 +202,20 @@ class Config(object):
]
if qt_install_path:
_pyside_tools = available_pyside_tools(qt_tools_path=qt_install_path)
- setup_kwargs['entry_points'] = {
- 'console_scripts': [f'{PYSIDE}-{tool} = {package_name}.scripts.pyside_tool:'
- f'{tool}'
- for tool in _pyside_tools]
- }
+
+ # replacing pyside6-android_deploy by pyside6-android-deploy for consistency
+ # Also, the tool should not exist in any other platform than Linux
+ _console_scripts = []
+ if ("android_deploy" in _pyside_tools) and sys.platform.startswith("linux"):
+ _console_scripts = [(f"{PYSIDE}-android-deploy ="
+ " PySide6.scripts.pyside_tool:android_deploy")]
+ _pyside_tools.remove("android_deploy")
+
+ _console_scripts.extend([f'{PYSIDE}-{tool} = {package_name}.scripts.pyside_tool:'
+ f'{tool}' for tool in _pyside_tools])
+
+ setup_kwargs['entry_points'] = {'console_scripts': _console_scripts}
+
self.setup_kwargs = setup_kwargs
def get_long_description(self):
diff --git a/build_scripts/platforms/unix.py b/build_scripts/platforms/unix.py
index 0c26297e2..98c2945ce 100644
--- a/build_scripts/platforms/unix.py
+++ b/build_scripts/platforms/unix.py
@@ -119,14 +119,24 @@ def prepare_packages_posix(pyside_build, _vars, cross_build=False):
"{st_build_dir}/{st_package_name}/scripts/__init__.py",
_vars=_vars)
+ scripts = ["pyside_tool.py", "metaobjectdump.py", "project.py", "qml.py",
+ "qtpy2cpp.py", "deploy.py"]
+
+ script_dirs = ["qtpy2cpp_lib", "deploy_lib", "project"]
+
+ if sys.platform.startswith("linux"):
+ scripts.append("android_deploy.py")
+ script_dirs.extend(["deploy_lib/android",
+ "deploy_lib/android/recipes/PySide6",
+ "deploy_lib/android/recipes/shiboken6",])
+
# For setting up setuptools entry points
- for script in ("pyside_tool.py", "metaobjectdump.py", "project.py", "qml.py",
- "qtpy2cpp.py", "deploy.py"):
+ for script in scripts:
src = f"{{install_dir}}/bin/{script}"
target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script}"
copyfile(src, target, force=False, _vars=_vars)
- for script_dir in ("qtpy2cpp_lib", "deploy_lib", "project"):
+ for script_dir in script_dirs:
src = f"{{install_dir}}/bin/{script_dir}"
target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script_dir}"
# Exclude subdirectory tests
diff --git a/create_wheels.py b/create_wheels.py
index a56fb4957..5fd99d2aa 100644
--- a/create_wheels.py
+++ b/create_wheels.py
@@ -194,8 +194,17 @@ def wheel_shiboken_module() -> Tuple[SetupData, None]:
def wheel_pyside6_essentials(packaged_qt_tools_path: Path) -> Tuple[SetupData, List[ModuleData]]:
_pyside_tools = available_pyside_tools(packaged_qt_tools_path, package_for_wheels=True)
- _console_scripts = [f"pyside6-{tool} = PySide6.scripts.pyside_tool:{tool}"
- for tool in _pyside_tools]
+
+ # replacing pyside6-android_deploy by pyside6-android-deploy for consistency
+ # Also, the tool should not exist in any other platform than Linux
+ _console_scripts = []
+ if ("android_deploy" in _pyside_tools) and sys.platform.startswith("linux"):
+ _console_scripts = ["pyside6-android-deploy = PySide6.scripts.pyside_tool:android_deploy"]
+ _pyside_tools.remove("android_deploy")
+
+ _console_scripts.extend([f"pyside6-{tool} = PySide6.scripts.pyside_tool:{tool}"
+ for tool in _pyside_tools])
+
setup = SetupData(
name="PySide6_Essentials",
version=get_version_from_package("PySide6"), # we use 'PySide6' here
diff --git a/requirements.txt b/requirements.txt
index df4759377..d23611ef2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,3 +10,8 @@ nuitka==1.4.8
distro; sys_platform == 'linux'
patchelf==0.15; sys_platform == 'linux'
importlib-metadata; python_version < '3.8'
+pkginfo; sys_platform == 'linux'
+jinja2; sys_platform == 'linux'
+buildozer==1.5.0; sys_platform == 'linux'
+tqdm; sys_platform == 'linux'
+gitpython; sys_platform == 'linux'
diff --git a/sources/pyside-tools/CMakeLists.txt b/sources/pyside-tools/CMakeLists.txt
index b4873459e..b6f7808e3 100644
--- a/sources/pyside-tools/CMakeLists.txt
+++ b/sources/pyside-tools/CMakeLists.txt
@@ -24,7 +24,8 @@ else()
${CMAKE_CURRENT_SOURCE_DIR}/project.py
${CMAKE_CURRENT_SOURCE_DIR}/qml.py
${CMAKE_CURRENT_SOURCE_DIR}/qtpy2cpp.py
- ${CMAKE_CURRENT_SOURCE_DIR}/deploy.py)
+ ${CMAKE_CURRENT_SOURCE_DIR}/deploy.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/android_deploy.py)
set(directories ${CMAKE_CURRENT_SOURCE_DIR}/deploy_lib
${CMAKE_CURRENT_SOURCE_DIR}/project)
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)
diff --git a/sources/pyside-tools/android_deploy.pyproject b/sources/pyside-tools/android_deploy.pyproject
new file mode 100644
index 000000000..f976cb5a6
--- /dev/null
+++ b/sources/pyside-tools/android_deploy.pyproject
@@ -0,0 +1,9 @@
+{
+ "files": ["deploy.py", "deploy_lib/__init__.py", "deploy_lib/commands.py", "deploy_lib/config.py",
+ "deploy_lib/default.spec", "deploy_lib/python_helper.py", "deploy_lib/deploy_util.py",
+ "deploy_lib/android/recipes/PySide6/__init__.tmpl.py",
+ "deploy_lib/android/recipes/shiboken6/__init__.tmpl.py",
+ "deploy_lib/android/__init__.py", "deploy_lib/android/android_helper.py",
+ "deploy_lib/android/buildozer.py"
+ ]
+}
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()
diff --git a/sources/pyside-tools/deploy_lib/config.py b/sources/pyside-tools/deploy_lib/config.py
index 2575ab063..781c2155d 100644
--- a/sources/pyside-tools/deploy_lib/config.py
+++ b/sources/pyside-tools/deploy_lib/config.py
@@ -19,7 +19,7 @@ class BaseConfig:
def __init__(self, config_file: Path, dry_run: bool, comment_prefixes: str = "/") -> None:
self.config_file = config_file
- self.parser = ConfigParser(comment_prefixes="/", allow_no_value=True)
+ self.parser = ConfigParser(comment_prefixes=comment_prefixes, allow_no_value=True)
if not self.config_file.exists():
if not dry_run:
logging.info(f"[DEPLOY] Creating config file {self.config_file}")
@@ -62,7 +62,8 @@ class Config(BaseConfig):
creation
"""
- def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool):
+ def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool,
+ android_data, is_android: bool):
super().__init__(config_file, dry_run)
self._dry_run = dry_run
@@ -81,6 +82,7 @@ class Config(BaseConfig):
)
self.title = self.get_value("app", "title")
+
self.project_dir = None
if self.get_value("app", "project_dir"):
self.project_dir = Path(self.get_value("app", "project_dir")).absolute()
@@ -113,6 +115,50 @@ class Config(BaseConfig):
else:
self._find_and_set_excluded_qml_plugins()
+ # Android
+ if is_android:
+ if android_data.wheel_pyside:
+ self.wheel_pyside = android_data.wheel_pyside
+ else:
+ wheel_pyside_temp = self.get_value("qt", "wheel_pyside")
+ self.wheel_pyside = Path(wheel_pyside_temp) if wheel_pyside_temp else None
+
+ if android_data.wheel_shiboken:
+ self.wheel_shiboken = android_data.wheel_shiboken
+ else:
+ wheel_shiboken_temp = self.get_value("qt", "wheel_shiboken")
+ self.wheel_shiboken = Path(wheel_shiboken_temp) if wheel_shiboken_temp else None
+
+ if android_data.ndk_path:
+ 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 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
+
+ recipe_dir_temp = self.get_value("buildozer", "recipe_dir")
+ self.recipe_dir = Path(recipe_dir_temp) if recipe_dir_temp else None
+
+ jars_dir_temp = self.get_value("buildozer", "jars_dir")
+ self.jars_dir = Path(jars_dir_temp) if jars_dir_temp else None
+
+ self._modules = []
+ if self.get_value("buildozer", "modules"):
+ self.modules = self.get_value("buildozer", "modules").split(",")
+
+ self.arch = self.get_value("buildozer", "arch")
+
+ self._local_libs = []
+ if self.get_value("buildozer", "local_libs"):
+ self.local_libs = self.get_value("buildozer", "local_libs").split(",")
+
+ self._mode = self.get_value("buildozer", "mode")
+
def set_or_fetch(self, config_property_val, config_property_key, config_property_group="app"):
"""
Write to config_file if 'config_property_key' is known without config_file
@@ -164,7 +210,7 @@ class Config(BaseConfig):
return self._source_file
@source_file.setter
- def source_file(self, source_file):
+ def source_file(self, source_file: Path):
self._source_file = source_file
@property
@@ -172,7 +218,7 @@ class Config(BaseConfig):
return self._python_path
@python_path.setter
- def python_path(self, python_path):
+ def python_path(self, python_path: Path):
self._python_path = python_path
@property
@@ -184,13 +230,113 @@ class Config(BaseConfig):
self._excluded_qml_plugins = excluded_qml_plugins
@property
+ def recipe_dir(self):
+ return self._recipe_dir
+
+ @recipe_dir.setter
+ def recipe_dir(self, recipe_dir: Path):
+ self._recipe_dir = recipe_dir.resolve() if recipe_dir else None
+ if self._recipe_dir:
+ self.set_value("buildozer", "recipe_dir", str(self._recipe_dir))
+
+ def recipes_exist(self):
+ if not self._recipe_dir:
+ return False
+
+ pyside_recipe_dir = Path(self.recipe_dir) / "PySide6"
+ shiboken_recipe_dir = Path(self.recipe_dir) / "shiboken6"
+
+ return pyside_recipe_dir.is_dir() and shiboken_recipe_dir.is_dir()
+
+ @property
+ def jars_dir(self) -> Path:
+ return self._jars_dir
+
+ @jars_dir.setter
+ def jars_dir(self, jars_dir: Path):
+ self._jars_dir = jars_dir.resolve() if jars_dir else None
+ if self._jars_dir:
+ self.set_value("buildozer", "jars_dir", str(self._jars_dir))
+
+ @property
+ def modules(self):
+ return self._modules
+
+ @modules.setter
+ def modules(self, modules):
+ self._modules = modules
+ self.set_value("buildozer", "modules", ",".join(modules))
+
+ @property
+ def local_libs(self):
+ return self._local_libs
+
+ @local_libs.setter
+ def local_libs(self, local_libs):
+ self._local_libs = local_libs
+ self.set_value("buildozer", "local_libs", ",".join(local_libs))
+
+ @property
+ def ndk_path(self):
+ return self._ndk_path
+
+ @ndk_path.setter
+ def ndk_path(self, ndk_path: Path):
+ self._ndk_path = ndk_path.resolve() if ndk_path else None
+ if self._ndk_path:
+ self.set_value("buildozer", "ndk_path", str(self._ndk_path))
+
+ @property
+ def sdk_path(self) -> Path:
+ return self._sdk_path
+
+ @sdk_path.setter
+ def sdk_path(self, sdk_path: Path):
+ self._sdk_path = sdk_path.resolve() if sdk_path else None
+ if self._sdk_path:
+ self.set_value("buildozer", "sdk_path", str(self._sdk_path))
+
+ @property
+ def arch(self):
+ return self._arch
+
+ @arch.setter
+ def arch(self, arch):
+ self._arch = arch
+ self.set_value("buildozer", "arch", arch)
+
+ @property
+ def mode(self):
+ return self._mode
+
+ @property
def exe_dir(self):
return self._exe_dir
@exe_dir.setter
- def exe_dir(self, exe_dir):
+ def exe_dir(self, exe_dir: Path):
self._exe_dir = exe_dir
+ @property
+ def wheel_pyside(self) -> Path:
+ return self._wheel_pyside
+
+ @wheel_pyside.setter
+ def wheel_pyside(self, wheel_pyside: Path):
+ self._wheel_pyside = wheel_pyside.resolve() if wheel_pyside else None
+ if self._wheel_pyside:
+ self.set_value("qt", "wheel_pyside", str(self._wheel_pyside))
+
+ @property
+ def wheel_shiboken(self) -> Path:
+ return self._wheel_shiboken
+
+ @wheel_shiboken.setter
+ def wheel_shiboken(self, wheel_shiboken: Path):
+ self._wheel_shiboken = wheel_shiboken.resolve() if wheel_shiboken else None
+ if self._wheel_shiboken:
+ self.set_value("qt", "wheel_shiboken", str(self._wheel_shiboken))
+
def _find_and_set_qml_files(self):
"""Fetches all the qml_files in the folder and sets them if the
field qml_files is empty in the config_dir"""
diff --git a/sources/pyside-tools/deploy_lib/default.spec b/sources/pyside-tools/deploy_lib/default.spec
index 17027e898..fdfb5397d 100644
--- a/sources/pyside-tools/deploy_lib/default.spec
+++ b/sources/pyside-tools/deploy_lib/default.spec
@@ -26,6 +26,9 @@ python_path =
# zstandard: provides final executable size optimization
packages = nuitka==1.5.4,ordered_set,zstandard
+# buildozer: for deploying Android application
+android_packages = buildozer==1.5.0,cython==0.29.33
+
[qt]
# Comma separated path to QML files required
@@ -35,8 +38,43 @@ qml_files =
# excluded qml plugin binaries
excluded_qml_plugins =
+# path to PySide wheel
+wheel_pyside =
+
+# path to Shiboken wheel
+wheel_shiboken =
+
[nuitka]
# (str) specify any extra nuitka arguments
# eg: extra_args = --show-modules --follow-stdlib
extra_args = --quiet --noinclude-qt-translations=True
+
+[buildozer]
+
+# build mode
+# possible options: [release, debug]
+# release creates an aab, while debug creates an apk
+mode = debug
+
+# contrains path to PySide6 and shiboken6 recipe dir
+recipe_dir =
+
+# path to extra Qt Android jars to be loaded by the application
+jars_dir =
+
+# if empty uses default ndk path downloaded by buildozer
+ndk_path =
+
+# if empty uses default sdk path downloaded by buildozer
+sdk_path =
+
+# modules used. Comma separated
+modules =
+
+# other libraries to be loaded. Comma separated.
+local_libs = plugins_platforms_qtforandroid
+
+# architecture of deployed platform
+# possible values: ["aarch64", "armv7a", "i686", "x86_64"]
+arch =
diff --git a/sources/pyside-tools/deploy_lib/deploy_util.py b/sources/pyside-tools/deploy_lib/deploy_util.py
index 843a21f5a..de545e3a5 100644
--- a/sources/pyside-tools/deploy_lib/deploy_util.py
+++ b/sources/pyside-tools/deploy_lib/deploy_util.py
@@ -19,7 +19,7 @@ def config_option_exists():
return False
-def cleanup(generated_files_path: Path, config: Config):
+def cleanup(generated_files_path: Path, config: Config, is_android: bool = False):
"""
Cleanup the generated build folders/files
"""
@@ -29,9 +29,20 @@ def cleanup(generated_files_path: Path, config: Config):
elif not config.dry_run:
logging.info(f"[DEPLOY] {generated_files_path} does not exist")
+ if is_android:
+ buildozer_spec: Path = config.project_dir / "buildozer.spec"
+ if buildozer_spec.exists():
+ buildozer_spec.unlink()
+ logging.info(f"[DEPLOY] {str(buildozer_spec)} removed")
+
+ buildozer_build: Path = config.project_dir / ".buildozer"
+ if buildozer_build.exists():
+ shutil.rmtree(buildozer_build)
+ logging.info(f"[DEPLOY] {str(buildozer_build)} removed")
+
def get_config(python_exe: Path, dry_run: bool = False, config_file: Path = None, main_file:
- Path = None):
+ Path = None, android_data = None, is_android: bool = False):
"""
Sets up a new deployment configuration or use an existing config file
"""
@@ -42,7 +53,7 @@ def get_config(python_exe: Path, dry_run: bool = False, config_file: Path = None
config_file = Path.cwd() / "pysidedeploy.spec"
config = Config(config_file=config_file, source_file=main_file, python_exe=python_exe,
- dry_run=dry_run)
+ dry_run=dry_run, android_data=android_data, is_android=is_android)
return config
@@ -70,7 +81,7 @@ def setup_python(dry_run: bool, force: bool, init: bool):
def install_python_dependencies(config: Config, python: PythonExecutable, init: bool,
- packages: str):
+ packages: str, is_android: bool = False):
"""
Installs the python package dependencies for the target deployment platform
"""
@@ -80,13 +91,14 @@ def install_python_dependencies(config: Config, python: PythonExecutable, init:
packages = config.get_value("python", packages).split(",")
python.install(packages=packages)
# nuitka requires patchelf to make patchelf rpath changes for some Qt files
- if sys.platform.startswith("linux"):
+ if sys.platform.startswith("linux") and not is_android:
python.install(packages=["patchelf"])
def finalize(generated_files_path: Path, config: Config):
"""
Copy the executable into the final location
+ For Android deployment, this is done through buildozer
"""
generated_exec_path = generated_files_path / (config.source_file.stem + EXE_FORMAT)
if generated_exec_path.exists() and config.exe_dir:
diff --git a/sources/pyside-tools/deploy_lib/python_helper.py b/sources/pyside-tools/deploy_lib/python_helper.py
index 2fc0236bc..e86ce2e1c 100644
--- a/sources/pyside-tools/deploy_lib/python_helper.py
+++ b/sources/pyside-tools/deploy_lib/python_helper.py
@@ -49,7 +49,6 @@ class PythonExecutable:
package_version = package_info[1]
else:
raise ValueError(f"{package} should be of the format 'package_name'=='version'")
-
if not self.is_installed(package=package_name):
logging.info(f"[DEPLOY] Installing package: {package}")
run_command(
diff --git a/sources/pyside-tools/pyside_tool.py b/sources/pyside-tools/pyside_tool.py
index 5e5f857bc..7c9be28b1 100644
--- a/sources/pyside-tools/pyside_tool.py
+++ b/sources/pyside-tools/pyside_tool.py
@@ -199,5 +199,9 @@ def deploy():
pyside_script_wrapper("deploy.py")
+def android_deploy():
+ pyside_script_wrapper("android_deploy.py")
+
+
if __name__ == "__main__":
main()