""" This module contains code to freeze a number of Python modules into a single (mostly) standalone DLL or EXE. """ import modulefinder import sys import os import marshal import imp import platform import struct import io import distutils.sysconfig as sysconf import zipfile import importlib import warnings from . import pefile # Temporary (?) try..except to protect against unbuilt p3extend_frozen. try: import p3extend_frozen except ImportError: p3extend_frozen = None from panda3d.core import Filename, Multifile, PandaSystem, StringStream # 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. # I wonder if there's a better way to determine this? python = os.path.splitext(os.path.split(sys.executable)[1])[0] isDebugBuild = (python.lower().endswith('_d')) # These are modules that Python always tries to import up-front. They # must be frozen in any main.exe. # 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', 'importlib.util', ] # These are some special init functions for some built-in Python modules that # deviate from the standard naming convention. A value of None means that a # dummy entry should be written to the inittab. builtinInitFuncs = { 'builtins': None, 'sys': None, 'exceptions': None, '_warnings': '_PyWarnings_Init', 'marshal': 'PyMarshal_Init', } if sys.version_info < (3, 7): builtinInitFuncs['_imp'] = 'PyInit_imp' # These are modules that are not found normally for these modules. Add them # to an include list so users do not have to do this manually. try: from pytest import freeze_includes as pytest_imports except ImportError: def pytest_imports(): return [] defaultHiddenImports = { 'pytest': pytest_imports(), 'pkg_resources': [ 'pkg_resources.*.*', ], 'xml.etree.cElementTree': ['xml.etree.ElementTree'], 'datetime': ['_strptime'], 'keyring.backends': ['keyring.backends.*'], 'matplotlib.font_manager': ['encodings.mac_roman'], 'matplotlib.backends._backend_tk': ['tkinter'], 'direct.particles': ['direct.particles.ParticleManagerGlobal'], 'numpy.core._multiarray_umath': [ 'numpy.core._internal', 'numpy.core._dtype_ctypes', 'numpy.core._methods', ], 'pandas.compat': ['lzma', 'cmath'], 'pandas._libs.tslibs.conversion': ['pandas._libs.tslibs.base'], 'plyer': ['plyer.platforms'], 'scipy.linalg': ['scipy.linalg.cython_blas', 'scipy.linalg.cython_lapack'], 'scipy.sparse.csgraph': ['scipy.sparse.csgraph._validation'], 'scipy.spatial.qhull': ['scipy._lib.messagestream'], 'scipy.spatial._qhull': ['scipy._lib.messagestream'], 'scipy.spatial.transform.rotation': ['scipy.spatial.transform._rotation_groups'], 'scipy.spatial.transform._rotation': ['scipy.spatial.transform._rotation_groups'], 'scipy.special._ufuncs': ['scipy.special._ufuncs_cxx'], 'scipy.stats._stats': ['scipy.special.cython_special'], } # These are modules that import other modules but shouldn't pick them up as # dependencies (usually because they are optional). This prevents picking up # unwanted dependencies. ignoreImports = { 'direct.showbase.PythonUtil': ['pstats', 'profile'], 'toml.encoder': ['numpy'], 'py._builtin': ['__builtin__'], 'site': ['android_log'], } if sys.version_info >= (3, 8): # importlib.metadata is a "provisional" module introduced in Python 3.8 that # conditionally pulls in dependency-rich packages like "email" and "pep517" # (the latter of which is a thirdparty package!) But it's only imported in # one obscure corner, so we don't want to pull it in by default. ignoreImports['importlib._bootstrap_external'] = ['importlib.metadata'] ignoreImports['importlib.metadata'] = ['pep517'] # These are overrides for specific modules. overrideModules = { # Used by the warnings module, among others, to get line numbers. Since # we set __file__, this would cause it to try and extract Python code # lines from the main executable, which we don't want. 'linecache': """__all__ = ["getline", "clearcache", "checkcache", "lazycache"] cache = {} def getline(filename, lineno, module_globals=None): return '' def clearcache(): global cache cache = {} def getlines(filename, module_globals=None): return [] def checkcache(filename=None): pass def updatecache(filename, module_globals=None): pass def lazycache(filename, module_globals): pass """, } # These are missing modules that we've reported already this session. reportedMissing = {} class CompilationEnvironment: """ Create an instance of this class to record the commands to invoke the compiler on a given platform. If needed, the caller can create a custom instance of this class (or simply set the compile strings directly) to customize the build environment. """ def __init__(self, platform): self.platform = platform # The command to compile a c to an object file. Replace %(basename)s # with the basename of the source file, and an implicit .c extension. self.compileObj = 'error' # The command to link a single object file into an executable. As # above, replace $(basename)s with the basename of the original source # file, and of the target executable. self.linkExe = 'error' # The command to link a single object file into a shared library. self.linkDll = 'error' # Paths to Python stuff. self.Python = None self.PythonIPath = sysconf.get_python_inc() self.PythonVersion = sysconf.get_config_var("LDVERSION") or sysconf.get_python_version() # The VC directory of Microsoft Visual Studio (if relevant) self.MSVC = None # Directory to Windows Platform SDK (if relevant) self.PSDK = None # The setting to control release vs. debug builds. Only relevant on # Windows. self.MD = None # Added to the path to the MSVC bin and lib directories on 64-bits Windows. self.suffix64 = '' # The _d extension to add to dll filenames on Windows in debug builds. self.dllext = '' # Any architecture-specific string. self.arch = '' self.determineStandardSetup() def determineStandardSetup(self): if self.platform.startswith('win'): self.Python = sysconf.PREFIX if 'VCINSTALLDIR' in os.environ: self.MSVC = os.environ['VCINSTALLDIR'] elif Filename('/c/Program Files/Microsoft Visual Studio 9.0/VC').exists(): self.MSVC = Filename('/c/Program Files/Microsoft Visual Studio 9.0/VC').toOsSpecific() elif Filename('/c/Program Files (x86)/Microsoft Visual Studio 9.0/VC').exists(): self.MSVC = Filename('/c/Program Files (x86)/Microsoft Visual Studio 9.0/VC').toOsSpecific() elif Filename('/c/Program Files/Microsoft Visual Studio .NET 2003/Vc7').exists(): self.MSVC = Filename('/c/Program Files/Microsoft Visual Studio .NET 2003/Vc7').toOsSpecific() else: print('Could not locate Microsoft Visual C++ Compiler! Try running from the Visual Studio Command Prompt.') sys.exit(1) if 'WindowsSdkDir' in os.environ: self.PSDK = os.environ['WindowsSdkDir'] elif platform.architecture()[0] == '32bit' and Filename('/c/Program Files/Microsoft Platform SDK for Windows Server 2003 R2').exists(): self.PSDK = Filename('/c/Program Files/Microsoft Platform SDK for Windows Server 2003 R2').toOsSpecific() elif os.path.exists(os.path.join(self.MSVC, 'PlatformSDK')): self.PSDK = os.path.join(self.MSVC, 'PlatformSDK') else: print('Could not locate the Microsoft Windows Platform SDK! Try running from the Visual Studio Command Prompt.') sys.exit(1) # We need to use the correct compiler setting for debug vs. release builds. self.MD = '/MD' if isDebugBuild: self.MD = '/MDd' self.dllext = '_d' # MSVC/bin and /lib directories have a different location # for win64. if self.platform == 'win_amd64': self.suffix64 = '\\amd64' # If it is run by makepanda, it handles the MSVC and PlatformSDK paths itself. if 'MAKEPANDA' in os.environ: self.compileObjExe = 'cl /wd4996 /Fo%(basename)s.obj /nologo /c %(MD)s /Zi /O2 /Ob2 /EHsc /Zm300 /W3 /I"%(pythonIPath)s" %(filename)s' self.compileObjDll = self.compileObjExe self.linkExe = 'link /nologo /MAP:NUL /FIXED:NO /OPT:REF /STACK:4194304 /INCREMENTAL:NO /LIBPATH:"%(python)s\\libs" /out:%(basename)s.exe %(basename)s.obj' self.linkDll = 'link /nologo /DLL /MAP:NUL /FIXED:NO /OPT:REF /INCREMENTAL:NO /LIBPATH:"%(python)s\\libs" /out:%(basename)s%(dllext)s.pyd %(basename)s.obj' else: os.environ['PATH'] += ';' + self.MSVC + '\\bin' + self.suffix64 + ';' + self.MSVC + '\\Common7\\IDE;' + self.PSDK + '\\bin' self.compileObjExe = 'cl /wd4996 /Fo%(basename)s.obj /nologo /c %(MD)s /Zi /O2 /Ob2 /EHsc /Zm300 /W3 /I"%(pythonIPath)s" /I"%(PSDK)s\\include" /I"%(MSVC)s\\include" %(filename)s' self.compileObjDll = self.compileObjExe self.linkExe = 'link /nologo /MAP:NUL /FIXED:NO /OPT:REF /STACK:4194304 /INCREMENTAL:NO /LIBPATH:"%(PSDK)s\\lib" /LIBPATH:"%(MSVC)s\\lib%(suffix64)s" /LIBPATH:"%(python)s\\libs" /out:%(basename)s.exe %(basename)s.obj' self.linkDll = 'link /nologo /DLL /MAP:NUL /FIXED:NO /OPT:REF /INCREMENTAL:NO /LIBPATH:"%(PSDK)s\\lib" /LIBPATH:"%(MSVC)s\\lib%(suffix64)s" /LIBPATH:"%(python)s\\libs" /out:%(basename)s%(dllext)s.pyd %(basename)s.obj' elif self.platform.startswith('osx_'): # macOS proc = self.platform.split('_', 1)[1] if proc == 'i386': self.arch = '-arch i386' elif proc == 'ppc': self.arch = '-arch ppc' elif proc == 'amd64': 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" else: # Unix lib_dir = sysconf.get_python_lib(plat_specific=1, standard_lib=1) #python_a = os.path.join(lib_dir, "config", "libpython%(pythonVersion)s.a") self.compileObjExe = "%(CC)s %(CFLAGS)s -c -o %(basename)s.o -pthread -O2 %(filename)s -I%(pythonIPath)s" self.compileObjDll = "%(CC)s %(CFLAGS)s %(CCSHARED)s -c -o %(basename)s.o -O2 %(filename)s -I%(pythonIPath)s" self.linkExe = "%(CC)s -o %(basename)s %(basename)s.o -L/usr/local/lib -lpython%(pythonVersion)s" self.linkDll = "%(LDSHARED)s -o %(basename)s.so %(basename)s.o -L/usr/local/lib -lpython%(pythonVersion)s" if os.path.isdir("/usr/PCBSD/local/lib"): self.linkExe += " -L/usr/PCBSD/local/lib" self.linkDll += " -L/usr/PCBSD/local/lib" def compileExe(self, filename, basename, extraLink=[]): compile = self.compileObjExe % dict({ 'python': self.Python, 'MSVC': self.MSVC, 'PSDK': self.PSDK, 'suffix64': self.suffix64, 'MD': self.MD, 'pythonIPath': self.PythonIPath, 'pythonVersion': self.PythonVersion, 'arch': self.arch, 'filename': filename, 'basename': basename, }, **sysconf.get_config_vars()) sys.stderr.write(compile + '\n') if os.system(compile) != 0: raise Exception('failed to compile %s.' % basename) link = self.linkExe % dict({ 'python': self.Python, 'MSVC': self.MSVC, 'PSDK': self.PSDK, 'suffix64': self.suffix64, 'pythonIPath': self.PythonIPath, 'pythonVersion': self.PythonVersion, 'arch': self.arch, 'filename': filename, 'basename': basename, }, **sysconf.get_config_vars()) link += ' ' + ' '.join(extraLink) sys.stderr.write(link + '\n') if os.system(link) != 0: raise Exception('failed to link %s.' % basename) def compileDll(self, filename, basename, extraLink=[]): compile = self.compileObjDll % dict({ 'python': self.Python, 'MSVC': self.MSVC, 'PSDK': self.PSDK, 'suffix64': self.suffix64, 'MD': self.MD, 'pythonIPath': self.PythonIPath, 'pythonVersion': self.PythonVersion, 'arch': self.arch, 'filename': filename, 'basename': basename, }, **sysconf.get_config_vars()) sys.stderr.write(compile + '\n') if os.system(compile) != 0: raise Exception('failed to compile %s.' % basename) link = self.linkDll % dict({ 'python': self.Python, 'MSVC': self.MSVC, 'PSDK': self.PSDK, 'suffix64': self.suffix64, 'pythonIPath': self.PythonIPath, 'pythonVersion': self.PythonVersion, 'arch': self.arch, 'filename': filename, 'basename': basename, 'dllext': self.dllext, }, **sysconf.get_config_vars()) link += ' ' + ' '.join(extraLink) sys.stderr.write(link + '\n') if os.system(link) != 0: raise Exception('failed to link %s.' % basename) # The code from frozenmain.c in the Python source repository. frozenMainCode = """ /* Python interpreter main program for frozen scripts */ #include #if PY_MAJOR_VERSION >= 3 #include #if PY_MINOR_VERSION < 5 #define Py_DecodeLocale _Py_char2wchar #endif #endif #ifdef MS_WINDOWS extern void PyWinFreeze_ExeInit(void); extern void PyWinFreeze_ExeTerm(void); extern PyAPI_FUNC(int) PyImport_ExtendInittab(struct _inittab *newtab); #endif /* Main program */ int Py_FrozenMain(int argc, char **argv) { char *p; int n, sts = 1; int inspect = 0; int unbuffered = 0; #if PY_MAJOR_VERSION >= 3 int i; char *oldloc; wchar_t **argv_copy = NULL; /* We need a second copies, as Python might modify the first one. */ wchar_t **argv_copy2 = NULL; if (argc > 0) { argv_copy = (wchar_t **)alloca(sizeof(wchar_t *) * argc); argv_copy2 = (wchar_t **)alloca(sizeof(wchar_t *) * argc); } #endif Py_FrozenFlag = 1; /* Suppress errors from getpath.c */ Py_NoSiteFlag = 1; Py_NoUserSiteDirectory = 1; if ((p = Py_GETENV("PYTHONINSPECT")) && *p != '\\0') inspect = 1; if ((p = Py_GETENV("PYTHONUNBUFFERED")) && *p != '\\0') unbuffered = 1; if (unbuffered) { setbuf(stdin, (char *)NULL); setbuf(stdout, (char *)NULL); setbuf(stderr, (char *)NULL); } #if PY_MAJOR_VERSION >= 3 oldloc = setlocale(LC_ALL, NULL); setlocale(LC_ALL, \"\"); for (i = 0; i < argc; i++) { argv_copy[i] = Py_DecodeLocale(argv[i], NULL); argv_copy2[i] = argv_copy[i]; if (!argv_copy[i]) { fprintf(stderr, \"Unable to decode the command line argument #%i\\n\", i + 1); argc = i; goto error; } } setlocale(LC_ALL, oldloc); #endif #ifdef MS_WINDOWS PyImport_ExtendInittab(extensions); #endif /* MS_WINDOWS */ if (argc >= 1) { #if PY_MAJOR_VERSION >= 3 Py_SetProgramName(argv_copy[0]); #else Py_SetProgramName(argv[0]); #endif } Py_Initialize(); #ifdef MS_WINDOWS PyWinFreeze_ExeInit(); #endif if (Py_VerboseFlag) fprintf(stderr, "Python %s\\n%s\\n", Py_GetVersion(), Py_GetCopyright()); #if PY_MAJOR_VERSION >= 3 PySys_SetArgv(argc, argv_copy); #else PySys_SetArgv(argc, argv); #endif n = PyImport_ImportFrozenModule("__main__"); if (n == 0) Py_FatalError("__main__ not frozen"); if (n < 0) { PyErr_Print(); sts = 1; } else sts = 0; if (inspect && isatty((int)fileno(stdin))) sts = PyRun_AnyFile(stdin, "") != 0; #ifdef MS_WINDOWS PyWinFreeze_ExeTerm(); #endif Py_Finalize(); #if PY_MAJOR_VERSION >= 3 error: if (argv_copy2) { for (i = 0; i < argc; i++) { #if PY_MINOR_VERSION >= 4 PyMem_RawFree(argv_copy2[i]); #else PyMem_Free(argv_copy2[i]); #endif } } #endif return sts; } """ # The code from frozen_dllmain.c in the Python source repository. # Windows only. frozenDllMainCode = """ #include static char *possibleModules[] = { "pywintypes", "pythoncom", "win32ui", NULL, }; BOOL CallModuleDllMain(char *modName, DWORD dwReason); /* Called by a frozen .EXE only, so that built-in extension modules are initialized correctly */ void PyWinFreeze_ExeInit(void) { char **modName; for (modName = possibleModules;*modName;*modName++) { /* printf("Initialising '%s'\\n", *modName); */ CallModuleDllMain(*modName, DLL_PROCESS_ATTACH); } } /* Called by a frozen .EXE only, so that built-in extension modules are cleaned up */ void PyWinFreeze_ExeTerm(void) { // Must go backwards char **modName; for (modName = possibleModules+(sizeof(possibleModules) / sizeof(char *))-2; modName >= possibleModules; *modName--) { /* printf("Terminating '%s'\\n", *modName);*/ CallModuleDllMain(*modName, DLL_PROCESS_DETACH); } } BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) { BOOL ret = TRUE; switch (dwReason) { case DLL_PROCESS_ATTACH: { char **modName; for (modName = possibleModules;*modName;*modName++) { BOOL ok = CallModuleDllMain(*modName, dwReason); if (!ok) ret = FALSE; } break; } case DLL_PROCESS_DETACH: { // Must go backwards char **modName; for (modName = possibleModules+(sizeof(possibleModules) / sizeof(char *))-2; modName >= possibleModules; *modName--) CallModuleDllMain(*modName, DLL_PROCESS_DETACH); break; } } return ret; } BOOL CallModuleDllMain(char *modName, DWORD dwReason) { BOOL (WINAPI * pfndllmain)(HINSTANCE, DWORD, LPVOID); char funcName[255]; HMODULE hmod = GetModuleHandle(NULL); strcpy(funcName, "_DllMain"); strcat(funcName, modName); strcat(funcName, "@12"); // stdcall convention. pfndllmain = (BOOL (WINAPI *)(HINSTANCE, DWORD, LPVOID))GetProcAddress(hmod, funcName); if (pfndllmain==NULL) { /* No function by that name exported - then that module does not appear in our frozen program - return OK */ return TRUE; } return (*pfndllmain)(hmod, dwReason, NULL); } """ # Our own glue code to start up a Python executable. mainInitCode = """ %(frozenMainCode)s int main(int argc, char *argv[]) { PyImport_FrozenModules = _PyImport_FrozenModules; return Py_FrozenMain(argc, argv); } """ # Our own glue code to start up a Python shared library. dllInitCode = """ /* * Call this function to extend the frozen modules array with a new * array of frozen modules, provided in a C-style array, at runtime. * Returns the total number of frozen modules. */ static int extend_frozen_modules(const struct _frozen *new_modules, int new_count) { int orig_count; struct _frozen *realloc_FrozenModules; /* First, count the number of frozen modules we had originally. */ orig_count = 0; while (PyImport_FrozenModules[orig_count].name != NULL) { ++orig_count; } if (new_count == 0) { /* Trivial no-op. */ return orig_count; } /* Reallocate the PyImport_FrozenModules array bigger to make room for the additional frozen modules. We just leak the original array; it's too risky to try to free it. */ realloc_FrozenModules = (struct _frozen *)malloc((orig_count + new_count + 1) * sizeof(struct _frozen)); /* The new frozen modules go at the front of the list. */ memcpy(realloc_FrozenModules, new_modules, new_count * sizeof(struct _frozen)); /* Then the original set of frozen modules. */ memcpy(realloc_FrozenModules + new_count, PyImport_FrozenModules, orig_count * sizeof(struct _frozen)); /* Finally, a single 0-valued entry marks the end of the array. */ memset(realloc_FrozenModules + orig_count + new_count, 0, sizeof(struct _frozen)); /* Assign the new pointer. */ PyImport_FrozenModules = realloc_FrozenModules; return orig_count + new_count; } #if PY_MAJOR_VERSION >= 3 static PyModuleDef mdef = { PyModuleDef_HEAD_INIT, "%(moduleName)s", "", -1, NULL, NULL, NULL, NULL, NULL }; %(dllexport)sPyObject *PyInit_%(moduleName)s(void) { extend_frozen_modules(_PyImport_FrozenModules, sizeof(_PyImport_FrozenModules) / sizeof(struct _frozen)); return PyModule_Create(&mdef); } #else static PyMethodDef nullMethods[] = { {NULL, NULL} }; %(dllexport)svoid init%(moduleName)s(void) { extend_frozen_modules(_PyImport_FrozenModules, sizeof(_PyImport_FrozenModules) / sizeof(struct _frozen)); Py_InitModule("%(moduleName)s", nullMethods); } #endif """ programFile = """ #include #ifdef _WIN32 #include #endif %(moduleDefs)s struct _frozen _PyImport_FrozenModules[] = { %(moduleList)s {NULL, NULL, 0} }; """ okMissing = [ '__main__', '_dummy_threading', 'Carbon', 'Carbon.Files', 'Carbon.Folder', 'Carbon.Folders', 'HouseGlobals', 'Carbon.File', 'MacOS', '_emx_link', 'ce', 'mac', 'org.python.core', 'os.path', 'os2', 'posix', 'pwd', 'readline', 'riscos', 'riscosenviron', 'riscospath', 'dbm', 'fcntl', 'win32api', 'win32pipe', 'usercustomize', '_winreg', 'winreg', 'ctypes', 'ctypes.wintypes', 'nt','msvcrt', 'EasyDialogs', 'SOCKS', 'ic', 'rourl2path', 'termios', 'vms_lib', 'OverrideFrom23._Res', 'email', 'email.Utils', 'email.Generator', 'email.Iterators', '_subprocess', 'gestalt', 'java.lang', 'direct.extensions_native.extensions_darwin', '_manylinux', 'collections.Iterable', 'collections.Mapping', 'collections.MutableMapping', 'collections.Sequence', 'numpy_distutils', '_winapi', ] # Since around macOS 10.15, Apple's codesigning process has become more strict. # Appending data to the end of a Mach-O binary is now explicitly forbidden. The # solution is to embed our own segment into the binary so it can be properly # signed. mach_header_64_layout = '