blob: 7854770dc9620e48c485e282b563c4949df6768c [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 Rossumceeb9622002-11-21 23:19:37 +000038import getopt
Just van Rossumad33d722002-11-21 10:23:04 +000039from plistlib import Plist
40
41
42plistDefaults = Plist(
43 CFBundleDevelopmentRegion = "English",
44 CFBundleInfoDictionaryVersion = "6.0",
45)
46
47
48class BundleBuilder:
49
50 """BundleBuilder is a barebones class for assembling bundles. It
51 knows nothing about executables or icons, it only copies files
52 and creates the PkgInfo and Info.plist files.
53
54 Constructor arguments:
55
56 name: Name of the bundle, with or without extension.
57 plist: A plistlib.Plist object.
58 type: The type of the bundle. Defaults to "APPL".
59 creator: The creator code of the bundle. Defaults to "????".
60 resources: List of files that have to be copied to
61 <bundle>/Contents/Resources. Defaults to an empty list.
62 files: List of (src, dest) tuples; dest should be a path relative
63 to the bundle (eg. "Contents/Resources/MyStuff/SomeFile.ext.
64 Defaults to an empty list.
65 builddir: Directory where the bundle will be assembled. Defaults
66 to "build" (in the current directory).
67 symlink: Make symlinks instead copying files. This is handy during
68 debugging, but makes the bundle non-distributable. Defaults to
69 False.
70 verbosity: verbosity level, defaults to 1
71 """
72
Just van Rossumceeb9622002-11-21 23:19:37 +000073 def __init__(self, name=None, plist=None, type="APPL", creator="????",
Just van Rossumad33d722002-11-21 10:23:04 +000074 resources=None, files=None, builddir="build", platform="MacOS",
75 symlink=0, verbosity=1):
76 """See the class doc string for a description of the arguments."""
Just van Rossumad33d722002-11-21 10:23:04 +000077 if plist is None:
78 plist = Plist()
Just van Rossumceeb9622002-11-21 23:19:37 +000079 if resources is None:
80 resources = []
81 if files is None:
82 files = []
83 self.name = name
Just van Rossumad33d722002-11-21 10:23:04 +000084 self.plist = plist
85 self.type = type
86 self.creator = creator
Just van Rossumad33d722002-11-21 10:23:04 +000087 self.resources = resources
88 self.files = files
89 self.builddir = builddir
90 self.platform = platform
91 self.symlink = symlink
Just van Rossumad33d722002-11-21 10:23:04 +000092 self.verbosity = verbosity
93
Just van Rossumceeb9622002-11-21 23:19:37 +000094 def setup(self):
95 self.name, ext = os.path.splitext(self.name)
96 if not ext:
97 ext = ".bundle"
98 self.bundleextension = ext
99 # misc (derived) attributes
100 self.bundlepath = pathjoin(self.builddir, self.name + self.bundleextension)
101 self.execdir = pathjoin("Contents", self.platform)
102
103 plist = plistDefaults.copy()
104 plist.CFBundleName = self.name
105 plist.CFBundlePackageType = self.type
106 plist.CFBundleSignature = self.creator
107 plist.update(self.plist)
108 self.plist = plist
109
Just van Rossumad33d722002-11-21 10:23:04 +0000110 def build(self):
111 """Build the bundle."""
112 builddir = self.builddir
113 if builddir and not os.path.exists(builddir):
114 os.mkdir(builddir)
115 self.message("Building %s" % repr(self.bundlepath), 1)
116 if os.path.exists(self.bundlepath):
117 shutil.rmtree(self.bundlepath)
118 os.mkdir(self.bundlepath)
119 self.preProcess()
120 self._copyFiles()
121 self._addMetaFiles()
122 self.postProcess()
123
124 def preProcess(self):
125 """Hook for subclasses."""
126 pass
127 def postProcess(self):
128 """Hook for subclasses."""
129 pass
130
131 def _addMetaFiles(self):
132 contents = pathjoin(self.bundlepath, "Contents")
133 makedirs(contents)
134 #
135 # Write Contents/PkgInfo
136 assert len(self.type) == len(self.creator) == 4, \
137 "type and creator must be 4-byte strings."
138 pkginfo = pathjoin(contents, "PkgInfo")
139 f = open(pkginfo, "wb")
140 f.write(self.type + self.creator)
141 f.close()
142 #
143 # Write Contents/Info.plist
Just van Rossumad33d722002-11-21 10:23:04 +0000144 infoplist = pathjoin(contents, "Info.plist")
Just van Rossumceeb9622002-11-21 23:19:37 +0000145 self.plist.write(infoplist)
Just van Rossumad33d722002-11-21 10:23:04 +0000146
147 def _copyFiles(self):
148 files = self.files[:]
149 for path in self.resources:
150 files.append((path, pathjoin("Contents", "Resources",
151 os.path.basename(path))))
152 if self.symlink:
153 self.message("Making symbolic links", 1)
154 msg = "Making symlink from"
155 else:
156 self.message("Copying files", 1)
157 msg = "Copying"
158 for src, dst in files:
Just van Rossumceeb9622002-11-21 23:19:37 +0000159 if os.path.isdir(src):
160 self.message("%s %s/ to %s/" % (msg, src, dst), 2)
161 else:
162 self.message("%s %s to %s" % (msg, src, dst), 2)
Just van Rossumad33d722002-11-21 10:23:04 +0000163 dst = pathjoin(self.bundlepath, dst)
164 if self.symlink:
165 symlink(src, dst, mkdirs=1)
166 else:
167 copy(src, dst, mkdirs=1)
168
169 def message(self, msg, level=0):
170 if level <= self.verbosity:
Just van Rossumceeb9622002-11-21 23:19:37 +0000171 indent = ""
172 if level > 1:
173 indent = (level - 1) * " "
174 sys.stderr.write(indent + msg + "\n")
175
176 def report(self):
177 # XXX something decent
178 import pprint
179 pprint.pprint(self.__dict__)
Just van Rossumad33d722002-11-21 10:23:04 +0000180
181
182mainWrapperTemplate = """\
183#!/usr/bin/env python
184
185import os
186from sys import argv, executable
187resources = os.path.join(os.path.dirname(os.path.dirname(argv[0])),
188 "Resources")
189mainprogram = os.path.join(resources, "%(mainprogram)s")
190assert os.path.exists(mainprogram)
191argv.insert(1, mainprogram)
Just van Rossumceeb9622002-11-21 23:19:37 +0000192os.environ["PYTHONPATH"] = resources
193%(setpythonhome)s
194%(setexecutable)s
Just van Rossumad33d722002-11-21 10:23:04 +0000195os.execve(executable, argv, os.environ)
196"""
197
Just van Rossumceeb9622002-11-21 23:19:37 +0000198setExecutableTemplate = """executable = os.path.join(resources, "%s")"""
199pythonhomeSnippet = """os.environ["home"] = resources"""
Just van Rossumad33d722002-11-21 10:23:04 +0000200
201class AppBuilder(BundleBuilder):
202
203 """This class extends the BundleBuilder constructor with these
204 arguments:
Just van Rossumceeb9622002-11-21 23:19:37 +0000205
Just van Rossumad33d722002-11-21 10:23:04 +0000206 mainprogram: A Python main program. If this argument is given,
207 the main executable in the bundle will be a small wrapper
208 that invokes the main program. (XXX Discuss why.)
209 executable: The main executable. If a Python main program is
210 specified the executable will be copied to Resources and
211 be invoked by the wrapper program mentioned above. Else
212 it will simply be used as the main executable.
Just van Rossumceeb9622002-11-21 23:19:37 +0000213 nibname: The name of the main nib, for Cocoa apps. Defaults
214 to None, but must be specified when building a Cocoa app.
215
Just van Rossumad33d722002-11-21 10:23:04 +0000216 For the other keyword arguments see the BundleBuilder doc string.
217 """
218
219 def __init__(self, name=None, mainprogram=None, executable=None,
Just van Rossumceeb9622002-11-21 23:19:37 +0000220 nibname=None, **kwargs):
Just van Rossumad33d722002-11-21 10:23:04 +0000221 """See the class doc string for a description of the arguments."""
Just van Rossumad33d722002-11-21 10:23:04 +0000222 self.mainprogram = mainprogram
223 self.executable = executable
Just van Rossumceeb9622002-11-21 23:19:37 +0000224 self.nibname = nibname
Just van Rossumad33d722002-11-21 10:23:04 +0000225 BundleBuilder.__init__(self, name=name, **kwargs)
226
Just van Rossumceeb9622002-11-21 23:19:37 +0000227 def setup(self):
228 if self.mainprogram is None and self.executable is None:
229 raise TypeError, ("must specify either or both of "
230 "'executable' and 'mainprogram'")
231
232 if self.name is not None:
233 pass
234 elif self.mainprogram is not None:
235 self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
236 elif executable is not None:
237 self.name = os.path.splitext(os.path.basename(self.executable))[0]
238 if self.name[-4:] != ".app":
239 self.name += ".app"
Just van Rossumceeb9622002-11-21 23:19:37 +0000240
241 if self.nibname:
242 self.plist.NSMainNibFile = self.nibname
243 if not hasattr(self.plist, "NSPrincipalClass"):
244 self.plist.NSPrincipalClass = "NSApplication"
245
246 BundleBuilder.setup(self)
247
Just van Rossum7fd69ad2002-11-22 00:08:47 +0000248 self.plist.CFBundleExecutable = self.name
Just van Rossumf7aba232002-11-22 00:31:50 +0000249
250 def preProcess(self):
Just van Rossumceeb9622002-11-21 23:19:37 +0000251 resdir = pathjoin("Contents", "Resources")
Just van Rossumad33d722002-11-21 10:23:04 +0000252 if self.executable is not None:
253 if self.mainprogram is None:
254 execpath = pathjoin(self.execdir, self.name)
255 else:
Just van Rossumceeb9622002-11-21 23:19:37 +0000256 execpath = pathjoin(resdir, os.path.basename(self.executable))
Just van Rossumad33d722002-11-21 10:23:04 +0000257 self.files.append((self.executable, execpath))
258 # For execve wrapper
Just van Rossumceeb9622002-11-21 23:19:37 +0000259 setexecutable = setExecutableTemplate % os.path.basename(self.executable)
Just van Rossumad33d722002-11-21 10:23:04 +0000260 else:
Just van Rossumceeb9622002-11-21 23:19:37 +0000261 setexecutable = "" # XXX for locals() call
Just van Rossumad33d722002-11-21 10:23:04 +0000262
263 if self.mainprogram is not None:
Just van Rossumceeb9622002-11-21 23:19:37 +0000264 setpythonhome = "" # pythonhomeSnippet if we're making a standalone app
Just van Rossumad33d722002-11-21 10:23:04 +0000265 mainname = os.path.basename(self.mainprogram)
Just van Rossumceeb9622002-11-21 23:19:37 +0000266 self.files.append((self.mainprogram, pathjoin(resdir, mainname)))
Just van Rossumad33d722002-11-21 10:23:04 +0000267 # Create execve wrapper
268 mainprogram = self.mainprogram # XXX for locals() call
269 execdir = pathjoin(self.bundlepath, self.execdir)
270 mainwrapperpath = pathjoin(execdir, self.name)
271 makedirs(execdir)
272 open(mainwrapperpath, "w").write(mainWrapperTemplate % locals())
273 os.chmod(mainwrapperpath, 0777)
274
275
Just van Rossumad33d722002-11-21 10:23:04 +0000276def copy(src, dst, mkdirs=0):
277 """Copy a file or a directory."""
278 if mkdirs:
279 makedirs(os.path.dirname(dst))
280 if os.path.isdir(src):
281 shutil.copytree(src, dst)
282 else:
283 shutil.copy2(src, dst)
284
285def copytodir(src, dstdir):
286 """Copy a file or a directory to an existing directory."""
287 dst = pathjoin(dstdir, os.path.basename(src))
288 copy(src, dst)
289
290def makedirs(dir):
291 """Make all directories leading up to 'dir' including the leaf
292 directory. Don't moan if any path element already exists."""
293 try:
294 os.makedirs(dir)
295 except OSError, why:
296 if why.errno != errno.EEXIST:
297 raise
298
299def symlink(src, dst, mkdirs=0):
300 """Copy a file or a directory."""
301 if mkdirs:
302 makedirs(os.path.dirname(dst))
303 os.symlink(os.path.abspath(src), dst)
304
305def pathjoin(*args):
306 """Safe wrapper for os.path.join: asserts that all but the first
307 argument are relative paths."""
308 for seg in args[1:]:
309 assert seg[0] != "/"
310 return os.path.join(*args)
311
312
Just van Rossumceeb9622002-11-21 23:19:37 +0000313cmdline_doc = """\
314Usage:
Just van Rossumf7aba232002-11-22 00:31:50 +0000315 python bundlebuilder.py [options] command
Just van Rossumceeb9622002-11-21 23:19:37 +0000316 python mybuildscript.py [options] command
317
318Commands:
319 build build the application
320 report print a report
321
322Options:
323 -b, --builddir=DIR the build directory; defaults to "build"
324 -n, --name=NAME application name
325 -r, --resource=FILE extra file or folder to be copied to Resources
326 -e, --executable=FILE the executable to be used
327 -m, --mainprogram=FILE the Python main program
328 -p, --plist=FILE .plist file (default: generate one)
329 --nib=NAME main nib name
330 -c, --creator=CCCC 4-char creator code (default: '????')
331 -l, --link symlink files/folder instead of copying them
332 -v, --verbose increase verbosity level
333 -q, --quiet decrease verbosity level
334 -h, --help print this message
335"""
336
337def usage(msg=None):
338 if msg:
339 print msg
340 print cmdline_doc
341 sys.exit(1)
342
343def main(builder=None):
344 if builder is None:
345 builder = AppBuilder(verbosity=1)
346
347 shortopts = "b:n:r:e:m:c:plhvq"
348 longopts = ("builddir=", "name=", "resource=", "executable=",
349 "mainprogram=", "creator=", "nib=", "plist=", "link", "help",
350 "verbose", "quiet")
351
352 try:
353 options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
354 except getopt.error:
355 usage()
356
357 for opt, arg in options:
358 if opt in ('-b', '--builddir'):
359 builder.builddir = arg
360 elif opt in ('-n', '--name'):
361 builder.name = arg
362 elif opt in ('-r', '--resource'):
363 builder.resources.append(arg)
364 elif opt in ('-e', '--executable'):
365 builder.executable = arg
366 elif opt in ('-m', '--mainprogram'):
367 builder.mainprogram = arg
368 elif opt in ('-c', '--creator'):
369 builder.creator = arg
370 elif opt == "--nib":
371 builder.nibname = arg
372 elif opt in ('-p', '--plist'):
373 builder.plist = Plist.fromFile(arg)
374 elif opt in ('-l', '--link'):
375 builder.symlink = 1
376 elif opt in ('-h', '--help'):
377 usage()
378 elif opt in ('-v', '--verbose'):
379 builder.verbosity += 1
380 elif opt in ('-q', '--quiet'):
381 builder.verbosity -= 1
382
383 if len(args) != 1:
384 usage("Must specify one command ('build', 'report' or 'help')")
385 command = args[0]
386
387 if command == "build":
388 builder.setup()
389 builder.build()
390 elif command == "report":
391 builder.setup()
392 builder.report()
393 elif command == "help":
394 usage()
395 else:
396 usage("Unknown command '%s'" % command)
397
398
Just van Rossumad33d722002-11-21 10:23:04 +0000399def buildapp(**kwargs):
Just van Rossumad33d722002-11-21 10:23:04 +0000400 builder = AppBuilder(**kwargs)
Just van Rossumceeb9622002-11-21 23:19:37 +0000401 main(builder)
Just van Rossumad33d722002-11-21 10:23:04 +0000402
403
404if __name__ == "__main__":
Just van Rossumceeb9622002-11-21 23:19:37 +0000405 main()