git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@19793 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
		
			
				
	
	
		
			471 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			471 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/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
 | 
						|
 | 
						|
Error = "buildpkg.Error"
 | 
						|
 | 
						|
PKG_INFO_FIELDS = """\
 | 
						|
Title
 | 
						|
Version
 | 
						|
Description
 | 
						|
DefaultLocation
 | 
						|
Diskname
 | 
						|
DeleteWarning
 | 
						|
NeedsAuthorization
 | 
						|
DisableStop
 | 
						|
UseUserMask
 | 
						|
Application
 | 
						|
Relocatable
 | 
						|
Required
 | 
						|
InstallOnly
 | 
						|
RequiresReboot
 | 
						|
RootVolumeOnly
 | 
						|
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',
 | 
						|
        'RootVolumeOnly' : '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.sourceFolder = 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.sourceFolder = root
 | 
						|
        if resources == None:
 | 
						|
            self.resourceFolder = root
 | 
						|
        else:
 | 
						|
            self.resourceFolder = resources
 | 
						|
 | 
						|
        # 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
 | 
						|
            elif not k in ["OutputDir"]:
 | 
						|
                raise Error, "Unknown package option: %s" % k
 | 
						|
 | 
						|
        # Check where we should leave the output. Default is current directory
 | 
						|
        outputdir = options.get("OutputDir", os.getcwd())
 | 
						|
        packageName = self.packageInfo["Title"]
 | 
						|
        self.PackageRootFolder = os.path.join(outputdir, packageName + ".pkg")
 | 
						|
 | 
						|
        # 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"]) # ??
 | 
						|
 | 
						|
        contFolder = join(self.PackageRootFolder, "Contents")
 | 
						|
        self.packageResourceFolder = join(contFolder, "Resources")
 | 
						|
        os.mkdir(self.PackageRootFolder)
 | 
						|
        os.mkdir(contFolder)
 | 
						|
        os.mkdir(self.packageResourceFolder)
 | 
						|
 | 
						|
    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 = self.packageInfo["Title"] + ".info"
 | 
						|
        path = join(self.packageResourceFolder, 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 = self.packageInfo["Title"] + ".bom"
 | 
						|
            bomPath = join(self.packageResourceFolder, base)
 | 
						|
            cmd = "mkbom %s %s" % (self.sourceFolder, 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()
 | 
						|
 | 
						|
        # create archive
 | 
						|
        os.chdir(self.sourceFolder)
 | 
						|
        base = basename(self.packageInfo["Title"]) + ".pax"
 | 
						|
        self.archPath = join(self.packageResourceFolder, base)
 | 
						|
        cmd = "pax -w -f %s %s" % (self.archPath, ".")
 | 
						|
        res = os.system(cmd)
 | 
						|
 | 
						|
        # compress archive
 | 
						|
        cmd = "gzip %s" % self.archPath
 | 
						|
        res = os.system(cmd)
 | 
						|
        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.resourceFolder:
 | 
						|
            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.resourceFolder, pat)
 | 
						|
            allFiles = allFiles + glob.glob(pattern)
 | 
						|
 | 
						|
        # find pre-process and post-process scripts
 | 
						|
        # naming convention: packageName.{pre,post}_{upgrade,install}
 | 
						|
        # Alternatively the filenames can be {pre,post}_{upgrade,install}
 | 
						|
        # in which case we prepend the package name
 | 
						|
        packageName = self.packageInfo["Title"]
 | 
						|
        for pat in ("*upgrade", "*install", "*flight"):
 | 
						|
            pattern = join(self.resourceFolder, packageName + pat)
 | 
						|
            pattern2 = join(self.resourceFolder, pat)
 | 
						|
            allFiles = allFiles + glob.glob(pattern)
 | 
						|
            allFiles = allFiles + glob.glob(pattern2)
 | 
						|
 | 
						|
        # check name patterns
 | 
						|
        files = []
 | 
						|
        for f in allFiles:
 | 
						|
            for s in ("Welcome", "License", "ReadMe"):
 | 
						|
                if string.find(basename(f), s) == 0:
 | 
						|
                    files.append((f, f))
 | 
						|
            if f[-6:] == ".lproj":
 | 
						|
                files.append((f, f))
 | 
						|
            elif basename(f) in ["pre_upgrade", "pre_install", "post_upgrade", "post_install"]:
 | 
						|
                files.append((f, packageName+"."+basename(f)))
 | 
						|
            elif basename(f) in ["preflight", "postflight"]:
 | 
						|
                files.append((f, f))
 | 
						|
            elif f[-8:] == "_upgrade":
 | 
						|
                files.append((f,f))
 | 
						|
            elif f[-8:] == "_install":
 | 
						|
                files.append((f,f))
 | 
						|
 | 
						|
        # copy files
 | 
						|
        for src, dst in files:
 | 
						|
            src = basename(src)
 | 
						|
            dst = basename(dst)
 | 
						|
            f = join(self.resourceFolder, src)
 | 
						|
            if isfile(f):
 | 
						|
                shutil.copy(f, os.path.join(self.packageResourceFolder, dst))
 | 
						|
            elif isdir(f):
 | 
						|
                # special case for .rtfd and .lproj folders...
 | 
						|
                d = join(self.packageResourceFolder, dst)
 | 
						|
                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
 | 
						|
 | 
						|
        files = GlobDirectoryWalker(self.sourceFolder)
 | 
						|
        for f in files:
 | 
						|
            numFiles = numFiles + 1
 | 
						|
            installedSize = installedSize + os.lstat(f)[6]
 | 
						|
 | 
						|
        try:
 | 
						|
            zippedSize = os.stat(self.archPath+ ".gz")[6]
 | 
						|
        except OSError: # ignore error
 | 
						|
            pass
 | 
						|
        base = self.packageInfo["Title"] + ".sizes"
 | 
						|
        f = open(join(self.packageResourceFolder, base), "w")
 | 
						|
        format = "NumFiles %d\nInstalledSize %d\nCompressedSize %d\n"
 | 
						|
        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()
 |