瀏覽代碼

Support deploying Python apps for Android using bdist_apps

rdb 4 年之前
父節點
當前提交
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
             # If it is a submodule of a frozen module, Python will have
             # trouble importing it as a builtin module.  Synthesize a frozen
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it dynamically.
             # 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:
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
                     # 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)
                     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()

文件差異過大導致無法顯示
+ 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.

文件差異過大導致無法顯示
+ 22 - 0
direct/src/dist/_proto/Resources_pb2.py


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


文件差異過大導致無法顯示
+ 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)

文件差異過大導致無法顯示
+ 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
 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):
 class build_apps(setuptools.Command):
     description = 'build Panda3D applications'
     description = 'build Panda3D applications'
@@ -171,6 +215,12 @@ class build_apps(setuptools.Command):
 
 
     def initialize_options(self):
     def initialize_options(self):
         self.build_base = os.path.join(os.getcwd(), 'build')
         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.gui_apps = {}
         self.console_apps = {}
         self.console_apps = {}
         self.macos_main_app = None
         self.macos_main_app = None
@@ -266,6 +316,11 @@ class build_apps(setuptools.Command):
             '/usr/lib/libxar.1.dylib',
             '/usr/lib/libxar.1.dylib',
             '/usr/lib/libmenu.5.4.dylib',
             '/usr/lib/libmenu.5.4.dylib',
             '/System/Library/**',
             '/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 = {}
         self.package_data_dirs = {}
@@ -508,6 +563,72 @@ class build_apps(setuptools.Command):
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
         with open(os.path.join(contentsdir, 'Info.plist'), 'wb') as f:
             plistlib.dump(plist, 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):
     def build_runtimes(self, platform, use_wheels):
         """ Builds the distributions for the given platform. """
         """ Builds the distributions for the given platform. """
 
 
@@ -607,6 +728,9 @@ class build_apps(setuptools.Command):
                     value = value[:c].rstrip()
                     value = value[:c].rstrip()
 
 
                 if var == 'model-cache-dir' and value:
                 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()))
                     value = value.replace('/panda3d', '/{}'.format(self.distribution.get_name()))
 
 
                 if var == 'audio-library-name':
                 if var == 'audio-library-name':
@@ -683,33 +807,42 @@ class build_apps(setuptools.Command):
 
 
             return search_path
             return search_path
 
 
-        def create_runtime(appname, mainscript, use_console):
+        def create_runtime(appname, mainscript, target_dir, use_console):
             freezer = FreezeTool.Freezer(
             freezer = FreezeTool.Freezer(
                 platform=platform,
                 platform=platform,
                 path=path,
                 path=path,
                 hiddenImports=self.hidden_imports
                 hiddenImports=self.hidden_imports
             )
             )
             freezer.addModule('__main__', filename=mainscript)
             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('*', []):
             for incmod in self.include_modules.get(appname, []) + self.include_modules.get('*', []):
                 freezer.addModule(incmod)
                 freezer.addModule(incmod)
             for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
             for exmod in self.exclude_modules.get(appname, []) + self.exclude_modules.get('*', []):
                 freezer.excludeModule(exmod)
                 freezer.excludeModule(exmod)
             freezer.done(addStartupModules=True)
             freezer.done(addStartupModules=True)
 
 
-            target_path = os.path.join(builddir, appname)
-
             stub_name = 'deploy-stub'
             stub_name = 'deploy-stub'
+            target_name = appname
             if platform.startswith('win') or 'macosx' in platform:
             if platform.startswith('win') or 'macosx' in platform:
                 if not use_console:
                 if not use_console:
                     stub_name = 'deploy-stubw'
                     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'):
             if platform.startswith('win'):
                 stub_name += '.exe'
                 stub_name += '.exe'
-                target_path += '.exe'
+                target_name += '.exe'
 
 
             if use_wheels:
             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:
             else:
                 dtool_path = p3d.Filename(p3d.ExecutionEnvironment.get_dtool_name()).to_os_specific()
                 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)
                 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:
             if not self.log_filename or '%' not in self.log_filename:
                 use_strftime = False
                 use_strftime = False
 
 
+            target_path = os.path.join(target_dir, target_name)
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'prc_data': prcexport if self.embed_prc_data else None,
                 'default_prc_dir': self.default_prc_dir,
                 'default_prc_dir': self.default_prc_dir,
@@ -750,10 +884,11 @@ class build_apps(setuptools.Command):
                 os.unlink(temp_file.name)
                 os.unlink(temp_file.name)
 
 
             # Copy the dependencies.
             # Copy the dependencies.
-            search_path = [builddir]
+            search_path = [target_dir]
             if use_wheels:
             if use_wheels:
+                search_path.append(os.path.join(p3dwhlfn, 'panda3d'))
                 search_path.append(os.path.join(p3dwhlfn, 'deploy_libs'))
                 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_extras.update(freezer.extras)
             freezer_modules.update(freezer.getAllModuleNames())
             freezer_modules.update(freezer.getAllModuleNames())
@@ -765,11 +900,37 @@ class build_apps(setuptools.Command):
                 if suffix[2] == imp.C_EXTENSION:
                 if suffix[2] == imp.C_EXTENSION:
                     ext_suffixes.add(suffix[0])
                     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():
         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():
         for appname, scriptname in self.console_apps.items():
-            create_runtime(appname, scriptname, True)
+            create_runtime(appname, scriptname, lib_dir, True)
 
 
         # Copy extension modules
         # Copy extension modules
         whl_modules = []
         whl_modules = []
@@ -805,7 +966,7 @@ class build_apps(setuptools.Command):
             plugname = lib.split('.', 1)[0]
             plugname = lib.split('.', 1)[0]
             if plugname in plugin_list:
             if plugname in plugin_list:
                 source_path = os.path.join(p3dwhlfn, lib)
                 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)]
                 search_path = [os.path.dirname(source_path)]
                 self.copy_with_dependencies(source_path, target_path, search_path)
                 self.copy_with_dependencies(source_path, target_path, search_path)
 
 
@@ -846,8 +1007,13 @@ class build_apps(setuptools.Command):
                 else:
                 else:
                     continue
                     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.
             # 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)
             search_path = get_search_path_for(source_path)
             self.copy_with_dependencies(source_path, target_path, search_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:
             if os.path.isdir(tcl_dir) and 'tkinter' in freezer_modules:
                 self.announce('Copying Tcl files', distutils.log.INFO)
                 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):
                 for dir in os.listdir(tcl_dir):
                     sub_dir = os.path.join(tcl_dir, dir)
                     sub_dir = os.path.join(tcl_dir, dir)
                     if os.path.isdir(sub_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))
                         self.announce('copying {0} -> {1}'.format(sub_dir, target_dir))
                         shutil.copytree(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.
         # Extract any other data files from dependency packages.
         for module, datadesc in self.package_data_dirs.items():
         for module, datadesc in self.package_data_dirs.items():
             if module not in freezer_modules:
             if module not in freezer_modules:
@@ -883,7 +1054,7 @@ class build_apps(setuptools.Command):
                     source_dir = os.path.dirname(source_pattern)
                     source_dir = os.path.dirname(source_pattern)
                     # Relocate the target dir to the build directory.
                     # Relocate the target dir to the build directory.
                     target_dir = target_dir.replace('/', os.sep)
                     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:
                     for wf in filenames:
                         if wf.lower().startswith(source_dir.lower() + '/'):
                         if wf.lower().startswith(source_dir.lower() + '/'):
@@ -1008,10 +1179,14 @@ class build_apps(setuptools.Command):
 
 
             for fname in filelist:
             for fname in filelist:
                 src = os.path.join(dirpath, fname)
                 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)
                 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
         # Bundle into an .app on macOS
         if self.macos_main_app and 'macosx' in platform:
         if self.macos_main_app and 'macosx' in platform:
             self.bundle_macos_app(builddir)
             self.bundle_macos_app(builddir)
@@ -1333,6 +1508,13 @@ class bdist_apps(setuptools.Command):
         'manylinux1_i686': ['gztar'],
         'manylinux1_i686': ['gztar'],
         'manylinux2010_x86_64': ['gztar'],
         'manylinux2010_x86_64': ['gztar'],
         'manylinux2010_i686': ['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']
         # Everything else defaults to ['zip']
     }
     }
 
 
@@ -1342,6 +1524,7 @@ class bdist_apps(setuptools.Command):
         'bztar': installers.create_bztar,
         'bztar': installers.create_bztar,
         'xztar': installers.create_xztar,
         'xztar': installers.create_xztar,
         'nsis': installers.create_nsis,
         'nsis': installers.create_nsis,
+        'aab': installers.create_aab,
     }
     }
 
 
     description = 'bundle built Panda3D applications into distributable forms'
     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())
     cmd.append(nsifile.to_os_specific())
     subprocess.check_call(cmd)
     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();
   _implicit_pages.clear();
 
 
+#ifndef ANDROID
   // If we are running inside a deployed application, see if it exposes
   // If we are running inside a deployed application, see if it exposes
   // information about how the PRC data should be initialized.
   // information about how the PRC data should be initialized.
   struct BlobInfo {
   struct BlobInfo {
@@ -459,6 +460,7 @@ reload_implicit_pages() {
       }
       }
     }
     }
   }
   }
+#endif  // ANDROID
 
 
   if (!_loaded_implicit) {
   if (!_loaded_implicit) {
     config_initialized();
     config_initialized();
@@ -498,7 +500,6 @@ reload_implicit_pages() {
     SetErrorMode(SEM_FAILCRITICALERRORS);
     SetErrorMode(SEM_FAILCRITICALERRORS);
   }
   }
 #endif
 #endif
-
 }
 }
 
 
 /**
 /**

+ 9 - 1
makepanda/makepanda.py

@@ -6036,10 +6036,11 @@ if PkgSkip("PYTHON") == 0:
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-rpath,\\$ORIGIN")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-Wl,-z,origin")
         LibName('DEPLOYSTUB', "-rdynamic")
         LibName('DEPLOYSTUB', "-rdynamic")
+
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     PyTargetAdd('deploy-stub.exe', input='deploy-stub.obj')
     if GetTarget() == 'windows':
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stub.exe', input='frozen_dllmain.obj')
         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':
     if GetTarget() == 'windows':
         PyTargetAdd('deploy-stubw.exe', input='deploy-stub.obj')
         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.obj', opts=OPTS, input='deploy-stub.c')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', input='deploy-stubw.obj')
         PyTargetAdd('deploy-stubw.exe', opts=['MACOS_APP_BUNDLE', 'DEPLOYSTUB', 'NOICON'])
         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
 # Generate the models directory and samples directory

+ 128 - 25
makepanda/makewheel.py

@@ -10,11 +10,12 @@ import hashlib
 import tempfile
 import tempfile
 import subprocess
 import subprocess
 import time
 import time
+import struct
 from distutils.util import get_platform
 from distutils.util import get_platform
 from distutils.sysconfig import get_config_var
 from distutils.sysconfig import get_config_var
 from optparse import OptionParser
 from optparse import OptionParser
 from base64 import urlsafe_b64encode
 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():
 def get_abi_tag():
@@ -65,8 +66,11 @@ def is_fat_file(path):
 
 
 
 
 def get_python_ext_module_dir():
 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'):
 if sys.platform in ('win32', 'cygwin'):
@@ -251,16 +255,72 @@ def parse_dependencies_unix(data):
     return filenames
     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):
 def scan_dependencies(pathname):
     """ Checks the named file for DLL dependencies, and adds any appropriate
     """ Checks the named file for DLL dependencies, and adds any appropriate
     dependencies found into pluginDependencies and dependentFiles. """
     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":
     if sys.platform == "darwin":
         command = ['otool', '-XL', pathname]
         command = ['otool', '-XL', pathname]
     elif sys.platform in ("win32", "cygwin"):
     elif sys.platform in ("win32", "cygwin"):
         command = ['dumpbin', '/dependents', pathname]
         command = ['dumpbin', '/dependents', pathname]
     else:
     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)
     process = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
     output, unused_err = process.communicate()
     output, unused_err = process.communicate()
@@ -322,18 +382,24 @@ class WheelFile(object):
 
 
         self.dep_paths[dep] = None
         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():
             if GetVerbose():
                 print("Ignoring {0} (explicitly ignored)".format(dep))
                 print("Ignoring {0} (explicitly ignored)".format(dep))
             return
             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('/'):
         if dep.startswith('/'):
             source_path = dep
             source_path = dep
@@ -386,7 +452,7 @@ class WheelFile(object):
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
             temp = tempfile.NamedTemporaryFile(suffix=suffix, prefix='whl', delete=False)
 
 
             # On macOS, if no fat wheel was requested, extract the right architecture.
             # 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 not self.platform.endswith("_intel") \
                 and "_fat" not in self.platform \
                 and "_fat" not in self.platform \
                 and "_universal" 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)
             os.chmod(temp.name, os.stat(temp.name).st_mode | 0o711)
 
 
             # Now add dependencies.  On macOS, fix @loader_path references.
             # Now add dependencies.  On macOS, fix @loader_path references.
-            if sys.platform == "darwin":
+            if self.platform.startswith("macosx"):
                 if source_path.endswith('deploy-stubw'):
                 if source_path.endswith('deploy-stubw'):
                     deps_path = '@executable_path/../Frameworks'
                     deps_path = '@executable_path/../Frameworks'
                 else:
                 else:
@@ -457,12 +523,32 @@ class WheelFile(object):
                 # On other unixes, we just add dependencies normally.
                 # On other unixes, we just add dependencies normally.
                 for dep in deps:
                 for dep in deps:
                     # Only include dependencies with relative path, for now.
                     # 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
                         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
             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.")
             raise Exception("patchelf is required when building a Linux wheel.")
 
 
     if sys.version_info < (3, 6):
     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:
     if platform is None:
         # Determine the platform from the build.
         # Determine the platform from the build.
@@ -571,6 +657,11 @@ def makewheel(version, output_dir, platform=None):
 
 
     platform = platform.replace('-', '_').replace('.', '_')
     platform = platform.replace('-', '_').replace('.', '_')
 
 
+    is_windows = platform == 'win32' \
+        or platform.startswith('win_') \
+        or platform.startswith('cygwin_')
+    is_macosx = platform.startswith('macosx_')
+
     # Global filepaths
     # Global filepaths
     panda3d_dir = join(output_dir, "panda3d")
     panda3d_dir = join(output_dir, "panda3d")
     pandac_dir = join(output_dir, "pandac")
     pandac_dir = join(output_dir, "pandac")
@@ -578,7 +669,7 @@ def makewheel(version, output_dir, platform=None):
     models_dir = join(output_dir, "models")
     models_dir = join(output_dir, "models")
     etc_dir = join(output_dir, "etc")
     etc_dir = join(output_dir, "etc")
     bin_dir = join(output_dir, "bin")
     bin_dir = join(output_dir, "bin")
-    if sys.platform == "win32":
+    if is_windows:
         libs_dir = join(output_dir, "bin")
         libs_dir = join(output_dir, "bin")
     else:
     else:
         libs_dir = join(output_dir, "lib")
         libs_dir = join(output_dir, "lib")
@@ -613,7 +704,7 @@ def makewheel(version, output_dir, platform=None):
     whl = WheelFile('panda3d', version, platform)
     whl = WheelFile('panda3d', version, platform)
     whl.lib_path = [libs_dir]
     whl.lib_path = [libs_dir]
 
 
-    if sys.platform == "win32":
+    if is_windows:
         whl.lib_path.append(ext_mod_dir)
         whl.lib_path.append(ext_mod_dir)
 
 
     if platform.startswith("manylinux"):
     if platform.startswith("manylinux"):
@@ -629,10 +720,10 @@ def makewheel(version, output_dir, platform=None):
         whl.ignore_deps.update(MANYLINUX_LIBS)
         whl.ignore_deps.update(MANYLINUX_LIBS)
 
 
     # Add libpython for deployment.
     # Add libpython for deployment.
-    if sys.platform in ('win32', 'cygwin'):
+    if is_windows:
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_name = 'python{0}{1}.dll'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('BINDIR'), pylib_name)
         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_name = 'libpython{0}.{1}.dylib'.format(*sys.version_info)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
         pylib_path = os.path.join(get_config_var('LIBDIR'), pylib_name)
     else:
     else:
@@ -679,6 +770,9 @@ if __debug__:
             if file.endswith('.pyd') and platform.startswith('cygwin'):
             if file.endswith('.pyd') and platform.startswith('cygwin'):
                 # Rename it to .dll for cygwin Python to be able to load it.
                 # Rename it to .dll for cygwin Python to be able to load it.
                 target_path = 'panda3d/' + os.path.splitext(file)[0] + '.dll'
                 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:
             else:
                 target_path = 'panda3d/' + file
                 target_path = 'panda3d/' + file
 
 
@@ -686,7 +780,7 @@ if __debug__:
 
 
     # And copy the extension modules from the Python installation into the
     # And copy the extension modules from the Python installation into the
     # deploy_libs directory, for use by deploy-ng.
     # 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)):
     for file in sorted(os.listdir(ext_mod_dir)):
         if file.endswith(ext_suffix):
         if file.endswith(ext_suffix):
@@ -703,9 +797,9 @@ if __debug__:
     # Add plug-ins.
     # Add plug-ins.
     for lib in PLUGIN_LIBS:
     for lib in PLUGIN_LIBS:
         plugin_name = 'lib' + lib
         plugin_name = 'lib' + lib
-        if sys.platform in ('win32', 'cygwin'):
+        if is_windows:
             plugin_name += '.dll'
             plugin_name += '.dll'
-        elif sys.platform == 'darwin':
+        elif is_macosx:
             plugin_name += '.dylib'
             plugin_name += '.dylib'
         else:
         else:
             plugin_name += '.so'
             plugin_name += '.so'
@@ -713,6 +807,15 @@ if __debug__:
         if os.path.isfile(plugin_path):
         if os.path.isfile(plugin_path):
             whl.write_file('panda3d/' + plugin_name, 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.
     # Add the .data directory, containing additional files.
     data_dir = 'panda3d-{0}.data'.format(version)
     data_dir = 'panda3d-{0}.data'.format(version)
     #whl.write_directory(data_dir + '/data/etc', etc_dir)
     #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.app.NativeActivity;
 import android.content.Intent;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
 import android.net.Uri;
 import android.net.Uri;
 import android.widget.Toast;
 import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.BitmapFactory;
+import dalvik.system.BaseDexClassLoader;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeOStream;
 import org.panda3d.android.NativeOStream;
 
 
@@ -74,6 +77,26 @@ public class PandaActivity extends NativeActivity {
         return Thread.currentThread().getName();
         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() {
     public String getIntentDataPath() {
         Intent intent = getIntent();
         Intent intent = getIntent();
         Uri data = intent.getData();
         Uri data = intent.getData();
@@ -96,6 +119,9 @@ public class PandaActivity extends NativeActivity {
         return getCacheDir().toString();
         return getCacheDir().toString();
     }
     }
 
 
+    /**
+     * Shows a pop-up notification.
+     */
     public void showToast(final String text, final int duration) {
     public void showToast(final String text, final int duration) {
         final PandaActivity activity = this;
         final PandaActivity activity = this;
         runOnUiThread(new Runnable() {
         runOnUiThread(new Runnable() {
@@ -107,14 +133,10 @@ public class PandaActivity extends NativeActivity {
     }
     }
 
 
     static {
     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::
 bool ThreadPosixImpl::
 attach_java_vm() {
 attach_java_vm() {
+  assert(java_vm != nullptr);
+
   JNIEnv *env;
   JNIEnv *env;
   std::string thread_name = _parent_obj->get_name();
   std::string thread_name = _parent_obj->get_name();
   JavaVMAttachArgs args;
   JavaVMAttachArgs args;
@@ -219,6 +221,8 @@ bind_java_thread() {
   Thread *thread = Thread::get_current_thread();
   Thread *thread = Thread::get_current_thread();
   nassertv(thread != nullptr);
   nassertv(thread != nullptr);
 
 
+  assert(java_vm != nullptr);
+
   // Get the JNIEnv for this Java thread, and store it on the corresponding
   // Get the JNIEnv for this Java thread, and store it on the corresponding
   // Panda thread object.
   // Panda thread object.
   JNIEnv *env;
   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();
+}

部分文件因文件數量過多而無法顯示