open_toontown_panda3d/direct/src/showbase/MakeAppMF.py

339 lines
12 KiB
Python

"""
This module will pack a Panda application, consisting of a
directory tree of .py files and models, into a multifile for
distribution and running with RunAppMF.py. To run it, use:
python MakeAppMF.py [opts] app.mf
Options:
-r application_root
Specify the root directory of the application source; this is a
directory tree that contains all of your .py files and models.
If this is omitted, the default is the current directory.
-m main.py
Names the Python file that begins the application. This should
be a file within the root directory. If this is omitted, the
default is a file named "main.py", or if there is only one Python
file present, it is used. If this file contains a function
called main(), that function will be called after importing it
(this is preferable to having the module start itself immediately
upon importing).
-c [py,pyc,pyo]
Specifies the compilation mode of python files. 'py' means to
leave them as source files, 'pyc' and 'pyo' are equivalent, and
mean to compile to byte code. pyc files will be written if the
interpreter is running in normal debug mode, while pyo files will
be written if it is running in optimize mode (-O or -OO).
"""
import sys
import getopt
import imp
import marshal
import direct
from direct.stdpy.file import open
from pandac.PandaModules import *
vfs = VirtualFileSystem.getGlobalPtr()
class ArgumentError(AttributeError):
pass
class AppPacker:
compression_level = 6
imported_maps = 'imported_maps'
# Text files that are copied (and compressed) to the multifile
# without processing.
text_extensions = [ 'prc' ]
# Binary files that are copied and compressed without processing.
binary_extensions = [ 'ttf', 'wav', 'mid' ]
# Binary files that are considered uncompressible, and are copied
# without compression.
uncompressible_extensions = [ 'mp3' ]
# Specifies how or if python files are compiled.
compilation_mode = 'pyc'
def __init__(self, multifile_name):
# Make sure any pre-existing file is removed.
Filename(multifile_name).unlink()
self.multifile = Multifile()
if not self.multifile.openReadWrite(multifile_name):
raise ArgumentError, 'Could not open %s for writing' % (multifile_name)
self.imported_textures = {}
# Get the list of filename extensions that are recognized as
# image files.
self.image_extensions = []
for type in PNMFileTypeRegistry.getGlobalPtr().getTypes():
self.image_extensions += type.getExtensions()
def scan(self, root, main):
if self.compilation_mode != 'py':
if __debug__:
self.compilation_mode = 'pyc'
else:
self.compilation_mode = 'pyo'
self.root = Filename(root)
self.root.makeAbsolute(vfs.getCwd())
# Check if there is just one .py file.
pyFiles = self.findPyFiles(self.root)
if main == None:
if len(pyFiles) == 1:
main = pyFiles[0]
else:
main = 'main.py'
if main not in pyFiles:
raise StandardError, 'No file %s in root directory.' % (main)
self.main = Filename(self.root, main)
self._recurse(self.root)
self.multifile.repack()
def findPyFiles(self, dirname):
""" Returns a list of Python filenames at the root directory
level. """
dirList = vfs.scanDirectory(dirname)
pyFiles = []
for file in dirList:
if file.getFilename().getExtension() == 'py':
pyFiles.append(file.getFilename().getBasename())
return pyFiles
def _recurse(self, filename):
dirList = vfs.scanDirectory(filename)
if dirList:
# It's a directory name. Recurse.
for subfile in dirList:
self._recurse(subfile.getFilename())
return
# It's a real file. Is it something we care about?
ext = filename.getExtension().lower()
outFilename = filename
if ext == 'pz':
# Strip off an implicit .pz extension.
outFilename = Filename(filename)
outFilename.setExtension('')
outFilename = Filename(outFilename.cStr())
ext = outFilename.getExtension().lower()
if ext == 'py':
self.addPyFile(filename)
elif ext == 'egg':
self.addEggFile(filename, outFilename)
elif ext == 'bam':
self.addBamFile(filename, outFilename)
elif ext in self.image_extensions:
self.addTexture(filename)
elif ext in self.text_extensions:
self.addTextFile(filename)
elif ext in self.binary_extensions:
self.addBinaryFile(filename)
elif ext in self.uncompressible_extensions:
self.addUncompressibleFile(filename)
def addPyFile(self, filename):
targetFilename = self.makeRelFilename(filename)
if filename == self.main:
# This one is the "main.py"; the starter file.
targetFilename = Filename('main.py')
if self.compilation_mode == 'py':
# Add python files as source files.
self.multifile.addSubfile(targetFilename.cStr(), filename, self.compression_level)
elif self.compilation_mode == 'pyc' or self.compilation_mode == 'pyo':
# Compile it to bytecode.
targetFilename.setExtension(self.compilation_mode)
source = open(filename, 'r').read()
if source and source[-1] != '\n':
source = source + '\n'
code = compile(source, targetFilename.cStr(), 'exec')
data = imp.get_magic() + '\0\0\0\0' + marshal.dumps(code)
stream = StringStream(data)
self.multifile.addSubfile(targetFilename.cStr(), stream, self.compression_level)
self.multifile.flush()
else:
raise StandardError, 'Unsupported compilation mode %s' % (self.compilation_mode)
def addEggFile(self, filename, outFilename):
# Precompile egg files to bam's.
node = loadEggFile(filename)
if not node:
raise StandardError, 'Could not read egg file %s' % (filename)
self.addNode(node, outFilename)
def addBamFile(self, filename, outFilename):
# Load the bam file so we can massage its textures.
bamFile = BamFile()
if not bamFile.openRead(filename):
raise StandardError, 'Could not read bam file %s' % (filename)
if not bamFile.resolve():
raise StandardError, 'Could not resolve bam file %s' % (filename)
node = bamFile.readNode()
if not node:
raise StandardError, 'Not a model file: %s' % (filename)
self.addNode(node, outFilename)
def addNode(self, node, filename):
""" Converts the indicated node to a bam stream, and adds the
bam file to the multifile under the indicated filename. """
# Be sure to import all of the referenced textures, and tell
# them their new location within the multifile.
for tex in NodePath(node).findAllTextures():
tex.setFilename(self.addTexture(tex.getFullpath()))
if tex.hasAlphaFilename():
tex.setAlphaFilename(self.addTexture(tex.getAlphaFullpath()))
# Now generate an in-memory bam file. Tell the bam writer to
# keep the textures referenced by their in-multifile path.
bamFile = BamFile()
stream = StringStream()
bamFile.openWrite(stream)
bamFile.getWriter().setFileTextureMode(BTMUnchanged)
bamFile.writeObject(node)
bamFile.close()
# Clean the node out of memory.
node.removeAllChildren()
# Now we have an in-memory bam file.
rel = self.makeRelFilename(filename)
rel.setExtension('bam')
stream.seekg(0)
self.multifile.addSubfile(rel.cStr(), stream, self.compression_level)
# Flush it so the data gets written to disk immediately, so we
# don't have to keep it around in ram.
self.multifile.flush()
def addTexture(self, filename):
""" Adds the texture to the multifile, if it has not already
been added. If it is not within the root directory, copies it
in (virtually) into a directory within the multifile named
imported_maps. Returns the new filename within the
multifile. """
assert not filename.isLocal()
filename = Filename(filename)
filename.makeAbsolute(vfs.getCwd())
filename.makeTrueCase()
rel = self.imported_textures.get(filename.cStr())
if not rel:
rel = Filename(filename)
if not rel.makeRelativeTo(self.root, False):
# Not within the multifile.
rel = Filename(self.imported_maps, filename.getBasename())
# Need to add it now.
self.imported_textures[filename.cStr()] = rel
filename = Filename(filename)
filename.setBinary()
self.multifile.addSubfile(rel.cStr(), filename, 0)
return rel
def mapTextureFilename(self, filename):
""" Returns the filename within the multifile of the
already-added texture. """
filename = Filename(filename)
filename.makeAbsolute(vfs.getCwd())
filename.makeTrueCase()
return self.imported_textures[filename.cStr()]
def addTextFile(self, filename):
""" Adds a generic text file to the multifile. """
rel = self.makeRelFilename(filename)
filename = Filename(filename)
filename.setText()
self.multifile.addSubfile(rel.cStr(), filename, self.compression_level)
def addBinaryFile(self, filename):
""" Adds a generic binary file to the multifile. """
rel = self.makeRelFilename(filename)
filename = Filename(filename)
filename.setBinary()
self.multifile.addSubfile(rel.cStr(), filename, self.compression_level)
def addUncompressibleFile(self, filename):
""" Adds a generic binary file to the multifile, without compression. """
rel = self.makeRelFilename(filename)
filename = Filename(filename)
filename.setBinary()
self.multifile.addSubfile(rel.cStr(), filename, 0)
def makeRelFilename(self, filename):
""" Returns the same filename, relative to self.root """
rel = Filename(filename)
result = rel.makeRelativeTo(self.root, False)
assert result
return rel
def makePackedApp(args):
opts, args = getopt.getopt(args, 'r:m:c:h')
root = '.'
main = None
compilation_mode = AppPacker.compilation_mode
for option, value in opts:
if option == '-r':
root = value
elif option == '-m':
main = value
elif option == '-c':
compilation_mode = value
elif option == '-h':
print __doc__
sys.exit(1)
if not args:
raise ArgumentError, "No destination app specified. Use:\npython MakeAppMF.py app.mf"
multifile_name = args[0]
if len(args) > 1:
raise ArgumentError, "Too many arguments."
p = AppPacker(multifile_name)
p.compilation_mode = compilation_mode
p.scan(root = root, main = main)
if __name__ == '__main__':
try:
makePackedApp(sys.argv[1:])
except ArgumentError, e:
print e.args[0]
sys.exit(1)