diff --git a/Mac/Lib/bundlebuilder.py b/Mac/Lib/bundlebuilder.py index 319522be8ea..ffcda5b1c0f 100755 --- a/Mac/Lib/bundlebuilder.py +++ b/Mac/Lib/bundlebuilder.py @@ -3,31 +3,39 @@ """\ bundlebuilder.py -- Tools to assemble MacOS X (application) bundles. -This module contains three classes to build so called "bundles" for +This module contains two classes to build so called "bundles" for MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass -specialized in building application bundles. CocoaAppBuilder is a -further specialization of AppBuilder. +specialized in building application bundles. -[Bundle|App|CocoaApp]Builder objects are instantiated with a bunch -of keyword arguments, and have a build() method that will do all the -work. See the class doc strings for a description of the constructor -arguments. +[Bundle|App]Builder objects are instantiated with a bunch of keyword +arguments, and have a build() method that will do all the work. See +the class doc strings for a description of the constructor arguments. + +The module contains a main program that can be used in two ways: + + % python bundlebuilder.py [options] build + % python buildapp.py [options] build + +Where "buildapp.py" is a user-supplied setup.py-like script following +this model: + + from bundlebuilder import buildapp + buildapp() """ # # XXX Todo: -# - a command line interface, also for use with the buildapp() and -# buildcocoaapp() convenience functions. # - modulefinder support to build standalone apps +# - consider turning this into a distutils extension # -__all__ = ["BundleBuilder", "AppBuilder", "CocoaAppBuilder", - "buildapp", "buildcocoaapp"] +__all__ = ["BundleBuilder", "AppBuilder", "buildapp"] import sys import os, errno, shutil +import getopt from plistlib import Plist @@ -62,34 +70,43 @@ class BundleBuilder: verbosity: verbosity level, defaults to 1 """ - def __init__(self, name, plist=None, type="APPL", creator="????", + def __init__(self, name=None, plist=None, type="APPL", creator="????", resources=None, files=None, builddir="build", platform="MacOS", symlink=0, verbosity=1): """See the class doc string for a description of the arguments.""" - self.name, ext = os.path.splitext(name) - if not ext: - ext = ".bundle" - self.bundleextension = ext if plist is None: plist = Plist() + if resources is None: + resources = [] + if files is None: + files = [] + self.name = name self.plist = plist self.type = type self.creator = creator - if files is None: - files = [] - if resources is None: - resources = [] self.resources = resources self.files = files self.builddir = builddir self.platform = platform self.symlink = symlink - # misc (derived) attributes - self.bundlepath = pathjoin(builddir, self.name + self.bundleextension) - self.execdir = pathjoin("Contents", platform) - self.resdir = pathjoin("Contents", "Resources") self.verbosity = verbosity + def setup(self): + self.name, ext = os.path.splitext(self.name) + if not ext: + ext = ".bundle" + self.bundleextension = ext + # misc (derived) attributes + self.bundlepath = pathjoin(self.builddir, self.name + self.bundleextension) + self.execdir = pathjoin("Contents", self.platform) + + plist = plistDefaults.copy() + plist.CFBundleName = self.name + plist.CFBundlePackageType = self.type + plist.CFBundleSignature = self.creator + plist.update(self.plist) + self.plist = plist + def build(self): """Build the bundle.""" builddir = self.builddir @@ -124,13 +141,8 @@ def _addMetaFiles(self): f.close() # # Write Contents/Info.plist - plist = plistDefaults.copy() - plist.CFBundleName = self.name - plist.CFBundlePackageType = self.type - plist.CFBundleSignature = self.creator - plist.update(self.plist) infoplist = pathjoin(contents, "Info.plist") - plist.write(infoplist) + self.plist.write(infoplist) def _copyFiles(self): files = self.files[:] @@ -144,7 +156,10 @@ def _copyFiles(self): self.message("Copying files", 1) msg = "Copying" for src, dst in files: - self.message("%s %s to %s" % (msg, src, dst), 2) + if os.path.isdir(src): + self.message("%s %s/ to %s/" % (msg, src, dst), 2) + else: + self.message("%s %s to %s" % (msg, src, dst), 2) dst = pathjoin(self.bundlepath, dst) if self.symlink: symlink(src, dst, mkdirs=1) @@ -153,7 +168,15 @@ def _copyFiles(self): def message(self, msg, level=0): if level <= self.verbosity: - sys.stderr.write(msg + "\n") + indent = "" + if level > 1: + indent = (level - 1) * " " + sys.stderr.write(indent + msg + "\n") + + def report(self): + # XXX something decent + import pprint + pprint.pprint(self.__dict__) mainWrapperTemplate = """\ @@ -166,18 +189,20 @@ def message(self, msg, level=0): mainprogram = os.path.join(resources, "%(mainprogram)s") assert os.path.exists(mainprogram) argv.insert(1, mainprogram) -%(executable)s +os.environ["PYTHONPATH"] = resources +%(setpythonhome)s +%(setexecutable)s os.execve(executable, argv, os.environ) """ -executableTemplate = "executable = os.path.join(resources, \"%s\")" - +setExecutableTemplate = """executable = os.path.join(resources, "%s")""" +pythonhomeSnippet = """os.environ["home"] = resources""" class AppBuilder(BundleBuilder): """This class extends the BundleBuilder constructor with these arguments: - + mainprogram: A Python main program. If this argument is given, the main executable in the bundle will be a small wrapper that invokes the main program. (XXX Discuss why.) @@ -185,46 +210,59 @@ class AppBuilder(BundleBuilder): specified the executable will be copied to Resources and be invoked by the wrapper program mentioned above. Else it will simply be used as the main executable. - + nibname: The name of the main nib, for Cocoa apps. Defaults + to None, but must be specified when building a Cocoa app. + For the other keyword arguments see the BundleBuilder doc string. """ def __init__(self, name=None, mainprogram=None, executable=None, - **kwargs): + nibname=None, **kwargs): """See the class doc string for a description of the arguments.""" - if mainprogram is None and executable is None: - raise TypeError, ("must specify either or both of " - "'executable' and 'mainprogram'") - if name is not None: - pass - elif mainprogram is not None: - name = os.path.splitext(os.path.basename(mainprogram))[0] - elif executable is not None: - name = os.path.splitext(os.path.basename(executable))[0] - if name[-4:] != ".app": - name += ".app" - self.mainprogram = mainprogram self.executable = executable - + self.nibname = nibname BundleBuilder.__init__(self, name=name, **kwargs) - def preProcess(self): + def setup(self): + if self.mainprogram is None and self.executable is None: + raise TypeError, ("must specify either or both of " + "'executable' and 'mainprogram'") + + if self.name is not None: + pass + elif self.mainprogram is not None: + self.name = os.path.splitext(os.path.basename(self.mainprogram))[0] + elif executable is not None: + self.name = os.path.splitext(os.path.basename(self.executable))[0] + if self.name[-4:] != ".app": + self.name += ".app" self.plist.CFBundleExecutable = self.name + + if self.nibname: + self.plist.NSMainNibFile = self.nibname + if not hasattr(self.plist, "NSPrincipalClass"): + self.plist.NSPrincipalClass = "NSApplication" + + BundleBuilder.setup(self) + + def preProcess(self): + resdir = pathjoin("Contents", "Resources") if self.executable is not None: if self.mainprogram is None: execpath = pathjoin(self.execdir, self.name) else: - execpath = pathjoin(self.resdir, os.path.basename(self.executable)) + execpath = pathjoin(resdir, os.path.basename(self.executable)) self.files.append((self.executable, execpath)) # For execve wrapper - executable = executableTemplate % os.path.basename(self.executable) + setexecutable = setExecutableTemplate % os.path.basename(self.executable) else: - executable = "" # XXX for locals() call + setexecutable = "" # XXX for locals() call if self.mainprogram is not None: + setpythonhome = "" # pythonhomeSnippet if we're making a standalone app mainname = os.path.basename(self.mainprogram) - self.files.append((self.mainprogram, pathjoin(self.resdir, mainname))) + self.files.append((self.mainprogram, pathjoin(resdir, mainname))) # Create execve wrapper mainprogram = self.mainprogram # XXX for locals() call execdir = pathjoin(self.bundlepath, self.execdir) @@ -234,22 +272,6 @@ def preProcess(self): os.chmod(mainwrapperpath, 0777) -class CocoaAppBuilder(AppBuilder): - - """Tiny specialization of AppBuilder. It has an extra constructor - argument called 'nibname' which defaults to 'MainMenu'. It will - set the appropriate fields in the plist. - """ - - def __init__(self, nibname="MainMenu", **kwargs): - """See the class doc string for a description of the arguments.""" - self.nibname = nibname - AppBuilder.__init__(self, **kwargs) - self.plist.NSMainNibFile = self.nibname - if not hasattr(self.plist, "NSPrincipalClass"): - self.plist.NSPrincipalClass = "NSApplication" - - def copy(src, dst, mkdirs=0): """Copy a file or a directory.""" if mkdirs: @@ -287,21 +309,96 @@ def pathjoin(*args): return os.path.join(*args) +cmdline_doc = """\ +Usage: + python [options] command + python mybuildscript.py [options] command + +Commands: + build build the application + report print a report + +Options: + -b, --builddir=DIR the build directory; defaults to "build" + -n, --name=NAME application name + -r, --resource=FILE extra file or folder to be copied to Resources + -e, --executable=FILE the executable to be used + -m, --mainprogram=FILE the Python main program + -p, --plist=FILE .plist file (default: generate one) + --nib=NAME main nib name + -c, --creator=CCCC 4-char creator code (default: '????') + -l, --link symlink files/folder instead of copying them + -v, --verbose increase verbosity level + -q, --quiet decrease verbosity level + -h, --help print this message +""" + +def usage(msg=None): + if msg: + print msg + print cmdline_doc + sys.exit(1) + +def main(builder=None): + if builder is None: + builder = AppBuilder(verbosity=1) + + shortopts = "b:n:r:e:m:c:plhvq" + longopts = ("builddir=", "name=", "resource=", "executable=", + "mainprogram=", "creator=", "nib=", "plist=", "link", "help", + "verbose", "quiet") + + try: + options, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + except getopt.error: + usage() + + for opt, arg in options: + if opt in ('-b', '--builddir'): + builder.builddir = arg + elif opt in ('-n', '--name'): + builder.name = arg + elif opt in ('-r', '--resource'): + builder.resources.append(arg) + elif opt in ('-e', '--executable'): + builder.executable = arg + elif opt in ('-m', '--mainprogram'): + builder.mainprogram = arg + elif opt in ('-c', '--creator'): + builder.creator = arg + elif opt == "--nib": + builder.nibname = arg + elif opt in ('-p', '--plist'): + builder.plist = Plist.fromFile(arg) + elif opt in ('-l', '--link'): + builder.symlink = 1 + elif opt in ('-h', '--help'): + usage() + elif opt in ('-v', '--verbose'): + builder.verbosity += 1 + elif opt in ('-q', '--quiet'): + builder.verbosity -= 1 + + if len(args) != 1: + usage("Must specify one command ('build', 'report' or 'help')") + command = args[0] + + if command == "build": + builder.setup() + builder.build() + elif command == "report": + builder.setup() + builder.report() + elif command == "help": + usage() + else: + usage("Unknown command '%s'" % command) + + def buildapp(**kwargs): - # XXX cmd line argument parsing builder = AppBuilder(**kwargs) - builder.build() - - -def buildcocoaapp(**kwargs): - # XXX cmd line argument parsing - builder = CocoaAppBuilder(**kwargs) - builder.build() + main(builder) if __name__ == "__main__": - # XXX This test is meant to be run in the Examples/TableModel/ folder - # of the pyobj project... It will go as soon as I've written a proper - # main program. - buildcocoaapp(mainprogram="TableModel.py", - resources=["English.lproj", "nibwrapper.py"], verbosity=4) + main()