added command line interface; refactored a bit; little things.

This commit is contained in:
Just van Rossum 2002-11-21 23:19:37 +00:00
parent 7d791240c0
commit ceeb9627c1
1 changed files with 182 additions and 85 deletions

View File

@ -3,31 +3,39 @@
"""\ """\
bundlebuilder.py -- Tools to assemble MacOS X (application) bundles. 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 MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
specialized in building application bundles. CocoaAppBuilder is a specialized in building application bundles.
further specialization of AppBuilder.
[Bundle|App|CocoaApp]Builder objects are instantiated with a bunch [Bundle|App]Builder objects are instantiated with a bunch of keyword
of keyword arguments, and have a build() method that will do all the arguments, and have a build() method that will do all the work. See
work. See the class doc strings for a description of the constructor the class doc strings for a description of the constructor arguments.
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: # XXX Todo:
# - a command line interface, also for use with the buildapp() and
# buildcocoaapp() convenience functions.
# - modulefinder support to build standalone apps # - modulefinder support to build standalone apps
# - consider turning this into a distutils extension
# #
__all__ = ["BundleBuilder", "AppBuilder", "CocoaAppBuilder", __all__ = ["BundleBuilder", "AppBuilder", "buildapp"]
"buildapp", "buildcocoaapp"]
import sys import sys
import os, errno, shutil import os, errno, shutil
import getopt
from plistlib import Plist from plistlib import Plist
@ -62,34 +70,43 @@ class BundleBuilder:
verbosity: verbosity level, defaults to 1 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", resources=None, files=None, builddir="build", platform="MacOS",
symlink=0, verbosity=1): symlink=0, verbosity=1):
"""See the class doc string for a description of the arguments.""" """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: if plist is None:
plist = Plist() plist = Plist()
if resources is None:
resources = []
if files is None:
files = []
self.name = name
self.plist = plist self.plist = plist
self.type = type self.type = type
self.creator = creator self.creator = creator
if files is None:
files = []
if resources is None:
resources = []
self.resources = resources self.resources = resources
self.files = files self.files = files
self.builddir = builddir self.builddir = builddir
self.platform = platform self.platform = platform
self.symlink = symlink 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 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): def build(self):
"""Build the bundle.""" """Build the bundle."""
builddir = self.builddir builddir = self.builddir
@ -124,13 +141,8 @@ class BundleBuilder:
f.close() f.close()
# #
# Write Contents/Info.plist # 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") infoplist = pathjoin(contents, "Info.plist")
plist.write(infoplist) self.plist.write(infoplist)
def _copyFiles(self): def _copyFiles(self):
files = self.files[:] files = self.files[:]
@ -144,7 +156,10 @@ class BundleBuilder:
self.message("Copying files", 1) self.message("Copying files", 1)
msg = "Copying" msg = "Copying"
for src, dst in files: 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) dst = pathjoin(self.bundlepath, dst)
if self.symlink: if self.symlink:
symlink(src, dst, mkdirs=1) symlink(src, dst, mkdirs=1)
@ -153,7 +168,15 @@ class BundleBuilder:
def message(self, msg, level=0): def message(self, msg, level=0):
if level <= self.verbosity: 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 = """\ mainWrapperTemplate = """\
@ -166,18 +189,20 @@ resources = os.path.join(os.path.dirname(os.path.dirname(argv[0])),
mainprogram = os.path.join(resources, "%(mainprogram)s") mainprogram = os.path.join(resources, "%(mainprogram)s")
assert os.path.exists(mainprogram) assert os.path.exists(mainprogram)
argv.insert(1, mainprogram) argv.insert(1, mainprogram)
%(executable)s os.environ["PYTHONPATH"] = resources
%(setpythonhome)s
%(setexecutable)s
os.execve(executable, argv, os.environ) 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): class AppBuilder(BundleBuilder):
"""This class extends the BundleBuilder constructor with these """This class extends the BundleBuilder constructor with these
arguments: arguments:
mainprogram: A Python main program. If this argument is given, mainprogram: A Python main program. If this argument is given,
the main executable in the bundle will be a small wrapper the main executable in the bundle will be a small wrapper
that invokes the main program. (XXX Discuss why.) that invokes the main program. (XXX Discuss why.)
@ -185,46 +210,59 @@ class AppBuilder(BundleBuilder):
specified the executable will be copied to Resources and specified the executable will be copied to Resources and
be invoked by the wrapper program mentioned above. Else be invoked by the wrapper program mentioned above. Else
it will simply be used as the main executable. 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. For the other keyword arguments see the BundleBuilder doc string.
""" """
def __init__(self, name=None, mainprogram=None, executable=None, def __init__(self, name=None, mainprogram=None, executable=None,
**kwargs): nibname=None, **kwargs):
"""See the class doc string for a description of the arguments.""" """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.mainprogram = mainprogram
self.executable = executable self.executable = executable
self.nibname = nibname
BundleBuilder.__init__(self, name=name, **kwargs) 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 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.executable is not None:
if self.mainprogram is None: if self.mainprogram is None:
execpath = pathjoin(self.execdir, self.name) execpath = pathjoin(self.execdir, self.name)
else: else:
execpath = pathjoin(self.resdir, os.path.basename(self.executable)) execpath = pathjoin(resdir, os.path.basename(self.executable))
self.files.append((self.executable, execpath)) self.files.append((self.executable, execpath))
# For execve wrapper # For execve wrapper
executable = executableTemplate % os.path.basename(self.executable) setexecutable = setExecutableTemplate % os.path.basename(self.executable)
else: else:
executable = "" # XXX for locals() call setexecutable = "" # XXX for locals() call
if self.mainprogram is not None: if self.mainprogram is not None:
setpythonhome = "" # pythonhomeSnippet if we're making a standalone app
mainname = os.path.basename(self.mainprogram) 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 # Create execve wrapper
mainprogram = self.mainprogram # XXX for locals() call mainprogram = self.mainprogram # XXX for locals() call
execdir = pathjoin(self.bundlepath, self.execdir) execdir = pathjoin(self.bundlepath, self.execdir)
@ -234,22 +272,6 @@ class AppBuilder(BundleBuilder):
os.chmod(mainwrapperpath, 0777) 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): def copy(src, dst, mkdirs=0):
"""Copy a file or a directory.""" """Copy a file or a directory."""
if mkdirs: if mkdirs:
@ -287,21 +309,96 @@ def pathjoin(*args):
return os.path.join(*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): def buildapp(**kwargs):
# XXX cmd line argument parsing
builder = AppBuilder(**kwargs) builder = AppBuilder(**kwargs)
builder.build() main(builder)
def buildcocoaapp(**kwargs):
# XXX cmd line argument parsing
builder = CocoaAppBuilder(**kwargs)
builder.build()
if __name__ == "__main__": if __name__ == "__main__":
# XXX This test is meant to be run in the Examples/TableModel/ folder main()
# 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)