Script to generate .pkg packages, donated by Dinu Gherman. This is his
original code, it still needs fiddling to make it work in general
circumstances.
diff --git a/Mac/scripts/buildpkg.py b/Mac/scripts/buildpkg.py
new file mode 100644
index 0000000..44e2662
--- /dev/null
+++ b/Mac/scripts/buildpkg.py
@@ -0,0 +1,464 @@
+#!/usr/bin/env python
+
+"""buildpkg.py -- Build OS X packages for Apple's Installer.app.
+
+This is an experimental command-line tool for building packages to be
+installed with the Mac OS X Installer.app application. 
+
+It is much inspired by Apple's GUI tool called PackageMaker.app, that 
+seems to be part of the OS X developer tools installed in the folder 
+/Developer/Applications. But apparently there are other free tools to 
+do the same thing which are also named PackageMaker like Brian Hill's 
+one: 
+
+  http://personalpages.tds.net/~brian_hill/packagemaker.html
+
+Beware of the multi-package features of Installer.app (which are not 
+yet supported here) that can potentially screw-up your installation 
+and are discussed in these articles on Stepwise:
+
+  http://www.stepwise.com/Articles/Technical/Packages/InstallerWoes.html
+  http://www.stepwise.com/Articles/Technical/Packages/InstallerOnX.html
+
+Beside using the PackageMaker class directly, by importing it inside 
+another module, say, there are additional ways of using this module:
+the top-level buildPackage() function provides a shortcut to the same 
+feature and is also called when using this module from the command-
+line.
+
+    ****************************************************************
+    NOTE: For now you should be able to run this even on a non-OS X 
+          system and get something similar to a package, but without
+          the real archive (needs pax) and bom files (needs mkbom) 
+          inside! This is only for providing a chance for testing to 
+          folks without OS X.
+    ****************************************************************
+
+TODO:
+  - test pre-process and post-process scripts (Python ones?)
+  - handle multi-volume packages (?)
+  - integrate into distutils (?)
+
+Dinu C. Gherman, 
+gherman@europemail.com
+November 2001
+
+!! USE AT YOUR OWN RISK !!
+"""
+
+__version__ = 0.2
+__license__ = "FreeBSD"
+
+
+import os, sys, glob, fnmatch, shutil, string, copy, getopt
+from os.path import basename, dirname, join, islink, isdir, isfile
+
+
+PKG_INFO_FIELDS = """\
+Title
+Version
+Description
+DefaultLocation
+Diskname
+DeleteWarning
+NeedsAuthorization
+DisableStop
+UseUserMask
+Application
+Relocatable
+Required
+InstallOnly
+RequiresReboot
+InstallFat\
+"""
+
+######################################################################
+# Helpers
+######################################################################
+
+# Convenience class, as suggested by /F.
+
+class GlobDirectoryWalker:
+    "A forward iterator that traverses files in a directory tree."
+
+    def __init__(self, directory, pattern="*"):
+        self.stack = [directory]
+        self.pattern = pattern
+        self.files = []
+        self.index = 0
+
+
+    def __getitem__(self, index):
+        while 1:
+            try:
+                file = self.files[self.index]
+                self.index = self.index + 1
+            except IndexError:
+                # pop next directory from stack
+                self.directory = self.stack.pop()
+                self.files = os.listdir(self.directory)
+                self.index = 0
+            else:
+                # got a filename
+                fullname = join(self.directory, file)
+                if isdir(fullname) and not islink(fullname):
+                    self.stack.append(fullname)
+                if fnmatch.fnmatch(file, self.pattern):
+                    return fullname
+
+
+######################################################################
+# The real thing
+######################################################################
+
+class PackageMaker:
+    """A class to generate packages for Mac OS X.
+
+    This is intended to create OS X packages (with extension .pkg)
+    containing archives of arbitrary files that the Installer.app 
+    will be able to handle.
+
+    As of now, PackageMaker instances need to be created with the 
+    title, version and description of the package to be built. 
+    The package is built after calling the instance method 
+    build(root, **options). It has the same name as the constructor's 
+    title argument plus a '.pkg' extension and is located in the same 
+    parent folder that contains the root folder.
+
+    E.g. this will create a package folder /my/space/distutils.pkg/:
+
+      pm = PackageMaker("distutils", "1.0.2", "Python distutils.")
+      pm.build("/my/space/distutils")
+    """
+
+    packageInfoDefaults = {
+        'Title': None,
+        'Version': None,
+        'Description': '',
+        'DefaultLocation': '/',
+        'Diskname': '(null)',
+        'DeleteWarning': '',
+        'NeedsAuthorization': 'NO',
+        'DisableStop': 'NO',
+        'UseUserMask': 'YES',
+        'Application': 'NO',
+        'Relocatable': 'YES',
+        'Required': 'NO',
+        'InstallOnly': 'NO',
+        'RequiresReboot': 'NO',
+        'InstallFat': 'NO'}
+
+
+    def __init__(self, title, version, desc):
+        "Init. with mandatory title/version/description arguments."
+
+        info = {"Title": title, "Version": version, "Description": desc}
+        self.packageInfo = copy.deepcopy(self.packageInfoDefaults)
+        self.packageInfo.update(info)
+        
+        # variables set later
+        self.packageRootFolder = None
+        self.packageResourceFolder = None
+        self.resourceFolder = None
+
+
+    def build(self, root, resources=None, **options):
+        """Create a package for some given root folder.
+
+        With no 'resources' argument set it is assumed to be the same 
+        as the root directory. Option items replace the default ones 
+        in the package info.
+        """
+
+        # set folder attributes
+        self.packageRootFolder = root
+        if resources == None:
+            self.packageResourceFolder = root
+
+        # replace default option settings with user ones if provided
+        fields = self. packageInfoDefaults.keys()
+        for k, v in options.items():
+            if k in fields:
+                self.packageInfo[k] = v
+
+        # do what needs to be done
+        self._makeFolders()
+        self._addInfo()
+        self._addBom()
+        self._addArchive()
+        self._addResources()
+        self._addSizes()
+
+
+    def _makeFolders(self):
+        "Create package folder structure."
+
+        # Not sure if the package name should contain the version or not...
+        # packageName = "%s-%s" % (self.packageInfo["Title"], 
+        #                          self.packageInfo["Version"]) # ??
+
+        packageName = self.packageInfo["Title"]
+        rootFolder = packageName + ".pkg"
+        contFolder = join(rootFolder, "Contents")
+        resourceFolder = join(contFolder, "Resources")
+        os.mkdir(rootFolder)
+        os.mkdir(contFolder)
+        os.mkdir(resourceFolder)
+
+        self.resourceFolder = resourceFolder
+
+
+    def _addInfo(self):
+        "Write .info file containing installing options."
+
+        # Not sure if options in PKG_INFO_FIELDS are complete...
+
+        info = ""
+        for f in string.split(PKG_INFO_FIELDS, "\n"):
+            info = info + "%s %%(%s)s\n" % (f, f)
+        info = info % self.packageInfo
+        base = basename(self.packageRootFolder) + ".info"
+        path = join(self.resourceFolder, base)
+        f = open(path, "w")
+        f.write(info)
+
+
+    def _addBom(self):
+        "Write .bom file containing 'Bill of Materials'."
+
+        # Currently ignores if the 'mkbom' tool is not available.
+
+        try:
+            base = basename(self.packageRootFolder) + ".bom"
+            bomPath = join(self.resourceFolder, base)
+            cmd = "mkbom %s %s" % (self.packageRootFolder, bomPath)
+            res = os.system(cmd)
+        except:
+            pass
+
+
+    def _addArchive(self):
+        "Write .pax.gz file, a compressed archive using pax/gzip."
+
+        # Currently ignores if the 'pax' tool is not available.
+
+        cwd = os.getcwd()
+
+        packageRootFolder = self.packageRootFolder
+
+        try:
+            # create archive
+            d = dirname(packageRootFolder)
+            os.chdir(packageRootFolder)
+            base = basename(packageRootFolder) + ".pax"
+            archPath = join(d, self.resourceFolder, base)
+            cmd = "pax -w -f %s %s" % (archPath, ".")
+            res = os.system(cmd)
+
+            # compress archive
+            cmd = "gzip %s" % archPath
+            res = os.system(cmd)
+        except:
+            pass
+
+        os.chdir(cwd)
+
+
+    def _addResources(self):
+        "Add Welcome/ReadMe/License files, .lproj folders and scripts."
+
+        # Currently we just copy everything that matches the allowed 
+        # filenames. So, it's left to Installer.app to deal with the 
+        # same file available in multiple formats...
+
+        if not self.packageResourceFolder:
+            return
+
+        # find candidate resource files (txt html rtf rtfd/ or lproj/)
+        allFiles = []
+        for pat in string.split("*.txt *.html *.rtf *.rtfd *.lproj", " "):
+            pattern = join(self.packageResourceFolder, pat)
+            allFiles = allFiles + glob.glob(pattern)
+
+        # find pre-process and post-process scripts
+        # naming convention: packageName.{pre,post}-{upgrade,install}
+        packageName = self.packageInfo["Title"]
+        for pat in ("*upgrade", "*install"):
+            pattern = join(self.packageResourceFolder, packageName + pat)
+            allFiles = allFiles + glob.glob(pattern)
+
+        # check name patterns
+        files = []
+        for f in allFiles:
+            for s in ("Welcome", "License", "ReadMe"):
+                if string.find(basename(f), s) == 0:
+                    files.append(f)
+            if f[-6:] == ".lproj":
+                files.append(f)
+            elif f[-8:] == "-upgrade":
+                files.append(f)
+            elif f[-8:] == "-install":
+                files.append(f)
+
+        # copy files
+        for g in files:
+            f = join(self.packageResourceFolder, g)
+            if isfile(f):
+                shutil.copy(f, self.resourceFolder)
+            elif isdir(f):
+                # special case for .rtfd and .lproj folders...
+                d = join(self.resourceFolder, basename(f))
+                os.mkdir(d)
+                files = GlobDirectoryWalker(f)
+                for file in files:
+                    shutil.copy(file, d)
+
+
+    def _addSizes(self):
+        "Write .sizes file with info about number and size of files."
+
+        # Not sure if this is correct, but 'installedSize' and 
+        # 'zippedSize' are now in Bytes. Maybe blocks are needed? 
+        # Well, Installer.app doesn't seem to care anyway, saying 
+        # the installation needs 100+ MB...
+
+        numFiles = 0
+        installedSize = 0
+        zippedSize = 0
+
+        packageRootFolder = self.packageRootFolder
+
+        files = GlobDirectoryWalker(packageRootFolder)
+        for f in files:
+            numFiles = numFiles + 1
+            installedSize = installedSize + os.stat(f)[6]
+
+        d = dirname(packageRootFolder)
+        base = basename(packageRootFolder) + ".pax.gz"
+        archPath = join(d, self.resourceFolder, base)
+        try:
+            zippedSize = os.stat(archPath)[6]
+        except OSError: # ignore error 
+            pass
+        base = basename(packageRootFolder) + ".sizes"
+        f = open(join(self.resourceFolder, base), "w")
+        format = "NumFiles %d\nInstalledSize %d\nCompressedSize %d"
+        f.write(format % (numFiles, installedSize, zippedSize))
+
+
+# Shortcut function interface
+
+def buildPackage(*args, **options):
+    "A Shortcut function for building a package."
+    
+    o = options
+    title, version, desc = o["Title"], o["Version"], o["Description"]
+    pm = PackageMaker(title, version, desc)
+    apply(pm.build, list(args), options)
+
+
+######################################################################
+# Tests
+######################################################################
+
+def test0():
+    "Vanilla test for the distutils distribution."
+
+    pm = PackageMaker("distutils2", "1.0.2", "Python distutils package.")
+    pm.build("/Users/dinu/Desktop/distutils2")
+
+
+def test1():
+    "Test for the reportlab distribution with modified options."
+
+    pm = PackageMaker("reportlab", "1.10", 
+                      "ReportLab's Open Source PDF toolkit.")
+    pm.build(root="/Users/dinu/Desktop/reportlab", 
+             DefaultLocation="/Applications/ReportLab",
+             Relocatable="YES")
+
+def test2():
+    "Shortcut test for the reportlab distribution with modified options."
+
+    buildPackage(
+        "/Users/dinu/Desktop/reportlab", 
+        Title="reportlab", 
+        Version="1.10", 
+        Description="ReportLab's Open Source PDF toolkit.",
+        DefaultLocation="/Applications/ReportLab",
+        Relocatable="YES")
+
+
+######################################################################
+# Command-line interface
+######################################################################
+
+def printUsage():
+    "Print usage message."
+
+    format = "Usage: %s <opts1> [<opts2>] <root> [<resources>]"
+    print format % basename(sys.argv[0])
+    print
+    print "       with arguments:"
+    print "           (mandatory) root:         the package root folder"
+    print "           (optional)  resources:    the package resources folder"
+    print
+    print "       and options:"
+    print "           (mandatory) opts1:"
+    mandatoryKeys = string.split("Title Version Description", " ")
+    for k in mandatoryKeys:
+        print "               --%s" % k
+    print "           (optional) opts2: (with default values)"
+
+    pmDefaults = PackageMaker.packageInfoDefaults
+    optionalKeys = pmDefaults.keys()
+    for k in mandatoryKeys:
+        optionalKeys.remove(k)
+    optionalKeys.sort()
+    maxKeyLen = max(map(len, optionalKeys))
+    for k in optionalKeys:
+        format = "               --%%s:%s %%s"
+        format = format % (" " * (maxKeyLen-len(k)))
+        print format % (k, repr(pmDefaults[k]))
+
+
+def main():
+    "Command-line interface."
+
+    shortOpts = ""
+    keys = PackageMaker.packageInfoDefaults.keys()
+    longOpts = map(lambda k: k+"=", keys)
+
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], shortOpts, longOpts)
+    except getopt.GetoptError, details:
+        print details
+        printUsage()
+        return
+
+    optsDict = {}
+    for k, v in opts:
+        optsDict[k[2:]] = v
+
+    ok = optsDict.keys()
+    if not (1 <= len(args) <= 2):
+        print "No argument given!"
+    elif not ("Title" in ok and \
+              "Version" in ok and \
+              "Description" in ok):
+        print "Missing mandatory option!"
+    else:
+        apply(buildPackage, args, optsDict)
+        return
+
+    printUsage()
+
+    # sample use:
+    # buildpkg.py --Title=distutils \
+    #             --Version=1.0.2 \
+    #             --Description="Python distutils package." \
+    #             /Users/dinu/Desktop/distutils
+
+
+if __name__ == "__main__":
+    main()