blob: 319522be8ea0611d081670e677122f5f1a7177b7 [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
6This module contains three classes to build so called "bundles" for
7MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
8specialized in building application bundles. CocoaAppBuilder is a
9further specialization of AppBuilder.
10
11[Bundle|App|CocoaApp]Builder objects are instantiated with a bunch
12of keyword arguments, and have a build() method that will do all the
13work. See the class doc strings for a description of the constructor
14arguments.
15
16"""
17
18#
19# XXX Todo:
20# - a command line interface, also for use with the buildapp() and
21# buildcocoaapp() convenience functions.
22# - modulefinder support to build standalone apps
23#
24
25__all__ = ["BundleBuilder", "AppBuilder", "CocoaAppBuilder",
26 "buildapp", "buildcocoaapp"]
27
28
29import sys
30import os, errno, shutil
31from plistlib import Plist
32
33
34plistDefaults = Plist(
35 CFBundleDevelopmentRegion = "English",
36 CFBundleInfoDictionaryVersion = "6.0",
37)
38
39
40class BundleBuilder:
41
42 """BundleBuilder is a barebones class for assembling bundles. It
43 knows nothing about executables or icons, it only copies files
44 and creates the PkgInfo and Info.plist files.
45
46 Constructor arguments:
47
48 name: Name of the bundle, with or without extension.
49 plist: A plistlib.Plist object.
50 type: The type of the bundle. Defaults to "APPL".
51 creator: The creator code of the bundle. Defaults to "????".
52 resources: List of files that have to be copied to
53 <bundle>/Contents/Resources. Defaults to an empty list.
54 files: List of (src, dest) tuples; dest should be a path relative
55 to the bundle (eg. "Contents/Resources/MyStuff/SomeFile.ext.
56 Defaults to an empty list.
57 builddir: Directory where the bundle will be assembled. Defaults
58 to "build" (in the current directory).
59 symlink: Make symlinks instead copying files. This is handy during
60 debugging, but makes the bundle non-distributable. Defaults to
61 False.
62 verbosity: verbosity level, defaults to 1
63 """
64
65 def __init__(self, name, plist=None, type="APPL", creator="????",
66 resources=None, files=None, builddir="build", platform="MacOS",
67 symlink=0, verbosity=1):
68 """See the class doc string for a description of the arguments."""
69 self.name, ext = os.path.splitext(name)
70 if not ext:
71 ext = ".bundle"
72 self.bundleextension = ext
73 if plist is None:
74 plist = Plist()
75 self.plist = plist
76 self.type = type
77 self.creator = creator
78 if files is None:
79 files = []
80 if resources is None:
81 resources = []
82 self.resources = resources
83 self.files = files
84 self.builddir = builddir
85 self.platform = platform
86 self.symlink = symlink
87 # misc (derived) attributes
88 self.bundlepath = pathjoin(builddir, self.name + self.bundleextension)
89 self.execdir = pathjoin("Contents", platform)
90 self.resdir = pathjoin("Contents", "Resources")
91 self.verbosity = verbosity
92
93 def build(self):
94 """Build the bundle."""
95 builddir = self.builddir
96 if builddir and not os.path.exists(builddir):
97 os.mkdir(builddir)
98 self.message("Building %s" % repr(self.bundlepath), 1)
99 if os.path.exists(self.bundlepath):
100 shutil.rmtree(self.bundlepath)
101 os.mkdir(self.bundlepath)
102 self.preProcess()
103 self._copyFiles()
104 self._addMetaFiles()
105 self.postProcess()
106
107 def preProcess(self):
108 """Hook for subclasses."""
109 pass
110 def postProcess(self):
111 """Hook for subclasses."""
112 pass
113
114 def _addMetaFiles(self):
115 contents = pathjoin(self.bundlepath, "Contents")
116 makedirs(contents)
117 #
118 # Write Contents/PkgInfo
119 assert len(self.type) == len(self.creator) == 4, \
120 "type and creator must be 4-byte strings."
121 pkginfo = pathjoin(contents, "PkgInfo")
122 f = open(pkginfo, "wb")
123 f.write(self.type + self.creator)
124 f.close()
125 #
126 # Write Contents/Info.plist
127 plist = plistDefaults.copy()
128 plist.CFBundleName = self.name
129 plist.CFBundlePackageType = self.type
130 plist.CFBundleSignature = self.creator
131 plist.update(self.plist)
132 infoplist = pathjoin(contents, "Info.plist")
133 plist.write(infoplist)
134
135 def _copyFiles(self):
136 files = self.files[:]
137 for path in self.resources:
138 files.append((path, pathjoin("Contents", "Resources",
139 os.path.basename(path))))
140 if self.symlink:
141 self.message("Making symbolic links", 1)
142 msg = "Making symlink from"
143 else:
144 self.message("Copying files", 1)
145 msg = "Copying"
146 for src, dst in files:
147 self.message("%s %s to %s" % (msg, src, dst), 2)
148 dst = pathjoin(self.bundlepath, dst)
149 if self.symlink:
150 symlink(src, dst, mkdirs=1)
151 else:
152 copy(src, dst, mkdirs=1)
153
154 def message(self, msg, level=0):
155 if level <= self.verbosity:
156 sys.stderr.write(msg + "\n")
157
158
159mainWrapperTemplate = """\
160#!/usr/bin/env python
161
162import os
163from sys import argv, executable
164resources = os.path.join(os.path.dirname(os.path.dirname(argv[0])),
165 "Resources")
166mainprogram = os.path.join(resources, "%(mainprogram)s")
167assert os.path.exists(mainprogram)
168argv.insert(1, mainprogram)
169%(executable)s
170os.execve(executable, argv, os.environ)
171"""
172
173executableTemplate = "executable = os.path.join(resources, \"%s\")"
174
175
176class AppBuilder(BundleBuilder):
177
178 """This class extends the BundleBuilder constructor with these
179 arguments:
180
181 mainprogram: A Python main program. If this argument is given,
182 the main executable in the bundle will be a small wrapper
183 that invokes the main program. (XXX Discuss why.)
184 executable: The main executable. If a Python main program is
185 specified the executable will be copied to Resources and
186 be invoked by the wrapper program mentioned above. Else
187 it will simply be used as the main executable.
188
189 For the other keyword arguments see the BundleBuilder doc string.
190 """
191
192 def __init__(self, name=None, mainprogram=None, executable=None,
193 **kwargs):
194 """See the class doc string for a description of the arguments."""
195 if mainprogram is None and executable is None:
196 raise TypeError, ("must specify either or both of "
197 "'executable' and 'mainprogram'")
198 if name is not None:
199 pass
200 elif mainprogram is not None:
201 name = os.path.splitext(os.path.basename(mainprogram))[0]
202 elif executable is not None:
203 name = os.path.splitext(os.path.basename(executable))[0]
204 if name[-4:] != ".app":
205 name += ".app"
206
207 self.mainprogram = mainprogram
208 self.executable = executable
209
210 BundleBuilder.__init__(self, name=name, **kwargs)
211
212 def preProcess(self):
213 self.plist.CFBundleExecutable = self.name
214 if self.executable is not None:
215 if self.mainprogram is None:
216 execpath = pathjoin(self.execdir, self.name)
217 else:
218 execpath = pathjoin(self.resdir, os.path.basename(self.executable))
219 self.files.append((self.executable, execpath))
220 # For execve wrapper
221 executable = executableTemplate % os.path.basename(self.executable)
222 else:
223 executable = "" # XXX for locals() call
224
225 if self.mainprogram is not None:
226 mainname = os.path.basename(self.mainprogram)
227 self.files.append((self.mainprogram, pathjoin(self.resdir, mainname)))
228 # Create execve wrapper
229 mainprogram = self.mainprogram # XXX for locals() call
230 execdir = pathjoin(self.bundlepath, self.execdir)
231 mainwrapperpath = pathjoin(execdir, self.name)
232 makedirs(execdir)
233 open(mainwrapperpath, "w").write(mainWrapperTemplate % locals())
234 os.chmod(mainwrapperpath, 0777)
235
236
237class CocoaAppBuilder(AppBuilder):
238
239 """Tiny specialization of AppBuilder. It has an extra constructor
240 argument called 'nibname' which defaults to 'MainMenu'. It will
241 set the appropriate fields in the plist.
242 """
243
244 def __init__(self, nibname="MainMenu", **kwargs):
245 """See the class doc string for a description of the arguments."""
246 self.nibname = nibname
247 AppBuilder.__init__(self, **kwargs)
248 self.plist.NSMainNibFile = self.nibname
249 if not hasattr(self.plist, "NSPrincipalClass"):
250 self.plist.NSPrincipalClass = "NSApplication"
251
252
253def copy(src, dst, mkdirs=0):
254 """Copy a file or a directory."""
255 if mkdirs:
256 makedirs(os.path.dirname(dst))
257 if os.path.isdir(src):
258 shutil.copytree(src, dst)
259 else:
260 shutil.copy2(src, dst)
261
262def copytodir(src, dstdir):
263 """Copy a file or a directory to an existing directory."""
264 dst = pathjoin(dstdir, os.path.basename(src))
265 copy(src, dst)
266
267def makedirs(dir):
268 """Make all directories leading up to 'dir' including the leaf
269 directory. Don't moan if any path element already exists."""
270 try:
271 os.makedirs(dir)
272 except OSError, why:
273 if why.errno != errno.EEXIST:
274 raise
275
276def symlink(src, dst, mkdirs=0):
277 """Copy a file or a directory."""
278 if mkdirs:
279 makedirs(os.path.dirname(dst))
280 os.symlink(os.path.abspath(src), dst)
281
282def pathjoin(*args):
283 """Safe wrapper for os.path.join: asserts that all but the first
284 argument are relative paths."""
285 for seg in args[1:]:
286 assert seg[0] != "/"
287 return os.path.join(*args)
288
289
290def buildapp(**kwargs):
291 # XXX cmd line argument parsing
292 builder = AppBuilder(**kwargs)
293 builder.build()
294
295
296def buildcocoaapp(**kwargs):
297 # XXX cmd line argument parsing
298 builder = CocoaAppBuilder(**kwargs)
299 builder.build()
300
301
302if __name__ == "__main__":
303 # XXX This test is meant to be run in the Examples/TableModel/ folder
304 # of the pyobj project... It will go as soon as I've written a proper
305 # main program.
306 buildcocoaapp(mainprogram="TableModel.py",
307 resources=["English.lproj", "nibwrapper.py"], verbosity=4)