Browse Source

Support deploying Python apps for Android using bdist_apps

rdb 4 years ago
parent
commit
44e10d10a0

+ 1 - 1
direct/src/dist/FreezeTool.py

@@ -1835,7 +1835,7 @@ class Freezer:
             # If it is a submodule of a frozen module, Python will have
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it dynamically.
-            if '.' in moduleName:
+            if '.' in moduleName and not self.platform.startswith('android'):
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
                     code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)

+ 250 - 0
direct/src/dist/_android.py

@@ -0,0 +1,250 @@
+"""Internal support module for Android builds."""
+
+import xml.etree.ElementTree as ET
+
+from ._proto.targeting_pb2 import Abi
+from ._proto.config_pb2 import BundleConfig
+from ._proto.files_pb2 import NativeLibraries
+from ._proto.Resources_pb2 import XmlNode, ResourceTable
+
+
+AbiAlias = Abi.AbiAlias
+
+
+def str_resource(id):
+    def compile(attrib):
+        attrib.resource_id = id
+    return compile
+
+
+def int_resource(id):
+    def compile(attrib):
+        attrib.resource_id = id
+        if attrib.value.startswith('0x') or attrib.value.startswith('0X'):
+            attrib.compiled_item.prim.int_hexadecimal_value = int(attrib.value, 16)
+        else:
+            attrib.compiled_item.prim.int_decimal_value = int(attrib.value)
+    return compile
+
+
+def bool_resource(id):
+    def compile(attrib):
+        attrib.resource_id = id
+        attrib.compiled_item.prim.boolean_value = {
+            'true': True, '1': True, 'false': False, '0': False
+        }[attrib.value]
+    return compile
+
+
+def enum_resource(id, /, *values):
+    def compile(attrib):
+        attrib.resource_id = id
+        attrib.compiled_item.prim.int_decimal_value = values.index(attrib.value)
+    return compile
+
+
+def flag_resource(id, /, **values):
+    def compile(attrib):
+        attrib.resource_id = id
+        bitmask = 0
+        flags = attrib.value.split('|')
+        for flag in flags:
+            bitmask = values[flag]
+        attrib.compiled_item.prim.int_hexadecimal_value = bitmask
+    return compile
+
+
+def ref_resource(id, type):
+    def compile(attrib):
+        assert attrib.value[0] == '@'
+        ref_type, ref_name = attrib.value[1:].split('/')
+        attrib.resource_id = id
+        if ref_type == 'android:style':
+            attrib.compiled_item.ref.id = ANDROID_STYLES[ref_name]
+            attrib.compiled_item.ref.name = ref_type + '/' + ref_name
+        else:
+            print(f'Warning: unhandled AndroidManifest.xml reference "{attrib.value}"')
+    return compile
+
+
+# See data/res/values/public.xml
+ANDROID_STYLES = {
+    'Animation': 0x01030000,
+    'Animation.Activity': 0x01030001,
+    'Animation.Dialog': 0x01030002,
+    'Animation.Translucent': 0x01030003,
+    'Animation.Toast': 0x01030004,
+    'Theme': 0x01030005,
+    'Theme.NoTitleBar': 0x01030006,
+    'Theme.NoTitleBar.Fullscreen': 0x01030007,
+    'Theme.Black': 0x01030008,
+    'Theme.Black.NoTitleBar': 0x01030009,
+    'Theme.Black.NoTitleBar.Fullscreen': 0x0103000a,
+    'Theme.Dialog': 0x0103000b,
+    'Theme.Light': 0x0103000c,
+    'Theme.Light.NoTitleBar': 0x0103000d,
+    'Theme.Light.NoTitleBar.Fullscreen': 0x0103000e,
+    'Theme.Translucent': 0x0103000f,
+    'Theme.Translucent.NoTitleBar': 0x01030010,
+    'Theme.Translucent.NoTitleBar.Fullscreen': 0x01030011,
+    'Widget': 0x01030012,
+    'Widget.AbsListView': 0x01030013,
+    'Widget.Button': 0x01030014,
+    'Widget.Button.Inset': 0x01030015,
+    'Widget.Button.Small': 0x01030016,
+    'Widget.Button.Toggle': 0x01030017,
+    'Widget.CompoundButton': 0x01030018,
+    'Widget.CompoundButton.CheckBox': 0x01030019,
+    'Widget.CompoundButton.RadioButton': 0x0103001a,
+    'Widget.CompoundButton.Star': 0x0103001b,
+    'Widget.ProgressBar': 0x0103001c,
+    'Widget.ProgressBar.Large': 0x0103001d,
+    'Widget.ProgressBar.Small': 0x0103001e,
+    'Widget.ProgressBar.Horizontal': 0x0103001f,
+    'Widget.SeekBar': 0x01030020,
+    'Widget.RatingBar': 0x01030021,
+    'Widget.TextView': 0x01030022,
+    'Widget.EditText': 0x01030023,
+    'Widget.ExpandableListView': 0x01030024,
+    'Widget.ImageWell': 0x01030025,
+    'Widget.ImageButton': 0x01030026,
+    'Widget.AutoCompleteTextView': 0x01030027,
+    'Widget.Spinner': 0x01030028,
+    'Widget.TextView.PopupMenu': 0x01030029,
+    'Widget.TextView.SpinnerItem': 0x0103002a,
+    'Widget.DropDownItem': 0x0103002b,
+    'Widget.DropDownItem.Spinner': 0x0103002c,
+    'Widget.ScrollView': 0x0103002d,
+    'Widget.ListView': 0x0103002e,
+    'Widget.ListView.White': 0x0103002f,
+    'Widget.ListView.DropDown': 0x01030030,
+    'Widget.ListView.Menu': 0x01030031,
+    'Widget.GridView': 0x01030032,
+    'Widget.WebView': 0x01030033,
+    'Widget.TabWidget': 0x01030034,
+    'Widget.Gallery': 0x01030035,
+    'Widget.PopupWindow': 0x01030036,
+    'MediaButton': 0x01030037,
+    'MediaButton.Previous': 0x01030038,
+    'MediaButton.Next': 0x01030039,
+    'MediaButton.Play': 0x0103003a,
+    'MediaButton.Ffwd': 0x0103003b,
+    'MediaButton.Rew': 0x0103003c,
+    'MediaButton.Pause': 0x0103003d,
+    'TextAppearance': 0x0103003e,
+    'TextAppearance.Inverse': 0x0103003f,
+    'TextAppearance.Theme': 0x01030040,
+    'TextAppearance.DialogWindowTitle': 0x01030041,
+    'TextAppearance.Large': 0x01030042,
+    'TextAppearance.Large.Inverse': 0x01030043,
+    'TextAppearance.Medium': 0x01030044,
+    'TextAppearance.Medium.Inverse': 0x01030045,
+    'TextAppearance.Small': 0x01030046,
+    'TextAppearance.Small.Inverse': 0x01030047,
+    'TextAppearance.Theme.Dialog': 0x01030048,
+    'TextAppearance.Widget': 0x01030049,
+    'TextAppearance.Widget.Button': 0x0103004a,
+    'TextAppearance.Widget.IconMenu.Item': 0x0103004b,
+    'TextAppearance.Widget.EditText': 0x0103004c,
+    'TextAppearance.Widget.TabWidget': 0x0103004d,
+    'TextAppearance.Widget.TextView': 0x0103004e,
+    'TextAppearance.Widget.TextView.PopupMenu': 0x0103004f,
+    'TextAppearance.Widget.DropDownHint': 0x01030050,
+    'TextAppearance.Widget.DropDownItem': 0x01030051,
+    'TextAppearance.Widget.TextView.SpinnerItem': 0x01030052,
+    'TextAppearance.WindowTitle': 0x01030053,
+}
+
+
+# See data/res/values/public.xml, attrs.xml and especially attrs_manifest.xml
+ANDROID_ATTRIBUTES = {
+    'allowBackup': bool_resource(0x1010280),
+    'allowClearUserData': bool_resource(0x1010005),
+    'allowParallelSyncs': bool_resource(0x1010332),
+    'allowSingleTap': bool_resource(0x1010259),
+    'allowTaskReparenting': bool_resource(0x1010204),
+    'alwaysRetainTaskState': bool_resource(0x1010203),
+    'clearTaskOnLaunch': bool_resource(0x1010015),
+    'debuggable': bool_resource(0x0101000f),
+    'configChanges': flag_resource(0x0101001f, mcc=0x0001, mnc=0x0002, locale=0x0004, touchscreen=0x0008, keyboard=0x0010, keyboardHidden=0x0020, navigation=0x0040, orientation=0x0080, screenLayout=0x0100, uiMode=0x0200, screenSize=0x0400, smallestScreenSize=0x0800, layoutDirection=0x2000, fontScale=0x40000000),
+    'enabled': bool_resource(0x101000e),
+    'excludeFromRecents': bool_resource(0x1010017),
+    'extractNativeLibs': bool_resource(0x10104ea),
+    'finishOnTaskLaunch': bool_resource(0x1010014),
+    'fullBackupContent': bool_resource(0x10104eb),
+    'glEsVersion': int_resource(0x1010281),
+    'hasCode': bool_resource(0x101000c),
+    'host': str_resource(0x1010028),
+    'immersive': bool_resource(0x10102c0),
+    'installLocation': enum_resource(0x10102b7, "auto", "internalOnly", "preferExternal"),
+    'isGame': bool_resource(0x010103f4),
+    'label': str_resource(0x01010001),
+    'launchMode': enum_resource(0x101001d, "standard", "singleTop", "singleTask", "singleInstance"),
+    'maxSdkVersion': int_resource(0x1010271),
+    'mimeType': str_resource(0x1010026),
+    'minSdkVersion': int_resource(0x101020c),
+    'multiprocess': bool_resource(0x1010013),
+    'name': str_resource(0x1010003),
+    'pathPattern': str_resource(0x101002c),
+    'required': bool_resource(0x101028e),
+    'scheme': str_resource(0x1010027),
+    'stateNotNeeded': bool_resource(0x1010016),
+    'supportsUploading': bool_resource(0x101029b),
+    'targetSandboxVersion': int_resource(0x101054c),
+    'targetSdkVersion': int_resource(0x1010270),
+    'theme': ref_resource(0x01010000, 'android:style'),
+    'value': str_resource(0x1010024),
+    'versionCode': int_resource(0x101021b),
+    'versionName': str_resource(0x101021c),
+}
+
+
+class AndroidManifest:
+    def __init__(self):
+        super().__init__()
+        self._stack = []
+        self.root = XmlNode()
+
+    def parse_xml(self, data):
+        parser = ET.XMLParser(target=self)
+        parser.feed(data)
+        parser.close()
+
+    def start_ns(self, prefix, uri):
+        decl = self.root.element.namespace_declaration.add()
+        decl.prefix = prefix
+        decl.uri = uri
+
+    def start(self, tag, attribs):
+        if not self._stack:
+            node = self.root
+        else:
+            node = self._stack[-1].child.add()
+
+        element = node.element
+        element.name = tag
+
+        self._stack.append(element)
+
+        for key, value in attribs.items():
+            attrib = element.attribute.add()
+            attrib.value = value
+
+            if key.startswith('{'):
+                attrib.namespace_uri, key = key[1:].split('}', 1)
+                res_compile = ANDROID_ATTRIBUTES.get(key, None)
+                if not res_compile:
+                    print(f'Warning: unhandled AndroidManifest.xml attribute "{key}"')
+            else:
+                res_compile = None
+
+            attrib.name = key
+
+            if res_compile:
+                res_compile(attrib)
+
+    def end(self, tag):
+        self._stack.pop()
+
+    def dumps(self):
+        return self.root.SerializeToString()

File diff suppressed because it is too large
+ 21 - 0
direct/src/dist/_proto/Configuration_pb2.py


+ 4 - 0
direct/src/dist/_proto/README

@@ -0,0 +1,4 @@
+The files in this directory were generated from the .proto files in the
+bundletool and aapt2 repositories.
+
+They are used by installer.py when generating an Android App Bundle.

File diff suppressed because it is too large
+ 22 - 0
direct/src/dist/_proto/Resources_pb2.py


+ 0 - 0
direct/src/dist/_proto/__init__.py


File diff suppressed because it is too large
+ 21 - 0
direct/src/dist/_proto/config_pb2.py


+ 307 - 0
direct/src/dist/_proto/files_pb2.py

@@ -0,0 +1,307 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: files.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import targeting_pb2 as targeting__pb2
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='files.proto',
+  package='android.bundle',
+  syntax='proto3',
+  serialized_options=b'\n\022com.android.bundle',
+  create_key=_descriptor._internal_create_key,
+  serialized_pb=b'\n\x0b\x66iles.proto\x12\x0e\x61ndroid.bundle\x1a\x0ftargeting.proto\"D\n\x06\x41ssets\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedAssetsDirectory\"M\n\x0fNativeLibraries\x12:\n\tdirectory\x18\x01 \x03(\x0b\x32\'.android.bundle.TargetedNativeDirectory\"D\n\nApexImages\x12\x30\n\x05image\x18\x01 \x03(\x0b\x32!.android.bundle.TargetedApexImageJ\x04\x08\x02\x10\x03\"d\n\x17TargetedAssetsDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.AssetsDirectoryTargeting\"d\n\x17TargetedNativeDirectory\x12\x0c\n\x04path\x18\x01 \x01(\t\x12;\n\ttargeting\x18\x02 \x01(\x0b\x32(.android.bundle.NativeDirectoryTargeting\"q\n\x11TargetedApexImage\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x17\n\x0f\x62uild_info_path\x18\x03 \x01(\t\x12\x35\n\ttargeting\x18\x02 \x01(\x0b\x32\".android.bundle.ApexImageTargetingB\x14\n\x12\x63om.android.bundleb\x06proto3'
+  ,
+  dependencies=[targeting__pb2.DESCRIPTOR,])
+
+
+
+
+_ASSETS = _descriptor.Descriptor(
+  name='Assets',
+  full_name='android.bundle.Assets',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='directory', full_name='android.bundle.Assets.directory', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=48,
+  serialized_end=116,
+)
+
+
+_NATIVELIBRARIES = _descriptor.Descriptor(
+  name='NativeLibraries',
+  full_name='android.bundle.NativeLibraries',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='directory', full_name='android.bundle.NativeLibraries.directory', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=118,
+  serialized_end=195,
+)
+
+
+_APEXIMAGES = _descriptor.Descriptor(
+  name='ApexImages',
+  full_name='android.bundle.ApexImages',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='image', full_name='android.bundle.ApexImages.image', index=0,
+      number=1, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=197,
+  serialized_end=265,
+)
+
+
+_TARGETEDASSETSDIRECTORY = _descriptor.Descriptor(
+  name='TargetedAssetsDirectory',
+  full_name='android.bundle.TargetedAssetsDirectory',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='android.bundle.TargetedAssetsDirectory.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='targeting', full_name='android.bundle.TargetedAssetsDirectory.targeting', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=267,
+  serialized_end=367,
+)
+
+
+_TARGETEDNATIVEDIRECTORY = _descriptor.Descriptor(
+  name='TargetedNativeDirectory',
+  full_name='android.bundle.TargetedNativeDirectory',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='android.bundle.TargetedNativeDirectory.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='targeting', full_name='android.bundle.TargetedNativeDirectory.targeting', index=1,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=369,
+  serialized_end=469,
+)
+
+
+_TARGETEDAPEXIMAGE = _descriptor.Descriptor(
+  name='TargetedApexImage',
+  full_name='android.bundle.TargetedApexImage',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='path', full_name='android.bundle.TargetedApexImage.path', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='build_info_path', full_name='android.bundle.TargetedApexImage.build_info_path', index=1,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='targeting', full_name='android.bundle.TargetedApexImage.targeting', index=2,
+      number=2, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=471,
+  serialized_end=584,
+)
+
+_ASSETS.fields_by_name['directory'].message_type = _TARGETEDASSETSDIRECTORY
+_NATIVELIBRARIES.fields_by_name['directory'].message_type = _TARGETEDNATIVEDIRECTORY
+_APEXIMAGES.fields_by_name['image'].message_type = _TARGETEDAPEXIMAGE
+_TARGETEDASSETSDIRECTORY.fields_by_name['targeting'].message_type = targeting__pb2._ASSETSDIRECTORYTARGETING
+_TARGETEDNATIVEDIRECTORY.fields_by_name['targeting'].message_type = targeting__pb2._NATIVEDIRECTORYTARGETING
+_TARGETEDAPEXIMAGE.fields_by_name['targeting'].message_type = targeting__pb2._APEXIMAGETARGETING
+DESCRIPTOR.message_types_by_name['Assets'] = _ASSETS
+DESCRIPTOR.message_types_by_name['NativeLibraries'] = _NATIVELIBRARIES
+DESCRIPTOR.message_types_by_name['ApexImages'] = _APEXIMAGES
+DESCRIPTOR.message_types_by_name['TargetedAssetsDirectory'] = _TARGETEDASSETSDIRECTORY
+DESCRIPTOR.message_types_by_name['TargetedNativeDirectory'] = _TARGETEDNATIVEDIRECTORY
+DESCRIPTOR.message_types_by_name['TargetedApexImage'] = _TARGETEDAPEXIMAGE
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+Assets = _reflection.GeneratedProtocolMessageType('Assets', (_message.Message,), {
+  'DESCRIPTOR' : _ASSETS,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.Assets)
+  })
+_sym_db.RegisterMessage(Assets)
+
+NativeLibraries = _reflection.GeneratedProtocolMessageType('NativeLibraries', (_message.Message,), {
+  'DESCRIPTOR' : _NATIVELIBRARIES,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.NativeLibraries)
+  })
+_sym_db.RegisterMessage(NativeLibraries)
+
+ApexImages = _reflection.GeneratedProtocolMessageType('ApexImages', (_message.Message,), {
+  'DESCRIPTOR' : _APEXIMAGES,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.ApexImages)
+  })
+_sym_db.RegisterMessage(ApexImages)
+
+TargetedAssetsDirectory = _reflection.GeneratedProtocolMessageType('TargetedAssetsDirectory', (_message.Message,), {
+  'DESCRIPTOR' : _TARGETEDASSETSDIRECTORY,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.TargetedAssetsDirectory)
+  })
+_sym_db.RegisterMessage(TargetedAssetsDirectory)
+
+TargetedNativeDirectory = _reflection.GeneratedProtocolMessageType('TargetedNativeDirectory', (_message.Message,), {
+  'DESCRIPTOR' : _TARGETEDNATIVEDIRECTORY,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.TargetedNativeDirectory)
+  })
+_sym_db.RegisterMessage(TargetedNativeDirectory)
+
+TargetedApexImage = _reflection.GeneratedProtocolMessageType('TargetedApexImage', (_message.Message,), {
+  'DESCRIPTOR' : _TARGETEDAPEXIMAGE,
+  '__module__' : 'files_pb2'
+  # @@protoc_insertion_point(class_scope:android.bundle.TargetedApexImage)
+  })
+_sym_db.RegisterMessage(TargetedApexImage)
+
+
+DESCRIPTOR._options = None
+# @@protoc_insertion_point(module_scope)

File diff suppressed because it is too large
+ 22 - 0
direct/src/dist/_proto/targeting_pb2.py


+ 199 - 16
direct/src/dist/commands.py

@@ -157,6 +157,50 @@ if os.path.isdir(tcl_dir):
 del os
 """
 
+SITE_PY_ANDROID = """
+import sys, os
+from _frozen_importlib import _imp, FrozenImporter
+from importlib import _bootstrap_external
+from importlib.abc import Loader, MetaPathFinder
+from importlib.machinery import ModuleSpec
+
+
+sys.frozen = True
+sys.platform = "android"
+
+
+# Alter FrozenImporter to give a __file__ property to frozen modules.
+_find_spec = FrozenImporter.find_spec
+
+def find_spec(fullname, path=None, target=None):
+    spec = _find_spec(fullname, path=path, target=target)
+    if spec:
+        spec.has_location = True
+        spec.origin = sys.executable
+    return spec
+
+def get_data(path):
+    with open(path, 'rb') as fp:
+        return fp.read()
+
+FrozenImporter.find_spec = find_spec
+FrozenImporter.get_data = get_data
+
+
+class AndroidExtensionFinder(MetaPathFinder):
+    @classmethod
+    def find_spec(cls, fullname, path=None, target=None):
+        soname = 'libpy.' + fullname + '.so'
+        path = os.path.join(sys._native_library_dir, soname)
+
+        if os.path.exists(path):
+            loader = _bootstrap_external.ExtensionFileLoader(fullname, path)
+            return ModuleSpec(fullname, loader, origin=path)
+
+
+sys.meta_path.append(AndroidExtensionFinder)
+"""
+
 
 class build_apps(setuptools.Command):
     description = 'build Panda3D applications'
@@ -171,6 +215,12 @@ class build_apps(setuptools.Command):
 
     def initialize_options(self):
         self.build_base = os.path.join(os.getcwd(), 'build')
+        self.application_id = None
+        self.android_debuggable = False
+        self.android_version_code = 1
+        self.android_min_sdk_version = 21
+        self.android_max_sdk_version = None
+        self.android_target_sdk_version = 30
         self.gui_apps = {}
         self.console_apps = {}
         self.macos_main_app = None
@@ -266,6 +316,11 @@ class build_apps(setuptools.Command):
             '/usr/lib/libxar.1.dylib',
             '/usr/lib/libmenu.5.4.dylib',
             '/System/Library/**',
+
+            # Android
+            'libc.so', 'libm.so', 'liblog.so', 'libdl.so', 'libandroid.so',
+            'libGLESv1_CM.so', 'libGLESv2.so', 'libjnigraphics.so', 'libEGL.so',
+            'libOpenSLES.so', 'libandroid.so', 'libOpenMAXAL.so',
         ]
 
         self.package_data_dirs = {}
@@ -508,6 +563,72 @@ class build_apps(setuptools.Command):
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
             plistlib.dump(plist, f)
 
+    def generate_android_manifest(self, path):
+        import xml.etree.ElementTree as ET
+
+        name = self.distribution.get_name()
+        version = self.distribution.get_version()
+        classifiers = self.distribution.get_classifiers()
+
+        is_game = False
+        for classifier in classifiers:
+            if classifier == 'Topic :: Games/Entertainment' or classifier.startswith('Topic :: Games/Entertainment ::'):
+                is_game = True
+
+        manifest = ET.Element('manifest')
+        manifest.set('xmlns:android', 'http://schemas.android.com/apk/res/android')
+        manifest.set('package', self.application_id)
+        manifest.set('android:versionCode', str(int(self.android_version_code)))
+        manifest.set('android:versionName', version)
+        manifest.set('android:installLocation', 'auto')
+
+        uses_sdk = ET.SubElement(manifest, 'uses-sdk')
+        uses_sdk.set('android:minSdkVersion', str(int(self.android_min_sdk_version)))
+        uses_sdk.set('android:targetSdkVersion', str(int(self.android_target_sdk_version)))
+        if self.android_max_sdk_version:
+            uses_sdk.set('android:maxSdkVersion', str(int(self.android_max_sdk_version)))
+
+        if 'pandagles2' in self.plugins:
+            uses_feature = ET.SubElement(manifest, 'uses-feature')
+            uses_feature.set('android:glEsVersion', '0x00020000')
+            uses_feature.set('android:required', 'false' if 'pandagles' in self.plugins else 'true')
+
+        if 'p3openal_audio' in self.plugins:
+            uses_feature = ET.SubElement(manifest, 'uses-feature')
+            uses_feature.set('android:name', 'android.hardware.audio.output')
+            uses_feature.set('android:required', 'false')
+
+        uses_feature = ET.SubElement(manifest, 'uses-feature')
+        uses_feature.set('android:name', 'android.hardware.gamepad')
+        uses_feature.set('android:required', 'false')
+
+        application = ET.SubElement(manifest, 'application')
+        application.set('android:label', name)
+        application.set('android:isGame', ('false', 'true')[is_game])
+        application.set('android:debuggable', ('false', 'true')[self.android_debuggable])
+        application.set('android:extractNativeLibs', 'true')
+
+        for appname in self.gui_apps:
+            activity = ET.SubElement(application, 'activity')
+            activity.set('android:name', 'org.panda3d.android.PandaActivity')
+            activity.set('android:label', appname)
+            activity.set('android:theme', '@android:style/Theme.NoTitleBar')
+            activity.set('android:configChanges', 'orientation|keyboardHidden')
+            activity.set('android:launchMode', 'singleInstance')
+
+            meta_data = ET.SubElement(activity, 'meta-data')
+            meta_data.set('android:name', 'android.app.lib_name')
+            meta_data.set('android:value', appname)
+
+            intent_filter = ET.SubElement(activity, 'intent-filter')
+            ET.SubElement(intent_filter, 'action').set('android:name', 'android.intent.action.MAIN')
+            ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LAUNCHER')
+            ET.SubElement(intent_filter, 'category').set('android:name', 'android.intent.category.LEANBACK_LAUNCHER')
+
+        tree = ET.ElementTree(manifest)
+        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. """
 
@@ -607,6 +728,9 @@ class build_apps(setuptools.Command):
                     value = value[:c].rstrip()
 
                 if var == 'model-cache-dir' and value:
+                    if platform.startswith('android'):
+                        # Ignore on Android, where the cache dir is fixed.
+                        continue
                     value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
 
                 if var == 'audio-library-name':
@@ -683,33 +807,42 @@ class build_apps(setuptools.Command):
 
             return search_path
 
-        def create_runtime(appname, mainscript, use_console):
+        def create_runtime(appname, mainscript, target_dir, use_console):
             freezer = FreezeTool.Freezer(
                 platform=platform,
                 path=path,
                 hiddenImports=self.hidden_imports
             )
             freezer.addModule('__main__', filename=mainscript)
-            freezer.addModule('site', filename='site.py', text=SITE_PY)
+            if platform.startswith('android'):
+                freezer.addModule('site', filename='site.py', text=SITE_PY_ANDROID)
+            else:
+                freezer.addModule('site', filename='site.py', text=SITE_PY)
             for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
                 freezer.addModule(incmod)
             for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
                 freezer.excludeModule(exmod)
             freezer.done(addStartupModules=True)
 
-            target_path = os.path.join(builddir, appname)
-
             stub_name = 'deploy-stub'
+            target_name = appname
             if platform.startswith('win') or 'macosx' in platform:
                 if not use_console:
                     stub_name = 'deploy-stubw'
+            elif platform.startswith('android'):
+                if not use_console:
+                    stub_name = 'libdeploy-stubw.so'
+                    target_name = 'lib' + target_name + '.so'
 
             if platform.startswith('win'):
                 stub_name += '.exe'
-                target_path += '.exe'
+                target_name += '.exe'
 
             if use_wheels:
-                stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
+                if stub_name.endswith('.so'):
+                    stub_file = p3dwhl.open('deploy_libs/{0}'.format(stub_name))
+                else:
+                    stub_file = p3dwhl.open('panda3d_tools/{0}'.format(stub_name))
             else:
                 dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
                 stub_path = os.path.join(os.path.dirname(dtool_path), '..', 'bin', stub_name)
@@ -731,6 +864,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)
             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,
@@ -750,10 +884,11 @@ class build_apps(setuptools.Command):
                 os.unlink(temp_file.name)
 
             # Copy the dependencies.
-            search_path = [builddir]
+            search_path = [target_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, builddir, search_path, stub_name)
+            self.copy_dependencies(target_path, target_dir, search_path, stub_name)
 
             freezer_extras.update(freezer.extras)
             freezer_modules.update(freezer.getAllModuleNames())
@@ -765,11 +900,37 @@ class build_apps(setuptools.Command):
                 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, False)
+            create_runtime(appname, scriptname, lib_dir, False)
 
         for appname, scriptname in self.console_apps.items():
-            create_runtime(appname, scriptname, True)
+            create_runtime(appname, scriptname, lib_dir, True)
 
         # Copy extension modules
         whl_modules = []
@@ -805,7 +966,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(builddir, os.path.basename(lib))
+                target_path = os.path.join(lib_dir, os.path.basename(lib))
                 search_path = [os.path.dirname(source_path)]
                 self.copy_with_dependencies(source_path, target_path, search_path)
 
@@ -846,8 +1007,13 @@ class build_apps(setuptools.Command):
                 else:
                     continue
 
+            if platform.startswith('android'):
+                # Python modules on Android need a special prefix to be loadable
+                # as a library.
+                basename = 'libpy.' + basename
+
             # If this is a dynamic library, search for dependencies.
-            target_path = os.path.join(builddir, basename)
+            target_path = os.path.join(lib_dir, basename)
             search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_path)
 
@@ -858,15 +1024,20 @@ 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(builddir, 'tcl'))
+                os.makedirs(os.path.join(data_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(builddir, 'tcl', dir)
+                        target_dir = os.path.join(data_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'))
+
         # Extract any other data files from dependency packages.
         for module, datadesc in self.package_data_dirs.items():
             if module not in freezer_modules:
@@ -883,7 +1054,7 @@ class build_apps(setuptools.Command):
                     source_dir = os.path.dirname(source_pattern)
                     # Relocate the target dir to the build directory.
                     target_dir = target_dir.replace('/', os.sep)
-                    target_dir = os.path.join(builddir, target_dir)
+                    target_dir = os.path.join(data_dir, target_dir)
 
                     for wf in filenames:
                         if wf.lower().startswith(source_dir.lower() + '/'):
@@ -1008,10 +1179,14 @@ class build_apps(setuptools.Command):
 
             for fname in filelist:
                 src = os.path.join(dirpath, fname)
-                dst = os.path.join(builddir, update_path(src))
+                dst = os.path.join(data_dir, update_path(src))
 
                 copy_file(src, dst)
 
+        if 'android' in platform:
+            # Generate an AndroidManifest.xml
+            self.generate_android_manifest(os.path.join(builddir, 'AndroidManifest.xml'))
+
         # Bundle into an .app on macOS
         if self.macos_main_app and 'macosx' in platform:
             self.bundle_macos_app(builddir)
@@ -1333,6 +1508,13 @@ 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'],
         # Everything else defaults to ['zip']
     }
 
@@ -1342,6 +1524,7 @@ class bdist_apps(setuptools.Command):
         'bztar': installers.create_bztar,
         'xztar': installers.create_xztar,
         'nsis': installers.create_nsis,
+        'aab': installers.create_aab,
     }
 
     description = 'bundle built Panda3D applications into distributable forms'

+ 68 - 0
direct/src/dist/installers.py

@@ -196,3 +196,71 @@ def create_nsis(command, basename, build_dir):
         )
     cmd.append(nsifile.to_os_specific())
     subprocess.check_call(cmd)
+
+
+def create_aab(command, basename, build_dir):
+    """Create an Android App Bundle.  This is a newer format that replaces
+    Android's .apk format for uploads to the Play Store.  Unlike .apk files, it
+    does not rely on a proprietary signing scheme or an undocumented binary XML
+    format (protobuf is used instead), so it is easier to create without
+    requiring external tools.  If desired, it is possible to install bundletool
+    and use it to convert an .aab into an .apk.
+    """
+
+    from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable
+
+    bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
+    build_dir_fn = p3d.Filename.from_os_specific(build_dir)
+
+    # We use our own zip implementation, which can create the correct
+    # alignment needed by Android automatically.
+    bundle = p3d.ZipArchive()
+    if not bundle.open_write(bundle_fn):
+        command.announce.error(
+            f'\tUnable to open {bundle_fn} for writing', distutils.log.ERROR)
+        return
+
+    config = BundleConfig()
+    config.bundletool.version = '1.1.0'
+    config.optimizations.splits_config.Clear()
+    config.optimizations.uncompress_native_libraries.enabled = False
+    bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
+
+    resources = ResourceTable()
+    bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9)
+
+    native = NativeLibraries()
+    for abi in os.listdir(os.path.join(build_dir, 'lib')):
+        native_dir = native.directory.add()
+        native_dir.path = 'lib/' + abi
+        native_dir.targeting.abi.alias = getattr(AbiAlias, abi.upper().replace('-', '_'))
+    bundle.add_subfile('base/native.pb', p3d.StringStream(native.SerializeToString()), 9)
+
+    # Convert the AndroidManifest.xml file to a protobuf-encoded version of it.
+    axml = AndroidManifest()
+    with open(os.path.join(build_dir, 'AndroidManifest.xml'), 'rb') as fh:
+        axml.parse_xml(fh.read())
+    bundle.add_subfile('base/manifest/AndroidManifest.xml', p3d.StringStream(axml.dumps()), 9)
+
+    # Add the classes.dex.
+    bundle.add_subfile(f'base/dex/classes.dex', build_dir_fn / 'classes.dex', 9)
+
+    # Add libraries, compressed.
+    for abi in os.listdir(os.path.join(build_dir, 'lib')):
+        abi_dir = os.path.join(build_dir, 'lib', abi)
+
+        for lib in os.listdir(abi_dir):
+            if lib.startswith('lib') and lib.endswith('.so'):
+                bundle.add_subfile(f'base/lib/{abi}/{lib}', build_dir_fn / 'lib' / abi / lib, 9)
+
+    # Add assets, compressed.
+    assets_dir = os.path.join(build_dir, 'assets')
+    for dirpath, dirnames, filenames in os.walk(assets_dir):
+        rel_dirpath = os.path.relpath(dirpath, build_dir).replace('\\', '/')
+        dirnames.sort()
+        filenames.sort()
+
+        for name in filenames:
+            fn = p3d.Filename.from_os_specific(dirpath) / name
+            if fn.is_regular_file():
+                bundle.add_subfile(f'base/{rel_dirpath}/{name}', fn, 9)

+ 2 - 1
dtool/src/prc/configPageManager.cxx

@@ -97,6 +97,7 @@ reload_implicit_pages() {
   }
   _implicit_pages.clear();
 
+#ifndef ANDROID
   // If we are running inside a deployed application, see if it exposes
   // information about how the PRC data should be initialized.
   struct BlobInfo {
@@ -459,6 +460,7 @@ reload_implicit_pages() {
       }
     }
   }
+#endif  // ANDROID
 
   if (!_loaded_implicit) {
     config_initialized();
@@ -498,7 +500,6 @@ reload_implicit_pages() {
     SetErrorMode(SEM_FAILCRITICALERRORS);
   }
 #endif
-
 }
 
 /**

+ 9 - 1
makepanda/makepanda.py

@@ -6036,10 +6036,11 @@ if PkgSkip("PYTHON") == 0:
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-rdynamic")
+
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stub.exe', input='frozen_dllmain.obj')
-    PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON'])
+    PyTargetAdd('deploy-stub.exe', opts=['WINSHELL', 'DEPLOYSTUB', 'NOICON', 'ANDROID'])
 
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stubw.exe', input='deploy-stub.obj')
@@ -6051,6 +6052,13 @@ if PkgSkip("PYTHON") == 0:
         PyTargetAdd('deploy-stubw.obj', opts=OPTS, input='deploy-stub.c')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', opts=['MACOS_APP_BUNDLE', 'DEPLOYSTUB', 'NOICON'])
+    elif GetTarget() == 'android':
+        PyTargetAdd('deploy-stubw_android_main.obj', opts=OPTS, input='android_main.cxx')
+        PyTargetAdd('libdeploy-stubw.dll', input='android_native_app_glue.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input='deploy-stubw_android_main.obj')
+        PyTargetAdd('libdeploy-stubw.dll', input=COMMON_PANDA_LIBS)
+        PyTargetAdd('libdeploy-stubw.dll', input='libp3android.dll')
+        PyTargetAdd('libdeploy-stubw.dll', opts=['DEPLOYSTUB', 'ANDROID'])
 
 #
 # Generate the models directory and samples directory

+ 128 - 25
makepanda/makewheel.py

@@ -10,11 +10,12 @@ import hashlib
 import tempfile
 import subprocess
 import time
+import struct
 from distutils.util import get_platform
 from distutils.sysconfig import get_config_var
 from optparse import OptionParser
 from base64 import urlsafe_b64encode
-from makepandacore import LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue
+from makepandacore import LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue, CrossCompiling, GetThirdpartyDir, SDK, GetStrip
 
 
 def get_abi_tag():
@@ -65,8 +66,11 @@ def is_fat_file(path):
 
 
 def get_python_ext_module_dir():
-    import _ctypes
-    return os.path.dirname(_ctypes.__file__)
+    if CrossCompiling():
+        return os.path.join(GetThirdpartyDir(), "python", "lib", SDK["PYTHONVERSION"], "lib-dynload")
+    else:
+        import _ctypes
+        return os.path.dirname(_ctypes.__file__)
 
 
 if sys.platform in ('win32', 'cygwin'):
@@ -251,16 +255,72 @@ def parse_dependencies_unix(data):
     return filenames
 
 
+def _scan_dependencies_elf(elf):
+    deps = []
+    ident = elf.read(12)
+
+    # Make sure we read in the correct endianness and integer size
+    byte_order = "<>"[ord(ident[1:2]) - 1]
+    elf_class = ord(ident[0:1]) - 1 # 0 = 32-bits, 1 = 64-bits
+    header_struct = byte_order + ("HHIIIIIHHHHHH", "HHIQQQIHHHHHH")[elf_class]
+    section_struct = byte_order + ("4xI8xIII8xI", "4xI16xQQI12xQ")[elf_class]
+    dynamic_struct = byte_order + ("iI", "qQ")[elf_class]
+
+    type, machine, version, entry, phoff, shoff, flags, ehsize, phentsize, phnum, shentsize, shnum, shstrndx \
+      = struct.unpack(header_struct, elf.read(struct.calcsize(header_struct)))
+    dynamic_sections = []
+    string_tables = {}
+
+    # Seek to the section header table and find the .dynamic section.
+    elf.seek(shoff)
+    for i in range(shnum):
+        type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
+        if type == 6 and link != 0: # DYNAMIC type, links to string table
+            dynamic_sections.append((offset, size, link, entsize))
+            string_tables[link] = None
+
+    # Read the relevant string tables.
+    for idx in string_tables.keys():
+        elf.seek(shoff + idx * shentsize)
+        type, offset, size, link, entsize = struct.unpack_from(section_struct, elf.read(shentsize))
+        if type != 3: continue
+        elf.seek(offset)
+        string_tables[idx] = elf.read(size)
+
+    # Loop through the dynamic sections to get the NEEDED entries.
+    needed = []
+    for offset, size, link, entsize in dynamic_sections:
+        elf.seek(offset)
+        data = elf.read(entsize)
+        tag, val = struct.unpack_from(dynamic_struct, data)
+
+        # Read tags until we find a NULL tag.
+        while tag != 0:
+            if tag == 1: # A NEEDED entry.  Read it from the string table.
+                string = string_tables[link][val : string_tables[link].find(b'\0', val)]
+                needed.append(string.decode('utf-8'))
+
+            data = elf.read(entsize)
+            tag, val = struct.unpack_from(dynamic_struct, data)
+
+    elf.close()
+    return needed
+
+
 def scan_dependencies(pathname):
     """ Checks the named file for DLL dependencies, and adds any appropriate
     dependencies found into pluginDependencies and dependentFiles. """
 
+    with open(pathname, 'rb') as fh:
+        if fh.read(4) == b'\x7FELF':
+            return _scan_dependencies_elf(fh)
+
     if sys.platform == "darwin":
         command = ['otool', '-XL', pathname]
     elif sys.platform in ("win32", "cygwin"):
         command = ['dumpbin', '/dependents', pathname]
     else:
-        command = ['ldd', pathname]
+        sys.exit("Don't know how to determine dependencies from %s" % (pathname))
 
     process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
     output, unused_err = process.communicate()
@@ -322,18 +382,24 @@ class WheelFile(object):
 
         self.dep_paths[dep] = None
 
-        if dep in self.ignore_deps or dep.lower().startswith("python") or os.path.basename(dep).startswith("libpython"):
-            # Don't include the Python library, or any other explicit ignore.
+        if dep in self.ignore_deps:
             if GetVerbose():
                 print("Ignoring {0} (explicitly ignored)".format(dep))
             return
 
-        if sys.platform == "darwin" and dep.endswith(".so"):
-            # Temporary hack for 1.9, which had link deps on modules.
-            return
+        if not self.platform.startswith("android"):
+            if dep.lower().startswith("python") or os.path.basename(dep).startswith("libpython"):
+                if GetVerbose():
+                    print("Ignoring {0} (explicitly ignored)".format(dep))
+                return
 
-        if sys.platform == "darwin" and dep.startswith("/System/"):
-            return
+        if self.platform.startswith("macosx"):
+            if dep.endswith(".so"):
+                # Temporary hack for 1.9, which had link deps on modules.
+                return
+
+            if dep.startswith("/System/"):
+                return
 
         if dep.startswith('/'):
             source_path = dep
@@ -386,7 +452,7 @@ class WheelFile(object):
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
 
             # On macOS, if no fat wheel was requested, extract the right architecture.
-            if sys.platform == "darwin" and is_fat_file(source_path) \
+            if self.platform.startswith("macosx") and is_fat_file(source_path) \
                 and not self.platform.endswith("_intel") \
                 and "_fat" not in self.platform \
                 and "_universal" not in self.platform:
@@ -404,7 +470,7 @@ class WheelFile(object):
             os.chmod(temp.name, os.stat(temp.name).st_mode | 0o711)
 
             # Now add dependencies.  On macOS, fix @loader_path references.
-            if sys.platform == "darwin":
+            if self.platform.startswith("macosx"):
                 if source_path.endswith('deploy-stubw'):
                     deps_path = '@executable_path/../Frameworks'
                 else:
@@ -457,12 +523,32 @@ class WheelFile(object):
                 # On other unixes, we just add dependencies normally.
                 for dep in deps:
                     # Only include dependencies with relative path, for now.
-                    if '/' not in dep:
+                    if '/' in dep:
+                        continue
+
+                    if self.platform.startswith('android') and '.so.' in dep:
+                        # Change .so.1.2 suffix to .so, to allow loading in .apk
+                        new_dep = dep.rpartition('.so.')[0] + '.so'
+                        subprocess.call(["patchelf", "--replace-needed", dep, new_dep, temp.name])
+                        target_dep = os.path.dirname(target_path) + '/' + new_dep
+                    else:
                         target_dep = os.path.dirname(target_path) + '/' + dep
-                        self.consider_add_dependency(target_dep, dep)
 
-                subprocess.call(["strip", "-s", temp.name])
-                subprocess.call(["patchelf", "--force-rpath", "--set-rpath", "$ORIGIN", temp.name])
+                    self.consider_add_dependency(target_dep, dep)
+
+                subprocess.call([GetStrip(), "-s", temp.name])
+
+                if self.platform.startswith('android'):
+                    # We must link explicitly with Python, because the usual
+                    # -rdynamic trick doesn't work from a shared library loaded
+                    # through ANativeActivity.
+                    if suffix == '.so' and not os.path.basename(source_path).startswith('lib'):
+                        pylib_name = "libpython" + get_config_var('LDVERSION') + ".so"
+                        subprocess.call(["patchelf", "--add-needed", pylib_name, temp.name])
+                else:
+                    # On other systems, we use the rpath to force it to locate
+                    # dependencies in the same directory.
+                    subprocess.call(["patchelf", "--force-rpath", "--set-rpath", "$ORIGIN", temp.name])
 
             source_path = temp.name
 
@@ -550,7 +636,7 @@ def makewheel(version, output_dir, platform=None):
             raise Exception("patchelf is required when building a Linux wheel.")
 
     if sys.version_info < (3, 6):
-        raise Exception("Python 3.6 is required to produce a wheel.")
+        raise Exception("Python 3.6 or higher is required to produce a wheel.")
 
     if platform is None:
         # Determine the platform from the build.
@@ -571,6 +657,11 @@ def makewheel(version, output_dir, platform=None):
 
     platform = platform.replace('-', '_').replace('.', '_')
 
+    is_windows = platform == 'win32' \
+        or platform.startswith('win_') \
+        or platform.startswith('cygwin_')
+    is_macosx = platform.startswith('macosx_')
+
     # Global filepaths
     panda3d_dir = join(output_dir, "panda3d")
     pandac_dir = join(output_dir, "pandac")
@@ -578,7 +669,7 @@ def makewheel(version, output_dir, platform=None):
     models_dir = join(output_dir, "models")
     etc_dir = join(output_dir, "etc")
     bin_dir = join(output_dir, "bin")
-    if sys.platform == "win32":
+    if is_windows:
         libs_dir = join(output_dir, "bin")
     else:
         libs_dir = join(output_dir, "lib")
@@ -613,7 +704,7 @@ def makewheel(version, output_dir, platform=None):
     whl = WheelFile('panda3d', version, platform)
     whl.lib_path = [libs_dir]
 
-    if sys.platform == "win32":
+    if is_windows:
         whl.lib_path.append(ext_mod_dir)
 
     if platform.startswith("manylinux"):
@@ -629,10 +720,10 @@ def makewheel(version, output_dir, platform=None):
         whl.ignore_deps.update(MANYLINUX_LIBS)
 
     # Add libpython for deployment.
-    if sys.platform in ('win32', 'cygwin'):
+    if is_windows:
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('BINDIR'), pylib_name)
-    elif sys.platform == 'darwin':
+    elif is_macosx:
         pylib_name = 'libpython{0}.{1}.dylib'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
     else:
@@ -679,6 +770,9 @@ if __debug__:
             if file.endswith('.pyd') and platform.startswith('cygwin'):
                 # Rename it to .dll for cygwin Python to be able to load it.
                 target_path = 'panda3d/' + os.path.splitext(file)[0] + '.dll'
+            elif file.endswith(ext_suffix) and platform.startswith('android'):
+                # Strip the extension suffix on Android.
+                target_path = 'panda3d/' + file[:-len(ext_suffix)] + '.so'
             else:
                 target_path = 'panda3d/' + file
 
@@ -686,7 +780,7 @@ if __debug__:
 
     # And copy the extension modules from the Python installation into the
     # deploy_libs directory, for use by deploy-ng.
-    ext_suffix = '.pyd' if sys.platform in ('win32', 'cygwin') else '.so'
+    ext_suffix = '.pyd' if is_windows else '.so'
 
     for file in sorted(os.listdir(ext_mod_dir)):
         if file.endswith(ext_suffix):
@@ -703,9 +797,9 @@ if __debug__:
     # Add plug-ins.
     for lib in PLUGIN_LIBS:
         plugin_name = 'lib' + lib
-        if sys.platform in ('win32', 'cygwin'):
+        if is_windows:
             plugin_name += '.dll'
-        elif sys.platform == 'darwin':
+        elif is_macosx:
             plugin_name += '.dylib'
         else:
             plugin_name += '.so'
@@ -713,6 +807,15 @@ if __debug__:
         if os.path.isfile(plugin_path):
             whl.write_file('panda3d/' + plugin_name, plugin_path)
 
+    if platform.startswith('android'):
+        deploy_stub_path = os.path.join(libs_dir, 'libdeploy-stubw.so')
+        if os.path.isfile(deploy_stub_path):
+            whl.write_file('deploy_libs/libdeploy-stubw.so', deploy_stub_path)
+
+        classes_dex_path = os.path.join(output_dir, 'classes.dex')
+        if os.path.isfile(classes_dex_path):
+            whl.write_file('deploy_libs/classes.dex', classes_dex_path)
+
     # Add the .data directory, containing additional files.
     data_dir = 'panda3d-{0}.data'.format(version)
     #whl.write_directory(data_dir + '/data/etc', etc_dir)

+ 31 - 9
panda/src/android/PandaActivity.java

@@ -15,10 +15,13 @@ package org.panda3d.android;
 
 import android.app.NativeActivity;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import dalvik.system.BaseDexClassLoader;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeOStream;
 
@@ -74,6 +77,26 @@ public class PandaActivity extends NativeActivity {
         return Thread.currentThread().getName();
     }
 
+    /**
+     * Returns the path to the main native library.
+     */
+    public String getNativeLibraryPath() {
+        String libname = "main";
+        try {
+            ActivityInfo ai = getPackageManager().getActivityInfo(
+                    getIntent().getComponent(), PackageManager.GET_META_DATA);
+            if (ai.metaData != null) {
+                String ln = ai.metaData.getString(META_DATA_LIB_NAME);
+                if (ln != null) libname = ln;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            throw new RuntimeException("Error getting activity info", e);
+        }
+
+        BaseDexClassLoader classLoader = (BaseDexClassLoader) getClassLoader();
+        return classLoader.findLibrary(libname);
+    }
+
     public String getIntentDataPath() {
         Intent intent = getIntent();
         Uri data = intent.getData();
@@ -96,6 +119,9 @@ public class PandaActivity extends NativeActivity {
         return getCacheDir().toString();
     }
 
+    /**
+     * Shows a pop-up notification.
+     */
     public void showToast(final String text, final int duration) {
         final PandaActivity activity = this;
         runOnUiThread(new Runnable() {
@@ -107,14 +133,10 @@ public class PandaActivity extends NativeActivity {
     }
 
     static {
-        //System.loadLibrary("gnustl_shared");
-        //System.loadLibrary("p3dtool");
-        //System.loadLibrary("p3dtoolconfig");
-        //System.loadLibrary("pandaexpress");
-        //System.loadLibrary("panda");
-        //System.loadLibrary("p3android");
-        //System.loadLibrary("p3framework");
-        System.loadLibrary("pandaegg");
-        System.loadLibrary("pandagles");
+        // Load this explicitly to initialize the JVM with the thread system.
+        System.loadLibrary("panda");
+
+        // Contains our JNI calls.
+        System.loadLibrary("p3android");
     }
 }

+ 4 - 0
panda/src/pipeline/threadPosixImpl.cxx

@@ -193,6 +193,8 @@ get_unique_id() const {
  */
 bool ThreadPosixImpl::
 attach_java_vm() {
+  assert(java_vm != nullptr);
+
   JNIEnv *env;
   std::string thread_name = _parent_obj->get_name();
   JavaVMAttachArgs args;
@@ -219,6 +221,8 @@ bind_java_thread() {
   Thread *thread = Thread::get_current_thread();
   nassertv(thread != nullptr);
 
+  assert(java_vm != nullptr);
+
   // Get the JNIEnv for this Java thread, and store it on the corresponding
   // Panda thread object.
   JNIEnv *env;

+ 361 - 0
pandatool/src/deploy-stub/android_main.cxx

@@ -0,0 +1,361 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file android_main.cxx
+ * @author rdb
+ * @date 2021-12-06
+ */
+
+#include "config_android.h"
+#include "config_putil.h"
+#include "virtualFileMountAndroidAsset.h"
+#include "virtualFileSystem.h"
+#include "filename.h"
+#include "thread.h"
+#include "urlSpec.h"
+
+#include "android_native_app_glue.h"
+
+#include "Python.h"
+#include "structmember.h"
+
+#include <sys/mman.h>
+#include <android/log.h>
+
+#include <thread>
+
+// Leave room for future expansion.
+#define MAX_NUM_POINTERS 24
+
+// Define an exposed symbol where we store the offset to the module data.
+extern "C" {
+  __attribute__((__visibility__("default"), used))
+  volatile struct {
+    uint64_t blob_offset;
+    uint64_t blob_size;
+    uint16_t version;
+    uint16_t num_pointers;
+    uint16_t codepage;
+    uint16_t flags;
+    uint64_t reserved;
+    void *pointers[MAX_NUM_POINTERS];
+
+    // The reason we initialize it to -1 is because otherwise, smart linkers may
+    // end up putting it in the .bss section for zero-initialized data.
+  } blobinfo = {(uint64_t)-1};
+}
+
+/**
+ * Maps the binary blob at the given memory address to memory, and returns the
+ * pointer to the beginning of it.
+ */
+static void *map_blob(const char *path, off_t offset, size_t size) {
+  FILE *runtime = fopen(path, "rb");
+  assert(runtime != NULL);
+
+  void *blob = (void *)mmap(0, size, PROT_READ | PROT_WRITE, MAP_PRIVATE, fileno(runtime), offset);
+  assert(blob != MAP_FAILED);
+
+  fclose(runtime);
+  return blob;
+}
+
+/**
+ * The inverse of map_blob.
+ */
+static void unmap_blob(void *blob) {
+  if (blob) {
+    munmap(blob, blobinfo.blob_size);
+  }
+}
+
+/**
+ * This function is called by native_app_glue to initialize the program.
+ *
+ * Note that this does not run in the main thread, but in a thread created
+ * specifically for this activity by android_native_app_glue.
+ *
+ * Unlike the regular deploy-stub, we need to interface directly with the
+ * Panda3D libraries here, since we can't pass the pointers from Java to Panda
+ * through the Python interpreter easily.
+ */
+void android_main(struct android_app *app) {
+  panda_android_app = app;
+
+  // Attach the app thread to the Java VM.
+  JNIEnv *env;
+  ANativeActivity *activity = app->activity;
+  int attach_status = activity->vm->AttachCurrentThread(&env, nullptr);
+  if (attach_status < 0 || env == nullptr) {
+    android_cat.error() << "Failed to attach thread to JVM!\n";
+    return;
+  }
+
+  // Pipe stdout/stderr to the Android log stream, for convenience.
+  int pfd[2];
+  setvbuf(stdout, 0, _IOLBF, 0);
+  setvbuf(stderr, 0, _IOLBF, 0);
+
+  pipe(pfd);
+  dup2(pfd[1], 1);
+  dup2(pfd[1], 2);
+
+  std::thread t([=] {
+    ssize_t size;
+    char buf[4096] = {0};
+    char *bufstart = buf;
+    char *const bufend = buf + sizeof(buf) - 1;
+
+    while ((size = read(pfd[0], bufstart, bufend - bufstart)) > 0) {
+      bufstart[size] = 0;
+      bufstart += size;
+
+      while (char *nl = (char *)memchr(buf, '\n', strnlen(buf, bufend - buf))) {
+        *nl = 0;
+        __android_log_write(ANDROID_LOG_VERBOSE, "Python", buf);
+
+        // Move everything after the newline to the beginning of the buffer.
+        memmove(buf, nl + 1, bufend - (nl + 1));
+        bufstart -= (nl + 1) - buf;
+      }
+    }
+  });
+
+  jclass activity_class = env->GetObjectClass(activity->clazz);
+
+  // Get the current Java thread name.  This just helps with debugging.
+  jmethodID methodID = env->GetStaticMethodID(activity_class, "getCurrentThreadName", "()Ljava/lang/String;");
+  jstring jthread_name = (jstring) env->CallStaticObjectMethod(activity_class, methodID);
+
+  std::string thread_name;
+  if (jthread_name != nullptr) {
+    const char *c_str = env->GetStringUTFChars(jthread_name, nullptr);
+    thread_name.assign(c_str);
+    env->ReleaseStringUTFChars(jthread_name, c_str);
+  }
+
+  // Before we make any Panda calls, we must make the thread known to Panda.
+  // This will also cause the JNIEnv pointer to be stored on the thread.
+  // Note that we must keep a reference to this thread around.
+  PT(Thread) current_thread = Thread::bind_thread(thread_name, "android_app");
+
+  android_cat.info()
+    << "New native activity started on " << *current_thread << "\n";
+
+  // Fetch the data directory.
+  jmethodID get_appinfo = env->GetMethodID(activity_class, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;");
+
+  jobject appinfo = env->CallObjectMethod(activity->clazz, get_appinfo);
+  jclass appinfo_class = env->GetObjectClass(appinfo);
+
+  // Fetch the path to the data directory.
+  jfieldID datadir_field = env->GetFieldID(appinfo_class, "dataDir", "Ljava/lang/String;");
+  jstring datadir = (jstring) env->GetObjectField(appinfo, datadir_field);
+  const char *data_path = env->GetStringUTFChars(datadir, nullptr);
+
+  if (data_path != nullptr) {
+    Filename::_internal_data_dir = data_path;
+    android_cat.info() << "Path to data: " << data_path << "\n";
+
+    env->ReleaseStringUTFChars(datadir, data_path);
+  }
+
+  // Get the cache directory.  Set the model-path to this location.
+  methodID = env->GetMethodID(activity_class, "getCacheDirString", "()Ljava/lang/String;");
+  jstring jcache_dir = (jstring) env->CallObjectMethod(activity->clazz, methodID);
+
+  if (jcache_dir != nullptr) {
+    const char *cache_dir;
+    cache_dir = env->GetStringUTFChars(jcache_dir, nullptr);
+    android_cat.info() << "Path to cache: " << cache_dir << "\n";
+
+    ConfigVariableFilename model_cache_dir("model-cache-dir", Filename());
+    model_cache_dir.set_value(cache_dir);
+    env->ReleaseStringUTFChars(jcache_dir, cache_dir);
+  }
+
+  // Get the path to the APK.
+  methodID = env->GetMethodID(activity_class, "getPackageCodePath", "()Ljava/lang/String;");
+  jstring code_path = (jstring) env->CallObjectMethod(activity->clazz, methodID);
+
+  const char *apk_path;
+  apk_path = env->GetStringUTFChars(code_path, nullptr);
+  android_cat.info() << "Path to APK: " << apk_path << "\n";
+
+  // Get the path to the native library.
+  methodID = env->GetMethodID(activity_class, "getNativeLibraryPath", "()Ljava/lang/String;");
+  jstring lib_path_jstr = (jstring) env->CallObjectMethod(activity->clazz, methodID);
+
+  const char *lib_path;
+  lib_path = env->GetStringUTFChars(lib_path_jstr, nullptr);
+  android_cat.info() << "Path to native library: " << lib_path << "\n";
+  ExecutionEnvironment::set_binary_name(lib_path);
+
+  // Map the blob to memory
+  void *blob = map_blob(lib_path, (off_t)blobinfo.blob_offset, (size_t)blobinfo.blob_size);
+  env->ReleaseStringUTFChars(lib_path_jstr, lib_path);
+  assert(blob != NULL);
+
+  assert(blobinfo.num_pointers <= MAX_NUM_POINTERS);
+  for (uint32_t i = 0; i < blobinfo.num_pointers; ++i) {
+    // Only offset if the pointer is non-NULL.  Except for the first
+    // pointer, which may never be NULL and usually (but not always)
+    // points to the beginning of the blob.
+    if (i == 0 || blobinfo.pointers[i] != nullptr) {
+      blobinfo.pointers[i] = (void *)((uintptr_t)blobinfo.pointers[i] + (uintptr_t)blob);
+    }
+  }
+
+  // Now load the configuration files.
+  ConfigPage *page = nullptr;
+  ConfigPageManager *cp_mgr;
+  const char *prc_data = (char *)blobinfo.pointers[1];
+  if (prc_data != nullptr) {
+    cp_mgr = ConfigPageManager::get_global_ptr();
+    std::istringstream in(prc_data);
+    page = cp_mgr->make_explicit_page("builtin");
+    page->read_prc(in);
+  }
+
+  // Mount the assets directory.
+  Filename apk_fn(apk_path);
+  PT(VirtualFileMountAndroidAsset) asset_mount;
+  asset_mount = new VirtualFileMountAndroidAsset(app->activity->assetManager, apk_fn);
+  VirtualFileSystem *vfs = VirtualFileSystem::get_global_ptr();
+
+  //Filename asset_dir(apk_fn.get_dirname(), "assets");
+  Filename asset_dir("/android_asset");
+  vfs->mount(asset_mount, asset_dir, 0);
+
+  // Release the apk_path.
+  env->ReleaseStringUTFChars(code_path, apk_path);
+
+  // Now add the asset directory to the model-path.
+  //TODO: prevent it from adding the directory multiple times.
+  get_model_path().append_directory(asset_dir);
+
+  // Offset the pointers in the module table using the base mmap address.
+  struct _frozen *moddef = (struct _frozen *)blobinfo.pointers[0];
+  while (moddef->name) {
+    moddef->name = (char *)((uintptr_t)moddef->name + (uintptr_t)blob);
+    if (moddef->code != nullptr) {
+      moddef->code = (unsigned char *)((uintptr_t)moddef->code + (uintptr_t)blob);
+    }
+    //__android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", moddef->name, (void*)moddef->code, moddef->size);
+    moddef++;
+  }
+
+  PyImport_FrozenModules = (struct _frozen *)blobinfo.pointers[0];
+
+  PyPreConfig preconfig;
+  PyPreConfig_InitIsolatedConfig(&preconfig);
+  preconfig.utf8_mode = 1;
+  PyStatus status = Py_PreInitialize(&preconfig);
+  if (PyStatus_Exception(status)) {
+      Py_ExitStatusException(status);
+      return;
+  }
+
+  PyConfig config;
+  PyConfig_InitIsolatedConfig(&config);
+  config.pathconfig_warnings = 0;   /* Suppress errors from getpath.c */
+  config.buffered_stdio = 0;
+  config.configure_c_stdio = 0;
+  config.write_bytecode = 0;
+
+  status = Py_InitializeFromConfig(&config);
+  PyConfig_Clear(&config);
+  if (PyStatus_Exception(status)) {
+      Py_ExitStatusException(status);
+      return;
+  }
+
+  // Fetch the path to the library directory.
+  jfieldID libdir_field = env->GetFieldID(appinfo_class, "nativeLibraryDir", "Ljava/lang/String;");
+  jstring libdir_jstr = (jstring) env->GetObjectField(appinfo, libdir_field);
+  const char *libdir = env->GetStringUTFChars(libdir_jstr, nullptr);
+
+  if (libdir != nullptr) {
+    // This is used by the import hook to locate the module libraries.
+    PyObject *py_native_dir = PyUnicode_FromString(libdir);
+    PySys_SetObject("_native_library_dir", py_native_dir);
+    Py_DECREF(py_native_dir);
+
+    if (ExecutionEnvironment::get_dtool_name().empty()) {
+      std::string dtool_name = std::string(libdir) + "/libp3dtool.so";
+      ExecutionEnvironment::set_dtool_name(dtool_name);
+      android_cat.info() << "Path to dtool: " << dtool_name << "\n";
+    }
+
+    env->ReleaseStringUTFChars(libdir_jstr, libdir);
+  }
+
+  while (!app->destroyRequested) {
+    // Call the main module.  This will not return until the app is done.
+    android_cat.info() << "Importing __main__\n";
+
+    int n = PyImport_ImportFrozenModule("__main__");
+    if (n == 0) {
+      Py_FatalError("__main__ not frozen");
+      break;
+    }
+    if (n < 0) {
+      PyErr_Print();
+    }
+
+    fsync(1);
+    fsync(2);
+    sched_yield();
+
+    if (app->destroyRequested) {
+      // The app closed responding to a destroy request.
+      break;
+    }
+
+    // Ask Android to clean up the activity.
+    android_cat.info() << "Exited from __main__, finishing activity\n";
+    ANativeActivity_finish(activity);
+
+    // We still need to keep an event loop going until Android gives us leave
+    // to end the process.
+    int looper_id;
+    int events;
+    struct android_poll_source *source;
+    while ((looper_id = ALooper_pollAll(-1, nullptr, &events, (void**)&source)) >= 0) {
+      // Process this event, but intercept application command events.
+      if (looper_id == LOOPER_ID_MAIN) {
+        int8_t cmd = android_app_read_cmd(app);
+        android_app_pre_exec_cmd(app, cmd);
+        android_app_post_exec_cmd(app, cmd);
+
+        // I don't think we can get a resume command after we call finish(),
+        // but let's handle it just in case.
+        if (cmd == APP_CMD_RESUME || cmd == APP_CMD_DESTROY) {
+          break;
+        }
+      } else if (source != nullptr) {
+        source->process(app, source);
+      }
+    }
+  }
+
+  Py_Finalize();
+
+  android_cat.info() << "Destroy requested, exiting from android_main\n";
+
+  vfs->unmount(asset_mount);
+
+  if (page != nullptr) {
+    cp_mgr->delete_explicit_page(page);
+  }
+
+  unmap_blob(blob);
+
+  // Detach the thread before exiting.
+  activity->vm->DetachCurrentThread();
+}

Some files were not shown because too many files changed in this diff