Skip to content

Commit 04bb63b

Browse files
authored
Add aab (Android App Bundle) support (#1356)
* Add aab (Android App Bundle) support * Add some tests * Add a return just after the error, so the user can notice it.
1 parent 5dfad3f commit 04bb63b

File tree

6 files changed

+143
-75
lines changed

6 files changed

+143
-75
lines changed

.github/workflows/android.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,8 @@ jobs:
3838
run: |
3939
touch main.py
4040
buildozer android debug
41+
- name: buildozer android release (aab)
42+
run: |
43+
touch main.py
44+
export BUILDOZER_ALLOW_ORG_TEST_DOMAIN=1
45+
buildozer android release

buildozer/default.spec

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ fullscreen = 0
101101
# (int) Target Android API, should be as high as possible.
102102
#android.api = 27
103103

104-
# (int) Minimum API your APK will support.
104+
# (int) Minimum API your APK / AAB will support.
105105
#android.minapi = 21
106106

107107
# (int) Android SDK version to use
@@ -260,8 +260,9 @@ fullscreen = 0
260260
# (bool) Copy library instead of making a libpymodules.so
261261
#android.copy_libs = 1
262262

263-
# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
264-
android.arch = armeabi-v7a
263+
# (list) The Android archs to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64
264+
# In past, was `android.arch` as we weren't supporting builds for multiple archs at the same time.
265+
android.archs = arm64-v8a, armeabi-v7a
265266

266267
# (int) overrides automatic versionCode computation (used in build.gradle)
267268
# this is not the same as app version and should only be edited if you know what you're doing
@@ -282,6 +283,9 @@ android.allow_backup = True
282283
# (bool) disables the compilation of py to pyc/pyo files when packaging
283284
# android.no-compile-pyo = True
284285

286+
# (str) The format used to package the app for release mode (aab or apk).
287+
# android.release_artifact = aab
288+
285289
#
286290
# Python for android (p4a) specific
287291
#
@@ -381,7 +385,7 @@ warn_on_root = 1
381385
# (str) Path to build artifact storage, absolute or relative to spec file
382386
# build_dir = ./.buildozer
383387

384-
# (str) Path to build output (i.e. .apk, .ipa) storage
388+
# (str) Path to build output (i.e. .apk, .aab, .ipa) storage
385389
# bin_dir = ./bin
386390

387391
# -----------------------------------------------------------------------------

buildozer/target.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Target:
1212
def __init__(self, buildozer):
1313
self.buildozer = buildozer
1414
self.build_mode = 'debug'
15+
self.artifact_format = 'apk'
1516
self.platform_update = False
1617

1718
def check_requirements(self):
@@ -101,6 +102,7 @@ def cmd_update(self, *args):
101102
def cmd_debug(self, *args):
102103
self.buildozer.prepare_for_build()
103104
self.build_mode = 'debug'
105+
self.artifact_format = 'apk'
104106
self.buildozer.build()
105107

106108
def cmd_release(self, *args):
@@ -137,6 +139,7 @@ def cmd_release(self, *args):
137139
exit(1)
138140

139141
self.build_mode = 'release'
142+
self.artifact_format = self.buildozer.config.getdefault('app', 'android.release_artifact', 'aab')
140143
self.buildozer.build()
141144

142145
def cmd_deploy(self, *args):

buildozer/targets/android.py

Lines changed: 64 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
# does.
4848
DEFAULT_SDK_TAG = '6514223'
4949

50-
DEFAULT_ARCH = 'armeabi-v7a'
50+
DEFAULT_ARCHS = ['arm64-v8a', 'armeabi-v7a']
5151

5252
MSG_P4A_RECOMMENDED_NDK_ERROR = (
5353
"WARNING: Unable to find recommended Android NDK for current "
@@ -62,21 +62,28 @@ class TargetAndroid(Target):
6262
p4a_fork = 'kivy'
6363
p4a_branch = 'master'
6464
p4a_commit = 'HEAD'
65-
p4a_apk_cmd = "apk --debug --bootstrap="
6665
p4a_recommended_ndk_version = None
6766
extra_p4a_args = ''
6867

6968
def __init__(self, *args, **kwargs):
7069
super().__init__(*args, **kwargs)
71-
self._arch = self.buildozer.config.getdefault(
72-
'app', 'android.arch', DEFAULT_ARCH)
70+
if self.buildozer.config.has_option(
71+
"app", "android.arch"
72+
) and not self.buildozer.config.has_option("app", "android.archs"):
73+
self.buildozer.error("`android.archs` not detected, instead `android.arch` is present.")
74+
self.buildozer.error("`android.arch` will be removed and ignored in future.")
75+
self.buildozer.error("If you're seeing this error, please migrate to `android.archs`.")
76+
self._archs = self.buildozer.config.getlist(
77+
'app', 'android.arch', DEFAULT_ARCHS)
78+
else:
79+
self._archs = self.buildozer.config.getlist(
80+
'app', 'android.archs', DEFAULT_ARCHS)
7381
self._build_dir = join(
74-
self.buildozer.platform_dir, 'build-{}'.format(self._arch))
82+
self.buildozer.platform_dir, 'build-{}'.format(self.archs_snake))
7583
executable = sys.executable or 'python'
7684
self._p4a_cmd = '{} -m pythonforandroid.toolchain '.format(executable)
7785
self._p4a_bootstrap = self.buildozer.config.getdefault(
7886
'app', 'p4a.bootstrap', 'sdl2')
79-
self.p4a_apk_cmd += self._p4a_bootstrap
8087
color = 'always' if USE_COLOR else 'never'
8188
self.extra_p4a_args = ' --color={} --storage-dir="{}"'.format(
8289
color, self._build_dir)
@@ -248,6 +255,10 @@ def sdkmanager_path(self):
248255
'installed'.format(sdkmanager_path)))
249256
return sdkmanager_path
250257

258+
@property
259+
def archs_snake(self):
260+
return "_".join(self._archs)
261+
251262
def check_requirements(self):
252263
if platform in ('win32', 'cygwin'):
253264
try:
@@ -315,6 +326,13 @@ def check_configuration_tokens(self):
315326

316327
super().check_configuration_tokens(errors)
317328

329+
def _p4a_have_aab_support(self):
330+
returncode = self._p4a("aab -h", break_on_error=False, show_output=False)[2]
331+
if returncode == 0:
332+
return True
333+
else:
334+
return False
335+
318336
def _get_available_permissions(self):
319337
key = 'android:available_permissions'
320338
key_sdk = 'android:available_permissions_sdk'
@@ -687,6 +705,12 @@ def install_platform(self):
687705
# ultimate configuration check.
688706
# some of our configuration cannot be check without platform.
689707
self.check_configuration_tokens()
708+
if not self._p4a_have_aab_support():
709+
self.buildozer.error(
710+
"This buildozer version requires a python-for-android version with AAB (Android App Bundle) support. "
711+
"Please update your pinned version accordingly."
712+
)
713+
raise BuildozerException()
690714

691715
self.buildozer.environ.update({
692716
'PACKAGES_PATH': self.buildozer.global_packages_dir,
@@ -809,33 +833,29 @@ def compile_platform(self):
809833
if local_recipes:
810834
options.append('--local-recipes')
811835
options.append(local_recipes)
812-
self._p4a(
813-
("create --dist_name={} --bootstrap={} --requirements={} "
814-
"--arch {} {}").format(
815-
dist_name, self._p4a_bootstrap, requirements,
816-
self._arch, " ".join(options)),
817-
get_stdout=True)[0]
836+
837+
p4a_create = "create --dist_name={} --bootstrap={} --requirements={} ".format(dist_name, self._p4a_bootstrap, requirements)
838+
839+
for arch in self._archs:
840+
p4a_create += "--arch {} ".format(arch)
841+
842+
p4a_create += " ".join(options)
843+
844+
self._p4a(p4a_create, get_stdout=True)[0]
818845

819846
def get_available_packages(self):
820847
return True
821848

822-
def get_dist_dir(self, dist_name, arch):
823-
"""Find the dist dir with the given name and target arch, if one
849+
def get_dist_dir(self, dist_name):
850+
"""Find the dist dir with the given name if one
824851
already exists, otherwise return a new dist_dir name.
825852
"""
826-
expected_dist_name = generate_dist_folder_name(dist_name, arch_names=[arch])
827853

828854
# If the expected dist name does exist, simply use that
829-
expected_dist_dir = join(self._build_dir, 'dists', expected_dist_name)
855+
expected_dist_dir = join(self._build_dir, 'dists', dist_name)
830856
if exists(expected_dist_dir):
831857
return expected_dist_dir
832858

833-
# For backwards compatibility, check if a directory without
834-
# the arch exists. If so, this is probably the target dist.
835-
old_dist_dir = join(self._build_dir, 'dists', dist_name)
836-
if exists(old_dist_dir):
837-
return old_dist_dir
838-
839859
# If no directory has been found yet, our dist probably
840860
# doesn't exist yet, so use the expected name
841861
return expected_dist_dir
@@ -848,7 +868,7 @@ def execute_build_package(self, build_cmd):
848868
# wrapper from previous old_toolchain to new toolchain
849869
dist_name = self.buildozer.config.get('app', 'package.name')
850870
local_recipes = self.get_local_recipes_dir()
851-
cmd = [self.p4a_apk_cmd, "--dist_name", dist_name]
871+
cmd = [self.artifact_format, "--bootstrap", self._p4a_bootstrap, "--dist_name", dist_name]
852872
for args in build_cmd:
853873
option, values = args[0], args[1:]
854874
if option == "debug":
@@ -962,14 +982,16 @@ def execute_build_package(self, build_cmd):
962982
if compile_py:
963983
cmd.append('--no-compile-pyo')
964984

965-
cmd.append('--arch')
966-
cmd.append(self._arch)
985+
for arch in self._archs:
986+
cmd.append('--arch')
987+
cmd.append(arch)
967988

968989
cmd = " ".join(cmd)
969990
self._p4a(cmd)
970991

971992
def get_release_mode(self):
972-
if self.check_p4a_sign_env():
993+
# aab, also if unsigned is named as *-release
994+
if self.check_p4a_sign_env() or self.artifact_format == "aab":
973995
return "release"
974996
return "release-unsigned"
975997

@@ -1060,8 +1082,7 @@ def _generate_whitelist(self, dist_dir):
10601082

10611083
def build_package(self):
10621084
dist_name = self.buildozer.config.get('app', 'package.name')
1063-
arch = self.buildozer.config.getdefault('app', 'android.arch', DEFAULT_ARCH)
1064-
dist_dir = self.get_dist_dir(dist_name, arch)
1085+
dist_dir = self.get_dist_dir(dist_name)
10651086
config = self.buildozer.config
10661087
package = self._get_package()
10671088
version = self.buildozer.get_version()
@@ -1078,7 +1099,7 @@ def build_package(self):
10781099
patterns = config.getlist('app', config_key, [])
10791100
if not patterns:
10801101
continue
1081-
if self._arch != lib_dir:
1102+
if lib_dir not in self._archs:
10821103
continue
10831104

10841105
self.buildozer.debug('Search and copy libs for {}'.format(lib_dir))
@@ -1294,33 +1315,36 @@ def build_package(self):
12941315
if is_gradle_build:
12951316
# on gradle build, the apk use the package name, and have no version
12961317
packagename_src = basename(dist_dir) # gradle specifically uses the folder name
1297-
apk = u'{packagename}-{mode}.apk'.format(
1298-
packagename=packagename_src, mode=mode)
1299-
apk_dir = join(dist_dir, "build", "outputs", "apk", mode_sign)
1318+
artifact = u'{packagename}-{mode}.{artifact_format}'.format(
1319+
packagename=packagename_src, mode=mode, artifact_format=self.artifact_format)
1320+
if self.artifact_format == "apk":
1321+
artifact_dir = join(dist_dir, "build", "outputs", "apk", mode_sign)
1322+
elif self.artifact_format == "aab":
1323+
artifact_dir = join(dist_dir, "build", "outputs", "bundle", mode_sign)
13001324
else:
13011325
# on ant, the apk use the title, and have version
13021326
bl = u'\'" ,'
13031327
apptitle = config.get('app', 'title')
13041328
if hasattr(apptitle, 'decode'):
13051329
apptitle = apptitle.decode('utf-8')
13061330
apktitle = ''.join([x for x in apptitle if x not in bl])
1307-
apk = u'{title}-{version}-{mode}.apk'.format(
1331+
artifact = u'{title}-{version}-{mode}.apk'.format(
13081332
title=apktitle,
13091333
version=version,
13101334
mode=mode)
1311-
apk_dir = join(dist_dir, "bin")
1335+
artifact_dir = join(dist_dir, "bin")
13121336

1313-
apk_dest = u'{packagename}-{version}-{arch}-{mode}.apk'.format(
1337+
artifact_dest = u'{packagename}-{version}-{arch}-{mode}.{artifact_format}'.format(
13141338
packagename=packagename, mode=mode, version=version,
1315-
arch=self._arch)
1339+
arch=self.archs_snake, artifact_format=self.artifact_format)
13161340

13171341
# copy to our place
1318-
copyfile(join(apk_dir, apk), join(self.buildozer.bin_dir, apk_dest))
1342+
copyfile(join(artifact_dir, artifact), join(self.buildozer.bin_dir, artifact_dest))
13191343

13201344
self.buildozer.info('Android packaging done!')
13211345
self.buildozer.info(
1322-
u'APK {0} available in the bin directory'.format(apk_dest))
1323-
self.buildozer.state['android:latestapk'] = apk_dest
1346+
u'APK {0} available in the bin directory'.format(artifact_dest))
1347+
self.buildozer.state['android:latestapk'] = artifact_dest
13241348
self.buildozer.state['android:latestmode'] = self.build_mode
13251349

13261350
def _update_libraries_references(self, dist_dir):
@@ -1438,6 +1462,7 @@ def cmd_deploy(self, *args):
14381462

14391463
if state.get('android:latestmode', '') != 'debug':
14401464
self.buildozer.error('Only debug APK are supported for deploy')
1465+
return
14411466

14421467
# search the APK in the bin dir
14431468
apk = state['android:latestapk']
@@ -1503,28 +1528,3 @@ def cmd_logcat(self, *args):
15031528
def get_target(buildozer):
15041529
buildozer.targetname = "android"
15051530
return TargetAndroid(buildozer)
1506-
1507-
1508-
def generate_dist_folder_name(base_dist_name, arch_names=None):
1509-
"""Generate the distribution folder name to use, based on a
1510-
combination of the input arguments.
1511-
1512-
WARNING: This function is copied from python-for-android. It would
1513-
be preferable to have a proper interface, either importing the p4a
1514-
code or having a p4a dist dir query option.
1515-
1516-
Parameters
1517-
----------
1518-
base_dist_name : str
1519-
The core distribution identifier string
1520-
arch_names : list of str
1521-
The architecture compile targets
1522-
1523-
"""
1524-
if arch_names is None:
1525-
arch_names = ["no_arch_specified"]
1526-
1527-
return '{}__{}'.format(
1528-
base_dist_name,
1529-
'_'.join(arch_names)
1530-
)

docs/source/quickstart.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Init and build for Android
2828
Don't worry, thoses files will be saved in a global directory and will be
2929
shared across the different project you'll manage with Buildozer.
3030

31-
#. At the end, you should have an APK file in the `bin/` directory.
31+
#. At the end, you should have an APK or AAB file in the `bin/` directory.
3232

3333

3434
Run my application

0 commit comments

Comments
 (0)