Commits

Ronald Oussoren committed 989815a

Added a script to build a distribution:
- pyobjc-X.Y.Z.tar.gz containing the sources
- pyobjc-X.Y.Z.pkg containing an installer

Comments (0)

Files changed (2)

pyobjc/Scripts/buildpkg.py

+#!/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
+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.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"):
+            pattern = join(self.resourceFolder, 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, basename(f)))
+            if f[-6:] == ".lproj":
+                files.append((f, basename(f)))
+            elif f in ["pre-upgrade", "pre-install", "post-upgrade", "post-install"]:
+                files.append((f, self.packageInfo["Title"]+"."+basename(f)))
+            elif f[-8:] == "-upgrade":
+                files.append((f,basename(f)))
+            elif f[-8:] == "-install":
+                files.append((f,basename(f)))
+
+        # copy files
+        for src, dst in files:
+            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()

pyobjc/Scripts/make_distrib.py

+#!/usr/bin/env python
+
+import sys
+import getopt
+import os
+import buildpkg
+import shutil
+
+USAGE='Usage: %s [-p python | --with-python=%s] [-h|--help] [-o release-dir|--output-directory=release-dir]\n'%(
+	sys.argv[0], sys.executable)
+
+PYTHON=sys.executable
+OUTPUTDIR='release-dir'
+
+def package_version():
+	fp = open('Modules/objc/pyobjc.h', 'r')  
+	for ln in fp.readlines():
+		if ln.startswith('#define OBJC_VERSION'):
+			fp.close()
+	return ln.split()[-1][1:-1]
+
+	raise ValueError, "Version not found"
+
+
+def escquotes(val):
+	return val.replace("'", "'\"'\"'")
+
+try:
+	opts, args = getopt.getopt(
+		sys.argv[1:],
+		'p:h?o:', ['with-python=', 'help', 'output-directory='])
+except getopt.error, msg:
+	sys.stderr.write('%s: %s\n'%(sys.argv[0], msg))
+	sys.stderr.write(USAGE)
+	sys.exit(1)
+
+for key, value in opts:
+	if key in [ '-h', '-?', '--help' ]:
+		sys.stdout.write(USAGE)
+		sys.exit(0)
+	elif key in [ '-p', '--with-python' ]:
+		PYTHON=value
+	elif key in [ '-o', '--output-directory' ]:
+		OUTPUTDIR=value
+	else:
+		raise ValueError, "Unsupported option: %s=%s"%(key, value)
+
+if not os.path.exists(OUTPUTDIR):
+	try:
+		os.mkdir(OUTPUTDIR)
+	except os.error, msg:
+		sys.stderr.write("%s: Cannot create %s: %s\n"%(
+			sys.argv[0], OUTPUTDIR, msg))
+		sys.exit(1)
+
+
+if PYTHON==sys.executable:
+	PYTHONVER='.'.join(map(str, sys.version_info[:2]))
+	PYTHONPATH=sys.path
+else:
+	fd = os.popen("'%s' -c 'import sys;print \".\".join(map(str, sys.version_info[:2]))'"%(
+		escquotes(PYTHON)))
+	PYTHONVER=fd.readline().strip()
+	fd = os.popen("'%s' -c 'import sys;print \"\\n\".join(sys.path)'"%(
+		escquotes(PYTHON)))
+	PYTHONPATH=map(lambda x:x[:-1], fd.readlines())
+
+
+basedir = ''
+for p in PYTHONPATH:
+	if p.endswith('lib/python%s'%PYTHONVER):
+		basedir = os.path.split(os.path.split(p)[0])[0]
+		break
+
+if not basedir:
+	sys.stderr.write("%s: Cannot determine basedir\n"%(sys.argv[0]))
+	sys.exit(1)
+
+print "Running: '%s' setup.py sdist -d '%s'"%(
+			escquotes(PYTHON), escquotes(OUTPUTDIR))
+fd = os.popen("'%s' setup.py sdist -d '%s'"%(
+			escquotes(PYTHON), escquotes(OUTPUTDIR)), 'r')
+for ln in fd.xreadlines():
+	sys.stdout.write(ln)
+
+print "Running: '%s' setup.py install --prefix='%s/package%s'"%(
+	escquotes(PYTHON), escquotes(OUTPUTDIR), escquotes(basedir))
+fd = os.popen("'%s' setup.py install --prefix='%s/package%s'"%(
+	escquotes(PYTHON), escquotes(OUTPUTDIR), escquotes(basedir)), 'r')
+for ln in fd.xreadlines():
+	sys.stdout.write(ln)
+
+print "Setting up developer templates"
+
+basedir = '%s/package'%(OUTPUTDIR)
+os.mkdir(os.path.join(basedir, 'Developer'))
+os.mkdir(os.path.join(basedir, 'Developer', 'ProjectBuilder Extras'))
+os.mkdir(os.path.join(basedir, 'Developer', 'ProjectBuilder Extras', 'Project Templates'))
+os.mkdir(os.path.join(basedir, 'Developer', 'ProjectBuilder Extras', 'Project Templates', 'Application'))
+shutil.copytree('Project Templates/Cocoa-Python Application', os.path.join(basedir, 'Developer', 'ProjectBuilder Extras', 'Project Templates', 'Application', 'Cocoa-Python Application'))
+
+print 'Building package'
+pm = buildpkg.PackageMaker('PyObjC', package_version(), 
+"""\
+Python <-> Objective-C bridge that supports building full featured Cocoa
+applications.
+""")
+pm.build(os.path.join(basedir), 
+	resources=os.path.join(os.getcwd(), 'Installer Package', 'Resources'),
+	OutputDir=os.path.join(os.getcwd(), OUTPUTDIR),
+	Version=package_version(),
+	NeedsAuthorization="YES",
+	Relocatable="NO")
+
+print "Done, don't forget to test the output!"