blob: 70b1bd3930655a9803d98b248e9e098969734dbd [file] [log] [blame]
Just van Rossumad33d722002-11-21 10:23:04 +00001#! /usr/bin/env python
2
3"""\
4bundlebuilder.py -- Tools to assemble MacOS X (application) bundles.
5
Just van Rossumceeb9622002-11-21 23:19:37 +00006This module contains two classes to build so called "bundles" for
Just van Rossumad33d722002-11-21 10:23:04 +00007MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
Just van Rossumceeb9622002-11-21 23:19:37 +00008specialized in building application bundles.
Just van Rossumad33d722002-11-21 10:23:04 +00009
Just van Rossumceeb9622002-11-21 23:19:37 +000010[Bundle|App]Builder objects are instantiated with a bunch of keyword
11arguments, and have a build() method that will do all the work. See
12the class doc strings for a description of the constructor arguments.
13
14The module contains a main program that can be used in two ways:
15
16 % python bundlebuilder.py [options] build
17 % python buildapp.py [options] build
18
19Where "buildapp.py" is a user-supplied setup.py-like script following
20this model:
21
22 from bundlebuilder import buildapp
23 buildapp(<lots-of-keyword-args>)
Just van Rossumad33d722002-11-21 10:23:04 +000024
25"""
26
27#
28# XXX Todo:
Just van Rossumad33d722002-11-21 10:23:04 +000029# - modulefinder support to build standalone apps
Just van Rossumceeb9622002-11-21 23:19:37 +000030# - consider turning this into a distutils extension
Just van Rossumad33d722002-11-21 10:23:04 +000031#
32
Just van Rossumceeb9622002-11-21 23:19:37 +000033__all__ = ["BundleBuilder", "AppBuilder", "buildapp"]
Just van Rossumad33d722002-11-21 10:23:04 +000034
35
36import sys
37import os, errno, shutil
Just van Rossumda302da2002-11-23 22:26:44 +000038from copy import deepcopy
Just van Rossumceeb9622002-11-21 23:19:37 +000039import getopt
Just van Rossumad33d722002-11-21 10:23:04 +000040from plistlib import Plist
Just van Rossumda302da2002-11-23 22:26:44 +000041from types import FunctionType as function
Just van Rossumad33d722002-11-21 10:23:04 +000042
43
Just van Rossumda302da2002-11-23 22:26:44 +000044class Defaults:
45
46 """Class attributes that don't start with an underscore and are
47 not functions or classmethods are (deep)copied to self.__dict__.
48 This allows for mutable default values.
49 """
50
51 def __init__(self, **kwargs):
52 defaults = self._getDefaults()
53 defaults.update(kwargs)
54 self.__dict__.update(defaults)
55
56 def _getDefaults(cls):
57 defaults = {}
58 for name, value in cls.__dict__.items():
59 if name[0] != "_" and not isinstance(value,
60 (function, classmethod)):
61 defaults[name] = deepcopy(value)
62 for base in cls.__bases__:
63 if hasattr(base, "_getDefaults"):
64 defaults.update(base._getDefaults())
65 return defaults
66 _getDefaults = classmethod(_getDefaults)
Just van Rossumad33d722002-11-21 10:23:04 +000067
68
Just van Rossumda302da2002-11-23 22:26:44 +000069class BundleBuilder(Defaults):
Just van Rossumad33d722002-11-21 10:23:04 +000070
71 """BundleBuilder is a barebones class for assembling bundles. It
72 knows nothing about executables or icons, it only copies files
73 and creates the PkgInfo and Info.plist files.
Just van Rossumad33d722002-11-21 10:23:04 +000074 """
75
Just van Rossumda302da2002-11-23 22:26:44 +000076 # (Note that Defaults.__init__ (deep)copies these values to
77 # instance variables. Mutable defaults are therefore safe.)
78
79 # Name of the bundle, with or without extension.
80 name = None
81
82 # The property list ("plist")
83 plist = Plist(CFBundleDevelopmentRegion = "English",
84 CFBundleInfoDictionaryVersion = "6.0")
85
86 # The type of the bundle.
87 type = "APPL"
88 # The creator code of the bundle.
Just van Rossume6b49022002-11-24 01:23:45 +000089 creator = None
Just van Rossumda302da2002-11-23 22:26:44 +000090
91 # List of files that have to be copied to <bundle>/Contents/Resources.
92 resources = []
93
94 # List of (src, dest) tuples; dest should be a path relative to the bundle
95 # (eg. "Contents/Resources/MyStuff/SomeFile.ext).
96 files = []
97
98 # Directory where the bundle will be assembled.
99 builddir = "build"
100
101 # platform, name of the subfolder of Contents that contains the executable.
102 platform = "MacOS"
103
104 # Make symlinks instead copying files. This is handy during debugging, but
105 # makes the bundle non-distributable.
106 symlink = 0
107
108 # Verbosity level.
109 verbosity = 1
Just van Rossumad33d722002-11-21 10:23:04 +0000110
Just van Rossumceeb9622002-11-21 23:19:37 +0000111 def setup(self):
Just van Rossumda302da2002-11-23 22:26:44 +0000112 # XXX rethink self.name munging, this is brittle.
Just van Rossumceeb9622002-11-21 23:19:37 +0000113 self.name, ext = os.path.splitext(self.name)
114 if not ext:
115 ext = ".bundle"
Just van Rossumda302da2002-11-23 22:26:44 +0000116 bundleextension = ext
Just van Rossumceeb9622002-11-21 23:19:37 +0000117 # misc (derived) attributes
Just van Rossumda302da2002-11-23 22:26:44 +0000118 self.bundlepath = pathjoin(self.builddir, self.name + bundleextension)
Just van Rossumceeb9622002-11-21 23:19:37 +0000119 self.execdir = pathjoin("Contents", self.platform)
120
Just van Rossumda302da2002-11-23 22:26:44 +0000121 plist = self.plist
Just van Rossumceeb9622002-11-21 23:19:37 +0000122 plist.CFBundleName = self.name
123 plist.CFBundlePackageType = self.type
Just van Rossume6b49022002-11-24 01:23:45 +0000124 if self.creator is None:
125 if hasattr(plist, "CFBundleSignature"):
126 self.creator = plist.CFBundleSignature
127 else:
128 self.creator = "????"
Just van Rossumceeb9622002-11-21 23:19:37 +0000129 plist.CFBundleSignature = self.creator
Just van Rossumceeb9622002-11-21 23:19:37 +0000130
Just van Rossumad33d722002-11-21 10:23:04 +0000131 def build(self):
132 """Build the bundle."""
133 builddir = self.builddir
134 if builddir and not os.path.exists(builddir):
135 os.mkdir(builddir)
136 self.message("Building %s" % repr(self.bundlepath), 1)
137 if os.path.exists(self.bundlepath):
138 shutil.rmtree(self.bundlepath)
139 os.mkdir(self.bundlepath)
140 self.preProcess()
141 self._copyFiles()
142 self._addMetaFiles()
143 self.postProcess()
144
145 def preProcess(self):
146 """Hook for subclasses."""
147 pass
148 def postProcess(self):
149 """Hook for subclasses."""
150 pass
151
152 def _addMetaFiles(self):
153 contents = pathjoin(self.bundlepath, "Contents")
154 makedirs(contents)
155 #
156 # Write Contents/PkgInfo
157 assert len(self.type) == len(self.creator) == 4, \
158 "type and creator must be 4-byte strings."
159 pkginfo = pathjoin(contents, "PkgInfo")
160 f = open(pkginfo, "wb")
161 f.write(self.type + self.creator)
162 f.close()
163 #
164 # Write Contents/Info.plist
Just van Rossumad33d722002-11-21 10:23:04 +0000165 infoplist = pathjoin(contents, "Info.plist")
Just van Rossumceeb9622002-11-21 23:19:37 +0000166 self.plist.write(infoplist)
Just van Rossumad33d722002-11-21 10:23:04 +0000167
168 def _copyFiles(self):
169 files = self.files[:]
170 for path in self.resources:
171 files.append((path, pathjoin("Contents", "Resources",
172 os.path.basename(path))))
173 if self.symlink:
174 self.message("Making symbolic links", 1)
175 msg = "Making symlink from"
176 else:
177 self.message("Copying files", 1)
178 msg = "Copying"
179 for src, dst in files:
Just van Rossumceeb9622002-11-21 23:19:37 +0000180 if os.path.isdir(src):
181 self.message("%s %s/ to %s/" % (msg, src, dst), 2)
182 else:
183 self.message("%s %s to %s" % (msg, src, dst), 2)
Just van Rossumad33d722002-11-21 10:23:04 +0000184 dst = pathjoin(self.bundlepath, dst)
185 if self.symlink:
186 symlink(src, dst, mkdirs=1)
187 else:
188 copy(src, dst, mkdirs=1)
189
190 def message(self, msg, level=0):
191 if level <= self.verbosity:
Just van Rossumceeb9622002-11-21 23:19:37 +0000192 indent = ""
193 if level > 1:
194 indent = (level - 1) * " "
195 sys.stderr.write(indent + msg + "\n")
196
197 def report(self):
198 # XXX something decent
199 import pprint
200 pprint.pprint(self.__dict__)
Just van Rossumad33d722002-11-21 10:23:04 +0000201
202
203mainWrapperTemplate = """\
204#!/usr/bin/env python
205
206import os
207from sys import argv, executable
208resources = os.path.join(os.path.dirname(os.path.dirname(argv[0])),
209 "Resources")
210mainprogram = os.path.join(resources, "%(mainprogram)s")
211assert os.path.exists(mainprogram)
212argv.insert(1, mainprogram)
Just van Rossumceeb9622002-11-21 23:19:37 +0000213os.environ["PYTHONPATH"] = resources
214%(setpythonhome)s
215%(setexecutable)s
Just van Rossumad33d722002-11-21 10:23:04 +0000216os.execve(executable, argv, os.environ)
217"""
218
Just van Rossumceeb9622002-11-21 23:19:37 +0000219setExecutableTemplate = """executable = os.path.join(resources, "%s")"""
220pythonhomeSnippet = """os.environ["home"] = resources"""
Just van Rossumad33d722002-11-21 10:23:04 +0000221
222class AppBuilder(BundleBuilder):
223
Just van Rossumda302da2002-11-23 22:26:44 +0000224 # A Python main program. If this argument is given, the main
225 # executable in the bundle will be a small wrapper that invokes
226 # the main program. (XXX Discuss why.)
227 mainprogram = None
Just van Rossumceeb9622002-11-21 23:19:37 +0000228
Just van Rossumda302da2002-11-23 22:26:44 +0000229 # The main executable. If a Python main program is specified
230 # the executable will be copied to Resources and be invoked
231 # by the wrapper program mentioned above. Otherwise it will
232 # simply be used as the main executable.
233 executable = None
Just van Rossumceeb9622002-11-21 23:19:37 +0000234
Just van Rossumda302da2002-11-23 22:26:44 +0000235 # The name of the main nib, for Cocoa apps. *Must* be specified
236 # when building a Cocoa app.
237 nibname = None
Just van Rossumad33d722002-11-21 10:23:04 +0000238
Just van Rossumda302da2002-11-23 22:26:44 +0000239 # Symlink the executable instead of copying it.
240 symlink_exec = 0
Just van Rossumad33d722002-11-21 10:23:04 +0000241
Just van Rossumceeb9622002-11-21 23:19:37 +0000242 def setup(self):
243 if self.mainprogram is None and self.executable is None:
244 raise TypeError, ("must specify either or both of "
245 "'executable' and 'mainprogram'")
246
247 if self.name is not None:
248 pass
249 elif self.mainprogram is not None:
250 self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
251 elif executable is not None:
252 self.name = os.path.splitext(os.path.basename(self.executable))[0]
253 if self.name[-4:] != ".app":
254 self.name += ".app"
Just van Rossumceeb9622002-11-21 23:19:37 +0000255
256 if self.nibname:
257 self.plist.NSMainNibFile = self.nibname
258 if not hasattr(self.plist, "NSPrincipalClass"):
259 self.plist.NSPrincipalClass = "NSApplication"
260
261 BundleBuilder.setup(self)
262
Just van Rossum7fd69ad2002-11-22 00:08:47 +0000263 self.plist.CFBundleExecutable = self.name
Just van Rossumf7aba232002-11-22 00:31:50 +0000264
265 def preProcess(self):
Just van Rossumceeb9622002-11-21 23:19:37 +0000266 resdir = pathjoin("Contents", "Resources")
Just van Rossumad33d722002-11-21 10:23:04 +0000267 if self.executable is not None:
268 if self.mainprogram is None:
269 execpath = pathjoin(self.execdir, self.name)
270 else:
Just van Rossumceeb9622002-11-21 23:19:37 +0000271 execpath = pathjoin(resdir, os.path.basename(self.executable))
Just van Rossum16aebf72002-11-22 11:43:10 +0000272 if not self.symlink_exec:
273 self.files.append((self.executable, execpath))
Just van Rossumda302da2002-11-23 22:26:44 +0000274 self.execpath = execpath
Just van Rossumad33d722002-11-21 10:23:04 +0000275 # For execve wrapper
Just van Rossumceeb9622002-11-21 23:19:37 +0000276 setexecutable = setExecutableTemplate % os.path.basename(self.executable)
Just van Rossumad33d722002-11-21 10:23:04 +0000277 else:
Just van Rossumceeb9622002-11-21 23:19:37 +0000278 setexecutable = "" # XXX for locals() call
Just van Rossumad33d722002-11-21 10:23:04 +0000279
280 if self.mainprogram is not None:
Just van Rossumceeb9622002-11-21 23:19:37 +0000281 setpythonhome = "" # pythonhomeSnippet if we're making a standalone app
Just van Rossumad33d722002-11-21 10:23:04 +0000282 mainname = os.path.basename(self.mainprogram)
Just van Rossumceeb9622002-11-21 23:19:37 +0000283 self.files.append((self.mainprogram, pathjoin(resdir, mainname)))
Just van Rossumad33d722002-11-21 10:23:04 +0000284 # Create execve wrapper
285 mainprogram = self.mainprogram # XXX for locals() call
286 execdir = pathjoin(self.bundlepath, self.execdir)
287 mainwrapperpath = pathjoin(execdir, self.name)
288 makedirs(execdir)
289 open(mainwrapperpath, "w").write(mainWrapperTemplate % locals())
290 os.chmod(mainwrapperpath, 0777)
291
Just van Rossum16aebf72002-11-22 11:43:10 +0000292 def postProcess(self):
293 if self.symlink_exec and self.executable:
294 self.message("Symlinking executable %s to %s" % (self.executable,
295 self.execpath), 2)
296 dst = pathjoin(self.bundlepath, self.execpath)
297 makedirs(os.path.dirname(dst))
298 os.symlink(os.path.abspath(self.executable), dst)
299
Just van Rossumad33d722002-11-21 10:23:04 +0000300
Just van Rossumad33d722002-11-21 10:23:04 +0000301def copy(src, dst, mkdirs=0):
302 """Copy a file or a directory."""
303 if mkdirs:
304 makedirs(os.path.dirname(dst))
305 if os.path.isdir(src):
306 shutil.copytree(src, dst)
307 else:
308 shutil.copy2(src, dst)
309
310def copytodir(src, dstdir):
311 """Copy a file or a directory to an existing directory."""
312 dst = pathjoin(dstdir, os.path.basename(src))
313 copy(src, dst)
314
315def makedirs(dir):
316 """Make all directories leading up to 'dir' including the leaf
317 directory. Don't moan if any path element already exists."""
318 try:
319 os.makedirs(dir)
320 except OSError, why:
321 if why.errno != errno.EEXIST:
322 raise
323
324def symlink(src, dst, mkdirs=0):
325 """Copy a file or a directory."""
326 if mkdirs:
327 makedirs(os.path.dirname(dst))
328 os.symlink(os.path.abspath(src), dst)
329
330def pathjoin(*args):
331 """Safe wrapper for os.path.join: asserts that all but the first
332 argument are relative paths."""
333 for seg in args[1:]:
334 assert seg[0] != "/"
335 return os.path.join(*args)
336
337
Just van Rossumceeb9622002-11-21 23:19:37 +0000338cmdline_doc = """\
339Usage:
Just van Rossumf7aba232002-11-22 00:31:50 +0000340 python bundlebuilder.py [options] command
Just van Rossumceeb9622002-11-21 23:19:37 +0000341 python mybuildscript.py [options] command
342
343Commands:
344 build build the application
345 report print a report
346
347Options:
348 -b, --builddir=DIR the build directory; defaults to "build"
349 -n, --name=NAME application name
350 -r, --resource=FILE extra file or folder to be copied to Resources
351 -e, --executable=FILE the executable to be used
352 -m, --mainprogram=FILE the Python main program
353 -p, --plist=FILE .plist file (default: generate one)
354 --nib=NAME main nib name
355 -c, --creator=CCCC 4-char creator code (default: '????')
356 -l, --link symlink files/folder instead of copying them
Just van Rossum16aebf72002-11-22 11:43:10 +0000357 --link-exec symlink the executable instead of copying it
Just van Rossumceeb9622002-11-21 23:19:37 +0000358 -v, --verbose increase verbosity level
359 -q, --quiet decrease verbosity level
360 -h, --help print this message
361"""
362
363def usage(msg=None):
364 if msg:
365 print msg
366 print cmdline_doc
367 sys.exit(1)
368
369def main(builder=None):
370 if builder is None:
371 builder = AppBuilder(verbosity=1)
372
Just van Rossumb8829b42002-11-24 01:15:20 +0000373 shortopts = "b:n:r:e:m:c:p:lhvq"
Just van Rossumceeb9622002-11-21 23:19:37 +0000374 longopts = ("builddir=", "name=", "resource=", "executable=",
Just van Rossum16aebf72002-11-22 11:43:10 +0000375 "mainprogram=", "creator=", "nib=", "plist=", "link",
376 "link-exec", "help", "verbose", "quiet")
Just van Rossumceeb9622002-11-21 23:19:37 +0000377
378 try:
379 options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
380 except getopt.error:
381 usage()
382
383 for opt, arg in options:
384 if opt in ('-b', '--builddir'):
385 builder.builddir = arg
386 elif opt in ('-n', '--name'):
387 builder.name = arg
388 elif opt in ('-r', '--resource'):
389 builder.resources.append(arg)
390 elif opt in ('-e', '--executable'):
391 builder.executable = arg
392 elif opt in ('-m', '--mainprogram'):
393 builder.mainprogram = arg
394 elif opt in ('-c', '--creator'):
395 builder.creator = arg
396 elif opt == "--nib":
397 builder.nibname = arg
398 elif opt in ('-p', '--plist'):
399 builder.plist = Plist.fromFile(arg)
400 elif opt in ('-l', '--link'):
401 builder.symlink = 1
Just van Rossum16aebf72002-11-22 11:43:10 +0000402 elif opt == '--link-exec':
403 builder.symlink_exec = 1
Just van Rossumceeb9622002-11-21 23:19:37 +0000404 elif opt in ('-h', '--help'):
405 usage()
406 elif opt in ('-v', '--verbose'):
407 builder.verbosity += 1
408 elif opt in ('-q', '--quiet'):
409 builder.verbosity -= 1
410
411 if len(args) != 1:
412 usage("Must specify one command ('build', 'report' or 'help')")
413 command = args[0]
414
415 if command == "build":
416 builder.setup()
417 builder.build()
418 elif command == "report":
419 builder.setup()
420 builder.report()
421 elif command == "help":
422 usage()
423 else:
424 usage("Unknown command '%s'" % command)
425
426
Just van Rossumad33d722002-11-21 10:23:04 +0000427def buildapp(**kwargs):
Just van Rossumad33d722002-11-21 10:23:04 +0000428 builder = AppBuilder(**kwargs)
Just van Rossumceeb9622002-11-21 23:19:37 +0000429 main(builder)
Just van Rossumad33d722002-11-21 10:23:04 +0000430
431
432if __name__ == "__main__":
Just van Rossumceeb9622002-11-21 23:19:37 +0000433 main()