Various pfreeze improvements, allow linking in extension modules

This commit is contained in:
rdb 2016-05-02 16:46:58 +02:00
parent 910a50de16
commit 6c70b41a21
2 changed files with 174 additions and 63 deletions

View File

@ -8,7 +8,7 @@ import marshal
import imp import imp
import platform import platform
from io import StringIO from io import StringIO
from distutils.sysconfig import PREFIX, get_python_inc, get_python_version, get_config_var import distutils.sysconfig as sysconf
# Temporary (?) try..except to protect against unbuilt p3extend_frozen. # Temporary (?) try..except to protect against unbuilt p3extend_frozen.
try: try:
@ -29,9 +29,23 @@ isDebugBuild = (python.lower().endswith('_d'))
# These are modules that Python always tries to import up-front. They # These are modules that Python always tries to import up-front. They
# must be frozen in any main.exe. # must be frozen in any main.exe.
startupModules = [ startupModules = [
'os', 'encodings.cp1252', 'encodings.cp1252', 'encodings.latin_1', 'encodings.utf_8',
'encodings.latin_1', 'encodings.utf_8', 'io',
] ]
if sys.version_info >= (3, 0):
startupModules += ['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,
'__builtin__': None,
'sys': None,
'exceptions': None,
'_imp': 'PyInit_imp',
'_warnings': '_PyWarnings_Init',
'marshal': 'PyMarshal_Init',
}
# These are missing modules that we've reported already this session. # These are missing modules that we've reported already this session.
reportedMissing = {} reportedMissing = {}
@ -60,8 +74,8 @@ class CompilationEnvironment:
# Paths to Python stuff. # Paths to Python stuff.
self.Python = None self.Python = None
self.PythonIPath = get_python_inc() self.PythonIPath = sysconf.get_python_inc()
self.PythonVersion = get_config_var("LDVERSION") or get_python_version() self.PythonVersion = sysconf.get_config_var("LDVERSION") or sysconf.get_python_version()
# The VC directory of Microsoft Visual Studio (if relevant) # The VC directory of Microsoft Visual Studio (if relevant)
self.MSVC = None self.MSVC = None
@ -85,7 +99,7 @@ class CompilationEnvironment:
def determineStandardSetup(self): def determineStandardSetup(self):
if self.platform.startswith('win'): if self.platform.startswith('win'):
self.Python = PREFIX self.Python = sysconf.PREFIX
if ('VCINSTALLDIR' in os.environ): if ('VCINSTALLDIR' in os.environ):
self.MSVC = os.environ['VCINSTALLDIR'] self.MSVC = os.environ['VCINSTALLDIR']
@ -122,13 +136,15 @@ class CompilationEnvironment:
# If it is run by makepanda, it handles the MSVC and PlatformSDK paths itself. # If it is run by makepanda, it handles the MSVC and PlatformSDK paths itself.
if ('MAKEPANDA' in os.environ): if ('MAKEPANDA' in os.environ):
self.compileObj = 'cl /wd4996 /Fo%(basename)s.obj /nologo /c %(MD)s /Zi /O2 /Ob2 /EHsc /Zm300 /W3 /I"%(pythonIPath)s" %(filename)s' 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.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' 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: else:
os.environ['PATH'] += ';' + self.MSVC + '\\bin' + self.suffix64 + ';' + self.MSVC + '\\Common7\\IDE;' + self.PSDK + '\\bin' os.environ['PATH'] += ';' + self.MSVC + '\\bin' + self.suffix64 + ';' + self.MSVC + '\\Common7\\IDE;' + self.PSDK + '\\bin'
self.compileObj = '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.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.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' 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'
@ -141,22 +157,26 @@ class CompilationEnvironment:
self.arch = '-arch ppc' self.arch = '-arch ppc'
elif proc == 'amd64': elif proc == 'amd64':
self.arch = '-arch x86_64' self.arch = '-arch x86_64'
self.compileObj = "gcc -fPIC -c %(arch)s -o %(basename)s.o -O2 -I%(pythonIPath)s %(filename)s" 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.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.linkDll = "gcc %(arch)s -undefined dynamic_lookup -bundle -o %(basename)s.so %(basename)s.o"
else: else:
# Unix # Unix
self.compileObj = "gcc -fPIC -c -o %(basename)s.o -O2 %(filename)s -I%(pythonIPath)s" lib_dir = sysconf.get_python_lib(plat_specific=1, standard_lib=1)
self.linkExe = "gcc -o %(basename)s %(basename)s.o -L/usr/local/lib -lpython%(pythonVersion)s" #python_a = os.path.join(lib_dir, "config", "libpython%(pythonVersion)s.a")
self.linkDll = "gcc -shared -o %(basename)s.so %(basename)s.o -L/usr/local/lib -lpython%(pythonVersion)s" 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")): if (os.path.isdir("/usr/PCBSD/local/lib")):
self.linkExe += " -L/usr/PCBSD/local/lib" self.linkExe += " -L/usr/PCBSD/local/lib"
self.linkDll += " -L/usr/PCBSD/local/lib" self.linkDll += " -L/usr/PCBSD/local/lib"
def compileExe(self, filename, basename): def compileExe(self, filename, basename, extraLink=[]):
compile = self.compileObj % { compile = self.compileObjExe % dict({
'python' : self.Python, 'python' : self.Python,
'MSVC' : self.MSVC, 'MSVC' : self.MSVC,
'PSDK' : self.PSDK, 'PSDK' : self.PSDK,
@ -167,12 +187,12 @@ class CompilationEnvironment:
'arch' : self.arch, 'arch' : self.arch,
'filename' : filename, 'filename' : filename,
'basename' : basename, 'basename' : basename,
} }, **sysconf.get_config_vars())
sys.stderr.write(compile + '\n') sys.stderr.write(compile + '\n')
if os.system(compile) != 0: if os.system(compile) != 0:
raise Exception('failed to compile %s.' % basename) raise Exception('failed to compile %s.' % basename)
link = self.linkExe % { link = self.linkExe % dict({
'python' : self.Python, 'python' : self.Python,
'MSVC' : self.MSVC, 'MSVC' : self.MSVC,
'PSDK' : self.PSDK, 'PSDK' : self.PSDK,
@ -182,13 +202,14 @@ class CompilationEnvironment:
'arch' : self.arch, 'arch' : self.arch,
'filename' : filename, 'filename' : filename,
'basename' : basename, 'basename' : basename,
} }, **sysconf.get_config_vars())
link += ' ' + ' '.join(extraLink)
sys.stderr.write(link + '\n') sys.stderr.write(link + '\n')
if os.system(link) != 0: if os.system(link) != 0:
raise Exception('failed to link %s.' % basename) raise Exception('failed to link %s.' % basename)
def compileDll(self, filename, basename): def compileDll(self, filename, basename, extraLink=[]):
compile = self.compileObj % { compile = self.compileObjDll % dict({
'python' : self.Python, 'python' : self.Python,
'MSVC' : self.MSVC, 'MSVC' : self.MSVC,
'PSDK' : self.PSDK, 'PSDK' : self.PSDK,
@ -199,12 +220,12 @@ class CompilationEnvironment:
'arch' : self.arch, 'arch' : self.arch,
'filename' : filename, 'filename' : filename,
'basename' : basename, 'basename' : basename,
} }, **sysconf.get_config_vars())
sys.stderr.write(compile + '\n') sys.stderr.write(compile + '\n')
if os.system(compile) != 0: if os.system(compile) != 0:
raise Exception('failed to compile %s.' % basename) raise Exception('failed to compile %s.' % basename)
link = self.linkDll % { link = self.linkDll % dict({
'python' : self.Python, 'python' : self.Python,
'MSVC' : self.MSVC, 'MSVC' : self.MSVC,
'PSDK' : self.PSDK, 'PSDK' : self.PSDK,
@ -215,7 +236,8 @@ class CompilationEnvironment:
'filename' : filename, 'filename' : filename,
'basename' : basename, 'basename' : basename,
'dllext' : self.dllext, 'dllext' : self.dllext,
} }, **sysconf.get_config_vars())
link += ' ' + ' '.join(extraLink)
sys.stderr.write(link + '\n') sys.stderr.write(link + '\n')
if os.system(link) != 0: if os.system(link) != 0:
raise Exception('failed to link %s.' % basename) raise Exception('failed to link %s.' % basename)
@ -264,6 +286,8 @@ Py_FrozenMain(int argc, char **argv)
#endif #endif
Py_FrozenFlag = 1; /* Suppress errors from getpath.c */ Py_FrozenFlag = 1; /* Suppress errors from getpath.c */
Py_NoSiteFlag = 1;
Py_NoUserSiteDirectory = 1;
if ((p = Py_GETENV("PYTHONINSPECT")) && *p != '\\0') if ((p = Py_GETENV("PYTHONINSPECT")) && *p != '\\0')
inspect = 1; inspect = 1;
@ -463,10 +487,6 @@ main(int argc, char *argv[]) {
# Our own glue code to start up a Python shared library. # Our own glue code to start up a Python shared library.
dllInitCode = """ dllInitCode = """
static PyMethodDef nullMethods[] = {
{NULL, NULL}
};
/* /*
* Call this function to extend the frozen modules array with a new * Call this function to extend the frozen modules array with a new
* array of frozen modules, provided in a C-style array, at runtime. * array of frozen modules, provided in a C-style array, at runtime.
@ -508,10 +528,29 @@ extend_frozen_modules(const struct _frozen *new_modules, int new_count) {
return orig_count + new_count; return orig_count + new_count;
} }
%(dllexport)svoid init%(moduleName)s() { #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)); extend_frozen_modules(_PyImport_FrozenModules, sizeof(_PyImport_FrozenModules) / sizeof(struct _frozen));
Py_InitModule("%(moduleName)s", nullMethods); Py_InitModule("%(moduleName)s", nullMethods);
} }
#endif
""" """
programFile = """ programFile = """
@ -526,8 +565,6 @@ struct _frozen _PyImport_FrozenModules[] = {
%(moduleList)s %(moduleList)s
{NULL, NULL, 0} {NULL, NULL, 0}
}; };
%(initCode)s
""" """
# Windows needs this bit. # Windows needs this bit.
@ -664,9 +701,14 @@ class Freezer:
# addToMultifile(). It contains a list of all the extension # addToMultifile(). It contains a list of all the extension
# modules that were discovered, which have not been added to # modules that were discovered, which have not been added to
# the output. The list is a list of tuples of the form # the output. The list is a list of tuples of the form
# (moduleName, filename). # (moduleName, filename). filename will be None for built-in
# modules.
self.extras = [] self.extras = []
# Set this to true if extension modules should be linked in to
# the resulting executable.
self.linkExtensionModules = False
# End of public interface. These remaining members should not # End of public interface. These remaining members should not
# be directly manipulated by callers. # be directly manipulated by callers.
self.previousModules = {} self.previousModules = {}
@ -676,6 +718,10 @@ class Freezer:
self.previousModules = dict(previous.modules) self.previousModules = dict(previous.modules)
self.modules = dict(previous.modules) self.modules = dict(previous.modules)
# Exclude doctest by default; it is not very useful in production
# builds. It can be explicitly included if desired.
self.modules['doctest'] = self.ModuleDef('doctest', exclude = True)
self.mf = None self.mf = None
# Actually, make sure we know how to find all of the # Actually, make sure we know how to find all of the
@ -951,8 +997,8 @@ class Freezer:
for mdef in includes: for mdef in includes:
try: try:
self.__loadModule(mdef) self.__loadModule(mdef)
except ImportError: except ImportError as ex:
print("Unknown module: %s" % (mdef.moduleName)) print("Unknown module: %s (%s)" % (mdef.moduleName, str(ex)))
# Also attempt to import any implicit modules. If any of # Also attempt to import any implicit modules. If any of
# these fail to import, we don't really care. # these fail to import, we don't really care.
@ -1296,41 +1342,91 @@ class Freezer:
if mdef.forbid: if mdef.forbid:
# Explicitly disallow importing this module. # Explicitly disallow importing this module.
moduleList.append(self.makeForbiddenModuleListEntry(moduleName)) moduleList.append(self.makeForbiddenModuleListEntry(moduleName))
else: continue
assert not mdef.exclude
# Allow importing this module.
module = self.mf.modules.get(origName, None)
code = getattr(module, "__code__", None)
if code:
code = marshal.dumps(code)
mangledName = self.mangleName(moduleName) assert not mdef.exclude
moduleDefs.append(self.makeModuleDef(mangledName, code)) # Allow importing this module.
moduleList.append(self.makeModuleListEntry(mangledName, code, moduleName, module)) module = self.mf.modules.get(origName, None)
code = getattr(module, "__code__", None)
if code:
code = marshal.dumps(code)
elif moduleName in startupModules: mangledName = self.mangleName(moduleName)
# Forbid the loading of this startup module. moduleDefs.append(self.makeModuleDef(mangledName, code))
moduleList.append(self.makeForbiddenModuleListEntry(moduleName)) moduleList.append(self.makeModuleListEntry(mangledName, code, moduleName, module))
continue
else: #if moduleName in startupModules:
# This is a module with no associated Python # # Forbid the loading of this startup module.
# code. It must be an extension module. Get the # moduleList.append(self.makeForbiddenModuleListEntry(moduleName))
# filename. # continue
extensionFilename = getattr(module, '__file__', None)
if extensionFilename: # This is a module with no associated Python code. It is either
self.extras.append((moduleName, extensionFilename)) # an extension module or a builtin module. Get the filename, if
else: # it is the former.
# It doesn't even have a filename; it must extensionFilename = getattr(module, '__file__', None)
# be a built-in module. No worries about
# this one, then. if extensionFilename or self.linkExtensionModules:
pass self.extras.append((moduleName, extensionFilename))
# If it is a submodule of a frozen module, Python will have
# 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')
code = marshal.dumps(code)
mangledName = self.mangleName(moduleName)
moduleDefs.append(self.makeModuleDef(mangledName, code))
moduleList.append(self.makeModuleListEntry(mangledName, code, moduleName, None))
elif '.' in moduleName:
# Nothing we can do about this case except warn the user they
# are in for some trouble.
print('WARNING: Python cannot import extension modules under '
'frozen Python packages; %s will be inaccessible. '
'passing either -l to link in extension modules or use '
'-x %s to exclude the entire package.' % (moduleName, moduleName.split('.')[0]))
text = programFile % { text = programFile % {
'moduleDefs': '\n'.join(moduleDefs), 'moduleDefs': '\n'.join(moduleDefs),
'moduleList': '\n'.join(moduleList), 'moduleList': '\n'.join(moduleList),
'initCode': initCode
} }
if self.linkExtensionModules and self.extras:
# Python 3 case.
text += '#if PY_MAJOR_VERSION >= 3\n'
for module, fn in self.extras:
libName = module.split('.')[-1]
initFunc = builtinInitFuncs.get(module, 'PyInit_' + libName)
if initFunc:
text += 'extern PyObject *%s(void);\n' % (initFunc)
text += '\n'
text += 'struct _inittab _PyImport_Inittab[] = {\n'
for module, fn in self.extras:
libName = module.split('.')[-1]
initFunc = builtinInitFuncs.get(module, 'PyInit_' + libName) or 'NULL'
text += ' {"%s", %s},\n' % (module, initFunc)
text += ' {0, 0},\n'
text += '};\n\n'
# Python 2 case.
text += '#else\n'
for module, fn in self.extras:
libName = module.split('.')[-1]
initFunc = builtinInitFuncs.get(module, 'init' + libName)
if initFunc:
text += 'extern void %s(void);\n' % (initFunc)
text += '\n'
text += 'struct _inittab _PyImport_Inittab[] = {\n'
for module, fn in self.extras:
libName = module.split('.')[-1]
initFunc = builtinInitFuncs.get(module, 'init' + libName) or 'NULL'
text += ' {"%s", %s},\n' % (module, initFunc)
text += ' {0, 0},\n'
text += '};\n'
text += '#endif\n\n'
text += initCode
if filename is not None: if filename is not None:
file = open(filename, 'w') file = open(filename, 'w')
file.write(text) file.write(text)
@ -1397,8 +1493,14 @@ class Freezer:
self.writeCode(filename, initCode=initCode) self.writeCode(filename, initCode=initCode)
extraLink = []
if self.linkExtensionModules:
for mod, fn in self.extras:
if fn:
extraLink.append(fn)
try: try:
compileFunc(filename, basename) compileFunc(filename, basename, extraLink=extraLink)
finally: finally:
if not self.keepTemporaryFiles: if not self.keepTemporaryFiles:
if os.path.exists(filename): if os.path.exists(filename):

View File

@ -15,7 +15,7 @@ imported directly or indirectly by the original startfile.py.
Usage: Usage:
pfreeze.py [opts] startfile pfreeze.py [opts] [startfile]
Options: Options:
@ -40,6 +40,11 @@ Options:
of the __path__ variable, and thus must be actually imported to of the __path__ variable, and thus must be actually imported to
determine the true value of __path__. determine the true value of __path__.
-P path
Specifies an additional directory in which we should search for
Python modules. This is equivalent to setting the PYTHONPATH
environment variable. May be repeated.
-s -s
Adds the standard set of modules that are necessary for embedding Adds the standard set of modules that are necessary for embedding
the Python interpreter. Implicitly set if an executable is the Python interpreter. Implicitly set if an executable is
@ -55,7 +60,7 @@ from direct.showutil import FreezeTool
def usage(code, msg = ''): def usage(code, msg = ''):
if __doc__: if __doc__:
sys.stderr.write(__doc__ + '\n') sys.stderr.write(__doc__ + '\n')
sys.stderr.write(msg + '\n') sys.stderr.write(str(msg) + '\n')
sys.exit(code) sys.exit(code)
# We're not protecting the next part under a __name__ == __main__ # We're not protecting the next part under a __name__ == __main__
@ -67,7 +72,7 @@ basename = None
addStartupModules = False addStartupModules = False
try: try:
opts, args = getopt.getopt(sys.argv[1:], 'o:i:x:p:sh') opts, args = getopt.getopt(sys.argv[1:], 'o:i:x:p:P:slh')
except getopt.error as msg: except getopt.error as msg:
usage(1, msg) usage(1, msg)
@ -83,8 +88,12 @@ for opt, arg in opts:
elif opt == '-p': elif opt == '-p':
for module in arg.split(','): for module in arg.split(','):
freezer.handleCustomPath(module) freezer.handleCustomPath(module)
elif opt == '-P':
sys.path.append(arg)
elif opt == '-s': elif opt == '-s':
addStartupModules = True addStartupModules = True
elif opt == '-l':
freezer.linkExtensionModules = True
elif opt == '-h': elif opt == '-h':
usage(0) usage(0)
else: else:
@ -126,7 +135,7 @@ if args:
elif outputType == 'exe': elif outputType == 'exe':
# We must have a main module when making an executable. # We must have a main module when making an executable.
usage(0) usage(1, 'A main file needs to be specified when creating an executable.')
freezer.done(addStartupModules = addStartupModules) freezer.done(addStartupModules = addStartupModules)