Kaydet (Commit) ceeb9627 authored tarafından Just van Rossum's avatar Just van Rossum

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

üst 7d791240
...@@ -3,31 +3,39 @@ ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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])), ...@@ -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): ...@@ -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: 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 " raise TypeError, ("must specify either or both of "
"'executable' and 'mainprogram'") "'executable' and 'mainprogram'")
if name is not None:
if self.name is not None:
pass pass
elif mainprogram is not None: elif self.mainprogram is not None:
name = os.path.splitext(os.path.basename(mainprogram))[0] self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
elif executable is not None: elif executable is not None:
name = os.path.splitext(os.path.basename(executable))[0] self.name = os.path.splitext(os.path.basename(self.executable))[0]
if name[-4:] != ".app": if self.name[-4:] != ".app":
name += ".app" self.name += ".app"
self.plist.CFBundleExecutable = self.name
self.mainprogram = mainprogram if self.nibname:
self.executable = executable 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): def preProcess(self):
self.plist.CFBundleExecutable = self.name 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): ...@@ -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): ...@@ -287,21 +309,96 @@ def pathjoin(*args):
return os.path.join(*args) return os.path.join(*args)
def buildapp(**kwargs): cmdline_doc = """\
# XXX cmd line argument parsing Usage:
builder = AppBuilder(**kwargs) python [options] command
builder.build() 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): def main(builder=None):
# XXX cmd line argument parsing if builder is None:
builder = CocoaAppBuilder(**kwargs) builder = AppBuilder(verbosity=1)
builder.build()
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__": 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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment