"""\
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(<lots-of-keyword-args>)
"""
#
# 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
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
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[:]
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)
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 = """\
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.)
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:
+ self.mainprogram = mainprogram
+ self.executable = executable
+ self.nibname = nibname
+ BundleBuilder.__init__(self, name=name, **kwargs)
+
+ 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 name is not None:
+
+ if self.name is not None:
pass
- elif mainprogram is not None:
- name = os.path.splitext(os.path.basename(mainprogram))[0]
+ elif self.mainprogram is not None:
+ self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
elif executable is not None:
- name = os.path.splitext(os.path.basename(executable))[0]
- if name[-4:] != ".app":
- name += ".app"
+ self.name = os.path.splitext(os.path.basename(self.executable))[0]
+ if self.name[-4:] != ".app":
+ self.name += ".app"
+ self.plist.CFBundleExecutable = self.name
- self.mainprogram = mainprogram
- self.executable = executable
+ if self.nibname:
+ self.plist.NSMainNibFile = self.nibname
+ if not hasattr(self.plist, "NSPrincipalClass"):
+ self.plist.NSPrincipalClass = "NSApplication"
- BundleBuilder.__init__(self, name=name, **kwargs)
+ BundleBuilder.setup(self)
def preProcess(self):
- self.plist.CFBundleExecutable = self.name
+ 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)
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:
return os.path.join(*args)
-def buildapp(**kwargs):
- # XXX cmd line argument parsing
- builder = AppBuilder(**kwargs)
- builder.build()
+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 buildcocoaapp(**kwargs):
- # XXX cmd line argument parsing
- builder = CocoaAppBuilder(**kwargs)
- builder.build()
+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):
+ builder = AppBuilder(**kwargs)
+ 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()