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.
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
@ -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 @@ class BundleBuilder:
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 @@ class BundleBuilder:
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 @@ class BundleBuilder:
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 @@ resources = os.path.join(os.path.dirname(os.path.dirname(argv[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 @@ class AppBuilder(BundleBuilder):
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()