blob: 0575a46939edee14b17f25267a535d6ed197429b [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
248 def preProcess(self):
Just van Rossum7fd69ad2002-11-22 00:08:47 +0000249 self.plist.CFBundleExecutable = self.name
Just van Rossumceeb9622002-11-21 23:19:37 +0000250 resdir = pathjoin("Contents", "Resources")
Just van Rossumad33d722002-11-21 10:23:04 +0000251 if self.executable is not None:
252 if self.mainprogram is None:
253 execpath = pathjoin(self.execdir, self.name)
254 else:
Just van Rossumceeb9622002-11-21 23:19:37 +0000255 execpath = pathjoin(resdir, os.path.basename(self.executable))
Just van Rossumad33d722002-11-21 10:23:04 +0000256 self.files.append((self.executable, execpath))
257 # For execve wrapper
Just van Rossumceeb9622002-11-21 23:19:37 +0000258 setexecutable = setExecutableTemplate % os.path.basename(self.executable)
Just van Rossumad33d722002-11-21 10:23:04 +0000259 else:
Just van Rossumceeb9622002-11-21 23:19:37 +0000260 setexecutable = "" # XXX for locals() call
Just van Rossumad33d722002-11-21 10:23:04 +0000261
262 if self.mainprogram is not None:
Just van Rossumceeb9622002-11-21 23:19:37 +0000263 setpythonhome = "" # pythonhomeSnippet if we're making a standalone app
Just van Rossumad33d722002-11-21 10:23:04 +0000264 mainname = os.path.basename(self.mainprogram)
Just van Rossumceeb9622002-11-21 23:19:37 +0000265 self.files.append((self.mainprogram, pathjoin(resdir, mainname)))
Just van Rossumad33d722002-11-21 10:23:04 +0000266 # Create execve wrapper
267 mainprogram = self.mainprogram # XXX for locals() call
268 execdir = pathjoin(self.bundlepath, self.execdir)
269 mainwrapperpath = pathjoin(execdir, self.name)
270 makedirs(execdir)
271 open(mainwrapperpath, "w").write(mainWrapperTemplate % locals())
272 os.chmod(mainwrapperpath, 0777)
273
274
Just van Rossumad33d722002-11-21 10:23:04 +0000275def copy(src, dst, mkdirs=0):
276 """Copy a file or a directory."""
277 if mkdirs:
278 makedirs(os.path.dirname(dst))
279 if os.path.isdir(src):
280 shutil.copytree(src, dst)
281 else:
282 shutil.copy2(src, dst)
283
284def copytodir(src, dstdir):
285 """Copy a file or a directory to an existing directory."""
286 dst = pathjoin(dstdir, os.path.basename(src))
287 copy(src, dst)
288
289def makedirs(dir):
290 """Make all directories leading up to 'dir' including the leaf
291 directory. Don't moan if any path element already exists."""
292 try:
293 os.makedirs(dir)
294 except OSError, why:
295 if why.errno != errno.EEXIST:
296 raise
297
298def symlink(src, dst, mkdirs=0):
299 """Copy a file or a directory."""
300 if mkdirs:
301 makedirs(os.path.dirname(dst))
302 os.symlink(os.path.abspath(src), dst)
303
304def pathjoin(*args):
305 """Safe wrapper for os.path.join: asserts that all but the first
306 argument are relative paths."""
307 for seg in args[1:]:
308 assert seg[0] != "/"
309 return os.path.join(*args)
310
311
Just van Rossumceeb9622002-11-21 23:19:37 +0000312cmdline_doc = """\
313Usage:
314 python [options] command
315 python mybuildscript.py [options] command
316
317Commands:
318 build build the application
319 report print a report
320
321Options:
322 -b, --builddir=DIR the build directory; defaults to "build"
323 -n, --name=NAME application name
324 -r, --resource=FILE extra file or folder to be copied to Resources
325 -e, --executable=FILE the executable to be used
326 -m, --mainprogram=FILE the Python main program
327 -p, --plist=FILE .plist file (default: generate one)
328 --nib=NAME main nib name
329 -c, --creator=CCCC 4-char creator code (default: '????')
330 -l, --link symlink files/folder instead of copying them
331 -v, --verbose increase verbosity level
332 -q, --quiet decrease verbosity level
333 -h, --help print this message
334"""
335
336def usage(msg=None):
337 if msg:
338 print msg
339 print cmdline_doc
340 sys.exit(1)
341
342def main(builder=None):
343 if builder is None:
344 builder = AppBuilder(verbosity=1)
345
346 shortopts = "b:n:r:e:m:c:plhvq"
347 longopts = ("builddir=", "name=", "resource=", "executable=",
348 "mainprogram=", "creator=", "nib=", "plist=", "link", "help",
349 "verbose", "quiet")
350
351 try:
352 options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
353 except getopt.error:
354 usage()
355
356 for opt, arg in options:
357 if opt in ('-b', '--builddir'):
358 builder.builddir = arg
359 elif opt in ('-n', '--name'):
360 builder.name = arg
361 elif opt in ('-r', '--resource'):
362 builder.resources.append(arg)
363 elif opt in ('-e', '--executable'):
364 builder.executable = arg
365 elif opt in ('-m', '--mainprogram'):
366 builder.mainprogram = arg
367 elif opt in ('-c', '--creator'):
368 builder.creator = arg
369 elif opt == "--nib":
370 builder.nibname = arg
371 elif opt in ('-p', '--plist'):
372 builder.plist = Plist.fromFile(arg)
373 elif opt in ('-l', '--link'):
374 builder.symlink = 1
375 elif opt in ('-h', '--help'):
376 usage()
377 elif opt in ('-v', '--verbose'):
378 builder.verbosity += 1
379 elif opt in ('-q', '--quiet'):
380 builder.verbosity -= 1
381
382 if len(args) != 1:
383 usage("Must specify one command ('build', 'report' or 'help')")
384 command = args[0]
385
386 if command == "build":
387 builder.setup()
388 builder.build()
389 elif command == "report":
390 builder.setup()
391 builder.report()
392 elif command == "help":
393 usage()
394 else:
395 usage("Unknown command '%s'" % command)
396
397
Just van Rossumad33d722002-11-21 10:23:04 +0000398def buildapp(**kwargs):
Just van Rossumad33d722002-11-21 10:23:04 +0000399 builder = AppBuilder(**kwargs)
Just van Rossumceeb9622002-11-21 23:19:37 +0000400 main(builder)
Just van Rossumad33d722002-11-21 10:23:04 +0000401
402
403if __name__ == "__main__":
Just van Rossumceeb9622002-11-21 23:19:37 +0000404 main()