• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#! /usr/bin/env python
2
3"""\
4bundlebuilder.py -- Tools to assemble MacOS X (application) bundles.
5
6This module contains two classes to build so called "bundles" for
7MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
8specialized in building application bundles.
9
10[Bundle|App]Builder objects are instantiated with a bunch of keyword
11arguments, and have a build() method that will do all the work. See
12the class doc strings for a description of the constructor arguments.
13
14The 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
19Where "buildapp.py" is a user-supplied setup.py-like script following
20this 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
31from warnings import warnpy3k
32warnpy3k("In 3.x, the bundlebuilder module is removed.", stacklevel=2)
33
34import sys
35import os, errno, shutil
36import imp, marshal
37import re
38from copy import deepcopy
39import getopt
40from plistlib import Plist
41from types import FunctionType as function
42
43class BundleBuilderError(Exception): pass
44
45
46class 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
71class 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
232if __debug__:
233    PYC_EXT = ".pyc"
234else:
235    PYC_EXT = ".pyo"
236
237MAGIC = imp.get_magic()
238USE_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.
242SITE_PY = """\
243import sys
244if not %(semi_standalone)s:
245    del sys.path[1:]  # sys.path[0] is Contents/Resources/
246"""
247
248ZIP_ARCHIVE = "Modules.zip"
249SITE_PY_ZIP = SITE_PY + ("sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE)
250
251def 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#
261EXT_LOADER = """\
262def __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()
273del __load
274"""
275
276MAYMISS_MODULES = ['os2', 'nt', 'ntpath', 'dos', 'dospath',
277    'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
278    'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
279]
280
281STRIP_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#
294BOOTSTRAP_SCRIPT = """\
295#!%(hashbang)s
296
297import sys, os
298execdir = os.path.dirname(sys.argv[0])
299executable = os.path.join(execdir, "%(executable)s")
300resdir = os.path.join(os.path.dirname(execdir), "Resources")
301libdir = os.path.join(os.path.dirname(execdir), "Frameworks")
302mainprogram = os.path.join(resdir, "%(mainprogram)s")
303
304if %(optimize)s:
305    sys.argv.insert(1, '-O')
306
307sys.argv.insert(1, mainprogram)
308if %(standalone)s or %(semi_standalone)s:
309    os.environ["PYTHONPATH"] = resdir
310    if %(standalone)s:
311        os.environ["PYTHONHOME"] = resdir
312else:
313    pypath = os.getenv("PYTHONPATH", "")
314    if pypath:
315        pypath = ":" + pypath
316    os.environ["PYTHONPATH"] = resdir + pypath
317
318os.environ["PYTHONEXECUTABLE"] = executable
319os.environ["DYLD_LIBRARY_PATH"] = libdir
320os.environ["DYLD_FRAMEWORK_PATH"] = libdir
321os.execve(executable, sys.argv, os.environ)
322"""
323
324
325#
326# Optional wrapper that converts "dropped files" into sys.argv values.
327#
328ARGV_EMULATOR = """\
329import argvemulator, os
330
331argvemulator.ArgvCollector().mainloop()
332execfile(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#
340PYTHONFRAMEWORKGOODIES = [
341    "Python",  # the Python core library
342    "Resources/English.lproj",
343    "Resources/Info.plist",
344]
345
346def isFramework():
347    return sys.exec_prefix.find("Python.framework") > 0
348
349
350LIB = os.path.join(sys.prefix, "lib", "python" + sys.version[:3])
351SITE_PACKAGES = os.path.join(LIB, "site-packages")
352
353
354class 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
730SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()]
731identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$")
732
733def 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
751def 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
758def 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
767def 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
772def 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
781def 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
789def 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
797cmdline_doc = """\
798Usage:
799  python bundlebuilder.py [options] command
800  python mybuildscript.py [options] command
801
802Commands:
803  build      build the application
804  report     print a report
805
806Options:
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
843def usage(msg=None):
844    if msg:
845        print msg
846    print cmdline_doc
847    sys.exit(1)
848
849def 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
943def buildapp(**kwargs):
944    builder = AppBuilder(**kwargs)
945    main(builder)
946
947
948if __name__ == "__main__":
949    main()
950