2424
2525"""
2626
27- #
28- # XXX Todo:
29- # - modulefinder support to build standalone apps
30- # - consider turning this into a distutils extension
31- #
3227
33- __all__ = ["BundleBuilder" , "AppBuilder" , "buildapp" ]
28+ __all__ = ["BundleBuilder" , "BundleBuilderError" , " AppBuilder" , "buildapp" ]
3429
3530
3631import sys
3732import os , errno , shutil
33+ import imp , marshal
34+ import re
3835from copy import deepcopy
3936import getopt
4037from plistlib import Plist
4138from types import FunctionType as function
4239
4340
41+ class BundleBuilderError (Exception ): pass
42+
43+
4444class Defaults :
4545
4646 """Class attributes that don't start with an underscore and are
@@ -176,6 +176,7 @@ def _copyFiles(self):
176176 else :
177177 self .message ("Copying files" , 1 )
178178 msg = "Copying"
179+ files .sort ()
179180 for src , dst in files :
180181 if os .path .isdir (src ):
181182 self .message ("%s %s/ to %s/" % (msg , src , dst ), 2 )
@@ -200,7 +201,42 @@ def report(self):
200201 pprint .pprint (self .__dict__ )
201202
202203
203- mainWrapperTemplate = """\
204+
205+ if __debug__ :
206+ PYC_EXT = ".pyc"
207+ else :
208+ PYC_EXT = ".pyo"
209+
210+ MAGIC = imp .get_magic ()
211+ USE_FROZEN = hasattr (imp , "set_frozenmodules" )
212+
213+ # For standalone apps, we have our own minimal site.py. We don't need
214+ # all the cruft of the real site.py.
215+ SITE_PY = """\
216+ import sys
217+ del sys.path[1:] # sys.path[0] is Contents/Resources/
218+ """
219+
220+ if USE_FROZEN :
221+ FROZEN_ARCHIVE = "FrozenModules.marshal"
222+ SITE_PY += """\
223+ # bootstrapping
224+ import imp, marshal
225+ f = open(sys.path[0] + "/%s", "rb")
226+ imp.set_frozenmodules(marshal.load(f))
227+ f.close()
228+ """ % FROZEN_ARCHIVE
229+
230+ SITE_CO = compile (SITE_PY , "<-bundlebuilder->" , "exec" )
231+
232+ MAYMISS_MODULES = ['mac' , 'os2' , 'nt' , 'ntpath' , 'dos' , 'dospath' ,
233+ 'win32api' , 'ce' , '_winreg' , 'nturl2path' , 'sitecustomize' ,
234+ 'org.python.core' , 'riscos' , 'riscosenviron' , 'riscospath'
235+ ]
236+
237+ STRIP_EXEC = "/usr/bin/strip"
238+
239+ EXECVE_WRAPPER = """\
204240 #!/usr/bin/env python
205241
206242import os
@@ -211,14 +247,14 @@ def report(self):
211247assert os.path.exists(mainprogram)
212248argv.insert(1, mainprogram)
213249os.environ["PYTHONPATH"] = resources
214- %(setpythonhome)s
215250%(setexecutable)s
216251os.execve(executable, argv, os.environ)
217252"""
218253
219254setExecutableTemplate = """executable = os.path.join(resources, "%s")"""
220255pythonhomeSnippet = """os.environ["home"] = resources"""
221256
257+
222258class AppBuilder (BundleBuilder ):
223259
224260 # A Python main program. If this argument is given, the main
@@ -239,9 +275,41 @@ class AppBuilder(BundleBuilder):
239275 # Symlink the executable instead of copying it.
240276 symlink_exec = 0
241277
278+ # If True, build standalone app.
279+ standalone = 0
280+
281+ # The following attributes are only used when building a standalone app.
282+
283+ # Exclude these modules.
284+ excludeModules = []
285+
286+ # Include these modules.
287+ includeModules = []
288+
289+ # Include these packages.
290+ includePackages = []
291+
292+ # Strip binaries.
293+ strip = 0
294+
295+ # Found C extension modules: [(name, path), ...]
296+ extensions = []
297+
298+ # Found Python modules: [(name, codeobject, ispkg), ...]
299+ pymodules = []
300+
301+ # Modules that modulefinder couldn't find:
302+ missingModules = []
303+
304+ # List of all binaries (executables or shared libs), for stripping purposes
305+ binaries = []
306+
242307 def setup (self ):
308+ if self .standalone and self .mainprogram is None :
309+ raise BundleBuilderError , ("must specify 'mainprogram' when "
310+ "building a standalone application." )
243311 if self .mainprogram is None and self .executable is None :
244- raise TypeError , ("must specify either or both of "
312+ raise BundleBuilderError , ("must specify either or both of "
245313 "'executable' and 'mainprogram'" )
246314
247315 if self .name is not None :
@@ -262,41 +330,201 @@ def setup(self):
262330
263331 self .plist .CFBundleExecutable = self .name
264332
333+ if self .standalone :
334+ if self .executable is None : # *assert* that it is None?
335+ self .executable = sys .executable
336+ self .findDependencies ()
337+
265338 def preProcess (self ):
266- resdir = pathjoin ( "Contents" , " Resources")
339+ resdir = "Contents/ Resources"
267340 if self .executable is not None :
268341 if self .mainprogram is None :
269342 execpath = pathjoin (self .execdir , self .name )
270343 else :
271344 execpath = pathjoin (resdir , os .path .basename (self .executable ))
272345 if not self .symlink_exec :
273346 self .files .append ((self .executable , execpath ))
347+ self .binaries .append (execpath )
274348 self .execpath = execpath
275349 # For execve wrapper
276350 setexecutable = setExecutableTemplate % os .path .basename (self .executable )
277351 else :
278352 setexecutable = "" # XXX for locals() call
279353
280354 if self .mainprogram is not None :
281- setpythonhome = "" # pythonhomeSnippet if we're making a standalone app
282355 mainname = os .path .basename (self .mainprogram )
283356 self .files .append ((self .mainprogram , pathjoin (resdir , mainname )))
284357 # Create execve wrapper
285358 mainprogram = self .mainprogram # XXX for locals() call
286359 execdir = pathjoin (self .bundlepath , self .execdir )
287360 mainwrapperpath = pathjoin (execdir , self .name )
288361 makedirs (execdir )
289- open (mainwrapperpath , "w" ).write (mainWrapperTemplate % locals ())
362+ open (mainwrapperpath , "w" ).write (EXECVE_WRAPPER % locals ())
290363 os .chmod (mainwrapperpath , 0777 )
291364
292365 def postProcess (self ):
366+ self .addPythonModules ()
367+ if self .strip and not self .symlink :
368+ self .stripBinaries ()
369+
293370 if self .symlink_exec and self .executable :
294371 self .message ("Symlinking executable %s to %s" % (self .executable ,
295372 self .execpath ), 2 )
296373 dst = pathjoin (self .bundlepath , self .execpath )
297374 makedirs (os .path .dirname (dst ))
298375 os .symlink (os .path .abspath (self .executable ), dst )
299376
377+ if self .missingModules :
378+ self .reportMissing ()
379+
380+ def addPythonModules (self ):
381+ self .message ("Adding Python modules" , 1 )
382+ pymodules = self .pymodules
383+
384+ if USE_FROZEN :
385+ # This anticipates the acceptance of this patch:
386+ # http://www.python.org/sf/642578
387+ # Create a file containing all modules, frozen.
388+ frozenmodules = []
389+ for name , code , ispkg in pymodules :
390+ if ispkg :
391+ self .message ("Adding Python package %s" % name , 2 )
392+ else :
393+ self .message ("Adding Python module %s" % name , 2 )
394+ frozenmodules .append ((name , marshal .dumps (code ), ispkg ))
395+ frozenmodules = tuple (frozenmodules )
396+ relpath = "Contents/Resources/" + FROZEN_ARCHIVE
397+ abspath = pathjoin (self .bundlepath , relpath )
398+ f = open (abspath , "wb" )
399+ marshal .dump (frozenmodules , f )
400+ f .close ()
401+ # add site.pyc
402+ sitepath = pathjoin (self .bundlepath , "Contents/Resources/site" + PYC_EXT )
403+ writePyc (SITE_CO , sitepath )
404+ else :
405+ # Create individual .pyc files.
406+ for name , code , ispkg in pymodules :
407+ if ispkg :
408+ name += ".__init__"
409+ path = name .split ("." )
410+ path = pathjoin ("Contents/Resources/" , * path ) + PYC_EXT
411+
412+ if ispkg :
413+ self .message ("Adding Python package %s" % path , 2 )
414+ else :
415+ self .message ("Adding Python module %s" % path , 2 )
416+
417+ abspath = pathjoin (self .bundlepath , path )
418+ makedirs (os .path .dirname (abspath ))
419+ writePyc (code , abspath )
420+
421+ def stripBinaries (self ):
422+ if not os .path .exists (STRIP_EXEC ):
423+ self .message ("Error: can't strip binaries: no strip program at "
424+ "%s" % STRIP_EXEC , 0 )
425+ else :
426+ self .message ("Stripping binaries" , 1 )
427+ for relpath in self .binaries :
428+ self .message ("Stripping %s" % relpath , 2 )
429+ abspath = pathjoin (self .bundlepath , relpath )
430+ assert not os .path .islink (abspath )
431+ rv = os .system ("%s -S \" %s\" " % (STRIP_EXEC , abspath ))
432+
433+ def findDependencies (self ):
434+ self .message ("Finding module dependencies" , 1 )
435+ import modulefinder
436+ mf = modulefinder .ModuleFinder (excludes = self .excludeModules )
437+ # manually add our own site.py
438+ site = mf .add_module ("site" )
439+ site .__code__ = SITE_CO
440+ mf .scan_code (SITE_CO , site )
441+
442+ includeModules = self .includeModules [:]
443+ for name in self .includePackages :
444+ includeModules .extend (findPackageContents (name ).keys ())
445+ for name in includeModules :
446+ try :
447+ mf .import_hook (name )
448+ except ImportError :
449+ self .missingModules .append (name )
450+
451+
452+ mf .run_script (self .mainprogram )
453+ modules = mf .modules .items ()
454+ modules .sort ()
455+ for name , mod in modules :
456+ if mod .__file__ and mod .__code__ is None :
457+ # C extension
458+ path = mod .__file__
459+ ext = os .path .splitext (path )[1 ]
460+ if USE_FROZEN : # "proper" freezing
461+ # rename extensions that are submodules of packages to
462+ # <packagename>.<modulename>.<ext>
463+ dstpath = "Contents/Resources/" + name + ext
464+ else :
465+ dstpath = name .split ("." )
466+ dstpath = pathjoin ("Contents/Resources/" , * dstpath ) + ext
467+ self .files .append ((path , dstpath ))
468+ self .extensions .append ((name , path , dstpath ))
469+ self .binaries .append (dstpath )
470+ elif mod .__code__ is not None :
471+ ispkg = mod .__path__ is not None
472+ if not USE_FROZEN or name != "site" :
473+ # Our site.py is doing the bootstrapping, so we must
474+ # include a real .pyc file if USE_FROZEN is True.
475+ self .pymodules .append ((name , mod .__code__ , ispkg ))
476+
477+ self .missingModules .extend (mf .any_missing ())
478+
479+ def reportMissing (self ):
480+ missing = [name for name in self .missingModules
481+ if name not in MAYMISS_MODULES ]
482+ missingsub = [name for name in missing if "." in name ]
483+ missing = [name for name in missing if "." not in name ]
484+ missing .sort ()
485+ missingsub .sort ()
486+ if missing :
487+ self .message ("Warning: couldn't find the following modules:" , 1 )
488+ self .message (" " + ", " .join (missing ))
489+ if missingsub :
490+ self .message ("Warning: couldn't find the following submodules "
491+ "(but it's probably OK since modulefinder can't distinguish "
492+ "between from \" module import submodule\" and "
493+ "\" from module import name\" ):" , 1 )
494+ self .message (" " + ", " .join (missingsub ))
495+
496+ #
497+ # Utilities.
498+ #
499+
500+ SUFFIXES = [_suf for _suf , _mode , _tp in imp .get_suffixes ()]
501+ identifierRE = re .compile (r"[_a-zA-z][_a-zA-Z0-9]*$" )
502+
503+ def findPackageContents (name , searchpath = None ):
504+ head = name .split ("." )[- 1 ]
505+ if identifierRE .match (head ) is None :
506+ return {}
507+ try :
508+ fp , path , (ext , mode , tp ) = imp .find_module (head , searchpath )
509+ except ImportError :
510+ return {}
511+ modules = {name : None }
512+ if tp == imp .PKG_DIRECTORY and path :
513+ files = os .listdir (path )
514+ for sub in files :
515+ sub , ext = os .path .splitext (sub )
516+ fullname = name + "." + sub
517+ if sub != "__init__" and fullname not in modules :
518+ modules .update (findPackageContents (fullname , [path ]))
519+ return modules
520+
521+ def writePyc (code , path ):
522+ f = open (path , "wb" )
523+ f .write ("\0 " * 8 ) # don't bother about a time stamp
524+ marshal .dump (code , f )
525+ f .seek (0 , 0 )
526+ f .write (MAGIC )
527+ f .close ()
300528
301529def copy (src , dst , mkdirs = 0 ):
302530 """Copy a file or a directory."""
@@ -355,6 +583,12 @@ def pathjoin(*args):
355583 -c, --creator=CCCC 4-char creator code (default: '????')
356584 -l, --link symlink files/folder instead of copying them
357585 --link-exec symlink the executable instead of copying it
586+ --standalone build a standalone application, which is fully
587+ independent of a Python installation
588+ -x, --exclude=MODULE exclude module (with --standalone)
589+ -i, --include=MODULE include module (with --standalone)
590+ --package=PACKAGE include a whole package (with --standalone)
591+ --strip strip binaries (remove debug info)
358592 -v, --verbose increase verbosity level
359593 -q, --quiet decrease verbosity level
360594 -h, --help print this message
@@ -370,10 +604,11 @@ def main(builder=None):
370604 if builder is None :
371605 builder = AppBuilder (verbosity = 1 )
372606
373- shortopts = "b:n:r:e:m:c:p:lhvq "
607+ shortopts = "b:n:r:e:m:c:p:lx:i:hvq "
374608 longopts = ("builddir=" , "name=" , "resource=" , "executable=" ,
375609 "mainprogram=" , "creator=" , "nib=" , "plist=" , "link" ,
376- "link-exec" , "help" , "verbose" , "quiet" )
610+ "link-exec" , "help" , "verbose" , "quiet" , "standalone" ,
611+ "exclude=" , "include=" , "package=" , "strip" )
377612
378613 try :
379614 options , args = getopt .getopt (sys .argv [1 :], shortopts , longopts )
@@ -407,6 +642,16 @@ def main(builder=None):
407642 builder .verbosity += 1
408643 elif opt in ('-q' , '--quiet' ):
409644 builder .verbosity -= 1
645+ elif opt == '--standalone' :
646+ builder .standalone = 1
647+ elif opt in ('-x' , '--exclude' ):
648+ builder .excludeModules .append (arg )
649+ elif opt in ('-i' , '--include' ):
650+ builder .includeModules .append (arg )
651+ elif opt == '--package' :
652+ builder .includePackages .append (arg )
653+ elif opt == '--strip' :
654+ builder .strip = 1
410655
411656 if len (args ) != 1 :
412657 usage ("Must specify one command ('build', 'report' or 'help')" )
0 commit comments