Home | History | Annotate | Download | only in plat-mac
      1 #! /usr/bin/env python
      2 
      3 """\
      4 bundlebuilder.py -- Tools to assemble MacOS X (application) bundles.
      5 
      6 This module contains two classes to build so called "bundles" for
      7 MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
      8 specialized in building application bundles.
      9 
     10 [Bundle|App]Builder objects are instantiated with a bunch of keyword
     11 arguments, and have a build() method that will do all the work. See
     12 the class doc strings for a description of the constructor arguments.
     13 
     14 The module contains a main program that can be used in two ways:
     15 
     16   % python bundlebuilder.py [options] build
     17   % python buildapp.py [options] build
     18 
     19 Where "buildapp.py" is a user-supplied setup.py-like script following
     20 this model:
     21 
     22   from bundlebuilder import buildapp
     23   buildapp(<lots-of-keyword-args>)
     24 
     25 """
     26 
     27 
     28 __all__ = ["BundleBuilder", "BundleBuilderError", "AppBuilder", "buildapp"]
     29 
     30 
     31 from warnings import warnpy3k
     32 warnpy3k("In 3.x, the bundlebuilder module is removed.", stacklevel=2)
     33 
     34 import sys
     35 import os, errno, shutil
     36 import imp, marshal
     37 import re
     38 from copy import deepcopy
     39 import getopt
     40 from plistlib import Plist
     41 from types import FunctionType as function
     42 
     43 class BundleBuilderError(Exception): pass
     44 
     45 
     46 class Defaults:
     47 
     48     """Class attributes that don't start with an underscore and are
     49     not functions or classmethods are (deep)copied to self.__dict__.
     50     This allows for mutable default values.
     51     """
     52 
     53     def __init__(self, **kwargs):
     54         defaults = self._getDefaults()
     55         defaults.update(kwargs)
     56         self.__dict__.update(defaults)
     57 
     58     def _getDefaults(cls):
     59         defaults = {}
     60         for base in cls.__bases__:
     61             if hasattr(base, "_getDefaults"):
     62                 defaults.update(base._getDefaults())
     63         for name, value in cls.__dict__.items():
     64             if name[0] != "_" and not isinstance(value,
     65                     (function, classmethod)):
     66                 defaults[name] = deepcopy(value)
     67         return defaults
     68     _getDefaults = classmethod(_getDefaults)
     69 
     70 
     71 class BundleBuilder(Defaults):
     72 
     73     """BundleBuilder is a barebones class for assembling bundles. It
     74     knows nothing about executables or icons, it only copies files
     75     and creates the PkgInfo and Info.plist files.
     76     """
     77 
     78     # (Note that Defaults.__init__ (deep)copies these values to
     79     # instance variables. Mutable defaults are therefore safe.)
     80 
     81     # Name of the bundle, with or without extension.
     82     name = None
     83 
     84     # The property list ("plist")
     85     plist = Plist(CFBundleDevelopmentRegion = "English",
     86                   CFBundleInfoDictionaryVersion = "6.0")
     87 
     88     # The type of the bundle.
     89     type = "BNDL"
     90     # The creator code of the bundle.
     91     creator = None
     92 
     93     # the CFBundleIdentifier (this is used for the preferences file name)
     94     bundle_id = None
     95 
     96     # List of files that have to be copied to <bundle>/Contents/Resources.
     97     resources = []
     98 
     99     # List of (src, dest) tuples; dest should be a path relative to the bundle
    100     # (eg. "Contents/Resources/MyStuff/SomeFile.ext).
    101     files = []
    102 
    103     # List of shared libraries (dylibs, Frameworks) to bundle with the app
    104     # will be placed in Contents/Frameworks
    105     libs = []
    106 
    107     # Directory where the bundle will be assembled.
    108     builddir = "build"
    109 
    110     # Make symlinks instead copying files. This is handy during debugging, but
    111     # makes the bundle non-distributable.
    112     symlink = 0
    113 
    114     # Verbosity level.
    115     verbosity = 1
    116 
    117     # Destination root directory
    118     destroot = ""
    119 
    120     def setup(self):
    121         # XXX rethink self.name munging, this is brittle.
    122         self.name, ext = os.path.splitext(self.name)
    123         if not ext:
    124             ext = ".bundle"
    125         bundleextension = ext
    126         # misc (derived) attributes
    127         self.bundlepath = pathjoin(self.builddir, self.name + bundleextension)
    128 
    129         plist = self.plist
    130         plist.CFBundleName = self.name
    131         plist.CFBundlePackageType = self.type
    132         if self.creator is None:
    133             if hasattr(plist, "CFBundleSignature"):
    134                 self.creator = plist.CFBundleSignature
    135             else:
    136                 self.creator = "????"
    137         plist.CFBundleSignature = self.creator
    138         if self.bundle_id:
    139             plist.CFBundleIdentifier = self.bundle_id
    140         elif not hasattr(plist, "CFBundleIdentifier"):
    141             plist.CFBundleIdentifier = self.name
    142 
    143     def build(self):
    144         """Build the bundle."""
    145         builddir = self.builddir
    146         if builddir and not os.path.exists(builddir):
    147             os.mkdir(builddir)
    148         self.message("Building %s" % repr(self.bundlepath), 1)
    149         if os.path.exists(self.bundlepath):
    150             shutil.rmtree(self.bundlepath)
    151         if os.path.exists(self.bundlepath + '~'):
    152             shutil.rmtree(self.bundlepath + '~')
    153         bp = self.bundlepath
    154 
    155         # Create the app bundle in a temporary location and then
    156         # rename the completed bundle. This way the Finder will
    157         # never see an incomplete bundle (where it might pick up
    158         # and cache the wrong meta data)
    159         self.bundlepath = bp + '~'
    160         try:
    161             os.mkdir(self.bundlepath)
    162             self.preProcess()
    163             self._copyFiles()
    164             self._addMetaFiles()
    165             self.postProcess()
    166             os.rename(self.bundlepath, bp)
    167         finally:
    168             self.bundlepath = bp
    169         self.message("Done.", 1)
    170 
    171     def preProcess(self):
    172         """Hook for subclasses."""
    173         pass
    174     def postProcess(self):
    175         """Hook for subclasses."""
    176         pass
    177 
    178     def _addMetaFiles(self):
    179         contents = pathjoin(self.bundlepath, "Contents")
    180         makedirs(contents)
    181         #
    182         # Write Contents/PkgInfo
    183         assert len(self.type) == len(self.creator) == 4, \
    184                 "type and creator must be 4-byte strings."
    185         pkginfo = pathjoin(contents, "PkgInfo")
    186         f = open(pkginfo, "wb")
    187         f.write(self.type + self.creator)
    188         f.close()
    189         #
    190         # Write Contents/Info.plist
    191         infoplist = pathjoin(contents, "Info.plist")
    192         self.plist.write(infoplist)
    193 
    194     def _copyFiles(self):
    195         files = self.files[:]
    196         for path in self.resources:
    197             files.append((path, pathjoin("Contents", "Resources",
    198                 os.path.basename(path))))
    199         for path in self.libs:
    200             files.append((path, pathjoin("Contents", "Frameworks",
    201                 os.path.basename(path))))
    202         if self.symlink:
    203             self.message("Making symbolic links", 1)
    204             msg = "Making symlink from"
    205         else:
    206             self.message("Copying files", 1)
    207             msg = "Copying"
    208         files.sort()
    209         for src, dst in files:
    210             if os.path.isdir(src):
    211                 self.message("%s %s/ to %s/" % (msg, src, dst), 2)
    212             else:
    213                 self.message("%s %s to %s" % (msg, src, dst), 2)
    214             dst = pathjoin(self.bundlepath, dst)
    215             if self.symlink:
    216                 symlink(src, dst, mkdirs=1)
    217             else:
    218                 copy(src, dst, mkdirs=1)
    219 
    220     def message(self, msg, level=0):
    221         if level <= self.verbosity:
    222             indent = ""
    223             if level > 1:
    224                 indent = (level - 1) * "  "
    225             sys.stderr.write(indent + msg + "\n")
    226 
    227     def report(self):
    228         # XXX something decent
    229         pass
    230 
    231 
    232 if __debug__:
    233     PYC_EXT = ".pyc"
    234 else:
    235     PYC_EXT = ".pyo"
    236 
    237 MAGIC = imp.get_magic()
    238 USE_ZIPIMPORT = "zipimport" in sys.builtin_module_names
    239 
    240 # For standalone apps, we have our own minimal site.py. We don't need
    241 # all the cruft of the real site.py.
    242 SITE_PY = """\
    243 import sys
    244 if not %(semi_standalone)s:
    245     del sys.path[1:]  # sys.path[0] is Contents/Resources/
    246 """
    247 
    248 ZIP_ARCHIVE = "Modules.zip"
    249 SITE_PY_ZIP = SITE_PY + ("sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE)
    250 
    251 def getPycData(fullname, code, ispkg):
    252     if ispkg:
    253         fullname += ".__init__"
    254     path = fullname.replace(".", os.sep) + PYC_EXT
    255     return path, MAGIC + '\0\0\0\0' + marshal.dumps(code)
    256 
    257 #
    258 # Extension modules can't be in the modules zip archive, so a placeholder
    259 # is added instead, that loads the extension from a specified location.
    260 #
    261 EXT_LOADER = """\
    262 def __load():
    263     import imp, sys, os
    264     for p in sys.path:
    265         path = os.path.join(p, "%(filename)s")
    266         if os.path.exists(path):
    267             break
    268     else:
    269         assert 0, "file not found: %(filename)s"
    270     mod = imp.load_dynamic("%(name)s", path)
    271 
    272 __load()
    273 del __load
    274 """
    275 
    276 MAYMISS_MODULES = ['os2', 'nt', 'ntpath', 'dos', 'dospath',
    277     'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
    278     'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
    279 ]
    280 
    281 STRIP_EXEC = "/usr/bin/strip"
    282 
    283 #
    284 # We're using a stock interpreter to run the app, yet we need
    285 # a way to pass the Python main program to the interpreter. The
    286 # bootstrapping script fires up the interpreter with the right
    287 # arguments. os.execve() is used as OSX doesn't like us to
    288 # start a real new process. Also, the executable name must match
    289 # the CFBundleExecutable value in the Info.plist, so we lie
    290 # deliberately with argv[0]. The actual Python executable is
    291 # passed in an environment variable so we can "repair"
    292 # sys.executable later.
    293 #
    294 BOOTSTRAP_SCRIPT = """\
    295 #!%(hashbang)s
    296 
    297 import sys, os
    298 execdir = os.path.dirname(sys.argv[0])
    299 executable = os.path.join(execdir, "%(executable)s")
    300 resdir = os.path.join(os.path.dirname(execdir), "Resources")
    301 libdir = os.path.join(os.path.dirname(execdir), "Frameworks")
    302 mainprogram = os.path.join(resdir, "%(mainprogram)s")
    303 
    304 if %(optimize)s:
    305     sys.argv.insert(1, '-O')
    306 
    307 sys.argv.insert(1, mainprogram)
    308 if %(standalone)s or %(semi_standalone)s:
    309     os.environ["PYTHONPATH"] = resdir
    310     if %(standalone)s:
    311         os.environ["PYTHONHOME"] = resdir
    312 else:
    313     pypath = os.getenv("PYTHONPATH", "")
    314     if pypath:
    315         pypath = ":" + pypath
    316     os.environ["PYTHONPATH"] = resdir + pypath
    317 
    318 os.environ["PYTHONEXECUTABLE"] = executable
    319 os.environ["DYLD_LIBRARY_PATH"] = libdir
    320 os.environ["DYLD_FRAMEWORK_PATH"] = libdir
    321 os.execve(executable, sys.argv, os.environ)
    322 """
    323 
    324 
    325 #
    326 # Optional wrapper that converts "dropped files" into sys.argv values.
    327 #
    328 ARGV_EMULATOR = """\
    329 import argvemulator, os
    330 
    331 argvemulator.ArgvCollector().mainloop()
    332 execfile(os.path.join(os.path.split(__file__)[0], "%(realmainprogram)s"))
    333 """
    334 
    335 #
    336 # When building a standalone app with Python.framework, we need to copy
    337 # a subset from Python.framework to the bundle. The following list
    338 # specifies exactly what items we'll copy.
    339 #
    340 PYTHONFRAMEWORKGOODIES = [
    341     "Python",  # the Python core library
    342     "Resources/English.lproj",
    343     "Resources/Info.plist",
    344 ]
    345 
    346 def isFramework():
    347     return sys.exec_prefix.find("Python.framework") > 0
    348 
    349 
    350 LIB = os.path.join(sys.prefix, "lib", "python" + sys.version[:3])
    351 SITE_PACKAGES = os.path.join(LIB, "site-packages")
    352 
    353 
    354 class AppBuilder(BundleBuilder):
    355 
    356     use_zipimport = USE_ZIPIMPORT
    357 
    358     # Override type of the bundle.
    359     type = "APPL"
    360 
    361     # platform, name of the subfolder of Contents that contains the executable.
    362     platform = "MacOS"
    363 
    364     # A Python main program. If this argument is given, the main
    365     # executable in the bundle will be a small wrapper that invokes
    366     # the main program. (XXX Discuss why.)
    367     mainprogram = None
    368 
    369     # The main executable. If a Python main program is specified
    370     # the executable will be copied to Resources and be invoked
    371     # by the wrapper program mentioned above. Otherwise it will
    372     # simply be used as the main executable.
    373     executable = None
    374 
    375     # The name of the main nib, for Cocoa apps. *Must* be specified
    376     # when building a Cocoa app.
    377     nibname = None
    378 
    379     # The name of the icon file to be copied to Resources and used for
    380     # the Finder icon.
    381     iconfile = None
    382 
    383     # Symlink the executable instead of copying it.
    384     symlink_exec = 0
    385 
    386     # If True, build standalone app.
    387     standalone = 0
    388 
    389     # If True, build semi-standalone app (only includes third-party modules).
    390     semi_standalone = 0
    391 
    392     # If set, use this for #! lines in stead of sys.executable
    393     python = None
    394 
    395     # If True, add a real main program that emulates sys.argv before calling
    396     # mainprogram
    397     argv_emulation = 0
    398 
    399     # The following attributes are only used when building a standalone app.
    400 
    401     # Exclude these modules.
    402     excludeModules = []
    403 
    404     # Include these modules.
    405     includeModules = []
    406 
    407     # Include these packages.
    408     includePackages = []
    409 
    410     # Strip binaries from debug info.
    411     strip = 0
    412 
    413     # Found Python modules: [(name, codeobject, ispkg), ...]
    414     pymodules = []
    415 
    416     # Modules that modulefinder couldn't find:
    417     missingModules = []
    418     maybeMissingModules = []
    419 
    420     def setup(self):
    421         if ((self.standalone or self.semi_standalone)
    422             and self.mainprogram is None):
    423             raise BundleBuilderError, ("must specify 'mainprogram' when "
    424                     "building a standalone application.")
    425         if self.mainprogram is None and self.executable is None:
    426             raise BundleBuilderError, ("must specify either or both of "
    427                     "'executable' and 'mainprogram'")
    428 
    429         self.execdir = pathjoin("Contents", self.platform)
    430 
    431         if self.name is not None:
    432             pass
    433         elif self.mainprogram is not None:
    434             self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
    435         elif self.executable is not None:
    436             self.name = os.path.splitext(os.path.basename(self.executable))[0]
    437         if self.name[-4:] != ".app":
    438             self.name += ".app"
    439 
    440         if self.executable is None:
    441             if not self.standalone and not isFramework():
    442                 self.symlink_exec = 1
    443             if self.python:
    444                 self.executable = self.python
    445             else:
    446                 self.executable = sys.executable
    447 
    448         if self.nibname:
    449             self.plist.NSMainNibFile = self.nibname
    450             if not hasattr(self.plist, "NSPrincipalClass"):
    451                 self.plist.NSPrincipalClass = "NSApplication"
    452 
    453         if self.standalone and isFramework():
    454             self.addPythonFramework()
    455 
    456         BundleBuilder.setup(self)
    457 
    458         self.plist.CFBundleExecutable = self.name
    459 
    460         if self.standalone or self.semi_standalone:
    461             self.findDependencies()
    462 
    463     def preProcess(self):
    464         resdir = "Contents/Resources"
    465         if self.executable is not None:
    466             if self.mainprogram is None:
    467                 execname = self.name
    468             else:
    469                 execname = os.path.basename(self.executable)
    470             execpath = pathjoin(self.execdir, execname)
    471             if not self.symlink_exec:
    472                 self.files.append((self.destroot + self.executable, execpath))
    473             self.execpath = execpath
    474 
    475         if self.mainprogram is not None:
    476             mainprogram = os.path.basename(self.mainprogram)
    477             self.files.append((self.mainprogram, pathjoin(resdir, mainprogram)))
    478             if self.argv_emulation:
    479                 # Change the main program, and create the helper main program (which
    480                 # does argv collection and then calls the real main).
    481                 # Also update the included modules (if we're creating a standalone
    482                 # program) and the plist
    483                 realmainprogram = mainprogram
    484                 mainprogram = '__argvemulator_' + mainprogram
    485                 resdirpath = pathjoin(self.bundlepath, resdir)
    486                 mainprogrampath = pathjoin(resdirpath, mainprogram)
    487                 makedirs(resdirpath)
    488                 open(mainprogrampath, "w").write(ARGV_EMULATOR % locals())
    489                 if self.standalone or self.semi_standalone:
    490                     self.includeModules.append("argvemulator")
    491                     self.includeModules.append("os")
    492                 if "CFBundleDocumentTypes" not in self.plist:
    493                     self.plist["CFBundleDocumentTypes"] = [
    494                         { "CFBundleTypeOSTypes" : [
    495                             "****",
    496                             "fold",
    497                             "disk"],
    498                           "CFBundleTypeRole": "Viewer"}]
    499             # Write bootstrap script
    500             executable = os.path.basename(self.executable)
    501             execdir = pathjoin(self.bundlepath, self.execdir)
    502             bootstrappath = pathjoin(execdir, self.name)
    503             makedirs(execdir)
    504             if self.standalone or self.semi_standalone:
    505                 # XXX we're screwed when the end user has deleted
    506                 # /usr/bin/python
    507                 hashbang = "/usr/bin/python"
    508             elif self.python:
    509                 hashbang = self.python
    510             else:
    511                 hashbang = os.path.realpath(sys.executable)
    512             standalone = self.standalone
    513             semi_standalone = self.semi_standalone
    514             optimize = sys.flags.optimize
    515             open(bootstrappath, "w").write(BOOTSTRAP_SCRIPT % locals())
    516             os.chmod(bootstrappath, 0775)
    517 
    518         if self.iconfile is not None:
    519             iconbase = os.path.basename(self.iconfile)
    520             self.plist.CFBundleIconFile = iconbase
    521             self.files.append((self.iconfile, pathjoin(resdir, iconbase)))
    522 
    523     def postProcess(self):
    524         if self.standalone or self.semi_standalone:
    525             self.addPythonModules()
    526         if self.strip and not self.symlink:
    527             self.stripBinaries()
    528 
    529         if self.symlink_exec and self.executable:
    530             self.message("Symlinking executable %s to %s" % (self.executable,
    531                     self.execpath), 2)
    532             dst = pathjoin(self.bundlepath, self.execpath)
    533             makedirs(os.path.dirname(dst))
    534             os.symlink(os.path.abspath(self.executable), dst)
    535 
    536         if self.missingModules or self.maybeMissingModules:
    537             self.reportMissing()
    538 
    539     def addPythonFramework(self):
    540         # If we're building a standalone app with Python.framework,
    541         # include a minimal subset of Python.framework, *unless*
    542         # Python.framework was specified manually in self.libs.
    543         for lib in self.libs:
    544             if os.path.basename(lib) == "Python.framework":
    545                 # a Python.framework was specified as a library
    546                 return
    547 
    548         frameworkpath = sys.exec_prefix[:sys.exec_prefix.find(
    549             "Python.framework") + len("Python.framework")]
    550 
    551         version = sys.version[:3]
    552         frameworkpath = pathjoin(frameworkpath, "Versions", version)
    553         destbase = pathjoin("Contents", "Frameworks", "Python.framework",
    554                             "Versions", version)
    555         for item in PYTHONFRAMEWORKGOODIES:
    556             src = pathjoin(frameworkpath, item)
    557             dst = pathjoin(destbase, item)
    558             self.files.append((src, dst))
    559 
    560     def _getSiteCode(self):
    561         if self.use_zipimport:
    562             return compile(SITE_PY % {"semi_standalone": self.semi_standalone},
    563                      "<-bundlebuilder.py->", "exec")
    564 
    565     def addPythonModules(self):
    566         self.message("Adding Python modules", 1)
    567 
    568         if self.use_zipimport:
    569             # Create a zip file containing all modules as pyc.
    570             import zipfile
    571             relpath = pathjoin("Contents", "Resources", ZIP_ARCHIVE)
    572             abspath = pathjoin(self.bundlepath, relpath)
    573             zf = zipfile.ZipFile(abspath, "w", zipfile.ZIP_DEFLATED)
    574             for name, code, ispkg in self.pymodules:
    575                 self.message("Adding Python module %s" % name, 2)
    576                 path, pyc = getPycData(name, code, ispkg)
    577                 zf.writestr(path, pyc)
    578             zf.close()
    579             # add site.pyc
    580             sitepath = pathjoin(self.bundlepath, "Contents", "Resources",
    581                     "site" + PYC_EXT)
    582             writePyc(self._getSiteCode(), sitepath)
    583         else:
    584             # Create individual .pyc files.
    585             for name, code, ispkg in self.pymodules:
    586                 if ispkg:
    587                     name += ".__init__"
    588                 path = name.split(".")
    589                 path = pathjoin("Contents", "Resources", *path) + PYC_EXT
    590 
    591                 if ispkg:
    592                     self.message("Adding Python package %s" % path, 2)
    593                 else:
    594                     self.message("Adding Python module %s" % path, 2)
    595 
    596                 abspath = pathjoin(self.bundlepath, path)
    597                 makedirs(os.path.dirname(abspath))
    598                 writePyc(code, abspath)
    599 
    600     def stripBinaries(self):
    601         if not os.path.exists(STRIP_EXEC):
    602             self.message("Error: can't strip binaries: no strip program at "
    603                 "%s" % STRIP_EXEC, 0)
    604         else:
    605             import stat
    606             self.message("Stripping binaries", 1)
    607             def walk(top):
    608                 for name in os.listdir(top):
    609                     path = pathjoin(top, name)
    610                     if os.path.islink(path):
    611                         continue
    612                     if os.path.isdir(path):
    613                         walk(path)
    614                     else:
    615                         mod = os.stat(path)[stat.ST_MODE]
    616                         if not (mod & 0100):
    617                             continue
    618                         relpath = path[len(self.bundlepath):]
    619                         self.message("Stripping %s" % relpath, 2)
    620                         inf, outf = os.popen4("%s -S \"%s\"" %
    621                                               (STRIP_EXEC, path))
    622                         output = outf.read().strip()
    623                         if output:
    624                             # usually not a real problem, like when we're
    625                             # trying to strip a script
    626                             self.message("Problem stripping %s:" % relpath, 3)
    627                             self.message(output, 3)
    628             walk(self.bundlepath)
    629 
    630     def findDependencies(self):
    631         self.message("Finding module dependencies", 1)
    632         import modulefinder
    633         mf = modulefinder.ModuleFinder(excludes=self.excludeModules)
    634         if self.use_zipimport:
    635             # zipimport imports zlib, must add it manually
    636             mf.import_hook("zlib")
    637         # manually add our own site.py
    638         site = mf.add_module("site")
    639         site.__code__ = self._getSiteCode()
    640         mf.scan_code(site.__code__, site)
    641 
    642         # warnings.py gets imported implicitly from C
    643         mf.import_hook("warnings")
    644 
    645         includeModules = self.includeModules[:]
    646         for name in self.includePackages:
    647             includeModules.extend(findPackageContents(name).keys())
    648         for name in includeModules:
    649             try:
    650                 mf.import_hook(name)
    651             except ImportError:
    652                 self.missingModules.append(name)
    653 
    654         mf.run_script(self.mainprogram)
    655         modules = mf.modules.items()
    656         modules.sort()
    657         for name, mod in modules:
    658             path = mod.__file__
    659             if path and self.semi_standalone:
    660                 # skip the standard library
    661                 if path.startswith(LIB) and not path.startswith(SITE_PACKAGES):
    662                     continue
    663             if path and mod.__code__ is None:
    664                 # C extension
    665                 filename = os.path.basename(path)
    666                 pathitems = name.split(".")[:-1] + [filename]
    667                 dstpath = pathjoin(*pathitems)
    668                 if self.use_zipimport:
    669                     if name != "zlib":
    670                         # neatly pack all extension modules in a subdirectory,
    671                         # except zlib, since it's necessary for bootstrapping.
    672                         dstpath = pathjoin("ExtensionModules", dstpath)
    673                     # Python modules are stored in a Zip archive, but put
    674                     # extensions in Contents/Resources/. Add a tiny "loader"
    675                     # program in the Zip archive. Due to Thomas Heller.
    676                     source = EXT_LOADER % {"name": name, "filename": dstpath}
    677                     code = compile(source, "<dynloader for %s>" % name, "exec")
    678                     mod.__code__ = code
    679                 self.files.append((path, pathjoin("Contents", "Resources", dstpath)))
    680             if mod.__code__ is not None:
    681                 ispkg = mod.__path__ is not None
    682                 if not self.use_zipimport or name != "site":
    683                     # Our site.py is doing the bootstrapping, so we must
    684                     # include a real .pyc file if self.use_zipimport is True.
    685                     self.pymodules.append((name, mod.__code__, ispkg))
    686 
    687         if hasattr(mf, "any_missing_maybe"):
    688             missing, maybe = mf.any_missing_maybe()
    689         else:
    690             missing = mf.any_missing()
    691             maybe = []
    692         self.missingModules.extend(missing)
    693         self.maybeMissingModules.extend(maybe)
    694 
    695     def reportMissing(self):
    696         missing = [name for name in self.missingModules
    697                 if name not in MAYMISS_MODULES]
    698         if self.maybeMissingModules:
    699             maybe = self.maybeMissingModules
    700         else:
    701             maybe = [name for name in missing if "." in name]
    702             missing = [name for name in missing if "." not in name]
    703         missing.sort()
    704         maybe.sort()
    705         if maybe:
    706             self.message("Warning: couldn't find the following submodules:", 1)
    707             self.message("    (Note that these could be false alarms -- "
    708                          "it's not always", 1)
    709             self.message("    possible to distinguish between \"from package "
    710                          "import submodule\" ", 1)
    711             self.message("    and \"from package import name\")", 1)
    712             for name in maybe:
    713                 self.message("  ? " + name, 1)
    714         if missing:
    715             self.message("Warning: couldn't find the following modules:", 1)
    716             for name in missing:
    717                 self.message("  ? " + name, 1)
    718 
    719     def report(self):
    720         # XXX something decent
    721         import pprint
    722         pprint.pprint(self.__dict__)
    723         if self.standalone or self.semi_standalone:
    724             self.reportMissing()
    725 
    726 #
    727 # Utilities.
    728 #
    729 
    730 SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()]
    731 identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$")
    732 
    733 def findPackageContents(name, searchpath=None):
    734     head = name.split(".")[-1]
    735     if identifierRE.match(head) is None:
    736         return {}
    737     try:
    738         fp, path, (ext, mode, tp) = imp.find_module(head, searchpath)
    739     except ImportError:
    740         return {}
    741     modules = {name: None}
    742     if tp == imp.PKG_DIRECTORY and path:
    743         files = os.listdir(path)
    744         for sub in files:
    745             sub, ext = os.path.splitext(sub)
    746             fullname = name + "." + sub
    747             if sub != "__init__" and fullname not in modules:
    748                 modules.update(findPackageContents(fullname, [path]))
    749     return modules
    750 
    751 def writePyc(code, path):
    752     f = open(path, "wb")
    753     f.write(MAGIC)
    754     f.write("\0" * 4)  # don't bother about a time stamp
    755     marshal.dump(code, f)
    756     f.close()
    757 
    758 def copy(src, dst, mkdirs=0):
    759     """Copy a file or a directory."""
    760     if mkdirs:
    761         makedirs(os.path.dirname(dst))
    762     if os.path.isdir(src):
    763         shutil.copytree(src, dst, symlinks=1)
    764     else:
    765         shutil.copy2(src, dst)
    766 
    767 def copytodir(src, dstdir):
    768     """Copy a file or a directory to an existing directory."""
    769     dst = pathjoin(dstdir, os.path.basename(src))
    770     copy(src, dst)
    771 
    772 def makedirs(dir):
    773     """Make all directories leading up to 'dir' including the leaf
    774     directory. Don't moan if any path element already exists."""
    775     try:
    776         os.makedirs(dir)
    777     except OSError, why:
    778         if why.errno != errno.EEXIST:
    779             raise
    780 
    781 def symlink(src, dst, mkdirs=0):
    782     """Copy a file or a directory."""
    783     if not os.path.exists(src):
    784         raise IOError, "No such file or directory: '%s'" % src
    785     if mkdirs:
    786         makedirs(os.path.dirname(dst))
    787     os.symlink(os.path.abspath(src), dst)
    788 
    789 def pathjoin(*args):
    790     """Safe wrapper for os.path.join: asserts that all but the first
    791     argument are relative paths."""
    792     for seg in args[1:]:
    793         assert seg[0] != "/"
    794     return os.path.join(*args)
    795 
    796 
    797 cmdline_doc = """\
    798 Usage:
    799   python bundlebuilder.py [options] command
    800   python mybuildscript.py [options] command
    801 
    802 Commands:
    803   build      build the application
    804   report     print a report
    805 
    806 Options:
    807   -b, --builddir=DIR     the build directory; defaults to "build"
    808   -n, --name=NAME        application name
    809   -r, --resource=FILE    extra file or folder to be copied to Resources
    810   -f, --file=SRC:DST     extra file or folder to be copied into the bundle;
    811                          DST must be a path relative to the bundle root
    812   -e, --executable=FILE  the executable to be used
    813   -m, --mainprogram=FILE the Python main program
    814   -a, --argv             add a wrapper main program to create sys.argv
    815   -p, --plist=FILE       .plist file (default: generate one)
    816       --nib=NAME         main nib name
    817   -c, --creator=CCCC     4-char creator code (default: '????')
    818       --iconfile=FILE    filename of the icon (an .icns file) to be used
    819                          as the Finder icon
    820       --bundle-id=ID     the CFBundleIdentifier, in reverse-dns format
    821                          (eg. org.python.BuildApplet; this is used for
    822                          the preferences file name)
    823   -l, --link             symlink files/folder instead of copying them
    824       --link-exec        symlink the executable instead of copying it
    825       --standalone       build a standalone application, which is fully
    826                          independent of a Python installation
    827       --semi-standalone  build a standalone application, which depends on
    828                          an installed Python, yet includes all third-party
    829                          modules.
    830       --no-zipimport     Do not copy code into a zip file
    831       --python=FILE      Python to use in #! line in stead of current Python
    832       --lib=FILE         shared library or framework to be copied into
    833                          the bundle
    834   -x, --exclude=MODULE   exclude module (with --(semi-)standalone)
    835   -i, --include=MODULE   include module (with --(semi-)standalone)
    836       --package=PACKAGE  include a whole package (with --(semi-)standalone)
    837       --strip            strip binaries (remove debug info)
    838   -v, --verbose          increase verbosity level
    839   -q, --quiet            decrease verbosity level
    840   -h, --help             print this message
    841 """
    842 
    843 def usage(msg=None):
    844     if msg:
    845         print msg
    846     print cmdline_doc
    847     sys.exit(1)
    848 
    849 def main(builder=None):
    850     if builder is None:
    851         builder = AppBuilder(verbosity=1)
    852 
    853     shortopts = "b:n:r:f:e:m:c:p:lx:i:hvqa"
    854     longopts = ("builddir=", "name=", "resource=", "file=", "executable=",
    855         "mainprogram=", "creator=", "nib=", "plist=", "link",
    856         "link-exec", "help", "verbose", "quiet", "argv", "standalone",
    857         "exclude=", "include=", "package=", "strip", "iconfile=",
    858         "lib=", "python=", "semi-standalone", "bundle-id=", "destroot="
    859         "no-zipimport"
    860         )
    861 
    862     try:
    863         options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
    864     except getopt.error:
    865         usage()
    866 
    867     for opt, arg in options:
    868         if opt in ('-b', '--builddir'):
    869             builder.builddir = arg
    870         elif opt in ('-n', '--name'):
    871             builder.name = arg
    872         elif opt in ('-r', '--resource'):
    873             builder.resources.append(os.path.normpath(arg))
    874         elif opt in ('-f', '--file'):
    875             srcdst = arg.split(':')
    876             if len(srcdst) != 2:
    877                 usage("-f or --file argument must be two paths, "
    878                       "separated by a colon")
    879             builder.files.append(srcdst)
    880         elif opt in ('-e', '--executable'):
    881             builder.executable = arg
    882         elif opt in ('-m', '--mainprogram'):
    883             builder.mainprogram = arg
    884         elif opt in ('-a', '--argv'):
    885             builder.argv_emulation = 1
    886         elif opt in ('-c', '--creator'):
    887             builder.creator = arg
    888         elif opt == '--bundle-id':
    889             builder.bundle_id = arg
    890         elif opt == '--iconfile':
    891             builder.iconfile = arg
    892         elif opt == "--lib":
    893             builder.libs.append(os.path.normpath(arg))
    894         elif opt == "--nib":
    895             builder.nibname = arg
    896         elif opt in ('-p', '--plist'):
    897             builder.plist = Plist.fromFile(arg)
    898         elif opt in ('-l', '--link'):
    899             builder.symlink = 1
    900         elif opt == '--link-exec':
    901             builder.symlink_exec = 1
    902         elif opt in ('-h', '--help'):
    903             usage()
    904         elif opt in ('-v', '--verbose'):
    905             builder.verbosity += 1
    906         elif opt in ('-q', '--quiet'):
    907             builder.verbosity -= 1
    908         elif opt == '--standalone':
    909             builder.standalone = 1
    910         elif opt == '--semi-standalone':
    911             builder.semi_standalone = 1
    912         elif opt == '--python':
    913             builder.python = arg
    914         elif opt in ('-x', '--exclude'):
    915             builder.excludeModules.append(arg)
    916         elif opt in ('-i', '--include'):
    917             builder.includeModules.append(arg)
    918         elif opt == '--package':
    919             builder.includePackages.append(arg)
    920         elif opt == '--strip':
    921             builder.strip = 1
    922         elif opt == '--destroot':
    923             builder.destroot = arg
    924         elif opt == '--no-zipimport':
    925             builder.use_zipimport = False
    926 
    927     if len(args) != 1:
    928         usage("Must specify one command ('build', 'report' or 'help')")
    929     command = args[0]
    930 
    931     if command == "build":
    932         builder.setup()
    933         builder.build()
    934     elif command == "report":
    935         builder.setup()
    936         builder.report()
    937     elif command == "help":
    938         usage()
    939     else:
    940         usage("Unknown command '%s'" % command)
    941 
    942 
    943 def buildapp(**kwargs):
    944     builder = AppBuilder(**kwargs)
    945     main(builder)
    946 
    947 
    948 if __name__ == "__main__":
    949     main()
    950