diff options
Diffstat (limited to 'Mac/scripts/buildpkg.py')
-rw-r--r-- | Mac/scripts/buildpkg.py | 464 |
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() |