diff --git a/buildozer/default.spec b/buildozer/default.spec index c48488c75..75b89b6af 100644 --- a/buildozer/default.spec +++ b/buildozer/default.spec @@ -100,7 +100,7 @@ fullscreen = 0 # (int) Target Android API, should be as high as possible. #android.api = 27 -# (int) Minimum API your APK will support. +# (int) Minimum API your APK / AAB will support. #android.minapi = 21 # (int) Android SDK version to use @@ -253,8 +253,9 @@ fullscreen = 0 # (bool) Copy library instead of making a libpymodules.so #android.copy_libs = 1 -# (str) The Android arch to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 -android.arch = armeabi-v7a +# (list) The Android archs to build for, choices: armeabi-v7a, arm64-v8a, x86, x86_64 +# In past, was `android.arch` as we weren't supporting builds for multiple archs at the same time. +android.archs = arm64-v8a, armeabi-v7a # (int) overrides automatic versionCode computation (used in build.gradle) # this is not the same as app version and should only be edited if you know what you're doing @@ -275,6 +276,9 @@ android.allow_backup = True # (bool) disables the compilation of py to pyc/pyo files when packaging # android.no-compile-pyo = True +# (str) The format used to package the app for release mode (aab or apk). +# android.release_artifact = aab + # # Python for android (p4a) specific # @@ -374,7 +378,7 @@ warn_on_root = 1 # (str) Path to build artifact storage, absolute or relative to spec file # build_dir = ./.buildozer -# (str) Path to build output (i.e. .apk, .ipa) storage +# (str) Path to build output (i.e. .apk, .aab, .ipa) storage # bin_dir = ./bin # ----------------------------------------------------------------------------- diff --git a/buildozer/target.py b/buildozer/target.py index 7262a431c..b9515f61f 100644 --- a/buildozer/target.py +++ b/buildozer/target.py @@ -12,6 +12,7 @@ class Target: def __init__(self, buildozer): self.buildozer = buildozer self.build_mode = 'debug' + self.artifact_format = 'apk' self.platform_update = False def check_requirements(self): @@ -101,6 +102,7 @@ def cmd_update(self, *args): def cmd_debug(self, *args): self.buildozer.prepare_for_build() self.build_mode = 'debug' + self.artifact_format = 'apk' self.buildozer.build() def cmd_release(self, *args): @@ -137,6 +139,7 @@ def cmd_release(self, *args): exit(1) self.build_mode = 'release' + self.artifact_format = self.buildozer.config.getdefault('app', 'android.release_artifact', 'aab') self.buildozer.build() def cmd_deploy(self, *args): diff --git a/buildozer/targets/android.py b/buildozer/targets/android.py index b4373e704..d81df1cbf 100644 --- a/buildozer/targets/android.py +++ b/buildozer/targets/android.py @@ -47,7 +47,7 @@ # does. DEFAULT_SDK_TAG = '6514223' -DEFAULT_ARCH = 'armeabi-v7a' +DEFAULT_ARCHS = ['arm64-v8a', 'armeabi-v7a'] MSG_P4A_RECOMMENDED_NDK_ERROR = ( "WARNING: Unable to find recommended Android NDK for current " @@ -62,21 +62,28 @@ class TargetAndroid(Target): p4a_fork = 'kivy' p4a_branch = 'master' p4a_commit = 'HEAD' - p4a_apk_cmd = "apk --debug --bootstrap=" p4a_recommended_ndk_version = None extra_p4a_args = '' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._arch = self.buildozer.config.getdefault( - 'app', 'android.arch', DEFAULT_ARCH) + if self.buildozer.config.has_option( + "app", "android.arch" + ) and not self.buildozer.config.has_option("app", "android.archs"): + self.buildozer.error("`android.archs` not detected, instead `android.arch` is present.") + self.buildozer.error("`android.arch` will be removed and ignored in future.") + self.buildozer.error("If you're seeing this error, please migrate to `android.archs`.") + self._archs = self.buildozer.config.getlist( + 'app', 'android.arch', DEFAULT_ARCHS) + else: + self._archs = self.buildozer.config.getlist( + 'app', 'android.archs', DEFAULT_ARCHS) self._build_dir = join( - self.buildozer.platform_dir, 'build-{}'.format(self._arch)) + self.buildozer.platform_dir, 'build-{}'.format(self.archs_snake)) executable = sys.executable or 'python' self._p4a_cmd = '{} -m pythonforandroid.toolchain '.format(executable) self._p4a_bootstrap = self.buildozer.config.getdefault( 'app', 'p4a.bootstrap', 'sdl2') - self.p4a_apk_cmd += self._p4a_bootstrap color = 'always' if USE_COLOR else 'never' self.extra_p4a_args = ' --color={} --storage-dir="{}"'.format( color, self._build_dir) @@ -248,6 +255,10 @@ def sdkmanager_path(self): 'installed'.format(sdkmanager_path))) return sdkmanager_path + @property + def archs_snake(self): + return "_".join(self._archs) + def check_requirements(self): if platform in ('win32', 'cygwin'): try: @@ -315,6 +326,13 @@ def check_configuration_tokens(self): super().check_configuration_tokens(errors) + def _p4a_have_aab_support(self): + returncode = self._p4a("aab -h", break_on_error=False, show_output=False)[2] + if returncode == 0: + return True + else: + return False + def _get_available_permissions(self): key = 'android:available_permissions' key_sdk = 'android:available_permissions_sdk' @@ -687,6 +705,12 @@ def install_platform(self): # ultimate configuration check. # some of our configuration cannot be check without platform. self.check_configuration_tokens() + if not self._p4a_have_aab_support(): + self.buildozer.error( + "This buildozer version requires a python-for-android version with AAB (Android App Bundle) support. " + "Please update your pinned version accordingly." + ) + raise BuildozerException() self.buildozer.environ.update({ 'PACKAGES_PATH': self.buildozer.global_packages_dir, @@ -809,33 +833,29 @@ def compile_platform(self): if local_recipes: options.append('--local-recipes') options.append(local_recipes) - self._p4a( - ("create --dist_name={} --bootstrap={} --requirements={} " - "--arch {} {}").format( - dist_name, self._p4a_bootstrap, requirements, - self._arch, " ".join(options)), - get_stdout=True)[0] + + p4a_create = "create --dist_name={} --bootstrap={} --requirements={} ".format(dist_name, self._p4a_bootstrap, requirements) + + for arch in self._archs: + p4a_create += "--arch {} ".format(arch) + + p4a_create += " ".join(options) + + self._p4a(p4a_create, get_stdout=True)[0] def get_available_packages(self): return True - def get_dist_dir(self, dist_name, arch): - """Find the dist dir with the given name and target arch, if one + def get_dist_dir(self, dist_name): + """Find the dist dir with the given name if one already exists, otherwise return a new dist_dir name. """ - expected_dist_name = generate_dist_folder_name(dist_name, arch_names=[arch]) # If the expected dist name does exist, simply use that - expected_dist_dir = join(self._build_dir, 'dists', expected_dist_name) + expected_dist_dir = join(self._build_dir, 'dists', dist_name) if exists(expected_dist_dir): return expected_dist_dir - # For backwards compatibility, check if a directory without - # the arch exists. If so, this is probably the target dist. - old_dist_dir = join(self._build_dir, 'dists', dist_name) - if exists(old_dist_dir): - return old_dist_dir - # If no directory has been found yet, our dist probably # doesn't exist yet, so use the expected name return expected_dist_dir @@ -848,7 +868,7 @@ def execute_build_package(self, build_cmd): # wrapper from previous old_toolchain to new toolchain dist_name = self.buildozer.config.get('app', 'package.name') local_recipes = self.get_local_recipes_dir() - cmd = [self.p4a_apk_cmd, "--dist_name", dist_name] + cmd = [self.artifact_format, "--bootstrap", self._p4a_bootstrap, "--dist_name", dist_name] for args in build_cmd: option, values = args[0], args[1:] if option == "debug": @@ -951,14 +971,16 @@ def execute_build_package(self, build_cmd): if compile_py: cmd.append('--no-compile-pyo') - cmd.append('--arch') - cmd.append(self._arch) + for arch in self._archs: + cmd.append('--arch') + cmd.append(arch) cmd = " ".join(cmd) self._p4a(cmd) def get_release_mode(self): - if self.check_p4a_sign_env(): + # aab, also if unsigned is named as *-release + if self.check_p4a_sign_env() or self.artifact_format == "aab": return "release" return "release-unsigned" @@ -1049,8 +1071,7 @@ def _generate_whitelist(self, dist_dir): def build_package(self): dist_name = self.buildozer.config.get('app', 'package.name') - arch = self.buildozer.config.getdefault('app', 'android.arch', DEFAULT_ARCH) - dist_dir = self.get_dist_dir(dist_name, arch) + dist_dir = self.get_dist_dir(dist_name) config = self.buildozer.config package = self._get_package() version = self.buildozer.get_version() @@ -1067,7 +1088,7 @@ def build_package(self): patterns = config.getlist('app', config_key, []) if not patterns: continue - if self._arch != lib_dir: + if lib_dir not in self._archs: continue self.buildozer.debug('Search and copy libs for {}'.format(lib_dir)) @@ -1283,9 +1304,12 @@ def build_package(self): if is_gradle_build: # on gradle build, the apk use the package name, and have no version packagename_src = basename(dist_dir) # gradle specifically uses the folder name - apk = u'{packagename}-{mode}.apk'.format( - packagename=packagename_src, mode=mode) - apk_dir = join(dist_dir, "build", "outputs", "apk", mode_sign) + artifact = u'{packagename}-{mode}.{artifact_format}'.format( + packagename=packagename_src, mode=mode, artifact_format=self.artifact_format) + if self.artifact_format == "apk": + artifact_dir = join(dist_dir, "build", "outputs", "apk", mode_sign) + elif self.artifact_format == "aab": + artifact_dir = join(dist_dir, "build", "outputs", "bundle", mode_sign) else: # on ant, the apk use the title, and have version bl = u'\'" ,' @@ -1293,23 +1317,23 @@ def build_package(self): if hasattr(apptitle, 'decode'): apptitle = apptitle.decode('utf-8') apktitle = ''.join([x for x in apptitle if x not in bl]) - apk = u'{title}-{version}-{mode}.apk'.format( + artifact = u'{title}-{version}-{mode}.apk'.format( title=apktitle, version=version, mode=mode) - apk_dir = join(dist_dir, "bin") + artifact_dir = join(dist_dir, "bin") - apk_dest = u'{packagename}-{version}-{arch}-{mode}.apk'.format( + artifact_dest = u'{packagename}-{version}-{arch}-{mode}.{artifact_format}'.format( packagename=packagename, mode=mode, version=version, - arch=self._arch) + arch=self.archs_snake, artifact_format=self.artifact_format) # copy to our place - copyfile(join(apk_dir, apk), join(self.buildozer.bin_dir, apk_dest)) + copyfile(join(artifact_dir, artifact), join(self.buildozer.bin_dir, artifact_dest)) self.buildozer.info('Android packaging done!') self.buildozer.info( - u'APK {0} available in the bin directory'.format(apk_dest)) - self.buildozer.state['android:latestapk'] = apk_dest + u'APK {0} available in the bin directory'.format(artifact_dest)) + self.buildozer.state['android:latestapk'] = artifact_dest self.buildozer.state['android:latestmode'] = self.build_mode def _update_libraries_references(self, dist_dir): @@ -1492,28 +1516,3 @@ def cmd_logcat(self, *args): def get_target(buildozer): buildozer.targetname = "android" return TargetAndroid(buildozer) - - -def generate_dist_folder_name(base_dist_name, arch_names=None): - """Generate the distribution folder name to use, based on a - combination of the input arguments. - - WARNING: This function is copied from python-for-android. It would - be preferable to have a proper interface, either importing the p4a - code or having a p4a dist dir query option. - - Parameters - ---------- - base_dist_name : str - The core distribution identifier string - arch_names : list of str - The architecture compile targets - - """ - if arch_names is None: - arch_names = ["no_arch_specified"] - - return '{}__{}'.format( - base_dist_name, - '_'.join(arch_names) - ) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 839c27579..128b9fc8c 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -28,7 +28,7 @@ Init and build for Android Don't worry, thoses files will be saved in a global directory and will be shared across the different project you'll manage with Buildozer. -#. At the end, you should have an APK file in the `bin/` directory. +#. At the end, you should have an APK or AAB file in the `bin/` directory. Run my application diff --git a/tests/targets/test_android.py b/tests/targets/test_android.py index 9d2f0b490..56b6a6d6a 100644 --- a/tests/targets/test_android.py +++ b/tests/targets/test_android.py @@ -57,7 +57,7 @@ def call_build_package(target_android): """ buildozer = target_android.buildozer expected_dist_dir = ( - '{buildozer_dir}/android/platform/build-armeabi-v7a/dists/myapp__armeabi-v7a'.format( + '{buildozer_dir}/android/platform/build-arm64-v8a_armeabi-v7a/dists/myapp'.format( buildozer_dir=buildozer.buildozer_dir) ) @@ -79,7 +79,7 @@ def call_build_package(target_android): '{expected_dist_dir}/bin/MyApplication-0.1-debug.apk'.format( expected_dist_dir=expected_dist_dir ), - '{bin_dir}/myapp-0.1-armeabi-v7a-debug.apk'.format(bin_dir=buildozer.bin_dir), + '{bin_dir}/myapp-0.1-arm64-v8a_armeabi-v7a-debug.apk'.format(bin_dir=buildozer.bin_dir), ) ] return m_execute_build_package @@ -104,9 +104,9 @@ def test_init(self): """Tests init defaults.""" target_android = init_target(self.temp_dir) buildozer = target_android.buildozer - assert target_android._arch == "armeabi-v7a" + assert target_android._archs == ["arm64-v8a", "armeabi-v7a"] assert target_android._build_dir.endswith( - ".buildozer/android/platform/build-armeabi-v7a" + ".buildozer/android/platform/build-arm64-v8a_armeabi-v7a" ) assert target_android._p4a_bootstrap == "sdl2" assert target_android._p4a_cmd.endswith( @@ -116,11 +116,10 @@ def test_init(self): assert ( target_android.extra_p4a_args == ( ' --color=always' - ' --storage-dir="{buildozer_dir}/android/platform/build-armeabi-v7a" --ndk-api=21 --ignore-setup-py --debug'.format( + ' --storage-dir="{buildozer_dir}/android/platform/build-arm64-v8a_armeabi-v7a" --ndk-api=21 --ignore-setup-py --debug'.format( buildozer_dir=buildozer.buildozer_dir) ) ) - assert target_android.p4a_apk_cmd == "apk --debug --bootstrap=sdl2" assert target_android.platform_update is False def test_init_positional_buildozer(self):