summaryrefslogtreecommitdiffstats
path: root/Mac/scripts/buildpkg.py
diff options
context:
space:
mode:
Diffstat (limited to 'Mac/scripts/buildpkg.py')
-rw-r--r--Mac/scripts/buildpkg.py464
1 files changed, 464 insertions, 0 deletions
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()