From 1289d5bfb462d33cb3b41f274b6aa00664edf88b Mon Sep 17 00:00:00 2001 From: rdb Date: Thu, 9 Dec 2021 15:36:23 +0100 Subject: [PATCH] dist: Build multi-ABI distributions on Android [skip ci] --- direct/src/dist/commands.py | 182 +++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 86 deletions(-) diff --git a/direct/src/dist/commands.py b/direct/src/dist/commands.py index e067906973..3ec6866e03 100644 --- a/direct/src/dist/commands.py +++ b/direct/src/dist/commands.py @@ -216,6 +216,7 @@ class build_apps(setuptools.Command): def initialize_options(self): self.build_base = os.path.join(os.getcwd(), 'build') self.application_id = None + self.android_abis = None self.android_debuggable = False self.android_version_code = 1 self.android_min_sdk_version = 21 @@ -418,6 +419,18 @@ class build_apps(setuptools.Command): tmp.update(self.package_data_dirs) self.package_data_dirs = tmp + # Default to all supported ABIs (for the given Android version). + if self.android_max_sdk_version and self.android_max_sdk_version < 21: + if self.android_abis: + for abi in self.android_abis: + assert abi in ('mips64', 'x86_64', 'arm64-v8a'), \ + f'{abi} was not a valid Android ABI before Android 21!' + else: + self.android_abis = ['armeabi-v7a', 'x86'] + + elif not self.android_abis: + self.android_abis = ['arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86'] + self.icon_objects = {} for app, iconpaths in self.icons.items(): if not isinstance(iconpaths, list) and not isinstance(iconpaths, tuple): @@ -434,7 +447,68 @@ class build_apps(setuptools.Command): self.announce('Building platforms: {0}'.format(','.join(self.platforms)), distutils.log.INFO) for platform in self.platforms: - self.build_runtimes(platform, True) + # Create the build directory, or ensure it is empty. + build_dir = os.path.join(self.build_base, platform) + + if os.path.exists(build_dir): + for entry in os.listdir(build_dir): + path = os.path.join(build_dir, entry) + if os.path.islink(path) or os.path.isfile(path): + os.unlink(path) + else: + shutil.rmtree(path) + else: + os.makedirs(build_dir) + + if platform == 'android': + # Make a multi-arch build for Android. + data_dir = os.path.join(build_dir, 'assets') + os.makedirs(data_dir, exist_ok=True) + + for abi in self.android_abis: + lib_dir = os.path.join(build_dir, 'lib', abi) + os.makedirs(lib_dir, exist_ok=True) + + suffix = None + if abi == 'arm64-v8a': + suffix = '_arm64' + elif abi == 'armeabi-v7a': + suffix = '_armv7a' + elif abi == 'armeabi': + suffix = '_arm' + else: # e.g. x86, x86_64, mips, mips64 + suffix = '_' + abi.replace('-', '_') + + self.build_binaries(lib_dir, platform + suffix) + + # Write out the icons to the res directory. + for appname, icon in self.icon_objects.items(): + if appname == '*' or (appname == self.macos_main_app and '*' not in self.icon_objects): + # Conventional name for icon on Android. + basename = 'ic_launcher.png' + else: + basename = f'ic_{appname}.png' + + res_dir = os.path.join(build_dir, 'res') + icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename)) + icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename)) + icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename)) + icon.writeSize(144, os.path.join(res_dir, 'mipmap-xxhdpi-v4', basename)) + + if icon.getLargestSize() >= 192: + icon.writeSize(192, os.path.join(res_dir, 'mipmap-xxxhdpi-v4', basename)) + + self.build_data(data_dir, platform) + + # Generate an AndroidManifest.xml + self.generate_android_manifest(os.path.join(build_dir, 'AndroidManifest.xml')) + else: + self.build_binaries(build_dir, platform) + self.build_data(build_dir, platform) + + # Bundle into an .app on macOS + if self.macos_main_app and 'macosx' in platform: + self.bundle_macos_app(build_dir) def download_wheels(self, platform): """ Downloads wheels for the given platform using pip. This includes panda3d @@ -637,15 +711,10 @@ class build_apps(setuptools.Command): with open(path, 'wb') as fh: tree.write(fh, encoding='utf-8', xml_declaration=True) - def build_runtimes(self, platform, use_wheels): - """ Builds the distributions for the given platform. """ - - builddir = os.path.join(self.build_base, platform) - - if os.path.exists(builddir): - shutil.rmtree(builddir) - os.makedirs(builddir) + def build_binaries(self, binary_dir, platform): + """ Builds the binary data for the given platform. """ + use_wheels = True path = sys.path[:] p3dwhl = None wheelpaths = [] @@ -774,15 +843,14 @@ class build_apps(setuptools.Command): prcexport = '\n'.join(prcexport) if not self.embed_prc_data: prcdir = self.default_prc_dir.replace('', '') - prcdir = os.path.join(builddir, prcdir) + prcdir = os.path.join(binary_dir, prcdir) os.makedirs(prcdir) - with open (os.path.join(prcdir, '00-panda3d.prc'), 'w') as f: + with open(os.path.join(prcdir, '00-panda3d.prc'), 'w') as f: f.write(prcexport) # Create runtimes freezer_extras = set() freezer_modules = set() - freezer_modpaths = set() ext_suffixes = set() def get_search_path_for(source_path): @@ -815,7 +883,7 @@ class build_apps(setuptools.Command): return search_path - def create_runtime(appname, mainscript, target_dir, use_console): + def create_runtime(platform, appname, mainscript, use_console): freezer = FreezeTool.Freezer( platform=platform, path=path, @@ -872,7 +940,7 @@ class build_apps(setuptools.Command): if not self.log_filename or '%' not in self.log_filename: use_strftime = False - target_path = os.path.join(target_dir, target_name) + target_path = os.path.join(binary_dir, target_name) freezer.generateRuntimeFromStub(target_path, stub_file, use_console, { 'prc_data': prcexport if self.embed_prc_data else None, 'default_prc_dir': self.default_prc_dir, @@ -892,53 +960,23 @@ class build_apps(setuptools.Command): os.unlink(temp_file.name) # Copy the dependencies. - search_path = [target_dir] + search_path = [binary_dir] if use_wheels: search_path.append(os.path.join(p3dwhlfn, 'panda3d')) search_path.append(os.path.join(p3dwhlfn, 'deploy_libs')) - self.copy_dependencies(target_path, target_dir, search_path, stub_name) + self.copy_dependencies(target_path, binary_dir, search_path, stub_name) freezer_extras.update(freezer.extras) freezer_modules.update(freezer.getAllModuleNames()) - freezer_modpaths.update({ - mod[1].filename.to_os_specific() - for mod in freezer.getModuleDefs() if mod[1].filename - }) for suffix in freezer.moduleSuffixes: if suffix[2] == imp.C_EXTENSION: ext_suffixes.add(suffix[0]) - # Where should we copy the various file types to? - lib_dir = builddir - data_dir = builddir - - if platform.startswith('android'): - data_dir = os.path.join(data_dir, 'assets') - if platform == 'android_arm64': - lib_dir = os.path.join(lib_dir, 'lib', 'arm64-v8a') - elif platform == 'android_armv7a': - lib_dir = os.path.join(lib_dir, 'lib', 'armeabi-v7a') - elif platform == 'android_arm': - lib_dir = os.path.join(lib_dir, 'lib', 'armeabi') - elif platform == 'androidmips': - lib_dir = os.path.join(lib_dir, 'lib', 'mips') - elif platform == 'android_mips64': - lib_dir = os.path.join(lib_dir, 'lib', 'mips64') - elif platform == 'android_x86': - lib_dir = os.path.join(lib_dir, 'lib', 'x86') - elif platform == 'android_x86_64': - lib_dir = os.path.join(lib_dir, 'lib', 'x86_64') - else: - self.announce('Unrecognized Android architecture {}'.format(platform.split('_', 1)[-1]), distutils.log.ERROR) - - os.makedirs(data_dir, exist_ok=True) - os.makedirs(lib_dir, exist_ok=True) - for appname, scriptname in self.gui_apps.items(): - create_runtime(appname, scriptname, lib_dir, False) + create_runtime(platform, appname, scriptname, False) for appname, scriptname in self.console_apps.items(): - create_runtime(appname, scriptname, lib_dir, True) + create_runtime(platform, appname, scriptname, True) # Copy extension modules whl_modules = [] @@ -974,7 +1012,7 @@ class build_apps(setuptools.Command): plugname = lib.split('.', 1)[0] if plugname in plugin_list: source_path = os.path.join(p3dwhlfn, lib) - target_path = os.path.join(lib_dir, os.path.basename(lib)) + target_path = os.path.join(binary_dir, os.path.basename(lib)) search_path = [os.path.dirname(source_path)] self.copy_with_dependencies(source_path, target_path, search_path) @@ -1021,7 +1059,7 @@ class build_apps(setuptools.Command): basename = 'libpy.' + basename # If this is a dynamic library, search for dependencies. - target_path = os.path.join(lib_dir, basename) + target_path = os.path.join(binary_dir, basename) search_path = get_search_path_for(source_path) self.copy_with_dependencies(source_path, target_path, search_path) @@ -1032,19 +1070,19 @@ class build_apps(setuptools.Command): if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules: self.announce('Copying Tcl files', distutils.log.INFO) - os.makedirs(os.path.join(data_dir, 'tcl')) + os.makedirs(os.path.join(binary_dir, 'tcl')) for dir in os.listdir(tcl_dir): sub_dir = os.path.join(tcl_dir, dir) if os.path.isdir(sub_dir): - target_dir = os.path.join(data_dir, 'tcl', dir) + target_dir = os.path.join(binary_dir, 'tcl', dir) self.announce('copying {0} -> {1}'.format(sub_dir, target_dir)) shutil.copytree(sub_dir, target_dir) # Copy classes.dex on Android if use_wheels and platform.startswith('android'): self.copy(os.path.join(p3dwhlfn, 'deploy_libs', 'classes.dex'), - os.path.join(builddir, 'classes.dex')) + os.path.join(binary_dir, '..', '..', 'classes.dex')) # Extract any other data files from dependency packages. for module, datadesc in self.package_data_dirs.items(): @@ -1082,15 +1120,18 @@ class build_apps(setuptools.Command): else: self.copy(source_path, target_path) + def build_data(self, data_dir, platform): + """ Builds the data files for the given platform. """ + # Copy Game Files self.announce('Copying game files for platform: {}'.format(platform), distutils.log.INFO) ignore_copy_list = [ '**/__pycache__/**', '**/*.pyc', + '**/*.py', '{}/**'.format(self.build_base), ] ignore_copy_list += self.exclude_patterns - ignore_copy_list += freezer_modpaths ignore_copy_list += self.extra_prc_files ignore_copy_list = [p3d.GlobPattern(p3d.Filename.from_os_specific(i).get_fullpath()) for i in ignore_copy_list] @@ -1191,31 +1232,6 @@ class build_apps(setuptools.Command): copy_file(src, dst) - if 'android' in platform: - # Generate an AndroidManifest.xml - self.generate_android_manifest(os.path.join(builddir, 'AndroidManifest.xml')) - - # Write out the icons to the res directory. - for appname, icon in self.icon_objects.items(): - if appname == '*' or (appname == self.macos_main_app and '*' not in self.icon_objects): - # Conventional name for icon on Android. - basename = 'ic_launcher.png' - else: - basename = f'ic_{appname}.png' - - res_dir = os.path.join(builddir, 'res') - icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename)) - icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename)) - icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename)) - icon.writeSize(144, os.path.join(res_dir, 'mipmap-xxhdpi-v4', basename)) - - if icon.getLargestSize() >= 192: - icon.writeSize(192, os.path.join(res_dir, 'mipmap-xxxhdpi-v4', basename)) - - # Bundle into an .app on macOS - if self.macos_main_app and 'macosx' in platform: - self.bundle_macos_app(builddir) - def add_dependency(self, name, target_dir, search_path, referenced_by): """ Searches for the given DLL on the search path. If it exists, copies it to the target_dir. """ @@ -1533,13 +1549,7 @@ class bdist_apps(setuptools.Command): 'manylinux1_i686': ['gztar'], 'manylinux2010_x86_64': ['gztar'], 'manylinux2010_i686': ['gztar'], - 'android_arm64': ['aab'], - 'android_armv7a': ['aab'], - 'android_arm': ['aab'], - 'android_mips': ['aab'], - 'android_mips64': ['aab'], - 'android_x86': ['aab'], - 'android_x86_64': ['aab'], + 'android': ['aab'], # Everything else defaults to ['zip'] }