Make it possible to install a framework build of Python 3.x on OSX.

This the build machinery on OSX to re-enable building (and installing)
PythonLauncher.app and IDLE.app.

This needs ports of fixes to Lib/distutils/util.py and Include/pymacconfig.h
to be fully functiontional, to be fully functional.

I also have a patch for Makefile.pre.in that I'll post on bugs.python.org
for review.
diff --git a/Mac/Tools/bundlebuilder.py b/Mac/Tools/bundlebuilder.py
new file mode 100644
index 0000000..a66801b
--- /dev/null
+++ b/Mac/Tools/bundlebuilder.py
@@ -0,0 +1,934 @@
+#! /usr/bin/env python
+
+"""\
+bundlebuilder.py -- Tools to assemble MacOS X (application) bundles.
+
+This module contains two classes to build so called "bundles" for
+MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
+specialized in building application bundles.
+
+[Bundle|App]Builder objects are instantiated with a bunch of keyword
+arguments, and have a build() method that will do all the work. See
+the class doc strings for a description of the constructor arguments.
+
+The module contains a main program that can be used in two ways:
+
+  % python bundlebuilder.py [options] build
+  % python buildapp.py [options] build
+
+Where "buildapp.py" is a user-supplied setup.py-like script following
+this model:
+
+  from bundlebuilder import buildapp
+  buildapp(<lots-of-keyword-args>)
+
+"""
+
+
+__all__ = ["BundleBuilder", "BundleBuilderError", "AppBuilder", "buildapp"]
+
+
+import sys
+import os, errno, shutil
+import imp, marshal
+import re
+from copy import deepcopy
+import getopt
+from plistlib import Plist
+from types import FunctionType as function
+
+class BundleBuilderError(Exception): pass
+
+
+class Defaults:
+
+    """Class attributes that don't start with an underscore and are
+    not functions or classmethods are (deep)copied to self.__dict__.
+    This allows for mutable default values.
+    """
+
+    def __init__(self, **kwargs):
+        defaults = self._getDefaults()
+        defaults.update(kwargs)
+        self.__dict__.update(defaults)
+
+    def _getDefaults(cls):
+        defaults = {}
+        for base in cls.__bases__:
+            if hasattr(base, "_getDefaults"):
+                defaults.update(base._getDefaults())
+        for name, value in list(cls.__dict__.items()):
+            if name[0] != "_" and not isinstance(value,
+                    (function, classmethod)):
+                defaults[name] = deepcopy(value)
+        return defaults
+    _getDefaults = classmethod(_getDefaults)
+
+
+class BundleBuilder(Defaults):
+
+    """BundleBuilder is a barebones class for assembling bundles. It
+    knows nothing about executables or icons, it only copies files
+    and creates the PkgInfo and Info.plist files.
+    """
+
+    # (Note that Defaults.__init__ (deep)copies these values to
+    # instance variables. Mutable defaults are therefore safe.)
+
+    # Name of the bundle, with or without extension.
+    name = None
+
+    # The property list ("plist")
+    plist = Plist(CFBundleDevelopmentRegion = "English",
+                  CFBundleInfoDictionaryVersion = "6.0")
+
+    # The type of the bundle.
+    type = "BNDL"
+    # The creator code of the bundle.
+    creator = None
+
+    # the CFBundleIdentifier (this is used for the preferences file name)
+    bundle_id = None
+
+    # List of files that have to be copied to <bundle>/Contents/Resources.
+    resources = []
+
+    # List of (src, dest) tuples; dest should be a path relative to the bundle
+    # (eg. "Contents/Resources/MyStuff/SomeFile.ext).
+    files = []
+
+    # List of shared libraries (dylibs, Frameworks) to bundle with the app
+    # will be placed in Contents/Frameworks
+    libs = []
+
+    # Directory where the bundle will be assembled.
+    builddir = "build"
+
+    # Make symlinks instead copying files. This is handy during debugging, but
+    # makes the bundle non-distributable.
+    symlink = 0
+
+    # Verbosity level.
+    verbosity = 1
+
+    # Destination root directory
+    destroot = ""
+
+    def setup(self):
+        # XXX rethink self.name munging, this is brittle.
+        self.name, ext = os.path.splitext(self.name)
+        if not ext:
+            ext = ".bundle"
+        bundleextension = ext
+        # misc (derived) attributes
+        self.bundlepath = pathjoin(self.builddir, self.name + bundleextension)
+
+        plist = self.plist
+        plist.CFBundleName = self.name
+        plist.CFBundlePackageType = self.type
+        if self.creator is None:
+            if hasattr(plist, "CFBundleSignature"):
+                self.creator = plist.CFBundleSignature
+            else:
+                self.creator = "????"
+        plist.CFBundleSignature = self.creator
+        if self.bundle_id:
+            plist.CFBundleIdentifier = self.bundle_id
+        elif not hasattr(plist, "CFBundleIdentifier"):
+            plist.CFBundleIdentifier = self.name
+
+    def build(self):
+        """Build the bundle."""
+        builddir = self.builddir
+        if builddir and not os.path.exists(builddir):
+            os.mkdir(builddir)
+        self.message("Building %s" % repr(self.bundlepath), 1)
+        if os.path.exists(self.bundlepath):
+            shutil.rmtree(self.bundlepath)
+        if os.path.exists(self.bundlepath + '~'):
+            shutil.rmtree(self.bundlepath + '~')
+        bp = self.bundlepath
+
+        # Create the app bundle in a temporary location and then
+        # rename the completed bundle. This way the Finder will
+        # never see an incomplete bundle (where it might pick up
+        # and cache the wrong meta data)
+        self.bundlepath = bp + '~'
+        try:
+            os.mkdir(self.bundlepath)
+            self.preProcess()
+            self._copyFiles()
+            self._addMetaFiles()
+            self.postProcess()
+            os.rename(self.bundlepath, bp)
+        finally:
+            self.bundlepath = bp
+        self.message("Done.", 1)
+
+    def preProcess(self):
+        """Hook for subclasses."""
+        pass
+    def postProcess(self):
+        """Hook for subclasses."""
+        pass
+
+    def _addMetaFiles(self):
+        contents = pathjoin(self.bundlepath, "Contents")
+        makedirs(contents)
+        #
+        # Write Contents/PkgInfo
+        assert len(self.type) == len(self.creator) == 4, \
+                "type and creator must be 4-byte strings."
+        pkginfo = pathjoin(contents, "PkgInfo")
+        f = open(pkginfo, "wb")
+        f.write((self.type + self.creator).encode('latin1'))
+        f.close()
+        #
+        # Write Contents/Info.plist
+        infoplist = pathjoin(contents, "Info.plist")
+        self.plist.write(infoplist)
+
+    def _copyFiles(self):
+        files = self.files[:]
+        for path in self.resources:
+            files.append((path, pathjoin("Contents", "Resources",
+                os.path.basename(path))))
+        for path in self.libs:
+            files.append((path, pathjoin("Contents", "Frameworks",
+                os.path.basename(path))))
+        if self.symlink:
+            self.message("Making symbolic links", 1)
+            msg = "Making symlink from"
+        else:
+            self.message("Copying files", 1)
+            msg = "Copying"
+        files.sort()
+        for src, dst in files:
+            if os.path.isdir(src):
+                self.message("%s %s/ to %s/" % (msg, src, dst), 2)
+            else:
+                self.message("%s %s to %s" % (msg, src, dst), 2)
+            dst = pathjoin(self.bundlepath, dst)
+            if self.symlink:
+                symlink(src, dst, mkdirs=1)
+            else:
+                copy(src, dst, mkdirs=1)
+
+    def message(self, msg, level=0):
+        if level <= self.verbosity:
+            indent = ""
+            if level > 1:
+                indent = (level - 1) * "  "
+            sys.stderr.write(indent + msg + "\n")
+
+    def report(self):
+        # XXX something decent
+        pass
+
+
+if __debug__:
+    PYC_EXT = ".pyc"
+else:
+    PYC_EXT = ".pyo"
+
+MAGIC = imp.get_magic()
+USE_ZIPIMPORT = "zipimport" in sys.builtin_module_names
+
+# For standalone apps, we have our own minimal site.py. We don't need
+# all the cruft of the real site.py.
+SITE_PY = """\
+import sys
+if not %(semi_standalone)s:
+    del sys.path[1:]  # sys.path[0] is Contents/Resources/
+"""
+
+if USE_ZIPIMPORT:
+    ZIP_ARCHIVE = "Modules.zip"
+    SITE_PY += "sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE
+    def getPycData(fullname, code, ispkg):
+        if ispkg:
+            fullname += ".__init__"
+        path = fullname.replace(".", os.sep) + PYC_EXT
+        return path, MAGIC + '\0\0\0\0' + marshal.dumps(code)
+
+#
+# Extension modules can't be in the modules zip archive, so a placeholder
+# is added instead, that loads the extension from a specified location.
+#
+EXT_LOADER = """\
+def __load():
+    import imp, sys, os
+    for p in sys.path:
+        path = os.path.join(p, "%(filename)s")
+        if os.path.exists(path):
+            break
+    else:
+        assert 0, "file not found: %(filename)s"
+    mod = imp.load_dynamic("%(name)s", path)
+
+__load()
+del __load
+"""
+
+MAYMISS_MODULES = ['mac', 'os2', 'nt', 'ntpath', 'dos', 'dospath',
+    'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
+    'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
+]
+
+STRIP_EXEC = "/usr/bin/strip"
+
+#
+# We're using a stock interpreter to run the app, yet we need
+# a way to pass the Python main program to the interpreter. The
+# bootstrapping script fires up the interpreter with the right
+# arguments. os.execve() is used as OSX doesn't like us to
+# start a real new process. Also, the executable name must match
+# the CFBundleExecutable value in the Info.plist, so we lie
+# deliberately with argv[0]. The actual Python executable is
+# passed in an environment variable so we can "repair"
+# sys.executable later.
+#
+BOOTSTRAP_SCRIPT = """\
+#!%(hashbang)s
+
+import sys, os
+execdir = os.path.dirname(sys.argv[0])
+executable = os.path.join(execdir, "%(executable)s")
+resdir = os.path.join(os.path.dirname(execdir), "Resources")
+libdir = os.path.join(os.path.dirname(execdir), "Frameworks")
+mainprogram = os.path.join(resdir, "%(mainprogram)s")
+
+sys.argv.insert(1, mainprogram)
+if %(standalone)s or %(semi_standalone)s:
+    os.environ["PYTHONPATH"] = resdir
+    if %(standalone)s:
+        os.environ["PYTHONHOME"] = resdir
+else:
+    pypath = os.getenv("PYTHONPATH", "")
+    if pypath:
+        pypath = ":" + pypath
+    os.environ["PYTHONPATH"] = resdir + pypath
+os.environ["PYTHONEXECUTABLE"] = executable
+os.environ["DYLD_LIBRARY_PATH"] = libdir
+os.environ["DYLD_FRAMEWORK_PATH"] = libdir
+os.execve(executable, sys.argv, os.environ)
+"""
+
+
+#
+# Optional wrapper that converts "dropped files" into sys.argv values.
+#
+ARGV_EMULATOR = """\
+import argvemulator, os
+
+argvemulator.ArgvCollector().mainloop()
+execfile(os.path.join(os.path.split(__file__)[0], "%(realmainprogram)s"))
+"""
+
+#
+# When building a standalone app with Python.framework, we need to copy
+# a subset from Python.framework to the bundle. The following list
+# specifies exactly what items we'll copy.
+#
+PYTHONFRAMEWORKGOODIES = [
+    "Python",  # the Python core library
+    "Resources/English.lproj",
+    "Resources/Info.plist",
+    "Resources/version.plist",
+]
+
+def isFramework():
+    return sys.exec_prefix.find("Python.framework") > 0
+
+
+LIB = os.path.join(sys.prefix, "lib", "python" + sys.version[:3])
+SITE_PACKAGES = os.path.join(LIB, "site-packages")
+
+
+class AppBuilder(BundleBuilder):
+
+    # Override type of the bundle.
+    type = "APPL"
+
+    # platform, name of the subfolder of Contents that contains the executable.
+    platform = "MacOS"
+
+    # A Python main program. If this argument is given, the main
+    # executable in the bundle will be a small wrapper that invokes
+    # the main program. (XXX Discuss why.)
+    mainprogram = None
+
+    # The main executable. If a Python main program is specified
+    # the executable will be copied to Resources and be invoked
+    # by the wrapper program mentioned above. Otherwise it will
+    # simply be used as the main executable.
+    executable = None
+
+    # The name of the main nib, for Cocoa apps. *Must* be specified
+    # when building a Cocoa app.
+    nibname = None
+
+    # The name of the icon file to be copied to Resources and used for
+    # the Finder icon.
+    iconfile = None
+
+    # Symlink the executable instead of copying it.
+    symlink_exec = 0
+
+    # If True, build standalone app.
+    standalone = 0
+
+    # If True, build semi-standalone app (only includes third-party modules).
+    semi_standalone = 0
+
+    # If set, use this for #! lines in stead of sys.executable
+    python = None
+
+    # If True, add a real main program that emulates sys.argv before calling
+    # mainprogram
+    argv_emulation = 0
+
+    # The following attributes are only used when building a standalone app.
+
+    # Exclude these modules.
+    excludeModules = []
+
+    # Include these modules.
+    includeModules = []
+
+    # Include these packages.
+    includePackages = []
+
+    # Strip binaries from debug info.
+    strip = 0
+
+    # Found Python modules: [(name, codeobject, ispkg), ...]
+    pymodules = []
+
+    # Modules that modulefinder couldn't find:
+    missingModules = []
+    maybeMissingModules = []
+
+    def setup(self):
+        if ((self.standalone or self.semi_standalone)
+            and self.mainprogram is None):
+            raise BundleBuilderError("must specify 'mainprogram' when "
+                    "building a standalone application.")
+        if self.mainprogram is None and self.executable is None:
+            raise BundleBuilderError("must specify either or both of "
+                    "'executable' and 'mainprogram'")
+
+        self.execdir = pathjoin("Contents", self.platform)
+
+        if self.name is not None:
+            pass
+        elif self.mainprogram is not None:
+            self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
+        elif executable is not None:
+            self.name = os.path.splitext(os.path.basename(self.executable))[0]
+        if self.name[-4:] != ".app":
+            self.name += ".app"
+
+        if self.executable is None:
+            if not self.standalone and not isFramework():
+                self.symlink_exec = 1
+            if self.python:
+                self.executable = self.python
+            else:
+                self.executable = sys.executable
+
+        if self.nibname:
+            self.plist.NSMainNibFile = self.nibname
+            if not hasattr(self.plist, "NSPrincipalClass"):
+                self.plist.NSPrincipalClass = "NSApplication"
+
+        if self.standalone and isFramework():
+            self.addPythonFramework()
+
+        BundleBuilder.setup(self)
+
+        self.plist.CFBundleExecutable = self.name
+
+        if self.standalone or self.semi_standalone:
+            self.findDependencies()
+
+    def preProcess(self):
+        resdir = "Contents/Resources"
+        if self.executable is not None:
+            if self.mainprogram is None:
+                execname = self.name
+            else:
+                execname = os.path.basename(self.executable)
+            execpath = pathjoin(self.execdir, execname)
+            if not self.symlink_exec:
+                self.files.append((self.destroot + self.executable, execpath))
+            self.execpath = execpath
+
+        if self.mainprogram is not None:
+            mainprogram = os.path.basename(self.mainprogram)
+            self.files.append((self.mainprogram, pathjoin(resdir, mainprogram)))
+            if self.argv_emulation:
+                # Change the main program, and create the helper main program (which
+                # does argv collection and then calls the real main).
+                # Also update the included modules (if we're creating a standalone
+                # program) and the plist
+                realmainprogram = mainprogram
+                mainprogram = '__argvemulator_' + mainprogram
+                resdirpath = pathjoin(self.bundlepath, resdir)
+                mainprogrampath = pathjoin(resdirpath, mainprogram)
+                makedirs(resdirpath)
+                open(mainprogrampath, "w").write(ARGV_EMULATOR % locals())
+                if self.standalone or self.semi_standalone:
+                    self.includeModules.append("argvemulator")
+                    self.includeModules.append("os")
+                if "CFBundleDocumentTypes" not in self.plist:
+                    self.plist["CFBundleDocumentTypes"] = [
+                        { "CFBundleTypeOSTypes" : [
+                            "****",
+                            "fold",
+                            "disk"],
+                          "CFBundleTypeRole": "Viewer"}]
+            # Write bootstrap script
+            executable = os.path.basename(self.executable)
+            execdir = pathjoin(self.bundlepath, self.execdir)
+            bootstrappath = pathjoin(execdir, self.name)
+            makedirs(execdir)
+            if self.standalone or self.semi_standalone:
+                # XXX we're screwed when the end user has deleted
+                # /usr/bin/python
+                hashbang = "/usr/bin/python"
+            elif self.python:
+                hashbang = self.python
+            else:
+                hashbang = os.path.realpath(sys.executable)
+            standalone = self.standalone
+            semi_standalone = self.semi_standalone
+            open(bootstrappath, "w").write(BOOTSTRAP_SCRIPT % locals())
+            os.chmod(bootstrappath, 0o775)
+
+        if self.iconfile is not None:
+            iconbase = os.path.basename(self.iconfile)
+            self.plist.CFBundleIconFile = iconbase
+            self.files.append((self.iconfile, pathjoin(resdir, iconbase)))
+
+    def postProcess(self):
+        if self.standalone or self.semi_standalone:
+            self.addPythonModules()
+        if self.strip and not self.symlink:
+            self.stripBinaries()
+
+        if self.symlink_exec and self.executable:
+            self.message("Symlinking executable %s to %s" % (self.executable,
+                    self.execpath), 2)
+            dst = pathjoin(self.bundlepath, self.execpath)
+            makedirs(os.path.dirname(dst))
+            os.symlink(os.path.abspath(self.executable), dst)
+
+        if self.missingModules or self.maybeMissingModules:
+            self.reportMissing()
+
+    def addPythonFramework(self):
+        # If we're building a standalone app with Python.framework,
+        # include a minimal subset of Python.framework, *unless*
+        # Python.framework was specified manually in self.libs.
+        for lib in self.libs:
+            if os.path.basename(lib) == "Python.framework":
+                # a Python.framework was specified as a library
+                return
+
+        frameworkpath = sys.exec_prefix[:sys.exec_prefix.find(
+            "Python.framework") + len("Python.framework")]
+
+        version = sys.version[:3]
+        frameworkpath = pathjoin(frameworkpath, "Versions", version)
+        destbase = pathjoin("Contents", "Frameworks", "Python.framework",
+                            "Versions", version)
+        for item in PYTHONFRAMEWORKGOODIES:
+            src = pathjoin(frameworkpath, item)
+            dst = pathjoin(destbase, item)
+            self.files.append((src, dst))
+
+    def _getSiteCode(self):
+        return compile(SITE_PY % {"semi_standalone": self.semi_standalone},
+                     "<-bundlebuilder.py->", "exec")
+
+    def addPythonModules(self):
+        self.message("Adding Python modules", 1)
+
+        if USE_ZIPIMPORT:
+            # Create a zip file containing all modules as pyc.
+            import zipfile
+            relpath = pathjoin("Contents", "Resources", ZIP_ARCHIVE)
+            abspath = pathjoin(self.bundlepath, relpath)
+            zf = zipfile.ZipFile(abspath, "w", zipfile.ZIP_DEFLATED)
+            for name, code, ispkg in self.pymodules:
+                self.message("Adding Python module %s" % name, 2)
+                path, pyc = getPycData(name, code, ispkg)
+                zf.writestr(path, pyc)
+            zf.close()
+            # add site.pyc
+            sitepath = pathjoin(self.bundlepath, "Contents", "Resources",
+                    "site" + PYC_EXT)
+            writePyc(self._getSiteCode(), sitepath)
+        else:
+            # Create individual .pyc files.
+            for name, code, ispkg in self.pymodules:
+                if ispkg:
+                    name += ".__init__"
+                path = name.split(".")
+                path = pathjoin("Contents", "Resources", *path) + PYC_EXT
+
+                if ispkg:
+                    self.message("Adding Python package %s" % path, 2)
+                else:
+                    self.message("Adding Python module %s" % path, 2)
+
+                abspath = pathjoin(self.bundlepath, path)
+                makedirs(os.path.dirname(abspath))
+                writePyc(code, abspath)
+
+    def stripBinaries(self):
+        if not os.path.exists(STRIP_EXEC):
+            self.message("Error: can't strip binaries: no strip program at "
+                "%s" % STRIP_EXEC, 0)
+        else:
+            import stat
+            self.message("Stripping binaries", 1)
+            def walk(top):
+                for name in os.listdir(top):
+                    path = pathjoin(top, name)
+                    if os.path.islink(path):
+                        continue
+                    if os.path.isdir(path):
+                        walk(path)
+                    else:
+                        mod = os.stat(path)[stat.ST_MODE]
+                        if not (mod & 0o100):
+                            continue
+                        relpath = path[len(self.bundlepath):]
+                        self.message("Stripping %s" % relpath, 2)
+                        inf, outf = os.popen4("%s -S \"%s\"" %
+                                              (STRIP_EXEC, path))
+                        output = outf.read().strip()
+                        if output:
+                            # usually not a real problem, like when we're
+                            # trying to strip a script
+                            self.message("Problem stripping %s:" % relpath, 3)
+                            self.message(output, 3)
+            walk(self.bundlepath)
+
+    def findDependencies(self):
+        self.message("Finding module dependencies", 1)
+        import modulefinder
+        mf = modulefinder.ModuleFinder(excludes=self.excludeModules)
+        if USE_ZIPIMPORT:
+            # zipimport imports zlib, must add it manually
+            mf.import_hook("zlib")
+        # manually add our own site.py
+        site = mf.add_module("site")
+        site.__code__ = self._getSiteCode()
+        mf.scan_code(site.__code__, site)
+
+        # warnings.py gets imported implicitly from C
+        mf.import_hook("warnings")
+
+        includeModules = self.includeModules[:]
+        for name in self.includePackages:
+            includeModules.extend(list(findPackageContents(name).keys()))
+        for name in includeModules:
+            try:
+                mf.import_hook(name)
+            except ImportError:
+                self.missingModules.append(name)
+
+        mf.run_script(self.mainprogram)
+        modules = list(mf.modules.items())
+        modules.sort()
+        for name, mod in modules:
+            path = mod.__file__
+            if path and self.semi_standalone:
+                # skip the standard library
+                if path.startswith(LIB) and not path.startswith(SITE_PACKAGES):
+                    continue
+            if path and mod.__code__ is None:
+                # C extension
+                filename = os.path.basename(path)
+                pathitems = name.split(".")[:-1] + [filename]
+                dstpath = pathjoin(*pathitems)
+                if USE_ZIPIMPORT:
+                    if name != "zlib":
+                        # neatly pack all extension modules in a subdirectory,
+                        # except zlib, since it's neccesary for bootstrapping.
+                        dstpath = pathjoin("ExtensionModules", dstpath)
+                    # Python modules are stored in a Zip archive, but put
+                    # extensions in Contents/Resources/. Add a tiny "loader"
+                    # program in the Zip archive. Due to Thomas Heller.
+                    source = EXT_LOADER % {"name": name, "filename": dstpath}
+                    code = compile(source, "<dynloader for %s>" % name, "exec")
+                    mod.__code__ = code
+                self.files.append((path, pathjoin("Contents", "Resources", dstpath)))
+            if mod.__code__ is not None:
+                ispkg = mod.__path__ is not None
+                if not USE_ZIPIMPORT or name != "site":
+                    # Our site.py is doing the bootstrapping, so we must
+                    # include a real .pyc file if USE_ZIPIMPORT is True.
+                    self.pymodules.append((name, mod.__code__, ispkg))
+
+        if hasattr(mf, "any_missing_maybe"):
+            missing, maybe = mf.any_missing_maybe()
+        else:
+            missing = mf.any_missing()
+            maybe = []
+        self.missingModules.extend(missing)
+        self.maybeMissingModules.extend(maybe)
+
+    def reportMissing(self):
+        missing = [name for name in self.missingModules
+                if name not in MAYMISS_MODULES]
+        if self.maybeMissingModules:
+            maybe = self.maybeMissingModules
+        else:
+            maybe = [name for name in missing if "." in name]
+            missing = [name for name in missing if "." not in name]
+        missing.sort()
+        maybe.sort()
+        if maybe:
+            self.message("Warning: couldn't find the following submodules:", 1)
+            self.message("    (Note that these could be false alarms -- "
+                         "it's not always", 1)
+            self.message("    possible to distinguish between \"from package "
+                         "import submodule\" ", 1)
+            self.message("    and \"from package import name\")", 1)
+            for name in maybe:
+                self.message("  ? " + name, 1)
+        if missing:
+            self.message("Warning: couldn't find the following modules:", 1)
+            for name in missing:
+                self.message("  ? " + name, 1)
+
+    def report(self):
+        # XXX something decent
+        import pprint
+        pprint.pprint(self.__dict__)
+        if self.standalone or self.semi_standalone:
+            self.reportMissing()
+
+#
+# Utilities.
+#
+
+SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()]
+identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$")
+
+def findPackageContents(name, searchpath=None):
+    head = name.split(".")[-1]
+    if identifierRE.match(head) is None:
+        return {}
+    try:
+        fp, path, (ext, mode, tp) = imp.find_module(head, searchpath)
+    except ImportError:
+        return {}
+    modules = {name: None}
+    if tp == imp.PKG_DIRECTORY and path:
+        files = os.listdir(path)
+        for sub in files:
+            sub, ext = os.path.splitext(sub)
+            fullname = name + "." + sub
+            if sub != "__init__" and fullname not in modules:
+                modules.update(findPackageContents(fullname, [path]))
+    return modules
+
+def writePyc(code, path):
+    f = open(path, "wb")
+    f.write(MAGIC)
+    f.write("\0" * 4)  # don't bother about a time stamp
+    marshal.dump(code, f)
+    f.close()
+
+def copy(src, dst, mkdirs=0):
+    """Copy a file or a directory."""
+    if mkdirs:
+        makedirs(os.path.dirname(dst))
+    if os.path.isdir(src):
+        shutil.copytree(src, dst, symlinks=1)
+    else:
+        shutil.copy2(src, dst)
+
+def copytodir(src, dstdir):
+    """Copy a file or a directory to an existing directory."""
+    dst = pathjoin(dstdir, os.path.basename(src))
+    copy(src, dst)
+
+def makedirs(dir):
+    """Make all directories leading up to 'dir' including the leaf
+    directory. Don't moan if any path element already exists."""
+    try:
+        os.makedirs(dir)
+    except OSError as why:
+        if why.errno != errno.EEXIST:
+            raise
+
+def symlink(src, dst, mkdirs=0):
+    """Copy a file or a directory."""
+    if not os.path.exists(src):
+        raise IOError("No such file or directory: '%s'" % src)
+    if mkdirs:
+        makedirs(os.path.dirname(dst))
+    os.symlink(os.path.abspath(src), dst)
+
+def pathjoin(*args):
+    """Safe wrapper for os.path.join: asserts that all but the first
+    argument are relative paths."""
+    for seg in args[1:]:
+        assert seg[0] != "/"
+    return os.path.join(*args)
+
+
+cmdline_doc = """\
+Usage:
+  python bundlebuilder.py [options] command
+  python mybuildscript.py [options] command
+
+Commands:
+  build      build the application
+  report     print a report
+
+Options:
+  -b, --builddir=DIR     the build directory; defaults to "build"
+  -n, --name=NAME        application name
+  -r, --resource=FILE    extra file or folder to be copied to Resources
+  -f, --file=SRC:DST     extra file or folder to be copied into the bundle;
+                         DST must be a path relative to the bundle root
+  -e, --executable=FILE  the executable to be used
+  -m, --mainprogram=FILE the Python main program
+  -a, --argv             add a wrapper main program to create sys.argv
+  -p, --plist=FILE       .plist file (default: generate one)
+      --nib=NAME         main nib name
+  -c, --creator=CCCC     4-char creator code (default: '????')
+      --iconfile=FILE    filename of the icon (an .icns file) to be used
+                         as the Finder icon
+      --bundle-id=ID     the CFBundleIdentifier, in reverse-dns format
+                         (eg. org.python.BuildApplet; this is used for
+                         the preferences file name)
+  -l, --link             symlink files/folder instead of copying them
+      --link-exec        symlink the executable instead of copying it
+      --standalone       build a standalone application, which is fully
+                         independent of a Python installation
+      --semi-standalone  build a standalone application, which depends on
+                         an installed Python, yet includes all third-party
+                         modules.
+      --python=FILE      Python to use in #! line in stead of current Python
+      --lib=FILE         shared library or framework to be copied into
+                         the bundle
+  -x, --exclude=MODULE   exclude module (with --(semi-)standalone)
+  -i, --include=MODULE   include module (with --(semi-)standalone)
+      --package=PACKAGE  include a whole package (with --(semi-)standalone)
+      --strip            strip binaries (remove debug info)
+  -v, --verbose          increase verbosity level
+  -q, --quiet            decrease verbosity level
+  -h, --help             print this message
+"""
+
+def usage(msg=None):
+    if msg:
+        print(msg)
+    print(cmdline_doc)
+    sys.exit(1)
+
+def main(builder=None):
+    if builder is None:
+        builder = AppBuilder(verbosity=1)
+
+    shortopts = "b:n:r:f:e:m:c:p:lx:i:hvqa"
+    longopts = ("builddir=", "name=", "resource=", "file=", "executable=",
+        "mainprogram=", "creator=", "nib=", "plist=", "link",
+        "link-exec", "help", "verbose", "quiet", "argv", "standalone",
+        "exclude=", "include=", "package=", "strip", "iconfile=",
+        "lib=", "python=", "semi-standalone", "bundle-id=", "destroot=")
+
+    try:
+        options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
+    except getopt.error:
+        usage()
+
+    for opt, arg in options:
+        if opt in ('-b', '--builddir'):
+            builder.builddir = arg
+        elif opt in ('-n', '--name'):
+            builder.name = arg
+        elif opt in ('-r', '--resource'):
+            builder.resources.append(os.path.normpath(arg))
+        elif opt in ('-f', '--file'):
+            srcdst = arg.split(':')
+            if len(srcdst) != 2:
+                usage("-f or --file argument must be two paths, "
+                      "separated by a colon")
+            builder.files.append(srcdst)
+        elif opt in ('-e', '--executable'):
+            builder.executable = arg
+        elif opt in ('-m', '--mainprogram'):
+            builder.mainprogram = arg
+        elif opt in ('-a', '--argv'):
+            builder.argv_emulation = 1
+        elif opt in ('-c', '--creator'):
+            builder.creator = arg
+        elif opt == '--bundle-id':
+            builder.bundle_id = arg
+        elif opt == '--iconfile':
+            builder.iconfile = arg
+        elif opt == "--lib":
+            builder.libs.append(os.path.normpath(arg))
+        elif opt == "--nib":
+            builder.nibname = arg
+        elif opt in ('-p', '--plist'):
+            builder.plist = Plist.fromFile(arg)
+        elif opt in ('-l', '--link'):
+            builder.symlink = 1
+        elif opt == '--link-exec':
+            builder.symlink_exec = 1
+        elif opt in ('-h', '--help'):
+            usage()
+        elif opt in ('-v', '--verbose'):
+            builder.verbosity += 1
+        elif opt in ('-q', '--quiet'):
+            builder.verbosity -= 1
+        elif opt == '--standalone':
+            builder.standalone = 1
+        elif opt == '--semi-standalone':
+            builder.semi_standalone = 1
+        elif opt == '--python':
+            builder.python = arg
+        elif opt in ('-x', '--exclude'):
+            builder.excludeModules.append(arg)
+        elif opt in ('-i', '--include'):
+            builder.includeModules.append(arg)
+        elif opt == '--package':
+            builder.includePackages.append(arg)
+        elif opt == '--strip':
+            builder.strip = 1
+        elif opt == '--destroot':
+            builder.destroot = arg
+
+    if len(args) != 1:
+        usage("Must specify one command ('build', 'report' or 'help')")
+    command = args[0]
+
+    if command == "build":
+        builder.setup()
+        builder.build()
+    elif command == "report":
+        builder.setup()
+        builder.report()
+    elif command == "help":
+        usage()
+    else:
+        usage("Unknown command '%s'" % command)
+
+
+def buildapp(**kwargs):
+    builder = AppBuilder(**kwargs)
+    main(builder)
+
+
+if __name__ == "__main__":
+    main()