MakeAppMF.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. """
  2. This module will pack a Panda application, consisting of a
  3. directory tree of .py files and models, into a multifile for
  4. distribution and running with RunAppMF.py. To run it, use:
  5. python MakeAppMF.py [opts] app.mf
  6. Options:
  7. -r application_root
  8. Specify the root directory of the application source; this is a
  9. directory tree that contains all of your .py files and models.
  10. If this is omitted, the default is the current directory.
  11. -m main.py
  12. Names the Python file that begins the application. This should
  13. be a file within the root directory. If this is omitted, the
  14. default is a file named "main.py", or if there is only one Python
  15. file present, it is used. If this file contains a function
  16. called main(), that function will be called after importing it
  17. (this is preferable to having the module start itself immediately
  18. upon importing).
  19. -c [py,pyc,pyo]
  20. Specifies the compilation mode of python files. 'py' means to
  21. leave them as source files, 'pyc' and 'pyo' are equivalent, and
  22. mean to compile to byte code. pyc files will be written if the
  23. interpreter is running in normal debug mode, while pyo files will
  24. be written if it is running in optimize mode (-O or -OO).
  25. """
  26. import sys
  27. import getopt
  28. import imp
  29. import marshal
  30. import direct
  31. from direct.stdpy.file import open
  32. from direct.showbase import Loader
  33. from pandac.PandaModules import *
  34. vfs = VirtualFileSystem.getGlobalPtr()
  35. class ArgumentError(AttributeError):
  36. pass
  37. class AppPacker:
  38. compression_level = 6
  39. imported_maps = 'imported_maps'
  40. # Text files that are copied (and compressed) to the multifile
  41. # without processing.
  42. text_extensions = [ 'prc' ]
  43. # Binary files that are copied and compressed without processing.
  44. binary_extensions = [ 'ttf', 'wav', 'mid' ]
  45. # Binary files that are considered uncompressible, and are copied
  46. # without compression.
  47. uncompressible_extensions = [ 'mp3' ]
  48. # Specifies how or if python files are compiled.
  49. compilation_mode = 'pyc'
  50. def __init__(self, multifile_name):
  51. # Make sure any pre-existing file is removed.
  52. Filename(multifile_name).unlink()
  53. self.multifile = Multifile()
  54. if not self.multifile.openReadWrite(multifile_name):
  55. raise ArgumentError, 'Could not open %s for writing' % (multifile_name)
  56. self.imported_textures = {}
  57. # Get the list of filename extensions that are recognized as
  58. # image files.
  59. self.image_extensions = []
  60. for type in PNMFileTypeRegistry.getGlobalPtr().getTypes():
  61. self.image_extensions += type.getExtensions()
  62. self.loader = Loader.Loader(self)
  63. def scan(self, root, main):
  64. if self.compilation_mode != 'py':
  65. if __debug__:
  66. self.compilation_mode = 'pyc'
  67. else:
  68. self.compilation_mode = 'pyo'
  69. self.root = Filename(root)
  70. self.root.makeAbsolute(vfs.getCwd())
  71. # Check if there is just one .py file.
  72. pyFiles = self.findPyFiles(self.root)
  73. if main == None:
  74. if len(pyFiles) == 1:
  75. main = pyFiles[0]
  76. else:
  77. main = 'main.py'
  78. if main not in pyFiles:
  79. raise StandardError, 'No file %s in root directory.' % (main)
  80. self.main = Filename(self.root, main)
  81. self._recurse(self.root)
  82. self.multifile.repack()
  83. def findPyFiles(self, dirname):
  84. """ Returns a list of Python filenames at the root directory
  85. level. """
  86. dirList = vfs.scanDirectory(dirname)
  87. pyFiles = []
  88. for file in dirList:
  89. if file.getFilename().getExtension() == 'py':
  90. pyFiles.append(file.getFilename().getBasename())
  91. return pyFiles
  92. def _recurse(self, filename):
  93. dirList = vfs.scanDirectory(filename)
  94. if dirList:
  95. # It's a directory name. Recurse.
  96. for subfile in dirList:
  97. self._recurse(subfile.getFilename())
  98. return
  99. # It's a real file. Is it something we care about?
  100. ext = filename.getExtension().lower()
  101. outFilename = filename
  102. if ext == 'pz':
  103. # Strip off an implicit .pz extension.
  104. outFilename = Filename(filename)
  105. outFilename.setExtension('')
  106. outFilename = Filename(outFilename.cStr())
  107. ext = outFilename.getExtension().lower()
  108. if ext == 'py':
  109. self.addPyFile(filename)
  110. elif ext == 'egg':
  111. self.addEggFile(filename, outFilename)
  112. elif ext == 'bam':
  113. self.addBamFile(filename, outFilename)
  114. elif ext in self.image_extensions:
  115. self.addTexture(filename)
  116. elif ext in self.text_extensions:
  117. self.addTextFile(filename)
  118. elif ext in self.binary_extensions:
  119. self.addBinaryFile(filename)
  120. elif ext in self.uncompressible_extensions:
  121. self.addUncompressibleFile(filename)
  122. def addPyFile(self, filename):
  123. targetFilename = self.makeRelFilename(filename)
  124. if filename == self.main:
  125. # This one is the "main.py"; the starter file.
  126. targetFilename = Filename('main.py')
  127. if self.compilation_mode == 'py':
  128. # Add python files as source files.
  129. self.multifile.addSubfile(targetFilename.cStr(), filename, self.compression_level)
  130. elif self.compilation_mode == 'pyc' or self.compilation_mode == 'pyo':
  131. # Compile it to bytecode.
  132. targetFilename.setExtension(self.compilation_mode)
  133. source = open(filename, 'r').read()
  134. if source and source[-1] != '\n':
  135. source = source + '\n'
  136. code = compile(source, targetFilename.cStr(), 'exec')
  137. data = imp.get_magic() + '\0\0\0\0' + marshal.dumps(code)
  138. stream = StringStream(data)
  139. self.multifile.addSubfile(targetFilename.cStr(), stream, self.compression_level)
  140. self.multifile.flush()
  141. else:
  142. raise StandardError, 'Unsupported compilation mode %s' % (self.compilation_mode)
  143. def addEggFile(self, filename, outFilename):
  144. # Precompile egg files to bam's.
  145. np = self.loader.loadModel(filename, okMissing = True)
  146. if np.isEmpty():
  147. raise StandardError, 'Could not read egg file %s' % (filename)
  148. self.addNode(np.node(), outFilename)
  149. def addBamFile(self, filename, outFilename):
  150. # Load the bam file so we can massage its textures.
  151. bamFile = BamFile()
  152. if not bamFile.openRead(filename):
  153. raise StandardError, 'Could not read bam file %s' % (filename)
  154. if not bamFile.resolve():
  155. raise StandardError, 'Could not resolve bam file %s' % (filename)
  156. node = bamFile.readNode()
  157. if not node:
  158. raise StandardError, 'Not a model file: %s' % (filename)
  159. self.addNode(node, outFilename)
  160. def addNode(self, node, filename):
  161. """ Converts the indicated node to a bam stream, and adds the
  162. bam file to the multifile under the indicated filename. """
  163. # Be sure to import all of the referenced textures, and tell
  164. # them their new location within the multifile.
  165. for tex in NodePath(node).findAllTextures():
  166. if not tex.hasFullpath() and tex.hasRamImage():
  167. # We need to store this texture as a raw-data image.
  168. # Clear the filename so this will happen
  169. # automatically.
  170. tex.clearFilename()
  171. tex.clearAlphaFilename()
  172. else:
  173. # We can store this texture as a file reference to its
  174. # image. Copy the file into our multifile, and rename
  175. # its reference in the texture.
  176. if tex.hasFilename():
  177. tex.setFilename(self.addTexture(tex.getFullpath()))
  178. if tex.hasAlphaFilename():
  179. tex.setAlphaFilename(self.addTexture(tex.getAlphaFullpath()))
  180. # Now generate an in-memory bam file. Tell the bam writer to
  181. # keep the textures referenced by their in-multifile path.
  182. bamFile = BamFile()
  183. stream = StringStream()
  184. bamFile.openWrite(stream)
  185. bamFile.getWriter().setFileTextureMode(bamFile.BTMUnchanged)
  186. bamFile.writeObject(node)
  187. bamFile.close()
  188. # Clean the node out of memory.
  189. node.removeAllChildren()
  190. # Now we have an in-memory bam file.
  191. rel = self.makeRelFilename(filename)
  192. rel.setExtension('bam')
  193. stream.seekg(0)
  194. self.multifile.addSubfile(rel.cStr(), stream, self.compression_level)
  195. # Flush it so the data gets written to disk immediately, so we
  196. # don't have to keep it around in ram.
  197. self.multifile.flush()
  198. def addTexture(self, filename):
  199. """ Adds the texture to the multifile, if it has not already
  200. been added. If it is not within the root directory, copies it
  201. in (virtually) into a directory within the multifile named
  202. imported_maps. Returns the new filename within the
  203. multifile. """
  204. assert not filename.isLocal()
  205. filename = Filename(filename)
  206. filename.makeAbsolute(vfs.getCwd())
  207. filename.makeTrueCase()
  208. rel = self.imported_textures.get(filename.cStr())
  209. if not rel:
  210. rel = Filename(filename)
  211. if not rel.makeRelativeTo(self.root, False):
  212. # Not within the multifile.
  213. rel = Filename(self.imported_maps, filename.getBasename())
  214. # Need to add it now.
  215. self.imported_textures[filename.cStr()] = rel
  216. filename = Filename(filename)
  217. filename.setBinary()
  218. self.multifile.addSubfile(rel.cStr(), filename, 0)
  219. return rel
  220. def mapTextureFilename(self, filename):
  221. """ Returns the filename within the multifile of the
  222. already-added texture. """
  223. filename = Filename(filename)
  224. filename.makeAbsolute(vfs.getCwd())
  225. filename.makeTrueCase()
  226. return self.imported_textures[filename.cStr()]
  227. def addTextFile(self, filename):
  228. """ Adds a generic text file to the multifile. """
  229. rel = self.makeRelFilename(filename)
  230. filename = Filename(filename)
  231. filename.setText()
  232. self.multifile.addSubfile(rel.cStr(), filename, self.compression_level)
  233. def addBinaryFile(self, filename):
  234. """ Adds a generic binary file to the multifile. """
  235. rel = self.makeRelFilename(filename)
  236. filename = Filename(filename)
  237. filename.setBinary()
  238. self.multifile.addSubfile(rel.cStr(), filename, self.compression_level)
  239. def addUncompressibleFile(self, filename):
  240. """ Adds a generic binary file to the multifile, without compression. """
  241. rel = self.makeRelFilename(filename)
  242. filename = Filename(filename)
  243. filename.setBinary()
  244. self.multifile.addSubfile(rel.cStr(), filename, 0)
  245. def makeRelFilename(self, filename):
  246. """ Returns the same filename, relative to self.root """
  247. rel = Filename(filename)
  248. result = rel.makeRelativeTo(self.root, False)
  249. assert result
  250. return rel
  251. def makePackedApp(args):
  252. opts, args = getopt.getopt(args, 'r:m:c:h')
  253. root = '.'
  254. main = None
  255. compilation_mode = AppPacker.compilation_mode
  256. for option, value in opts:
  257. if option == '-r':
  258. root = value
  259. elif option == '-m':
  260. main = value
  261. elif option == '-c':
  262. compilation_mode = value
  263. elif option == '-h':
  264. print __doc__
  265. sys.exit(1)
  266. if not args:
  267. raise ArgumentError, "No destination app specified. Use:\npython MakeAppMF.py app.mf"
  268. multifile_name = args[0]
  269. if len(args) > 1:
  270. raise ArgumentError, "Too many arguments."
  271. p = AppPacker(multifile_name)
  272. p.compilation_mode = compilation_mode
  273. p.scan(root = root, main = main)
  274. if __name__ == '__main__':
  275. try:
  276. makePackedApp(sys.argv[1:])
  277. except ArgumentError, e:
  278. print e.args[0]
  279. sys.exit(1)