Переглянути джерело

android: Work on supporting extractNativeLibs:false, separate blob

The blob is now stored in a raw resource, so no longer duplicated between each architecture

Setting extractNativeLibs:false doesn't yet work, the remaining step is somehow getting Python to load extension modules that are still part of the apk
rdb 1 місяць тому
батько
коміт
3d1b897c31

+ 35 - 24
direct/src/dist/FreezeTool.py

@@ -1835,7 +1835,8 @@ class Freezer:
         return target
 
     def generateRuntimeFromStub(self, target, stub_file, use_console, fields={},
-                                log_append=False, log_filename_strftime=False):
+                                log_append=False, log_filename_strftime=False,
+                                blob_path=None):
         self.__replacePaths()
 
         # We must have a __main__ module to make an exe file.
@@ -1983,29 +1984,33 @@ class Freezer:
             pad = (blob_align - (blob_size & (blob_align - 1)))
             blob_size += pad
 
-        # TODO: Support creating custom sections in universal binaries.
-        append_blob = True
-        if self.platform.startswith('macosx') and len(bitnesses) == 1:
-            # If our deploy-stub has a __PANDA segment, we know we're meant to
-            # put our blob there rather than attach it to the end.
-            load_commands = self._parse_macho_load_commands(stub_data)
-            if b'__PANDA' in load_commands.keys():
-                append_blob = False
-
-        if self.platform.startswith("macosx") and not append_blob:
-            # Take this time to shift any Mach-O structures around to fit our
-            # blob. We don't need to worry about aligning the offset since the
-            # compiler already took care of that when creating the segment.
-            blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size)
+        if blob_path is not None:
+            # We'll be writing the blob to a separate location.
+            blob_offset = 0
         else:
-            # Add padding before the blob if necessary.
-            blob_offset = len(stub_data)
-            if (blob_offset & (blob_align - 1)) != 0:
-                pad = (blob_align - (blob_offset & (blob_align - 1)))
-                stub_data += (b'\0' * pad)
-                blob_offset += pad
-            assert (blob_offset % blob_align) == 0
-            assert blob_offset == len(stub_data)
+            # TODO: Support creating custom sections in universal binaries.
+            append_blob = True
+            if self.platform.startswith('macosx') and len(bitnesses) == 1:
+                # If our deploy-stub has a __PANDA segment, we know we're meant to
+                # put our blob there rather than attach it to the end.
+                load_commands = self._parse_macho_load_commands(stub_data)
+                if b'__PANDA' in load_commands.keys():
+                    append_blob = False
+
+            if self.platform.startswith("macosx") and not append_blob:
+                # Take this time to shift any Mach-O structures around to fit our
+                # blob. We don't need to worry about aligning the offset since the
+                # compiler already took care of that when creating the segment.
+                blob_offset = self._shift_macho_structures(stub_data, load_commands, blob_size)
+            else:
+                # Add padding before the blob if necessary.
+                blob_offset = len(stub_data)
+                if (blob_offset & (blob_align - 1)) != 0:
+                    pad = (blob_align - (blob_offset & (blob_align - 1)))
+                    stub_data += (b'\0' * pad)
+                    blob_offset += pad
+                assert (blob_offset % blob_align) == 0
+                assert blob_offset == len(stub_data)
 
         # Calculate the offsets for the variables.  These are pointers,
         # relative to the beginning of the blob.
@@ -2091,7 +2096,9 @@ class Freezer:
             blob += struct.pack('<Q', blob_offset)
 
         with open(target, 'wb') as f:
-            if append_blob:
+            if blob_path is not None:
+                f.write(stub_data)
+            elif append_blob:
                 f.write(stub_data)
                 assert f.tell() == blob_offset
                 f.write(blob)
@@ -2099,6 +2106,10 @@ class Freezer:
                 stub_data[blob_offset:blob_offset + blob_size] = blob
                 f.write(stub_data)
 
+        if blob_path is not None:
+            with open(blob_path, 'wb') as f:
+                f.write(blob)
+
         os.chmod(target, 0o755)
         return target
 

+ 12 - 1
direct/src/dist/_android.py

@@ -3,7 +3,7 @@
 import xml.etree.ElementTree as ET
 
 from ._proto.targeting_pb2 import Abi
-from ._proto.config_pb2 import BundleConfig # pylint: disable=unused-import
+from ._proto.config_pb2 import BundleConfig, UncompressNativeLibraries # pylint: disable=unused-import
 from ._proto.files_pb2 import NativeLibraries # pylint: disable=unused-import
 from ._proto.Resources_pb2 import ResourceTable # pylint: disable=unused-import
 from ._proto.Resources_pb2 import XmlNode
@@ -199,6 +199,7 @@ ANDROID_ATTRIBUTES = {
     'pageSizeCompat': bool_resource(0x010106ab),
     'pathPattern': str_resource(0x101002c),
     'preferMinimalPostProcessing': bool_resource(0x101060c),
+    'resource': ref_resource(0x01010025),
     'required': bool_resource(0x101028e),
     'resizeableActivity': bool_resource(0x10104f6),
     'scheme': str_resource(0x1010027),
@@ -222,6 +223,7 @@ class AndroidManifest:
         self.root = XmlNode()
         self.resource_types = []
         self.resources = {}
+        self.extract_native_libs = None
 
     def parse_xml(self, data):
         parser = ET.XMLParser(target=self)
@@ -242,6 +244,15 @@ class AndroidManifest:
         element = node.element
         element.name = tag
 
+        if tag == 'application':
+            value = attribs.get('{http://schemas.android.com/apk/res/android}extractNativeLibs')
+            if value == 'false':
+                self.extract_native_libs = False
+            elif value == 'true':
+                self.extract_native_libs = True
+            else:
+                print(f'Warning: invalid value for android:extractNativeLibs: {value}')
+
         self._stack.append(element)
 
         for key, value in attribs.items():

+ 20 - 4
direct/src/dist/commands.py

@@ -587,6 +587,10 @@ class build_apps(setuptools.Command):
                 data_dir = os.path.join(build_dir, 'assets')
                 os.makedirs(data_dir, exist_ok=True)
 
+                res_dir = os.path.join(build_dir, 'res')
+                res_raw_dir = os.path.join(res_dir, 'raw')
+                os.makedirs(res_raw_dir, exist_ok=True)
+
                 for abi in self.android_abis:
                     lib_dir = os.path.join(build_dir, 'lib', abi)
                     os.makedirs(lib_dir, exist_ok=True)
@@ -603,7 +607,7 @@ class build_apps(setuptools.Command):
 
                     # We end up copying the data multiple times to the same
                     # directory, but that's probably fine for now.
-                    self.build_binaries(platform + suffix, lib_dir, data_dir)
+                    self.build_binaries(platform + suffix, lib_dir, data_dir, res_raw_dir)
 
                 # Write out the icons to the res directory.
                 for appname, icon in self.icon_objects.items():
@@ -614,7 +618,6 @@ class build_apps(setuptools.Command):
                         appname_sane = appname.replace(' ', '_')
                         basename = f'ic_{appname_sane}.png'
 
-                    res_dir = os.path.join(build_dir, 'res')
                     icon.writeSize(48, os.path.join(res_dir, 'mipmap-mdpi-v4', basename))
                     icon.writeSize(72, os.path.join(res_dir, 'mipmap-hdpi-v4', basename))
                     icon.writeSize(96, os.path.join(res_dir, 'mipmap-xhdpi-v4', basename))
@@ -894,6 +897,10 @@ class build_apps(setuptools.Command):
             meta_data.set('android:name', 'android.app.lib_name')
             meta_data.set('android:value', appname_sane)
 
+            meta_data = ET.SubElement(activity, 'meta-data')
+            meta_data.set('android:name', 'org.panda3d.android.BLOB_RESOURCE')
+            meta_data.set('android:resource', '@raw/' + appname_sane + '.so')
+
             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')
@@ -937,7 +944,7 @@ class build_apps(setuptools.Command):
             if self.android_target_sdk_version >= 31 and f'{android}exported' not in activity.attrib:
                 raise RuntimeError("<activity> element must have android:exported attribute when targeting Android API 31+")
 
-    def build_binaries(self, platform, binary_dir, data_dir=None):
+    def build_binaries(self, platform, binary_dir, data_dir=None, blob_dir=None):
         """ Builds the binary data for the given platform. """
 
         use_wheels = True
@@ -1133,6 +1140,7 @@ class build_apps(setuptools.Command):
 
             stub_name = 'deploy-stub'
             target_name = appname
+            appname_sane = appname
             if platform.startswith('win') or 'macosx' in platform:
                 if not use_console:
                     stub_name = 'deploy-stubw'
@@ -1172,6 +1180,14 @@ class build_apps(setuptools.Command):
             if not self.log_filename or '%' not in self.log_filename:
                 use_strftime = False
 
+            blob_path = None
+            if blob_dir is not None:
+                if platform.startswith('android'):
+                    # Not really a .so file, but it forces bundletool to align it
+                    blob_path = os.path.join(blob_dir, appname_sane + '.so')
+                else:
+                    blob_path = os.path.join(blob_dir, appname_sane)
+
             target_path = os.path.join(binary_dir, target_name)
             freezer.generateRuntimeFromStub(target_path, stub_file, use_console, {
                 'prc_data': prcexport if self.embed_prc_data else None,
@@ -1185,7 +1201,7 @@ class build_apps(setuptools.Command):
                 'prc_executable_args_envvar': None,
                 'main_dir': None,
                 'log_filename': self.expand_path(self.log_filename, platform),
-            }, self.log_append, use_strftime)
+            }, self.log_append, use_strftime, blob_path)
             stub_file.close()
 
             if temp_file:

+ 41 - 11
direct/src/dist/installers.py

@@ -207,7 +207,7 @@ def create_aab(command, basename, build_dir):
     and use it to convert an .aab into an .apk.
     """
 
-    from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable
+    from ._android import AndroidManifest, AbiAlias, BundleConfig, NativeLibraries, ResourceTable, UncompressNativeLibraries
 
     bundle_fn = p3d.Filename.from_os_specific(command.dist_dir) / (basename + '.aab')
     build_dir_fn = p3d.Filename.from_os_specific(build_dir)
@@ -230,7 +230,13 @@ def create_aab(command, basename, build_dir):
     config = BundleConfig()
     config.bundletool.version = '1.1.0'
     config.optimizations.splits_config.Clear()
-    config.optimizations.uncompress_native_libraries.enabled = False
+    if axml.extract_native_libs:
+        config.optimizations.uncompress_native_libraries.enabled = False
+        config.optimizations.uncompress_native_libraries.alignment = \
+            UncompressNativeLibraries.PageAlignment.PAGE_ALIGNMENT_16K
+    else:
+        config.optimizations.uncompress_native_libraries.enabled = True
+    config.compression.uncompressed_glob.append('res/raw/**')
     bundle.add_subfile('BundleConfig.pb', p3d.StringStream(config.SerializeToString()), 9)
 
     resources = ResourceTable()
@@ -251,13 +257,25 @@ def create_aab(command, basename, build_dir):
             entry.entry_id.id = entry_id
             entry.name = res_name
 
-            for density, tag in (120, 'ldpi'), (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'):
-                path = f'res/mipmap-{tag}-v4/{res_name}.png'
-                if (build_dir_fn / path).exists():
-                    bundle.add_subfile('base/' + path, build_dir_fn / path, 0)
-                    config_value = entry.config_value.add()
-                    config_value.config.density = density
-                    config_value.value.item.file.path = path
+            if type_name == 'raw':
+                path = f'res/raw/{res_name}'
+                if not (build_dir_fn / path).exists():
+                    command.announce(
+                        f'\tRaw resource {path} was not found on disk', distutils.log.ERROR)
+                    return
+
+                # These are aligned to page size for mmap.
+                bundle.add_subfile('base/' + path, build_dir_fn / path, 0, 16384)
+                config_value = entry.config_value.add()
+                config_value.value.item.file.path = path
+            else:
+                for density, tag in (120, 'ldpi'), (160, 'mdpi'), (240, 'hdpi'), (320, 'xhdpi'), (480, 'xxhdpi'), (640, 'xxxhdpi'):
+                    path = f'res/mipmap-{tag}-v4/{res_name}.png'
+                    if (build_dir_fn / path).exists():
+                        bundle.add_subfile('base/' + path, build_dir_fn / path, 0)
+                        config_value = entry.config_value.add()
+                        config_value.config.density = density
+                        config_value.value.item.file.path = path
 
     bundle.add_subfile('base/resources.pb', p3d.StringStream(resources.SerializeToString()), 9)
 
@@ -273,13 +291,25 @@ def create_aab(command, basename, build_dir):
     # Add the classes.dex.
     bundle.add_subfile('base/dex/classes.dex', build_dir_fn / 'classes.dex', 9)
 
-    # Add libraries, compressed.
+    # Add libraries, compressed, unless extractNativeLibs is false, in which
+    # case they have to be aligned to page boundaries (16 KiB on 64-bit).
+    lib_compress = 9 if axml.extract_native_libs else 0
+
     for abi in os.listdir(os.path.join(build_dir, 'lib')):
         abi_dir = os.path.join(build_dir, 'lib', abi)
 
+        if axml.extract_native_libs:
+            lib_align = 0
+        elif '64' in abi:
+            lib_align = 16384
+        else:
+            lib_align = 4096
+
         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)
+                bundle.add_subfile(f'base/lib/{abi}/{lib}',
+                                   build_dir_fn / 'lib' / abi / lib,
+                                   lib_compress, lib_align)
 
     # Add assets, compressed.
     assets_dir = os.path.join(build_dir, 'assets')

+ 42 - 0
panda/src/android/PandaActivity.java

@@ -17,7 +17,9 @@ import android.app.NativeActivity;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
+import android.content.res.AssetFileDescriptor;
 import android.net.Uri;
+import android.os.ParcelFileDescriptor;
 import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -25,6 +27,8 @@ import dalvik.system.BaseDexClassLoader;
 import org.panda3d.android.NativeIStream;
 import org.panda3d.android.NativeOStream;
 
+import android.util.Log;
+
 /**
  * The entry point for a Panda-based activity.  Loads the Panda libraries and
  * also provides some utility functions.
@@ -77,6 +81,34 @@ public class PandaActivity extends NativeActivity {
         return Thread.currentThread().getName();
     }
 
+    /**
+     * Maps the blob to memory and returns the pointer.
+     */
+    public long mapBlobFromResource() {
+        int resourceId = 0;
+        try {
+            ActivityInfo ai = getPackageManager().getActivityInfo(
+                    getIntent().getComponent(), PackageManager.GET_META_DATA);
+            if (ai.metaData == null) {
+                Log.e("Panda3D", "Failed to get activity metadata");
+                return 0;
+            }
+            resourceId = ai.metaData.getInt("org.panda3d.android.BLOB_RESOURCE");
+            if (resourceId == 0) {
+                return 0;
+            }
+
+            AssetFileDescriptor afd = getResources().openRawResourceFd(resourceId);
+            ParcelFileDescriptor pfd = afd.getParcelFileDescriptor();
+            long off = afd.getStartOffset();
+            long len = afd.getLength();
+            return nativeMmap(pfd.getFd(), off, len);
+        } catch (Exception e) {
+            Log.e("Panda3D", "Received exception while trying to map blob: " + e);
+            return 0;
+        }
+    }
+
     /**
      * Returns the path to the main native library.
      */
@@ -97,6 +129,14 @@ public class PandaActivity extends NativeActivity {
         return classLoader.findLibrary(libname);
     }
 
+    /**
+     * Returns the path to some other native library.
+     */
+    public String findLibraryPath(String libname) {
+        BaseDexClassLoader classLoader = (BaseDexClassLoader)getClassLoader();
+        return classLoader.findLibrary(libname);
+    }
+
     public String getIntentDataPath() {
         Intent intent = getIntent();
         Uri data = intent.getData();
@@ -139,4 +179,6 @@ public class PandaActivity extends NativeActivity {
         // Contains our JNI calls.
         System.loadLibrary("p3android");
     }
+
+    private static native long nativeMmap(int fd, long off, long len);
 }

+ 36 - 0
panda/src/android/jni_PandaActivity.cxx

@@ -0,0 +1,36 @@
+/**
+ * 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 jni_PandaActivity.cxx
+ * @author rdb
+ * @date 2025-11-09
+ */
+
+#include <jni.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#if __GNUC__ >= 4
+#define EXPORT_JNI extern "C" __attribute__((visibility("default")))
+#else
+#define EXPORT_JNI extern "C"
+#endif
+
+/**
+ *
+ */
+EXPORT_JNI jlong
+Java_org_panda3d_android_PandaActivity_nativeMmap(JNIEnv* env, jclass, jint fd, jlong off, jlong len) {
+  // Align the offset down to the page size boundary.
+  size_t page_size = getpagesize();
+  off_t aligned = off & ~((off_t)page_size - 1);
+  size_t delta = (size_t)(off - aligned);
+
+  void *ptr = mmap(nullptr, (size_t)len, PROT_READ, MAP_PRIVATE, fd, aligned);
+  return (jlong)((uintptr_t)ptr + delta);
+}

+ 1 - 0
panda/src/android/p3android_composite1.cxx

@@ -1,6 +1,7 @@
 #include "config_android.cxx"
 #include "jni_NativeIStream.cxx"
 #include "jni_NativeOStream.cxx"
+#include "jni_PandaActivity.cxx"
 #include "pnmFileTypeAndroid.cxx"
 #include "pnmFileTypeAndroidReader.cxx"
 #include "pnmFileTypeAndroidWriter.cxx"

+ 32 - 15
panda/src/express/zipArchive.cxx

@@ -317,7 +317,7 @@ close() {
  */
 std::string ZipArchive::
 add_subfile(const std::string &subfile_name, const Filename &filename,
-            int compression_level) {
+            int compression_level, size_t data_alignment) {
   nassertr(is_write_valid(), std::string());
 
 #ifndef HAVE_ZLIB
@@ -337,7 +337,7 @@ add_subfile(const std::string &subfile_name, const Filename &filename,
     return std::string();
   }
 
-  std::string name = add_subfile(subfile_name, in, compression_level);
+  std::string name = add_subfile(subfile_name, in, compression_level, data_alignment);
   vfs->close_read_file(in);
   return name;
 }
@@ -356,7 +356,7 @@ add_subfile(const std::string &subfile_name, const Filename &filename,
  */
 std::string ZipArchive::
 add_subfile(const std::string &subfile_name, std::istream *subfile_data,
-            int compression_level) {
+            int compression_level, size_t data_alignment) {
   nassertr(is_write_valid(), string());
 
 #ifndef HAVE_ZLIB
@@ -367,14 +367,14 @@ add_subfile(const std::string &subfile_name, std::istream *subfile_data,
 
   std::string name = standardize_subfile_name(subfile_name);
   if (!name.empty()) {
-    Subfile *subfile = new Subfile(subfile_name, compression_level);
+    Subfile *subfile = new Subfile(subfile_name, compression_level, data_alignment);
 
     // Write it straight away, overwriting the index at the end of the file.
     // This index will be rewritten at the next call to flush() or close().
     std::streampos fpos = _index_start;
     _write->seekp(fpos);
 
-    if (!subfile->write_header(*_write, fpos)) {
+    if (!subfile->write_header(*_write, fpos, data_alignment)) {
       delete subfile;
       return "";
     }
@@ -406,7 +406,7 @@ add_subfile(const std::string &subfile_name, std::istream *subfile_data,
  */
 string ZipArchive::
 update_subfile(const std::string &subfile_name, const Filename &filename,
-               int compression_level) {
+               int compression_level, size_t data_alignment) {
   nassertr(is_write_valid(), string());
 
 #ifndef HAVE_ZLIB
@@ -431,7 +431,7 @@ update_subfile(const std::string &subfile_name, const Filename &filename,
 
     // The subfile does not already exist or it is different from the source
     // file.  Add the new source file.
-    Subfile *subfile = new Subfile(name, compression_level);
+    Subfile *subfile = new Subfile(name, compression_level, data_alignment);
     add_new_subfile(subfile, compression_level);
   }
 
@@ -764,7 +764,7 @@ repack() {
     // the checksum and sizes.
     subfile->_flags &= ~SF_data_descriptor;
 
-    if (!subfile->write_header(temp, fpos)) {
+    if (!subfile->write_header(temp, fpos, subfile->_data_alignment)) {
       success = false;
       continue;
     }
@@ -1669,10 +1669,11 @@ write_index(std::ostream &write, std::streampos &fpos) {
  * Creates a new subfile record.
  */
 ZipArchive::Subfile::
-Subfile(const std::string &name, int compression_level) :
+Subfile(const std::string &name, int compression_level, size_t data_alignment) :
   _name(name),
   _timestamp(dos_epoch),
-  _compression_method((compression_level > 0) ? CM_deflate : CM_store)
+  _compression_method((compression_level > 0) ? CM_deflate : CM_store),
+  _data_alignment(data_alignment)
 {
   // If the name contains any non-ASCII characters, we set the UTF-8 flag.
   for (char c : name) {
@@ -2096,7 +2097,7 @@ write_index(std::ostream &write, streampos &fpos) {
  * than the actual size of the subfile).
  */
 bool ZipArchive::Subfile::
-write_header(std::ostream &write, std::streampos &fpos) {
+write_header(std::ostream &write, std::streampos &fpos, size_t data_alignment) {
   nassertr(write.tellp() == fpos, false);
 
   std::string encoded_name;
@@ -2109,13 +2110,29 @@ write_header(std::ostream &write, std::streampos &fpos) {
   std::streamoff header_size = 30 + encoded_name.size();
 
   StreamWriter writer(write);
-  int modulo = (fpos + header_size) % 4;
-  if (!is_compressed() && modulo != 0) {
+  if (!is_compressed()) {
     // Align uncompressed files to 4-byte boundary.  We don't really need to do
     // this, but it's needed when producing .apk files, and it doesn't really
     // cause harm to do it in other cases as well.
-    writer.pad_bytes(4 - modulo);
-    fpos += (4 - modulo);
+    if (data_alignment < 4) {
+      data_alignment = 4;
+    }
+    else if ((data_alignment % 4) != 0) {
+      data_alignment *= 2;
+      if ((data_alignment % 4) != 0) {
+        data_alignment *= 2;
+      }
+    }
+  }
+
+  if (data_alignment > 0) {
+    // The data follows the header directly, so the actual padding has to be
+    // inserted before the header.
+    int modulo = (fpos + header_size) % data_alignment;
+    if (modulo != 0) {
+      writer.pad_bytes(data_alignment - modulo);
+      fpos += (data_alignment - modulo);
+    }
   }
 
   _header_start = fpos;

+ 7 - 5
panda/src/express/zipArchive.h

@@ -66,11 +66,11 @@ PUBLISHED:
   INLINE bool get_record_timestamp() const;
 
   std::string add_subfile(const std::string &subfile_name, const Filename &filename,
-                          int compression_level);
+                          int compression_level, size_t data_alignment=0);
   std::string add_subfile(const std::string &subfile_name, std::istream *subfile_data,
-                          int compression_level);
+                          int compression_level, size_t data_alignment=0);
   std::string update_subfile(const std::string &subfile_name, const Filename &filename,
-                             int compression_level);
+                             int compression_level, size_t data_alignment=0);
 
 #ifdef HAVE_OPENSSL
   bool add_jar_signature(const Filename &certificate, const Filename &pkey,
@@ -152,7 +152,8 @@ private:
   class Subfile {
   public:
     Subfile() = default;
-    Subfile(const std::string &name, int compression_level);
+    Subfile(const std::string &name, int compression_level,
+            size_t data_alignment=0);
 
     INLINE bool operator < (const Subfile &other) const;
 
@@ -160,7 +161,7 @@ private:
     bool read_header(std::istream &read);
     bool verify_data(std::istream &read);
     bool write_index(std::ostream &write, std::streampos &fpos);
-    bool write_header(std::ostream &write, std::streampos &fpos);
+    bool write_header(std::ostream &write, std::streampos &fpos, size_t data_alignment=0);
     bool write_data(std::ostream &write, std::istream *read,
                     std::streampos &fpos, int compression_level);
     INLINE bool is_compressed() const;
@@ -180,6 +181,7 @@ private:
     std::string _comment;
     int _flags = SF_data_descriptor;
     CompressionMethod _compression_method = CM_store;
+    size_t _data_alignment = 0;
   };
 
   void add_new_subfile(Subfile *subfile, int compression_level);

+ 32 - 13
pandatool/src/deploy-stub/android_main.cxx

@@ -61,7 +61,7 @@ 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);
+  void *blob = (void *)mmap(0, size, PROT_READ, MAP_PRIVATE, fileno(runtime), offset);
   assert(blob != MAP_FAILED);
 
   fclose(runtime);
@@ -181,10 +181,17 @@ void android_main(struct android_app *app) {
   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);
+  // Nowadays we store the blob in a raw resource.
+  methodID = env->GetMethodID(activity_class, "mapBlobFromResource", "()J");
+  assert(methodID != nullptr);
+  void *blob = (void *)env->CallLongMethod(activity->clazz, methodID);
+
+  if (blob == nullptr) {
+    // Try the old method otherwise.
+    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(blob != nullptr);
 
   assert(blobinfo.num_pointers <= MAX_NUM_POINTERS);
   for (uint32_t i = 0; i < blobinfo.num_pointers; ++i) {
@@ -225,17 +232,28 @@ void android_main(struct android_app *app) {
   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++;
+  // We did a read-only mmap, so we have to create a copy of this table.
+  // First count how many modules there are.
+  struct _frozen *src_moddef = (struct _frozen *)blobinfo.pointers[0];
+  size_t num_modules = 0;
+  while (src_moddef->name) {
+    num_modules++;
+    src_moddef++;
   }
 
-  PyImport_FrozenModules = (struct _frozen *)blobinfo.pointers[0];
+  struct _frozen *new_modules = (struct _frozen *)malloc(sizeof(struct _frozen) * (num_modules + 1));
+  memcpy(new_modules, blobinfo.pointers[0], sizeof(struct _frozen) * (num_modules + 1));
+
+  struct _frozen *dst_moddef = new_modules;
+  while (dst_moddef->name) {
+    dst_moddef->name = (char *)((uintptr_t)dst_moddef->name + (uintptr_t)blob);
+    if (dst_moddef->code != nullptr) {
+      dst_moddef->code = (unsigned char *)((uintptr_t)dst_moddef->code + (uintptr_t)blob);
+    }
+    __android_log_print(ANDROID_LOG_DEBUG, "Panda3D", "MOD: %s %p %d\n", dst_moddef->name, (void*)dst_moddef->code, dst_moddef->size);
+    dst_moddef++;
+  }
+  PyImport_FrozenModules = new_modules;
 
   PyPreConfig preconfig;
   PyPreConfig_InitIsolatedConfig(&preconfig);
@@ -328,6 +346,7 @@ void android_main(struct android_app *app) {
     cp_mgr->delete_explicit_page(page);
   }
 
+  free(new_modules);
   unmap_blob(blob);
 
   // Detach the thread before exiting.