Skip to content

Commit 75be018

Browse files
shyamnathpShyamQtmisl6
authored
Initial support for PySide6 and Qt bootstrap (#2918)
* Initial support for PySide6 - Add a new bootstrap for Qt - This bootstrap will be used by `pyside6-android-deploy` tool shipped with PySide6, which interally calls pythonforandroid using buildozer. - The Qt bootstrap depends on recipes PySide6 and shiboken6 among other mandatory recipes. The recipes for PySide6 and shiboken6 resides in the PySide repository - https://code.qt.io/cgit/pyside/pyside-setup.git/tree/sources/pyside-tools/deploy_lib/android/recipes - The PythonActivity entrypoint class is derived from QtActivity class which is the main acitivty class when a Qt C++ application is packaged for Android. The jar containing QtActivity class is supplied through buildozer `android.add_jars` option. - The C wrapper binary to the application main.py is named as `main_{abi_name}` instead of just `main` for other bootstraps. - Multi architecture deployment is not supported at the moment. - Adapt tests based on the new Qt bootstrap * Add Qt boostrap to docs - update the docs to include sections depicting the Qt boostrap. * Tweak gradle build properties for Qt bootstrap - Sometimes a flaky Java heap out of memory error is throw. By, tweaking the memory setting we can get rid of that error. * Fix bug - check for main.py - Qt boostrap removed from comparison in the changed line because its expects a value is args.launcher, which is not applicable for Qt boostrap. Hence, it exits with an value not found exception. - Removing Qt boostrap from the comparison leads to checking for main.py or --private, which is to be done for the Qt boostrap. * Make --init-classes truly optional - check if empty, otherwise store empty string * Add a non-gui test app build to CI that uses Qt bootstrap - for the purpose of testing, the pyside6 and shiboken6 wheels, the extra .jar files needed and the recipes for pyside6 and shiboken6 are manually added into testapps/on_device_unit_tests/test_qt. These files are normally generated by the `pyside6-android-deploy` tool that is shipped with PySide. Generating the wheels and the .jar files belongs to the scope of PySide and not python-for-android. Hence, they are not done here. This also reduces the load on the CI which will otherwise have to cross-compile CPython and PySide. - The Android aarch64 wheels for testing are downloaded from Qt servers. These wheels are for testing purposes only and the download link will be updated when official PySide6 Android wheels are generated. - Tests were added in test_requirements.py so that when running the apk the current date and time are printed on the terminal. The tests also checks shiboken6 and PySide6 module imports. * Remove superfluous whitespace removal - This was introduced by a VSCode setting and is unrealated to this patch. Although this might be good, this has to be introduced through a different patch. - pyside6 recipe typo adapted in buildoptions.rst * Add aab generation to test app with Qt bootstrap * Fix typo in doc/source/buildoptions.rst Co-authored-by: Mirko Galimberti <me@mirkogalimberti.com> --------- Co-authored-by: Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> Co-authored-by: Mirko Galimberti <me@mirkogalimberti.com>
1 parent 66ba3e5 commit 75be018

File tree

34 files changed

+819
-14
lines changed

34 files changed

+819
-14
lines changed

.github/workflows/push.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ jobs:
7070
target: testapps-webview
7171
- name: service_library
7272
target: testapps-service_library-aar
73+
- name: qt
74+
target: testapps-qt
7375
steps:
7476
- name: Checkout python-for-android
7577
uses: actions/checkout@v4

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ testapps-service_library-aar: virtualenv
8282
--requirements python3 \
8383
--arch=arm64-v8a --arch=x86 --release
8484

85+
testapps-qt: testapps-qt/debug/apk testapps-qt/release/aab
86+
87+
# testapps-webview/MODE/ARTIFACT
88+
testapps-qt/%: virtualenv
89+
$(eval MODE := $(word 2, $(subst /, ,$@)))
90+
$(eval ARTIFACT := $(word 3, $(subst /, ,$@)))
91+
@echo Building testapps-qt for $(MODE) mode and $(ARTIFACT) artifact
92+
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \
93+
python setup.py $(ARTIFACT) --$(MODE) --sdk-dir $(ANDROID_SDK_HOME) --ndk-dir $(ANDROID_NDK_HOME) \
94+
--bootstrap qt \
95+
--requirements python3,shiboken6,pyside6 \
96+
--arch=arm64-v8a \
97+
--local-recipes ./test_qt/recipes \
98+
--qt-libs Core \
99+
--load-local-libs plugins_platforms_qtforandroid \
100+
--add-jar ./test_qt/jar/PySide6/jar/Qt6Android.jar \
101+
--add-jar ./test_qt/jar/PySide6/jar/Qt6AndroidBindings.jar \
102+
--permission android.permission.WRITE_EXTERNAL_STORAGE \
103+
--permission android.permission.INTERNET
104+
85105
testapps/%: virtualenv
86106
$(eval $@_APP_ARCH := $(shell basename $*))
87107
. $(ACTIVATE) && cd testapps/on_device_unit_tests/ && \

doc/source/buildoptions.rst

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,49 @@ systems and frameworks.
204204
include multiple jar files, pass this argument multiple times.
205205
- ``add-source``: Add a source directory to the app's Java code.
206206

207+
Qt
208+
~~
209+
210+
This bootstrap can be used with ``--bootstrap=qt`` or by including the ``PySide6`` or
211+
``shiboken6`` recipe, e.g. ``--requirements=pyside6,shiboken6``. Currently, the only way
212+
to use this bootstrap is through `pyside6-android-deploy <https://www.qt.io/blog/taking-qt-for-python-to-android>`__
213+
tool shipped with ``PySide6``, as the recipes for ``PySide6`` and ``shiboken6`` are created
214+
dynamically. The tool builds ``PySide6`` and ``shiboken6`` wheels for a specific Android platform
215+
and the recipes simply unpack the built wheels. You can see the recipes `here <https://code.qt.io/cgit/pyside/pyside-setup.git/tree/sources/pyside-tools/deploy_lib/android/recipes>`__.
216+
217+
.. note::
218+
The ``pyside6-android-deploy`` tool and hence the Qt bootstrap does not support multi-architecture
219+
builds currently.
220+
221+
What are Qt and PySide?
222+
%%%%%%%%%%%%%%%%%%%%%%%%
223+
224+
`Qt <https://www.qt.io/>`__ is a popularly used cross-platform C++ framework for developing
225+
GUI applications. `PySide6 <https://doc.qt.io/qtforpython-6/quickstart.html>`__ refers to the
226+
Python bindings for Qt6, and enables the Python developers access to the Qt6 API.
227+
`Shiboken6 <https://doc.qt.io/qtforpython-6/shiboken6/index.html>`__ is the binding generator
228+
tool used for generating the Python bindings from C++ code.
229+
230+
.. note:: The `shiboken6` recipe is for the `Shiboken Python module <https://doc.qt.io/qtforpython-6/shiboken6/shibokenmodule.html>`__
231+
which includes a couple of utility functions for inspecting and debugging PySide6 code.
232+
233+
Build Options
234+
%%%%%%%%%%%%%
235+
236+
``pyside6-android-deploy`` works by generating a ``buildozer.spec`` file and thereby using
237+
`buildozer <https://buildozer.readthedocs.io/en/latest/>`__ to control the build options used by
238+
``python-for-android`` with the Qt bootstrap. Apart from the general build options that works
239+
across all the other bootstraps, the Qt bootstrap introduces the following 3 new build options.
240+
241+
- ``--qt-libs``: list of Qt libraries(modules) to be loaded.
242+
- ``--load-local-libs``: list of Qt plugin libraries to be loaded.
243+
- ``--init-classes``: list of Java class names to the loaded from the Qt jar files supplied through
244+
the ``--add-jar`` option.
245+
246+
These build options are automatically populated by the ``pyside6-android-deploy`` tool, but can be
247+
modified by updating the ``buildozer.spec`` file. Apart from the above 3 build options, the tool
248+
also automatically identifies the values to be fed into the cli options ``--permission``, ``--add-jar``
249+
depending on the PySide6 modules used by the applicaiton.
207250

208251
Requirements blacklist (APK size optimization)
209252
----------------------------------------------

doc/source/commands.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,15 @@ behaviour, though not all commands make use of them.
2626

2727
``--debug``
2828
Print extra debug information about the build, including all compilation output.
29-
29+
3030
``--sdk_dir``
3131
The filepath where the Android SDK is installed. This can
3232
alternatively be set in several other ways.
3333

3434
``--android_api``
3535
The Android API level to target; python-for-android will check if
3636
the platform tools for this level are installed.
37-
37+
3838
``--ndk_dir``
3939
The filepath where the Android NDK is installed. This can
4040
alternatively be set in several other ways.
@@ -74,12 +74,12 @@ supply those that you need.
7474
The architecture to build for. You can specify multiple architectures to build for
7575
at the same time. As an example ``p4a ... --arch arm64-v8a --arch armeabi-v7a ...``
7676
will build a distribution for both ``arm64-v8a`` and ``armeabi-v7a``.
77-
77+
7878
``--bootstrap BOOTSTRAP``
7979
The Java bootstrap to use for your application. You mostly don't
8080
need to worry about this or set it manually, as an appropriate
8181
bootstrap will be chosen from your ``--requirements``. Current
82-
choices are ``sdl2`` (used with Kivy and most other apps) or ``webview``.
82+
choices are ``sdl2`` (used with Kivy and most other apps), ``webview`` or ``qt``.
8383

8484

8585
.. note:: These options are preliminary. Others will include toggles

doc/source/quickstart.rst

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ Concepts
2626

2727
- **bootstrap:** A bootstrap is the app backend that will start your
2828
application. The default for graphical applications is SDL2.
29-
You can also use e.g. the webview for web apps, or service_only/service_library for
30-
background services. Different bootstraps have different additional
29+
You can also use e.g. the webview for web apps, or service_only/service_library for
30+
background services, or qt for PySide6 apps. Different bootstraps have different additional
3131
build options.
3232

3333
*Advanced:*
@@ -281,7 +281,7 @@ Recipe management
281281
You can see the list of the available recipes with::
282282

283283
p4a recipes
284-
284+
285285
If you are contributing to p4a and want to test a recipes again,
286286
you need to clean the build and rebuild your distribution::
287287

@@ -295,7 +295,6 @@ it (edit the ``__init__.py``)::
295295

296296
mkdir -p p4a-recipes/myrecipe
297297
touch p4a-recipes/myrecipe/__init__.py
298-
299298

300299
Distribution management
301300
~~~~~~~~~~~~~~~~~~~~~~~

pythonforandroid/bootstraps/common/build/build.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def get_bootstrap_name():
8383
if PYTHON is not None and not exists(PYTHON):
8484
PYTHON = None
8585

86-
if _bootstrap_name in ('sdl2', 'webview', 'service_only'):
86+
if _bootstrap_name in ('sdl2', 'webview', 'service_only', 'qt'):
8787
WHITELIST_PATTERNS.append('pyconfig.h')
8888

8989
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
@@ -543,6 +543,7 @@ def make_package(args):
543543
}
544544
if get_bootstrap_name() == "sdl2":
545545
render_args["url_scheme"] = url_scheme
546+
546547
render(
547548
'AndroidManifest.tmpl.xml',
548549
manifest_path,
@@ -571,7 +572,8 @@ def make_package(args):
571572
render(
572573
'gradle.tmpl.properties',
573574
'gradle.properties',
574-
args=args)
575+
args=args,
576+
bootstrap_name=get_bootstrap_name())
575577

576578
# ant build templates
577579
render(
@@ -601,6 +603,26 @@ def make_package(args):
601603
join(res_dir, 'values/strings.xml'),
602604
**render_args)
603605

606+
# Library resources from Qt
607+
# These are referred by QtLoader.java in Qt6AndroidBindings.jar
608+
# qt_libs and load_local_libs are loaded at App startup
609+
if get_bootstrap_name() == "qt":
610+
qt_libs = args.qt_libs.split(",")
611+
load_local_libs = args.load_local_libs.split(",")
612+
init_classes = args.init_classes
613+
if init_classes:
614+
init_classes = init_classes.split(",")
615+
init_classes = ":".join(init_classes)
616+
arch = get_dist_info_for("archs")[0]
617+
render(
618+
'libs.tmpl.xml',
619+
join(res_dir, 'values/libs.xml'),
620+
qt_libs=qt_libs,
621+
load_local_libs=load_local_libs,
622+
init_classes=init_classes,
623+
arch=arch
624+
)
625+
604626
if exists(join("templates", "custom_rules.tmpl.xml")):
605627
render(
606628
'custom_rules.tmpl.xml',
@@ -951,6 +973,14 @@ def create_argument_parser():
951973
help='Use that parameter if you need to implement your own PythonServive Java class')
952974
ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS,
953975
help='The full java class name of the main activity')
976+
if get_bootstrap_name() == "qt":
977+
ap.add_argument('--qt-libs', dest='qt_libs', required=True,
978+
help='comma separated list of Qt libraries to be loaded')
979+
ap.add_argument('--load-local-libs', dest='load_local_libs', required=True,
980+
help='comma separated list of Qt plugin libraries to be loaded')
981+
ap.add_argument('--init-classes', dest='init_classes', default='',
982+
help='comma separated list of java class names to be loaded from the Qt jar files, '
983+
'specified through add_jar cli option')
954984

955985
return ap
956986

pythonforandroid/bootstraps/common/build/templates/gradle.tmpl.properties

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
{% if bootstrap_name == "qt" %}
2+
# For tweaking memory settings. Otherwise, a p4a session with Qt bootstrap and PySide6 recipe
3+
# terminates with a Java out of memory exception
4+
org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
5+
{% endif %}
16
{% if args.enable_androidx %}
27
android.useAndroidX=true
38
android.enableJetifier=true
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import sh
2+
from os.path import join
3+
from pythonforandroid.toolchain import (
4+
Bootstrap, current_directory, info, info_main, shprint)
5+
from pythonforandroid.util import ensure_dir, rmdir
6+
7+
8+
class QtBootstrap(Bootstrap):
9+
name = 'qt'
10+
recipe_depends = ['python3', 'genericndkbuild', 'PySide6', 'shiboken6']
11+
# this is needed because the recipes PySide6 and shiboken6 resides in the PySide Qt repository
12+
# - https://code.qt.io/cgit/pyside/pyside-setup.git/
13+
# Without this some tests will error because it cannot find the recipes within pythonforandroid
14+
# repository
15+
can_be_chosen_automatically = False
16+
17+
def assemble_distribution(self):
18+
info_main("# Creating Android project using Qt bootstrap")
19+
20+
rmdir(self.dist_dir)
21+
info("Copying gradle build")
22+
shprint(sh.cp, '-r', self.build_dir, self.dist_dir)
23+
24+
with current_directory(self.dist_dir):
25+
with open('local.properties', 'w') as fileh:
26+
fileh.write('sdk.dir={}'.format(self.ctx.sdk_dir))
27+
28+
arch = self.ctx.archs[0]
29+
if len(self.ctx.archs) > 1:
30+
raise ValueError("Trying to build for more than one arch. Qt bootstrap cannot handle that yet")
31+
32+
info(f"Bootstrap running with arch {arch}")
33+
34+
with current_directory(self.dist_dir):
35+
info("Copying Python distribution")
36+
37+
self.distribute_libs(arch, [self.ctx.get_libs_dir(arch.arch)])
38+
self.distribute_aars(arch)
39+
self.distribute_javaclasses(self.ctx.javaclass_dir,
40+
dest_dir=join("src", "main", "java"))
41+
42+
python_bundle_dir = join(f'_python_bundle__{arch.arch}', '_python_bundle')
43+
ensure_dir(python_bundle_dir)
44+
site_packages_dir = self.ctx.python_recipe.create_python_bundle(
45+
join(self.dist_dir, python_bundle_dir), arch)
46+
47+
if not self.ctx.with_debug_symbols:
48+
self.strip_libraries(arch)
49+
self.fry_eggs(site_packages_dir)
50+
super().assemble_distribution()
51+
52+
53+
bootstrap = QtBootstrap()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.gradle
2+
/build/
3+
4+
# Ignore Gradle GUI config
5+
gradle-app.setting
6+
7+
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
8+
!gradle-wrapper.jar
9+
10+
# Cache of project
11+
.gradletasknamecache
12+
13+
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
14+
# gradle/wrapper/gradle-wrapper.properties
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# prevent user to include invalid extensions
2+
*.apk
3+
*.aab
4+
*.apks
5+
*.pxd
6+
7+
# eggs
8+
*.egg-info
9+
10+
# unit test
11+
unittest/*
12+
13+
# python config
14+
config/makesetup
15+
16+
# unused encodings
17+
lib-dynload/*codec*
18+
encodings/cp*.pyo
19+
encodings/tis*
20+
encodings/shift*
21+
encodings/bz2*
22+
encodings/iso*
23+
encodings/undefined*
24+
encodings/johab*
25+
encodings/p*
26+
encodings/m*
27+
encodings/euc*
28+
encodings/k*
29+
encodings/unicode_internal*
30+
encodings/quo*
31+
encodings/gb*
32+
encodings/big5*
33+
encodings/hp*
34+
encodings/hz*
35+
36+
# unused python modules
37+
bsddb/*
38+
wsgiref/*
39+
hotshot/*
40+
pydoc_data/*
41+
tty.pyo
42+
anydbm.pyo
43+
nturl2path.pyo
44+
LICENCE.txt
45+
macurl2path.pyo
46+
dummy_threading.pyo
47+
audiodev.pyo
48+
antigravity.pyo
49+
dumbdbm.pyo
50+
sndhdr.pyo
51+
__phello__.foo.pyo
52+
sunaudio.pyo
53+
os2emxpath.pyo
54+
multiprocessing/dummy*
55+
56+
# unused binaries python modules
57+
lib-dynload/termios.so
58+
lib-dynload/_lsprof.so
59+
lib-dynload/*audioop.so
60+
lib-dynload/_hotshot.so
61+
lib-dynload/_heapq.so
62+
lib-dynload/_json.so
63+
lib-dynload/grp.so
64+
lib-dynload/resource.so
65+
lib-dynload/pyexpat.so
66+
lib-dynload/_ctypes_test.so
67+
lib-dynload/_testcapi.so
68+
69+
# odd files
70+
plat-linux3/regen
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
# Uncomment this if you're using STL in your project
3+
# See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information
4+
# APP_STL := stlport_static
5+
6+
# APP_ABI := armeabi armeabi-v7a x86
7+
APP_ABI := $(ARCH)
8+
APP_PLATFORM := $(NDK_API)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
LOCAL_PATH := $(call my-dir)
2+
3+
include $(CLEAR_VARS)
4+
5+
LOCAL_MODULE := main_$(PREFERRED_ABI)
6+
7+
# Add your application source files here...
8+
LOCAL_SRC_FILES := start.c
9+
10+
LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS)
11+
12+
LOCAL_SHARED_LIBRARIES := python_shared
13+
14+
LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS)
15+
16+
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
17+
18+
include $(BUILD_SHARED_LIBRARY)

0 commit comments

Comments
 (0)