Browse Source

Fix compatibility with Python 3.12 by removing use of imp module

Some modules (such as VFSImporter, and various modules in direct.p3d that depend on it) are still unavailable.
rdb 2 years ago
parent
commit
2a5228b05f

+ 151 - 46
direct/src/dist/FreezeTool.py

@@ -5,7 +5,6 @@ import modulefinder
 import sys
 import os
 import marshal
-import imp
 import platform
 import struct
 import io
@@ -18,6 +17,12 @@ import importlib
 
 from . import pefile
 
+if sys.version_info >= (3, 4):
+    import _imp
+    from importlib import machinery
+else:
+    import imp
+
 # Temporary (?) try..except to protect against unbuilt p3extend_frozen.
 try:
     import p3extend_frozen
@@ -26,6 +31,16 @@ except ImportError:
 
 from panda3d.core import *
 
+# Old imp constants.
+_PY_SOURCE = 1
+_PY_COMPILED = 2
+_C_EXTENSION = 3
+_PKG_DIRECTORY = 5
+_C_BUILTIN = 6
+_PY_FROZEN = 7
+
+_PKG_NAMESPACE_DIRECTORY = object()
+
 # Check to see if we are running python_d, which implies we have a
 # debug build, and we have to build the module with debug options.
 # This is only relevant on Windows.
@@ -39,8 +54,11 @@ isDebugBuild = (python.lower().endswith('_d'))
 # NB. if encodings are removed, be sure to remove them from the shortcut in
 # deploy-stub.c.
 startupModules = [
-    'imp', 'encodings', 'encodings.*',
+    'encodings', 'encodings.*',
 ]
+if sys.version_info < (3, 12):
+    startupModules.insert(0, 'imp')
+
 if sys.version_info >= (3, 0):
     # Modules specific to Python 3
     startupModules += ['io', 'marshal', 'importlib.machinery', 'importlib.util']
@@ -885,12 +903,19 @@ class Freezer:
 
         # Suffix/extension for Python C extension modules
         if self.platform == PandaSystem.getPlatform():
-            self.moduleSuffixes = imp.get_suffixes()
+            if sys.version_info >= (3, 4):
+                self.moduleSuffixes = (
+                    [(s, 'rb', _C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES] +
+                    [(s, 'rb', _PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] +
+                    [(s, 'rb', _PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES]
+                )
+            else:
+                self.moduleSuffixes = imp.get_suffixes()
 
-            # Set extension for Python files to binary mode
-            for i, suffix in enumerate(self.moduleSuffixes):
-                if suffix[2] == imp.PY_SOURCE:
-                    self.moduleSuffixes[i] = (suffix[0], 'rb', imp.PY_SOURCE)
+                # Set extension for Python files to binary mode
+                for i, suffix in enumerate(self.moduleSuffixes):
+                    if suffix[2] == _PY_SOURCE:
+                        self.moduleSuffixes[i] = (suffix[0], 'rb', _PY_SOURCE)
         else:
             self.moduleSuffixes = [('.py', 'rb', 1), ('.pyc', 'rb', 2)]
 
@@ -990,21 +1015,45 @@ class Freezer:
         # whatever--then just look for file on disk.  That's usually
         # good enough.
         path = None
-        baseName = moduleName
-        if '.' in baseName:
-            parentName, baseName = moduleName.rsplit('.', 1)
+        name = moduleName
+        if '.' in name:
+            parentName, name = moduleName.rsplit('.', 1)
             path = self.getModulePath(parentName)
             if path is None:
                 return None
 
-        try:
-            file, pathname, description = imp.find_module(baseName, path)
-        except ImportError:
-            return None
+        if sys.version_info < (3, 4):
+            try:
+                file, pathname, description = imp.find_module(name, path)
+            except ImportError:
+                return None
 
-        if not os.path.isdir(pathname):
-            return None
-        return [pathname]
+            if not os.path.isdir(pathname):
+                return None
+            return [pathname]
+
+        if path is None:
+            if _imp.is_builtin(name) or _imp.is_frozen(name):
+                return None
+
+            path = sys.path
+
+        for entry in path:
+            package_directory = os.path.join(entry, name)
+            for suffix in ('.py', machinery.BYTECODE_SUFFIXES[0]):
+                package_file_name = '__init__' + suffix
+                file_path = os.path.join(package_directory, package_file_name)
+                if os.path.isfile(file_path):
+                    return [package_directory]
+
+            for suffix in machinery.EXTENSION_SUFFIXES + machinery.SOURCE_SUFFIXES + machinery.BYTECODE_SUFFIXES:
+                file_name = name + suffix
+                file_path = os.path.join(entry, file_name)
+                if os.path.isfile(file_path):
+                    # Not a package.
+                    return None
+
+        return None
 
     def getModuleStar(self, moduleName):
         """ Looks for the indicated directory module and returns the
@@ -1027,20 +1076,49 @@ class Freezer:
         # If it didn't work, just open the directory and scan for *.py
         # files.
         path = None
-        baseName = moduleName
-        if '.' in baseName:
-            parentName, baseName = moduleName.rsplit('.', 1)
+        name = moduleName
+        if '.' in name:
+            parentName, name = moduleName.rsplit('.', 1)
             path = self.getModulePath(parentName)
             if path is None:
                 return None
 
-        try:
-            file, pathname, description = imp.find_module(baseName, path)
-        except ImportError:
-            return None
+        if sys.version_info < (3, 4):
+            try:
+                file, pathname, description = imp.find_module(name, path)
+            except ImportError:
+                return None
 
-        if not os.path.isdir(pathname):
-            return None
+            if not os.path.isdir(pathname):
+                return None
+        else:
+            if path is None:
+                if _imp.is_builtin(name) or _imp.is_frozen(name):
+                    return None
+
+                path = sys.path
+
+            for entry in path:
+                package_directory = os.path.join(entry, name)
+                for suffix in ('.py', machinery.BYTECODE_SUFFIXES[0]):
+                    package_file_name = '__init__' + suffix
+                    file_path = os.path.join(package_directory, package_file_name)
+                    if os.path.isfile(file_path):
+                        pathname = package_directory
+                        break
+                else:
+                    for suffix in machinery.EXTENSION_SUFFIXES + machinery.SOURCE_SUFFIXES + machinery.BYTECODE_SUFFIXES:
+                        file_name = name + suffix
+                        file_path = os.path.join(entry, file_name)
+                        if os.path.isfile(file_path):
+                            # Not a package.
+                            return None
+                    else:
+                        continue
+
+                break  # Break out of outer loop when breaking out of inner loop.
+            else:
+                return None
 
         # Scan the directory, looking for .py files.
         modules = []
@@ -1335,10 +1413,10 @@ class Freezer:
             ext = mdef.filename.getExtension()
             if ext == 'pyc' or ext == 'pyo':
                 fp = open(pathname, 'rb')
-                stuff = ("", "rb", imp.PY_COMPILED)
+                stuff = ("", "rb", _PY_COMPILED)
                 self.mf.load_module(mdef.moduleName, fp, pathname, stuff)
             else:
-                stuff = ("", "rb", imp.PY_SOURCE)
+                stuff = ("", "rb", _PY_SOURCE)
                 if mdef.text:
                     fp = io.StringIO(mdef.text)
                 else:
@@ -1434,10 +1512,10 @@ class Freezer:
 
     def __addPyc(self, multifile, filename, code, compressionLevel):
         if code:
-            data = imp.get_magic() + b'\0\0\0\0'
-
-            if sys.version_info >= (3, 0):
-                data += b'\0\0\0\0'
+            if sys.version_info >= (3, 4):
+                data = importlib.util.MAGIC_NUMBER + b'\0\0\0\0\0\0\0\0'
+            else:
+                data = imp.get_magic() + b'\0\0\0\0'
 
             data += marshal.dumps(code)
 
@@ -1634,7 +1712,11 @@ class Freezer:
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it as builtin.
             if '.' in moduleName and self.linkExtensionModules:
-                if sys.version_info >= (3, 2):
+                if sys.version_info >= (3, 5):
+                    code = compile('import sys;del sys.modules["%s"];from importlib._bootstrap import _builtin_from_name;_builtin_from_name("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize)
+                elif sys.version_info >= (3, 4):
+                    code = compile('import sys;del sys.modules["%s"];import _imp;_imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize)
+                elif sys.version_info >= (3, 2):
                     code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize)
                 else:
                     code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec')
@@ -1910,9 +1992,25 @@ class Freezer:
             if '.' in moduleName:
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
-                    code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)
+                    direxpr = 'sys.path[0]'
+                else:
+                    direxpr = 'os.path.dirname(sys.executable)'
+
+                if sys.version_info >= (3, 5):
+                    code = \
+                        'import sys;' \
+                        'del sys.modules["{name}"];' \
+                        'import sys,os;' \
+                        'from importlib.machinery import ExtensionFileLoader,ModuleSpec;' \
+                        'from importlib._bootstrap import _load;' \
+                        'path=os.path.join({direxpr}, "{name}{ext}");' \
+                        '_load(ModuleSpec(name="{name}", loader=ExtensionFileLoader("{name}", path), origin=path))' \
+                        ''.format(name=moduleName, ext=modext, direxpr=direxpr)
+                elif sys.version_info >= (3, 4):
+                    code = 'import sys;del sys.modules["%s"];import sys,os,_imp;_imp.load_dynamic("%s",os.path.join(%s, "%s%s"))' % (moduleName, moduleName, direxpr, moduleName, modext)
                 else:
-                    code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(os.path.dirname(sys.executable), "%s%s"))' % (moduleName, moduleName, moduleName, modext)
+                    code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(%s, "%s%s"))' % (moduleName, moduleName, direxpr, moduleName, modext)
+
                 if sys.version_info >= (3, 2):
                     code = compile(code, moduleName, 'exec', optimize=self.optimize)
                 else:
@@ -2362,9 +2460,6 @@ class Freezer:
         return True
 
 
-_PKG_NAMESPACE_DIRECTORY = object()
-
-
 class PandaModuleFinder(modulefinder.ModuleFinder):
 
     def __init__(self, *args, **kw):
@@ -2375,7 +2470,17 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
         :param debug: an integer indicating the level of verbosity
         """
 
-        self.suffixes = kw.pop('suffixes', imp.get_suffixes())
+        if 'suffixes' in kw:
+            self.suffixes = kw.pop('suffixes')
+        elif sys.version_info >= (3, 4):
+            self.suffixes = (
+                [(s, 'rb', _C_EXTENSION) for s in machinery.EXTENSION_SUFFIXES] +
+                [(s, 'r', _PY_SOURCE) for s in machinery.SOURCE_SUFFIXES] +
+                [(s, 'rb', _PY_COMPILED) for s in machinery.BYTECODE_SUFFIXES]
+            )
+        else:
+            self.suffixes = imp.get_suffixes()
+
         self.optimize = kw.pop('optimize', -1)
 
         modulefinder.ModuleFinder.__init__(self, *args, **kw)
@@ -2475,7 +2580,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         suffix, mode, type = file_info
         self.msgin(2, "load_module", fqname, fp and "fp", pathname)
-        if type == imp.PKG_DIRECTORY:
+        if type == _PKG_DIRECTORY:
             m = self.load_package(fqname, pathname)
             self.msgout(2, "load_module ->", m)
             return m
@@ -2489,7 +2594,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             m.__path__ = pathname
             return m
 
-        if type == imp.PY_SOURCE:
+        if type == _PY_SOURCE:
             if fqname in overrideModules:
                 # This module has a custom override.
                 code = overrideModules[fqname]
@@ -2516,7 +2621,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
                 co = compile(code, pathname, 'exec', optimize=self.optimize)
             else:
                 co = compile(code, pathname, 'exec')
-        elif type == imp.PY_COMPILED:
+        elif type == _PY_COMPILED:
             if sys.version_info >= (3, 7):
                 try:
                     data = fp.read()
@@ -2681,12 +2786,12 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         # If we have a custom override for this module, we know we have it.
         if fullname in overrideModules:
-            return (None, '', ('.py', 'r', imp.PY_SOURCE))
+            return (None, '', ('.py', 'r', _PY_SOURCE))
 
         # If no search path is given, look for a built-in module.
         if path is None:
             if name in sys.builtin_module_names:
-                return (None, None, ('', '', imp.C_BUILTIN))
+                return (None, None, ('', '', _C_BUILTIN))
 
             path = self.path
 
@@ -2718,7 +2823,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             for suffix, mode, _ in self.suffixes:
                 init = os.path.join(basename, '__init__' + suffix)
                 if self._open_file(init, mode):
-                    return (None, basename, ('', '', imp.PKG_DIRECTORY))
+                    return (None, basename, ('', '', _PKG_DIRECTORY))
 
             # This may be a namespace package.
             if self._dir_exists(basename):
@@ -2730,7 +2835,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
             # Only if we're not looking on a particular path, though.
             if p3extend_frozen and p3extend_frozen.is_frozen_module(name):
                 # It's a frozen module.
-                return (None, name, ('', '', imp.PY_FROZEN))
+                return (None, name, ('', '', _PY_FROZEN))
 
         # If we found folders on the path with this module name without an
         # __init__.py file, we should consider this a namespace package.

+ 2 - 3
direct/src/dist/commands.py

@@ -15,7 +15,6 @@ import re
 import shutil
 import stat
 import struct
-import imp
 import string
 import time
 import tempfile
@@ -25,7 +24,7 @@ import distutils.log
 
 from . import FreezeTool
 from . import pefile
-from direct.p3d.DeploymentTools import Icon
+from .icon import Icon
 import panda3d.core as p3d
 
 
@@ -912,7 +911,7 @@ class build_apps(setuptools.Command):
                 for mod in freezer.getModuleDefs() if mod[1].filename
             })
             for suffix in freezer.moduleSuffixes:
-                if suffix[2] == imp.C_EXTENSION:
+                if suffix[2] == 3: # imp.C_EXTENSION:
                     ext_suffixes.add(suffix[0])
 
         for appname, scriptname in self.gui_apps.items():

+ 265 - 0
direct/src/dist/icon.py

@@ -0,0 +1,265 @@
+from direct.directnotify.DirectNotifyGlobal import directNotify
+from panda3d.core import PNMImage, Filename, PNMFileTypeRegistry, StringStream
+import struct
+
+
+class Icon:
+    """ This class is used to create an icon for various platforms. """
+    notify = directNotify.newCategory("Icon")
+
+    def __init__(self):
+        self.images = {}
+
+    def addImage(self, image):
+        """ Adds an image to the icon.  Returns False on failure, True on success.
+        Only one image per size can be loaded, and the image size must be square. """
+
+        if not isinstance(image, PNMImage):
+            fn = image
+            if not isinstance(fn, Filename):
+                fn = Filename.fromOsSpecific(fn)
+
+            image = PNMImage()
+            if not image.read(fn):
+                Icon.notify.warning("Image '%s' could not be read" % fn.getBasename())
+                return False
+
+        if image.getXSize() != image.getYSize():
+            Icon.notify.warning("Ignoring image without square size")
+            return False
+
+        self.images[image.getXSize()] = image
+
+        return True
+
+    def generateMissingImages(self):
+        """ Generates image sizes that should be present but aren't by scaling
+        from the next higher size. """
+
+        for required_size in (256, 128, 48, 32, 16):
+            if required_size in self.images:
+                continue
+
+            sizes = sorted(self.images.keys())
+            if required_size * 2 in sizes:
+                from_size = required_size * 2
+            else:
+                from_size = 0
+                for from_size in sizes:
+                    if from_size > required_size:
+                        break
+
+            if from_size > required_size:
+                Icon.notify.warning("Generating %dx%d icon by scaling down %dx%d image" % (required_size, required_size, from_size, from_size))
+
+                image = PNMImage(required_size, required_size)
+                image.setColorType(self.images[from_size].getColorType())
+                image.quickFilterFrom(self.images[from_size])
+                self.images[required_size] = image
+            else:
+                Icon.notify.warning("Cannot generate %dx%d icon; no higher resolution image available" % (required_size, required_size))
+
+    def _write_bitmap(self, fp, image, size, bpp):
+        """ Writes the bitmap header and data of an .ico file. """
+
+        fp.write(struct.pack('<IiiHHIIiiII', 40, size, size * 2, 1, bpp, 0, 0, 0, 0, 0, 0))
+
+        # XOR mask
+        if bpp == 24:
+            # Align rows to 4-byte boundary
+            rowalign = b'\0' * (-(size * 3) & 3)
+            for y in range(size):
+                for x in range(size):
+                    r, g, b = image.getXel(x, size - y - 1)
+                    fp.write(struct.pack('<BBB', int(b * 255), int(g * 255), int(r * 255)))
+                fp.write(rowalign)
+
+        elif bpp == 32:
+            for y in range(size):
+                for x in range(size):
+                    r, g, b, a = image.getXelA(x, size - y - 1)
+                    fp.write(struct.pack('<BBBB', int(b * 255), int(g * 255), int(r * 255), int(a * 255)))
+
+        elif bpp == 8:
+            # We'll have to generate a palette of 256 colors.
+            hist = PNMImage.Histogram()
+            image2 = PNMImage(image)
+            if image2.hasAlpha():
+                image2.premultiplyAlpha()
+                image2.removeAlpha()
+            image2.quantize(256)
+            image2.make_histogram(hist)
+            colors = list(hist.get_pixels())
+            assert len(colors) <= 256
+
+            # Write the palette.
+            i = 0
+            while i < 256 and i < len(colors):
+                r, g, b, a = colors[i]
+                fp.write(struct.pack('<BBBB', b, g, r, 0))
+                i += 1
+            if i < 256:
+                # Fill the rest with zeroes.
+                fp.write(b'\x00' * (4 * (256 - i)))
+
+            # Write indices.  Align rows to 4-byte boundary.
+            rowalign = b'\0' * (-size & 3)
+            for y in range(size):
+                for x in range(size):
+                    pixel = image2.get_pixel(x, size - y - 1)
+                    index = colors.index(pixel)
+                    fp.write(struct.pack('<B', index))
+                fp.write(rowalign)
+        else:
+            raise ValueError("Invalid bpp %d" % (bpp))
+
+        # Create an AND mask, aligned to 4-byte boundary
+        if image.hasAlpha() and bpp <= 8:
+            rowalign = b'\0' * (-((size + 7) >> 3) & 3)
+            for y in range(size):
+                mask = 0
+                num_bits = 7
+                for x in range(size):
+                    a = image.get_alpha_val(x, size - y - 1)
+                    if a <= 1:
+                        mask |= (1 << num_bits)
+                    num_bits -= 1
+                    if num_bits < 0:
+                        fp.write(struct.pack('<B', mask))
+                        mask = 0
+                        num_bits = 7
+                if num_bits < 7:
+                    fp.write(struct.pack('<B', mask))
+                fp.write(rowalign)
+        else:
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            fp.write(b'\x00' * (andsize * size))
+
+    def makeICO(self, fn):
+        """ Writes the images to a Windows ICO file.  Returns True on success. """
+
+        if not isinstance(fn, Filename):
+            fn = Filename.fromOsSpecific(fn)
+        fn.setBinary()
+
+        # ICO files only support resolutions up to 256x256.
+        count = 0
+        for size in self.images:
+            if size < 256:
+                count += 1
+            if size <= 256:
+                count += 1
+        dataoffs = 6 + count * 16
+
+        ico = open(fn, 'wb')
+        ico.write(struct.pack('<HHH', 0, 1, count))
+
+        # Write 8-bpp image headers for sizes under 256x256.
+        for size, image in self.images.items():
+            if size >= 256:
+                continue
+            ico.write(struct.pack('<BB', size, size))
+
+            # Calculate row sizes
+            xorsize = size
+            if xorsize % 4 != 0:
+                xorsize += 4 - (xorsize % 4)
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            datasize = 40 + 256 * 4 + (xorsize + andsize) * size
+
+            ico.write(struct.pack('<BBHHII', 0, 0, 1, 8, datasize, dataoffs))
+            dataoffs += datasize
+
+        # Write 24/32-bpp image headers.
+        for size, image in self.images.items():
+            if size > 256:
+                continue
+            elif size == 256:
+                ico.write(b'\0\0')
+            else:
+                ico.write(struct.pack('<BB', size, size))
+
+            # Calculate the size so we can write the offset within the file.
+            if image.hasAlpha():
+                bpp = 32
+                xorsize = size * 4
+            else:
+                bpp = 24
+                xorsize = size * 3 + (-(size * 3) & 3)
+            andsize = (size + 7) >> 3
+            if andsize % 4 != 0:
+                andsize += 4 - (andsize % 4)
+            datasize = 40 + (xorsize + andsize) * size
+
+            ico.write(struct.pack('<BBHHII', 0, 0, 1, bpp, datasize, dataoffs))
+            dataoffs += datasize
+
+        # Now write the actual icon bitmap data.
+        for size, image in self.images.items():
+            if size < 256:
+                self._write_bitmap(ico, image, size, 8)
+
+        for size, image in self.images.items():
+            if size <= 256:
+                bpp = 32 if image.hasAlpha() else 24
+                self._write_bitmap(ico, image, size, bpp)
+
+        assert ico.tell() == dataoffs
+        ico.close()
+
+        return True
+
+    def makeICNS(self, fn):
+        """ Writes the images to an Apple ICNS file.  Returns True on success. """
+
+        if not isinstance(fn, Filename):
+            fn = Filename.fromOsSpecific(fn)
+        fn.setBinary()
+
+        icns = open(fn, 'wb')
+        icns.write(b'icns\0\0\0\0')
+
+        icon_types = {16: b'is32', 32: b'il32', 48: b'ih32', 128: b'it32'}
+        mask_types = {16: b's8mk', 32: b'l8mk', 48: b'h8mk', 128: b't8mk'}
+        png_types = {256: b'ic08', 512: b'ic09', 1024: b'ic10'}
+
+        pngtype = PNMFileTypeRegistry.getGlobalPtr().getTypeFromExtension("png")
+
+        for size, image in sorted(self.images.items(), key=lambda item:item[0]):
+            if size in png_types and pngtype is not None:
+                stream = StringStream()
+                image.write(stream, "", pngtype)
+                pngdata = stream.data
+
+                icns.write(png_types[size])
+                icns.write(struct.pack('>I', len(pngdata)))
+                icns.write(pngdata)
+
+            elif size in icon_types:
+                # If it has an alpha channel, we write out a mask too.
+                if image.hasAlpha():
+                    icns.write(mask_types[size])
+                    icns.write(struct.pack('>I', size * size + 8))
+
+                    for y in range(size):
+                        for x in range(size):
+                            icns.write(struct.pack('<B', int(image.getAlpha(x, y) * 255)))
+
+                icns.write(icon_types[size])
+                icns.write(struct.pack('>I', size * size * 4 + 8))
+
+                for y in range(size):
+                    for x in range(size):
+                        r, g, b = image.getXel(x, y)
+                        icns.write(struct.pack('>BBBB', 0, int(r * 255), int(g * 255), int(b * 255)))
+
+        length = icns.tell()
+        icns.seek(4)
+        icns.write(struct.pack('>I', length))
+        icns.close()
+
+        return True

+ 21 - 15
makepanda/test_imports.py

@@ -6,15 +6,19 @@ import os, importlib
 # This will print out imports on the command line.
 import direct.showbase.VerboseImport
 
+import sys
+if sys.version_info >= (3, 4):
+    from importlib import machinery
+    extensions = machinery.EXTENSION_SUFFIXES + machinery.SOURCE_SUFFIXES + machinery.BYTECODE_SUFFIXES
+else:
+    import imp
+    extensions = set()
+    for suffix in imp.get_suffixes():
+        extensions.add(suffix[0])
 
-import imp
 import panda3d
 dir = os.path.dirname(panda3d.__file__)
 
-extensions = set()
-for suffix in imp.get_suffixes():
-    extensions.add(suffix[0])
-
 for basename in os.listdir(dir):
     module = basename.split('.', 1)[0]
     ext = basename[len(module):]
@@ -159,18 +163,19 @@ import direct.interval.ProjectileIntervalTest
 import direct.interval.SoundInterval
 import direct.interval.TestInterval
 import direct.motiontrail.MotionTrail
-import direct.p3d.AppRunner
-import direct.p3d.DWBPackageInstaller
-import direct.p3d.DeploymentTools
+if sys.version_info < (3, 12):
+    import direct.p3d.AppRunner
+    import direct.p3d.DWBPackageInstaller
+    import direct.p3d.DeploymentTools
+    import direct.p3d.HostInfo
+    import direct.p3d.JavaScript
+    import direct.p3d.PackageInfo
+    import direct.p3d.PackageInstaller
+    import direct.p3d.PackageMerger
+    import direct.p3d.Packager
 import direct.p3d.FileSpec
-import direct.p3d.HostInfo
 import direct.p3d.InstalledHostData
 import direct.p3d.InstalledPackageData
-import direct.p3d.JavaScript
-import direct.p3d.PackageInfo
-import direct.p3d.PackageInstaller
-import direct.p3d.PackageMerger
-import direct.p3d.Packager
 import direct.p3d.PatchMaker
 import direct.p3d.ScanDirectoryNode
 import direct.p3d.SeqValue
@@ -231,7 +236,8 @@ import direct.showbase.ShowBase
 import direct.showbase.TaskThreaded
 import direct.showbase.ThreeUpShow
 import direct.showbase.Transitions
-import direct.showbase.VFSImporter
+if sys.version_info < (3, 12):
+    import direct.showbase.VFSImporter
 import direct.showbase.WxGlobal
 import direct.showutil.BuildGeometry
 import direct.showutil.Effects

+ 64 - 0
tests/dist/test_FreezeTool.py

@@ -0,0 +1,64 @@
+from direct.dist.FreezeTool import Freezer, PandaModuleFinder
+import sys
+
+
+def test_Freezer_moduleSuffixes():
+    freezer = Freezer()
+
+    for suffix, mode, type in freezer.moduleSuffixes:
+        if type == 2: # imp.PY_SOURCE
+            assert mode == 'rb'
+
+
+def test_Freezer_getModulePath_getModuleStar(tmp_path):
+    # Package 1 can be imported
+    package1 = tmp_path / "package1"
+    package1.mkdir()
+    (package1 / "submodule1.py").write_text("")
+    (package1 / "__init__.py").write_text("")
+
+    # Package 2 can not be imported
+    package2 = tmp_path / "package2"
+    package2.mkdir()
+    (package2 / "submodule2.py").write_text("")
+    (package2 / "__init__.py").write_text("raise ImportError\n")
+
+    # Module 1 can be imported
+    (tmp_path / "module1.py").write_text("")
+
+    # Module 2 can not be imported
+    (tmp_path / "module2.py").write_text("raise ImportError\n")
+
+    # Module 3 has a custom __path__ and __all__
+    (tmp_path / "module3.py").write_text("__path__ = ['foobar']\n"
+                                         "__all__ = ['test']\n")
+
+    backup = sys.path
+    try:
+        # Don't fail if first item on path does not exist
+        sys.path = [str(tmp_path / "nonexistent"), str(tmp_path)]
+
+        freezer = Freezer()
+        assert freezer.getModulePath("nonexist") == None
+        assert freezer.getModulePath("package1") == [str(package1)]
+        assert freezer.getModulePath("package2") == [str(package2)]
+        assert freezer.getModulePath("package1.submodule1") == None
+        assert freezer.getModulePath("package1.nonexist") == None
+        assert freezer.getModulePath("package2.submodule2") == None
+        assert freezer.getModulePath("package2.nonexist") == None
+        assert freezer.getModulePath("module1") == None
+        assert freezer.getModulePath("module2") == None
+        assert freezer.getModulePath("module3") == ['foobar']
+
+        assert freezer.getModuleStar("nonexist") == None
+        assert freezer.getModuleStar("package1") == ['submodule1']
+        assert freezer.getModuleStar("package2") == ['submodule2']
+        assert freezer.getModuleStar("package1.submodule1") == None
+        assert freezer.getModuleStar("package1.nonexist") == None
+        assert freezer.getModuleStar("package2.submodule2") == None
+        assert freezer.getModuleStar("package2.nonexist") == None
+        assert freezer.getModuleStar("module1") == None
+        assert freezer.getModuleStar("module2") == None
+        assert freezer.getModuleStar("module3") == ['test']
+    finally:
+        sys.path = backup