Browse Source

Merge branch 'release/1.10.x'

rdb 2 years ago
parent
commit
e01cb590de

+ 17 - 0
.github/workflows/ci.yml

@@ -355,6 +355,23 @@ jobs:
         rmdir panda3d-1.10.14
         (cd thirdparty/darwin-libs-a && rm -rf rocket)
 
+    - name: Set up Python 3.12
+      if: matrix.os != 'windows-2019'
+      uses: actions/setup-python@v4
+      with:
+        python-version: '3.12'
+    - name: Build Python 3.12
+      if: matrix.os != 'windows-2019'
+      shell: bash
+      run: |
+        python makepanda/makepanda.py --git-commit=${{github.sha}} --outputdir=built --everything --no-eigen --python-incdir="$pythonLocation/include" --python-libdir="$pythonLocation/lib" --verbose --threads=4 --windows-sdk=10
+    - name: Test Python 3.12
+      if: matrix.os != 'windows-2019'
+      shell: bash
+      run: |
+        python -m pip install -r requirements-test.txt
+        PYTHONPATH=built LD_LIBRARY_PATH=built/lib DYLD_LIBRARY_PATH=built/lib python -m pytest
+
     - name: Set up Python 3.11
       uses: actions/setup-python@v4
       with:

+ 3 - 3
contrib/src/panda3dtoolsgui/Panda3DToolsGUI.py

@@ -2650,11 +2650,11 @@ class main(wx.Frame):
                                             for inputFile in inputs:
                                                 if (inputFile != ''):
                                                     inputFilename = inputFile.split('\\')[-1]
-                                                    print "Compare: ", inFile, filename, inputFile, inputFilename
+                                                    print("Compare: ", inFile, filename, inputFile, inputFilename)
                                                     if inputFilename == filename:
                                                         inputTime = os.path.getmtime(inputFile)
                                                         outputTime = os.path.getmtime(inFile)
-                                                        print "Matched: ", (inputTime > outputTime)
+                                                        print("Matched: ", (inputTime > outputTime))
                                                         inputChanged = (inputTime > outputTime)
                                                         break
                                             '''
@@ -2848,7 +2848,7 @@ class main(wx.Frame):
 
         except ValueError:
             return
-        #print self.batchList
+        #print(self.batchList)
 
     def OnBatchItemEdit(self, event):
         selectedItemId = self.batchTree.GetSelections()

+ 1 - 1
contrib/src/panda3dtoolsgui/setup.py

@@ -1,4 +1,4 @@
-from distutils.core import setup
+from setuptools import setup
 import py2exe
 
 setup(console=['Panda3DToolsGUI.py'])

+ 55 - 29
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
@@ -13,6 +12,7 @@ import sysconfig
 import zipfile
 import importlib
 import warnings
+from importlib import machinery
 
 from . import pefile
 
@@ -24,6 +24,16 @@ except ImportError:
 
 from panda3d.core import Filename, Multifile, PandaSystem, StringStream
 
+# 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.
@@ -37,7 +47,7 @@ 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.*', 'io', 'marshal', 'importlib.machinery',
+    'encodings', 'encodings.*', 'io', 'marshal', 'importlib.machinery',
     'importlib.util',
 ]
 
@@ -262,10 +272,15 @@ class CompilationEnvironment:
                 self.arch = '-arch x86_64'
             elif proc in ('arm64', 'aarch64'):
                 self.arch = '-arch arm64'
-            self.compileObjExe = "gcc -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
-            self.compileObjDll = "gcc -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
-            self.linkExe = "gcc %(arch)s -o %(basename)s %(basename)s.o -framework Python"
-            self.linkDll = "gcc %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o"
+            self.compileObjExe = "clang -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
+            self.compileObjDll = "clang -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s"
+            self.linkExe = "clang %(arch)s -o %(basename)s %(basename)s.o"
+            if '/Python.framework/' in self.PythonIPath:
+                framework_dir = self.PythonIPath.split("/Python.framework/", 1)[0]
+                if framework_dir != "/System/Library/Frameworks":
+                    self.linkExe += " -F " + framework_dir
+            self.linkExe += " -framework Python"
+            self.linkDll = "clang %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o"
 
         else:
             # Unix
@@ -897,12 +912,11 @@ class Freezer:
 
         # Suffix/extension for Python C extension modules
         if self.platform == PandaSystem.getPlatform():
-            suffixes = imp.get_suffixes()
-
-            # Set extension for Python files to binary mode
-            for i, suffix in enumerate(suffixes):
-                if suffix[2] == imp.PY_SOURCE:
-                    suffixes[i] = (suffix[0], 'rb', imp.PY_SOURCE)
+            suffixes = (
+                [(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:
             suffixes = [('.py', 'rb', 1), ('.pyc', 'rb', 2)]
 
@@ -1316,10 +1330,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:
@@ -1415,7 +1429,7 @@ class Freezer:
 
     def __addPyc(self, multifile, filename, code, compressionLevel):
         if code:
-            data = imp.get_magic() + b'\0\0\0\0\0\0\0\0'
+            data = importlib.util.MAGIC_NUMBER + b'\0\0\0\0\0\0\0\0'
             data += marshal.dumps(code)
 
             stream = StringStream(data)
@@ -1605,7 +1619,7 @@ class Freezer:
             # trouble importing it as a builtin module.  Synthesize a frozen
             # module that loads it as builtin.
             if '.' in moduleName and self.linkExtensionModules:
-                code = compile('import sys;del sys.modules["%s"];import imp;imp.init_builtin("%s")' % (moduleName, moduleName), moduleName, 'exec', optimize=self.optimize)
+                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)
                 code = marshal.dumps(code)
                 mangledName = self.mangleName(moduleName)
                 moduleDefs.append(self.makeModuleDef(mangledName, code))
@@ -1887,9 +1901,19 @@ class Freezer:
             if '.' in moduleName and not self.platform.startswith('android'):
                 if self.platform.startswith("macosx") and not use_console:
                     # We write the Frameworks directory to sys.path[0].
-                    code = 'import sys;del sys.modules["%s"];import sys,os,imp;imp.load_dynamic("%s",os.path.join(sys.path[0], "%s%s"))' % (moduleName, moduleName, moduleName, modext)
+                    direxpr = 'sys.path[0]'
                 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)
+                    direxpr = 'os.path.dirname(sys.executable)'
+
+                code = \
+                    f'import sys;' \
+                    f'del sys.modules["{moduleName}"];' \
+                    f'import sys,os;' \
+                    f'from importlib.machinery import ExtensionFileLoader,ModuleSpec;' \
+                    f'from importlib._bootstrap import _load;' \
+                    f'path=os.path.join({direxpr}, "{moduleName}{modext}");' \
+                    f'_load(ModuleSpec(name="{moduleName}", loader=ExtensionFileLoader("{moduleName}", path), origin=path))'
+
                 code = compile(code, moduleName, 'exec', optimize=self.optimize)
                 code = marshal.dumps(code)
                 moduleList.append((moduleName, len(pool), len(code)))
@@ -2400,9 +2424,6 @@ class Freezer:
         return True
 
 
-_PKG_NAMESPACE_DIRECTORY = object()
-
-
 class PandaModuleFinder(modulefinder.ModuleFinder):
 
     def __init__(self, *args, **kw):
@@ -2415,7 +2436,12 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
         self.builtin_module_names = kw.pop('builtin_module_names', sys.builtin_module_names)
 
-        self.suffixes = kw.pop('suffixes', imp.get_suffixes())
+        self.suffixes = kw.pop('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]
+        ))
+
         self.optimize = kw.pop('optimize', -1)
 
         modulefinder.ModuleFinder.__init__(self, *args, **kw)
@@ -2563,7 +2589,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
@@ -2574,7 +2600,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]
@@ -2598,7 +2624,7 @@ class PandaModuleFinder(modulefinder.ModuleFinder):
 
             code += b'\n' if isinstance(code, bytes) else '\n'
             co = compile(code, pathname, 'exec', optimize=self.optimize)
-        elif type == imp.PY_COMPILED:
+        elif type == _PY_COMPILED:
             if sys.version_info >= (3, 7):
                 try:
                     data = fp.read()
@@ -2752,11 +2778,11 @@ 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))
 
         # It's built into the interpreter.
         if fullname in self.builtin_module_names:
-            return (None, None, ('', '', imp.C_BUILTIN))
+            return (None, None, ('', '', _C_BUILTIN))
 
         # If no search path is given, look for a built-in module.
         if path is None:
@@ -2806,7 +2832,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):
@@ -2818,7 +2844,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.

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

@@ -13,7 +13,6 @@ import re
 import shutil
 import stat
 import struct
-import imp
 import string
 import tempfile
 
@@ -1068,7 +1067,7 @@ class build_apps(setuptools.Command):
             freezer_extras.update(freezer.extras)
             freezer_modules.update(freezer.getAllModuleNames())
             for suffix in freezer.mf.suffixes:
-                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():

+ 12 - 0
dtool/src/interrogatedb/py_compat.h

@@ -241,6 +241,18 @@ INLINE PyObject *PyObject_CallMethodOneArg(PyObject *obj, PyObject *name, PyObje
 }
 #endif
 
+/* Python 3.12 */
+
+#if PY_VERSION_HEX < 0x030C0000
+#  define PyLong_IsNonNegative(value) (Py_SIZE((value)) >= 0)
+#else
+INLINE bool PyLong_IsNonNegative(PyObject *value) {
+  int overflow = 0;
+  long longval = PyLong_AsLongAndOverflow(value, &overflow);
+  return overflow == 1 || longval >= 0;
+}
+#endif
+
 /* Other Python implementations */
 
 #endif  // HAVE_PYTHON

+ 1 - 3
makepanda/installpanda.py

@@ -12,9 +12,7 @@ import os
 import sys
 from optparse import OptionParser
 from makepandacore import *
-
-# DO NOT CHANGE TO sysconfig - see GitHub issue #1230
-from distutils.sysconfig import get_python_lib
+from locations import get_python_lib
 
 
 MIME_INFO = (

+ 32 - 0
makepanda/locations.py

@@ -0,0 +1,32 @@
+__all__ = [
+    'get_python_inc',
+    'get_config_var',
+    'get_python_version',
+    'PREFIX',
+    'get_python_lib',
+    'get_config_vars',
+]
+
+import sys
+
+if sys.version_info < (3, 12):
+    from distutils.sysconfig import *
+else:
+    from sysconfig import *
+
+    PREFIX = get_config_var('prefix')
+
+    def get_python_inc(plat_specific=False):
+        path_name = 'platinclude' if plat_specific else 'include'
+        return get_path(path_name)
+
+    def get_python_lib(plat_specific=False, standard_lib=False):
+        if standard_lib:
+            path_name = 'stdlib'
+            if plat_specific:
+                path_name = 'plat' + path_name
+        elif plat_specific:
+            path_name = 'platlib'
+        else:
+            path_name = 'purelib'
+        return get_path(path_name)

+ 1 - 2
makepanda/makepackage.py

@@ -942,8 +942,7 @@ def MakeInstallerAndroid(version, **kwargs):
                     shutil.copy(os.path.join(source_dir, base), target)
 
     # Copy the Python standard library to the .apk as well.
-    # DO NOT CHANGE TO sysconfig - see #1230
-    from distutils.sysconfig import get_python_lib
+    from locations import get_python_lib
     stdlib_source = get_python_lib(False, True)
     stdlib_target = os.path.join("apkroot", "lib", "python{0}.{1}".format(*sys.version_info))
     copy_python_tree(stdlib_source, stdlib_target)

+ 2 - 4
makepanda/makepanda.py

@@ -30,12 +30,10 @@ except:
     print("Please install the development package of Python and try again.")
     exit(1)
 
-if sys.version_info >= (3, 10):
-    from sysconfig import get_platform
-else:
-    from distutils.util import get_platform
 from makepandacore import *
 
+from sysconfig import get_platform
+
 try:
     import zlib
 except:

+ 10 - 9
makepanda/makepandacore.py

@@ -21,6 +21,7 @@ import sys
 import threading
 import _thread as thread
 import time
+import locations
 
 SUFFIX_INC = [".cxx",".cpp",".c",".h",".I",".yxx",".lxx",".mm",".rc",".r"]
 SUFFIX_DLL = [".dll",".dlo",".dle",".dli",".dlm",".mll",".exe",".pyd",".ocx"]
@@ -2195,7 +2196,7 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         # On macOS, search for the Python framework directory matching the
         # version number of our current Python version.
         sysroot = SDK.get("MACOSX", "")
-        version = sysconfig.get_python_version()
+        version = locations.get_python_version()
 
         py_fwx = "{0}/System/Library/Frameworks/Python.framework/Versions/{1}".format(sysroot, version)
 
@@ -2220,19 +2221,19 @@ def SdkLocatePython(prefer_thirdparty_python=False):
         LibDirectory("PYTHON", py_fwx + "/lib")
 
     #elif GetTarget() == 'windows':
-    #    SDK["PYTHON"] = os.path.dirname(sysconfig.get_python_inc())
-    #    SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version()
+    #    SDK["PYTHON"] = os.path.dirname(locations.get_python_inc())
+    #    SDK["PYTHONVERSION"] = "python" + locations.get_python_version()
     #    SDK["PYTHONEXEC"] = sys.executable
 
     else:
-        SDK["PYTHON"] = sysconfig.get_python_inc()
-        SDK["PYTHONVERSION"] = "python" + sysconfig.get_python_version() + abiflags
+        SDK["PYTHON"] = locations.get_python_inc()
+        SDK["PYTHONVERSION"] = "python" + locations.get_python_version() + abiflags
         SDK["PYTHONEXEC"] = os.path.realpath(sys.executable)
 
     if CrossCompiling():
         # We need a version of Python we can run.
         SDK["PYTHONEXEC"] = sys.executable
-        host_version = "python" + sysconfig.get_python_version() + abiflags
+        host_version = "python" + locations.get_python_version() + abiflags
         if SDK["PYTHONVERSION"] != host_version:
             exit("Host Python version (%s) must be the same as target Python version (%s)!" % (host_version, SDK["PYTHONVERSION"]))
 
@@ -3386,7 +3387,7 @@ def GetExtensionSuffix():
 
 def GetPythonABI():
     if not CrossCompiling():
-        soabi = sysconfig.get_config_var('SOABI')
+        soabi = locations.get_config_var('SOABI')
         if soabi:
             return soabi
 
@@ -3507,8 +3508,8 @@ def GetCurrentPythonVersionInfo():
         "soabi": GetPythonABI(),
         "ext_suffix": GetExtensionSuffix(),
         "executable": sys.executable,
-        "purelib": sysconfig.get_python_lib(False),
-        "platlib": sysconfig.get_python_lib(True),
+        "purelib": locations.get_python_lib(False),
+        "platlib": locations.get_python_lib(True),
     }
 
 

+ 2 - 1
makepanda/makewheel.py

@@ -11,10 +11,11 @@ import tempfile
 import subprocess
 import time
 import struct
-from sysconfig import get_platform, get_config_var
 from optparse import OptionParser
 from base64 import urlsafe_b64encode
 from makepandacore import LocateBinary, GetExtensionSuffix, SetVerbose, GetVerbose, GetMetadataValue, CrossCompiling, GetThirdpartyDir, SDK, GetStrip
+from locations import get_config_var
+from sysconfig import get_platform
 
 
 def get_abi_tag():

+ 1 - 13
panda/src/pnmimagetypes/bmp.h

@@ -92,19 +92,7 @@ BMPlenrgbtable(int classv, unsigned long bitcount)
                 pm_error(er_internal, "BMPlenrgbtable");
                 return 0;
         }
-        switch (classv)
-        {
-        case C_WIN:
-                lenrgb = 4;
-                break;
-        case C_OS2:
-                lenrgb = 3;
-                break;
-        default:
-                pm_error(er_internal, "BMPlenrgbtable");
-                return 0;
-        }
-
+        lenrgb = (classv == C_OS2) ? 3 : 4;
         return (1 << bitcount) * lenrgb;
 }
 

+ 1 - 0
panda/src/pnmimagetypes/pnmFileTypeBMP.h

@@ -55,6 +55,7 @@ public:
     unsigned long offBits;
 
     unsigned short  cBitCount;
+    unsigned short  cCompression;
     int             indexed;
     int             classv;
 

+ 80 - 34
panda/src/pnmimagetypes/pnmFileTypeBMPReader.cxx

@@ -177,6 +177,7 @@ BMPreadinfoheader(
         unsigned long  *pcx,
         unsigned long  *pcy,
         unsigned short *pcBitCount,
+        unsigned short *pcCompression,
         int            *pclassv)
 {
         unsigned long   cbFix;
@@ -185,6 +186,7 @@ BMPreadinfoheader(
         unsigned long   cx = 0;
         unsigned long   cy = 0;
         unsigned short  cBitCount = 0;
+        unsigned long   cCompression = 0;
         int             classv = 0;
 
         cbFix = GetLong(fp);
@@ -229,7 +231,9 @@ BMPreadinfoheader(
          * for the required total.
          */
         if (classv != C_OS2) {
-            for (int i = 0; i < (int)cbFix - 16; i += 4) {
+            cCompression = GetLong(fp);
+
+            for (int i = 0; i < (int)cbFix - 20; i += 4) {
                 GetLong(fp);
             }
         }
@@ -273,11 +277,13 @@ BMPreadinfoheader(
         pm_message("cy: %d", cy);
         pm_message("cPlanes: %d", cPlanes);
         pm_message("cBitCount: %d", cBitCount);
+        pm_message("cCompression: %d", cCompression);
 #endif
 
         *pcx = cx;
         *pcy = cy;
         *pcBitCount = cBitCount;
+        *pcCompression = cCompression;
         *pclassv = classv;
 
         *ppos += cbFix;
@@ -401,45 +407,84 @@ BMPreadbits(xel *array, xelval *alpha_array,
         unsigned long   cx,
         unsigned long   cy,
         unsigned short  cBitCount,
-        int             /* classv */,
+        unsigned long   cCompression,
         int             indexed,
         pixval         *R,
         pixval         *G,
         pixval         *B)
 {
-        long            y;
-
-        readto(fp, ppos, offBits);
+  long y;
 
-        if(cBitCount > 24 && cBitCount != 32)
-        {
-                pm_error("%s: cannot handle cBitCount: %d"
-                         ,ifname
-                         ,cBitCount);
-        }
+  readto(fp, ppos, offBits);
 
-        /*
-         * The picture is stored bottom line first, top line last
-         */
+  if (cBitCount > 24 && cBitCount != 32) {
+    pm_error("%s: cannot handle cBitCount: %d", ifname, cBitCount);
+  }
 
-        for (y = (long)cy - 1; y >= 0; y--)
-        {
-                int rc;
-                rc = BMPreadrow(fp, ppos, array + y*cx, alpha_array + y*cx, cx, cBitCount, indexed, R, G, B);
-                if(rc == -1)
-                {
-                        pm_error("%s: couldn't read row %d"
-                                 ,ifname
-                                 ,y);
-                }
-                if(rc%4)
-                {
-                        pm_error("%s: row had bad number of bytes: %d"
-                                 ,ifname
-                                 ,rc);
-                }
+  if (cCompression == 1) {
+    // RLE8 compression
+    xel *row = array + (cy - 1) * cx;
+    xel *p = row;
+    unsigned long nbyte = 0;
+    while (true) {
+      int first = GetByte(fp);
+      int second = GetByte(fp);
+      nbyte += 2;
+
+      if (first != 0) {
+        // Repeated index.
+        for (int i = 0; i < first; ++i) {
+          PPM_ASSIGN(*p, R[second], G[second], B[second]);
+          ++p;
         }
-
+      }
+      else if (second == 0) {
+        // End of line.
+        row -= cx;
+        p = row;
+      }
+      else if (second == 1) {
+        // End of image.
+        break;
+      }
+      else if (second == 2) {
+        // Delta.
+        int xoffset = GetByte(fp);
+        int yoffset = GetByte(fp);
+        nbyte += 2;
+        row -= cx * yoffset;
+        p += xoffset - cx * yoffset;
+      }
+      else {
+        // Absolute run.
+        for (int i = 0; i < second; ++i) {
+          int v = GetByte(fp);
+          ++nbyte;
+          PPM_ASSIGN(*p, R[v], G[v], B[v]);
+          ++p;
+        }
+        nbyte += second;
+        if (second % 2) {
+          // Pad to 16-bit boundary.
+          GetByte(fp);
+          ++nbyte;
+        }
+      }
+    }
+    *ppos += nbyte;
+  }
+  else {
+    // The picture is stored bottom line first, top line last
+    for (y = (long)cy - 1; y >= 0; y--) {
+      int rc = BMPreadrow(fp, ppos, array + y*cx, alpha_array + y*cx, cx, cBitCount, indexed, R, G, B);
+      if (rc == -1) {
+        pm_error("%s: couldn't read row %d", ifname, y);
+      }
+      if (rc % 4) {
+        pm_error("%s: row had bad number of bytes: %d", ifname, rc);
+      }
+    }
+  }
 }
 
 /**
@@ -474,7 +519,7 @@ Reader(PNMFileType *type, istream *file, bool owns_file, string magic_number) :
   pos = 0;
 
   BMPreadfileheader(file, &pos, &offBits);
-  BMPreadinfoheader(file, &pos, &cx, &cy, &cBitCount, &classv);
+  BMPreadinfoheader(file, &pos, &cx, &cy, &cBitCount, &cCompression, &classv);
 
   if (offBits != BMPoffbits(classv, cBitCount)) {
     pnmimage_bmp_cat.warning()
@@ -523,9 +568,10 @@ Reader(PNMFileType *type, istream *file, bool owns_file, string magic_number) :
 int PNMFileTypeBMP::Reader::
 read_data(xel *array, xelval *alpha_array) {
   BMPreadbits(array, alpha_array, _file, &pos, offBits, _x_size, _y_size,
-              cBitCount, classv, indexed, R, G, B);
+              cBitCount, cCompression, indexed, R, G, B);
 
-  if (pos != BMPlenfile(classv, cBitCount, _x_size, _y_size)) {
+  if (cCompression != 1 &&
+      pos != BMPlenfile(classv, cBitCount, _x_size, _y_size)) {
     pnmimage_bmp_cat.warning()
       << "Read " << pos << " bytes, expected to read "
       << BMPlenfile(classv, cBitCount, _x_size, _y_size) << " bytes\n";

+ 2 - 2
panda/src/putil/bitArray_ext.cxx

@@ -20,7 +20,7 @@
  */
 void Extension<BitArray>::
 __init__(PyObject *init_value) {
-  if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) {
+  if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) {
     PyErr_SetString(PyExc_ValueError, "BitArray constructor requires a positive integer");
     return;
   }
@@ -76,7 +76,7 @@ __getstate__() const {
  */
 void Extension<BitArray>::
 __setstate__(PyObject *state) {
-  if (Py_SIZE(state) >= 0) {
+  if (PyLong_IsNonNegative(state)) {
     __init__(state);
   } else {
     PyObject *inverted = PyNumber_Invert(state);

+ 1 - 1
panda/src/putil/doubleBitMask_ext.I

@@ -17,7 +17,7 @@
 template<class BMType>
 INLINE void Extension<DoubleBitMask<BMType> >::
 __init__(PyObject *init_value) {
-  if (!PyLong_Check(init_value) || Py_SIZE(init_value) < 0) {
+  if (!PyLong_Check(init_value) || !PyLong_IsNonNegative(init_value)) {
     PyErr_SetString(PyExc_ValueError, "DoubleBitMask constructor requires a positive integer");
     return;
   }

+ 4 - 0
tests/display/test_cg_shader.py

@@ -1,4 +1,6 @@
 import os
+import platform
+import pytest
 
 from panda3d import core
 
@@ -16,12 +18,14 @@ def run_cg_compile_check(gsg, shader_path, expect_fail=False):
         assert shader is not None
 
 
[email protected](platform.machine().lower() == 'arm64', reason="Cg not supported on arm64")
 def test_cg_compile_error(gsg):
     """Test getting compile errors from bad Cg shaders"""
     shader_path = core.Filename(SHADERS_DIR, 'cg_bad.sha')
     run_cg_compile_check(gsg, shader_path, expect_fail=True)
 
 
[email protected](platform.machine().lower() == 'arm64', reason="Cg not supported on arm64")
 def test_cg_from_file(gsg):
     """Test compiling Cg shaders from files"""
     shader_path = core.Filename(SHADERS_DIR, 'cg_simple.sha')

+ 58 - 0
tests/dist/test_FreezeTool.py

@@ -0,0 +1,58 @@
+from direct.dist.FreezeTool import Freezer, PandaModuleFinder
+import sys
+
+
+def test_Freezer_moduleSuffixes():
+    freezer = Freezer()
+
+    for suffix, mode, type in freezer.mf.suffixes:
+        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")
+
+    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.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
+    finally:
+        sys.path = backup

+ 3 - 0
tests/putil/test_bitarray.py

@@ -118,6 +118,9 @@ def test_bitarray_pickle():
     ba = BitArray(123)
     assert ba == pickle.loads(pickle.dumps(ba, -1))
 
+    ba = BitArray(1 << 128)
+    assert ba == pickle.loads(pickle.dumps(ba, -1))
+
     ba = BitArray(94187049178237918273981729127381723)
     assert ba == pickle.loads(pickle.dumps(ba, -1))
 

+ 4 - 2
tests/test_imports.py

@@ -2,6 +2,7 @@
 # missing imports.  It is useful for a quick and dirty test to make sure
 # that there are no obvious build issues.
 import pytest
+import sys
 
 # This will print out imports on the command line.
 #import direct.showbase.VerboseImport
@@ -9,7 +10,7 @@ import pytest
 
 
 def test_imports_panda3d():
-    import importlib, os, sys
+    import importlib, os
     import panda3d
 
     # Look for panda3d.* modules in builtins - pfreeze might put them there.
@@ -165,7 +166,8 @@ def test_imports_direct():
     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