1 | #! /usr/bin/env python
|
---|
2 |
|
---|
3 | """\
|
---|
4 | bundlebuilder.py -- Tools to assemble MacOS X (application) bundles.
|
---|
5 |
|
---|
6 | This module contains two classes to build so called "bundles" for
|
---|
7 | MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass
|
---|
8 | specialized in building application bundles.
|
---|
9 |
|
---|
10 | [Bundle|App]Builder objects are instantiated with a bunch of keyword
|
---|
11 | arguments, and have a build() method that will do all the work. See
|
---|
12 | the class doc strings for a description of the constructor arguments.
|
---|
13 |
|
---|
14 | The 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 |
|
---|
19 | Where "buildapp.py" is a user-supplied setup.py-like script following
|
---|
20 | this model:
|
---|
21 |
|
---|
22 | from bundlebuilder import buildapp
|
---|
23 | buildapp(<lots-of-keyword-args>)
|
---|
24 |
|
---|
25 | """
|
---|
26 |
|
---|
27 |
|
---|
28 | __all__ = ["BundleBuilder", "BundleBuilderError", "AppBuilder", "buildapp"]
|
---|
29 |
|
---|
30 |
|
---|
31 | from warnings import warnpy3k
|
---|
32 | warnpy3k("In 3.x, the bundlebuilder module is removed.", stacklevel=2)
|
---|
33 |
|
---|
34 | import sys
|
---|
35 | import os, errno, shutil
|
---|
36 | import imp, marshal
|
---|
37 | import re
|
---|
38 | from copy import deepcopy
|
---|
39 | import getopt
|
---|
40 | from plistlib import Plist
|
---|
41 | from types import FunctionType as function
|
---|
42 |
|
---|
43 | class BundleBuilderError(Exception): pass
|
---|
44 |
|
---|
45 |
|
---|
46 | class Defaults:
|
---|
47 |
|
---|
48 | """Class attributes that don't start with an underscore and are
|
---|
49 | not functions or classmethods are (deep)copied to self.__dict__.
|
---|
50 | This allows for mutable default values.
|
---|
51 | """
|
---|
52 |
|
---|
53 | def __init__(self, **kwargs):
|
---|
54 | defaults = self._getDefaults()
|
---|
55 | defaults.update(kwargs)
|
---|
56 | self.__dict__.update(defaults)
|
---|
57 |
|
---|
58 | def _getDefaults(cls):
|
---|
59 | defaults = {}
|
---|
60 | for base in cls.__bases__:
|
---|
61 | if hasattr(base, "_getDefaults"):
|
---|
62 | defaults.update(base._getDefaults())
|
---|
63 | for name, value in cls.__dict__.items():
|
---|
64 | if name[0] != "_" and not isinstance(value,
|
---|
65 | (function, classmethod)):
|
---|
66 | defaults[name] = deepcopy(value)
|
---|
67 | return defaults
|
---|
68 | _getDefaults = classmethod(_getDefaults)
|
---|
69 |
|
---|
70 |
|
---|
71 | class BundleBuilder(Defaults):
|
---|
72 |
|
---|
73 | """BundleBuilder is a barebones class for assembling bundles. It
|
---|
74 | knows nothing about executables or icons, it only copies files
|
---|
75 | and creates the PkgInfo and Info.plist files.
|
---|
76 | """
|
---|
77 |
|
---|
78 | # (Note that Defaults.__init__ (deep)copies these values to
|
---|
79 | # instance variables. Mutable defaults are therefore safe.)
|
---|
80 |
|
---|
81 | # Name of the bundle, with or without extension.
|
---|
82 | name = None
|
---|
83 |
|
---|
84 | # The property list ("plist")
|
---|
85 | plist = Plist(CFBundleDevelopmentRegion = "English",
|
---|
86 | CFBundleInfoDictionaryVersion = "6.0")
|
---|
87 |
|
---|
88 | # The type of the bundle.
|
---|
89 | type = "BNDL"
|
---|
90 | # The creator code of the bundle.
|
---|
91 | creator = None
|
---|
92 |
|
---|
93 | # the CFBundleIdentifier (this is used for the preferences file name)
|
---|
94 | bundle_id = None
|
---|
95 |
|
---|
96 | # List of files that have to be copied to <bundle>/Contents/Resources.
|
---|
97 | resources = []
|
---|
98 |
|
---|
99 | # List of (src, dest) tuples; dest should be a path relative to the bundle
|
---|
100 | # (eg. "Contents/Resources/MyStuff/SomeFile.ext).
|
---|
101 | files = []
|
---|
102 |
|
---|
103 | # List of shared libraries (dylibs, Frameworks) to bundle with the app
|
---|
104 | # will be placed in Contents/Frameworks
|
---|
105 | libs = []
|
---|
106 |
|
---|
107 | # Directory where the bundle will be assembled.
|
---|
108 | builddir = "build"
|
---|
109 |
|
---|
110 | # Make symlinks instead copying files. This is handy during debugging, but
|
---|
111 | # makes the bundle non-distributable.
|
---|
112 | symlink = 0
|
---|
113 |
|
---|
114 | # Verbosity level.
|
---|
115 | verbosity = 1
|
---|
116 |
|
---|
117 | # Destination root directory
|
---|
118 | destroot = ""
|
---|
119 |
|
---|
120 | def setup(self):
|
---|
121 | # XXX rethink self.name munging, this is brittle.
|
---|
122 | self.name, ext = os.path.splitext(self.name)
|
---|
123 | if not ext:
|
---|
124 | ext = ".bundle"
|
---|
125 | bundleextension = ext
|
---|
126 | # misc (derived) attributes
|
---|
127 | self.bundlepath = pathjoin(self.builddir, self.name + bundleextension)
|
---|
128 |
|
---|
129 | plist = self.plist
|
---|
130 | plist.CFBundleName = self.name
|
---|
131 | plist.CFBundlePackageType = self.type
|
---|
132 | if self.creator is None:
|
---|
133 | if hasattr(plist, "CFBundleSignature"):
|
---|
134 | self.creator = plist.CFBundleSignature
|
---|
135 | else:
|
---|
136 | self.creator = "????"
|
---|
137 | plist.CFBundleSignature = self.creator
|
---|
138 | if self.bundle_id:
|
---|
139 | plist.CFBundleIdentifier = self.bundle_id
|
---|
140 | elif not hasattr(plist, "CFBundleIdentifier"):
|
---|
141 | plist.CFBundleIdentifier = self.name
|
---|
142 |
|
---|
143 | def build(self):
|
---|
144 | """Build the bundle."""
|
---|
145 | builddir = self.builddir
|
---|
146 | if builddir and not os.path.exists(builddir):
|
---|
147 | os.mkdir(builddir)
|
---|
148 | self.message("Building %s" % repr(self.bundlepath), 1)
|
---|
149 | if os.path.exists(self.bundlepath):
|
---|
150 | shutil.rmtree(self.bundlepath)
|
---|
151 | if os.path.exists(self.bundlepath + '~'):
|
---|
152 | shutil.rmtree(self.bundlepath + '~')
|
---|
153 | bp = self.bundlepath
|
---|
154 |
|
---|
155 | # Create the app bundle in a temporary location and then
|
---|
156 | # rename the completed bundle. This way the Finder will
|
---|
157 | # never see an incomplete bundle (where it might pick up
|
---|
158 | # and cache the wrong meta data)
|
---|
159 | self.bundlepath = bp + '~'
|
---|
160 | try:
|
---|
161 | os.mkdir(self.bundlepath)
|
---|
162 | self.preProcess()
|
---|
163 | self._copyFiles()
|
---|
164 | self._addMetaFiles()
|
---|
165 | self.postProcess()
|
---|
166 | os.rename(self.bundlepath, bp)
|
---|
167 | finally:
|
---|
168 | self.bundlepath = bp
|
---|
169 | self.message("Done.", 1)
|
---|
170 |
|
---|
171 | def preProcess(self):
|
---|
172 | """Hook for subclasses."""
|
---|
173 | pass
|
---|
174 | def postProcess(self):
|
---|
175 | """Hook for subclasses."""
|
---|
176 | pass
|
---|
177 |
|
---|
178 | def _addMetaFiles(self):
|
---|
179 | contents = pathjoin(self.bundlepath, "Contents")
|
---|
180 | makedirs(contents)
|
---|
181 | #
|
---|
182 | # Write Contents/PkgInfo
|
---|
183 | assert len(self.type) == len(self.creator) == 4, \
|
---|
184 | "type and creator must be 4-byte strings."
|
---|
185 | pkginfo = pathjoin(contents, "PkgInfo")
|
---|
186 | f = open(pkginfo, "wb")
|
---|
187 | f.write(self.type + self.creator)
|
---|
188 | f.close()
|
---|
189 | #
|
---|
190 | # Write Contents/Info.plist
|
---|
191 | infoplist = pathjoin(contents, "Info.plist")
|
---|
192 | self.plist.write(infoplist)
|
---|
193 |
|
---|
194 | def _copyFiles(self):
|
---|
195 | files = self.files[:]
|
---|
196 | for path in self.resources:
|
---|
197 | files.append((path, pathjoin("Contents", "Resources",
|
---|
198 | os.path.basename(path))))
|
---|
199 | for path in self.libs:
|
---|
200 | files.append((path, pathjoin("Contents", "Frameworks",
|
---|
201 | os.path.basename(path))))
|
---|
202 | if self.symlink:
|
---|
203 | self.message("Making symbolic links", 1)
|
---|
204 | msg = "Making symlink from"
|
---|
205 | else:
|
---|
206 | self.message("Copying files", 1)
|
---|
207 | msg = "Copying"
|
---|
208 | files.sort()
|
---|
209 | for src, dst in files:
|
---|
210 | if os.path.isdir(src):
|
---|
211 | self.message("%s %s/ to %s/" % (msg, src, dst), 2)
|
---|
212 | else:
|
---|
213 | self.message("%s %s to %s" % (msg, src, dst), 2)
|
---|
214 | dst = pathjoin(self.bundlepath, dst)
|
---|
215 | if self.symlink:
|
---|
216 | symlink(src, dst, mkdirs=1)
|
---|
217 | else:
|
---|
218 | copy(src, dst, mkdirs=1)
|
---|
219 |
|
---|
220 | def message(self, msg, level=0):
|
---|
221 | if level <= self.verbosity:
|
---|
222 | indent = ""
|
---|
223 | if level > 1:
|
---|
224 | indent = (level - 1) * " "
|
---|
225 | sys.stderr.write(indent + msg + "\n")
|
---|
226 |
|
---|
227 | def report(self):
|
---|
228 | # XXX something decent
|
---|
229 | pass
|
---|
230 |
|
---|
231 |
|
---|
232 | if __debug__:
|
---|
233 | PYC_EXT = ".pyc"
|
---|
234 | else:
|
---|
235 | PYC_EXT = ".pyo"
|
---|
236 |
|
---|
237 | MAGIC = imp.get_magic()
|
---|
238 | USE_ZIPIMPORT = "zipimport" in sys.builtin_module_names
|
---|
239 |
|
---|
240 | # For standalone apps, we have our own minimal site.py. We don't need
|
---|
241 | # all the cruft of the real site.py.
|
---|
242 | SITE_PY = """\
|
---|
243 | import sys
|
---|
244 | if not %(semi_standalone)s:
|
---|
245 | del sys.path[1:] # sys.path[0] is Contents/Resources/
|
---|
246 | """
|
---|
247 |
|
---|
248 | if USE_ZIPIMPORT:
|
---|
249 | ZIP_ARCHIVE = "Modules.zip"
|
---|
250 | SITE_PY += "sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE
|
---|
251 | def getPycData(fullname, code, ispkg):
|
---|
252 | if ispkg:
|
---|
253 | fullname += ".__init__"
|
---|
254 | path = fullname.replace(".", os.sep) + PYC_EXT
|
---|
255 | return path, MAGIC + '\0\0\0\0' + marshal.dumps(code)
|
---|
256 |
|
---|
257 | #
|
---|
258 | # Extension modules can't be in the modules zip archive, so a placeholder
|
---|
259 | # is added instead, that loads the extension from a specified location.
|
---|
260 | #
|
---|
261 | EXT_LOADER = """\
|
---|
262 | def __load():
|
---|
263 | import imp, sys, os
|
---|
264 | for p in sys.path:
|
---|
265 | path = os.path.join(p, "%(filename)s")
|
---|
266 | if os.path.exists(path):
|
---|
267 | break
|
---|
268 | else:
|
---|
269 | assert 0, "file not found: %(filename)s"
|
---|
270 | mod = imp.load_dynamic("%(name)s", path)
|
---|
271 |
|
---|
272 | __load()
|
---|
273 | del __load
|
---|
274 | """
|
---|
275 |
|
---|
276 | MAYMISS_MODULES = ['mac', 'os2', 'nt', 'ntpath', 'dos', 'dospath',
|
---|
277 | 'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
|
---|
278 | 'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
|
---|
279 | ]
|
---|
280 |
|
---|
281 | STRIP_EXEC = "/usr/bin/strip"
|
---|
282 |
|
---|
283 | #
|
---|
284 | # We're using a stock interpreter to run the app, yet we need
|
---|
285 | # a way to pass the Python main program to the interpreter. The
|
---|
286 | # bootstrapping script fires up the interpreter with the right
|
---|
287 | # arguments. os.execve() is used as OSX doesn't like us to
|
---|
288 | # start a real new process. Also, the executable name must match
|
---|
289 | # the CFBundleExecutable value in the Info.plist, so we lie
|
---|
290 | # deliberately with argv[0]. The actual Python executable is
|
---|
291 | # passed in an environment variable so we can "repair"
|
---|
292 | # sys.executable later.
|
---|
293 | #
|
---|
294 | BOOTSTRAP_SCRIPT = """\
|
---|
295 | #!%(hashbang)s
|
---|
296 |
|
---|
297 | import sys, os
|
---|
298 | execdir = os.path.dirname(sys.argv[0])
|
---|
299 | executable = os.path.join(execdir, "%(executable)s")
|
---|
300 | resdir = os.path.join(os.path.dirname(execdir), "Resources")
|
---|
301 | libdir = os.path.join(os.path.dirname(execdir), "Frameworks")
|
---|
302 | mainprogram = os.path.join(resdir, "%(mainprogram)s")
|
---|
303 |
|
---|
304 | sys.argv.insert(1, mainprogram)
|
---|
305 | if %(standalone)s or %(semi_standalone)s:
|
---|
306 | os.environ["PYTHONPATH"] = resdir
|
---|
307 | if %(standalone)s:
|
---|
308 | os.environ["PYTHONHOME"] = resdir
|
---|
309 | else:
|
---|
310 | pypath = os.getenv("PYTHONPATH", "")
|
---|
311 | if pypath:
|
---|
312 | pypath = ":" + pypath
|
---|
313 | os.environ["PYTHONPATH"] = resdir + pypath
|
---|
314 | os.environ["PYTHONEXECUTABLE"] = executable
|
---|
315 | os.environ["DYLD_LIBRARY_PATH"] = libdir
|
---|
316 | os.environ["DYLD_FRAMEWORK_PATH"] = libdir
|
---|
317 | os.execve(executable, sys.argv, os.environ)
|
---|
318 | """
|
---|
319 |
|
---|
320 |
|
---|
321 | #
|
---|
322 | # Optional wrapper that converts "dropped files" into sys.argv values.
|
---|
323 | #
|
---|
324 | ARGV_EMULATOR = """\
|
---|
325 | import argvemulator, os
|
---|
326 |
|
---|
327 | argvemulator.ArgvCollector().mainloop()
|
---|
328 | execfile(os.path.join(os.path.split(__file__)[0], "%(realmainprogram)s"))
|
---|
329 | """
|
---|
330 |
|
---|
331 | #
|
---|
332 | # When building a standalone app with Python.framework, we need to copy
|
---|
333 | # a subset from Python.framework to the bundle. The following list
|
---|
334 | # specifies exactly what items we'll copy.
|
---|
335 | #
|
---|
336 | PYTHONFRAMEWORKGOODIES = [
|
---|
337 | "Python", # the Python core library
|
---|
338 | "Resources/English.lproj",
|
---|
339 | "Resources/Info.plist",
|
---|
340 | ]
|
---|
341 |
|
---|
342 | def isFramework():
|
---|
343 | return sys.exec_prefix.find("Python.framework") > 0
|
---|
344 |
|
---|
345 |
|
---|
346 | LIB = os.path.join(sys.prefix, "lib", "python" + sys.version[:3])
|
---|
347 | SITE_PACKAGES = os.path.join(LIB, "site-packages")
|
---|
348 |
|
---|
349 |
|
---|
350 | class AppBuilder(BundleBuilder):
|
---|
351 |
|
---|
352 | # Override type of the bundle.
|
---|
353 | type = "APPL"
|
---|
354 |
|
---|
355 | # platform, name of the subfolder of Contents that contains the executable.
|
---|
356 | platform = "MacOS"
|
---|
357 |
|
---|
358 | # A Python main program. If this argument is given, the main
|
---|
359 | # executable in the bundle will be a small wrapper that invokes
|
---|
360 | # the main program. (XXX Discuss why.)
|
---|
361 | mainprogram = None
|
---|
362 |
|
---|
363 | # The main executable. If a Python main program is specified
|
---|
364 | # the executable will be copied to Resources and be invoked
|
---|
365 | # by the wrapper program mentioned above. Otherwise it will
|
---|
366 | # simply be used as the main executable.
|
---|
367 | executable = None
|
---|
368 |
|
---|
369 | # The name of the main nib, for Cocoa apps. *Must* be specified
|
---|
370 | # when building a Cocoa app.
|
---|
371 | nibname = None
|
---|
372 |
|
---|
373 | # The name of the icon file to be copied to Resources and used for
|
---|
374 | # the Finder icon.
|
---|
375 | iconfile = None
|
---|
376 |
|
---|
377 | # Symlink the executable instead of copying it.
|
---|
378 | symlink_exec = 0
|
---|
379 |
|
---|
380 | # If True, build standalone app.
|
---|
381 | standalone = 0
|
---|
382 |
|
---|
383 | # If True, build semi-standalone app (only includes third-party modules).
|
---|
384 | semi_standalone = 0
|
---|
385 |
|
---|
386 | # If set, use this for #! lines in stead of sys.executable
|
---|
387 | python = None
|
---|
388 |
|
---|
389 | # If True, add a real main program that emulates sys.argv before calling
|
---|
390 | # mainprogram
|
---|
391 | argv_emulation = 0
|
---|
392 |
|
---|
393 | # The following attributes are only used when building a standalone app.
|
---|
394 |
|
---|
395 | # Exclude these modules.
|
---|
396 | excludeModules = []
|
---|
397 |
|
---|
398 | # Include these modules.
|
---|
399 | includeModules = []
|
---|
400 |
|
---|
401 | # Include these packages.
|
---|
402 | includePackages = []
|
---|
403 |
|
---|
404 | # Strip binaries from debug info.
|
---|
405 | strip = 0
|
---|
406 |
|
---|
407 | # Found Python modules: [(name, codeobject, ispkg), ...]
|
---|
408 | pymodules = []
|
---|
409 |
|
---|
410 | # Modules that modulefinder couldn't find:
|
---|
411 | missingModules = []
|
---|
412 | maybeMissingModules = []
|
---|
413 |
|
---|
414 | def setup(self):
|
---|
415 | if ((self.standalone or self.semi_standalone)
|
---|
416 | and self.mainprogram is None):
|
---|
417 | raise BundleBuilderError, ("must specify 'mainprogram' when "
|
---|
418 | "building a standalone application.")
|
---|
419 | if self.mainprogram is None and self.executable is None:
|
---|
420 | raise BundleBuilderError, ("must specify either or both of "
|
---|
421 | "'executable' and 'mainprogram'")
|
---|
422 |
|
---|
423 | self.execdir = pathjoin("Contents", self.platform)
|
---|
424 |
|
---|
425 | if self.name is not None:
|
---|
426 | pass
|
---|
427 | elif self.mainprogram is not None:
|
---|
428 | self.name = os.path.splitext(os.path.basename(self.mainprogram))[0]
|
---|
429 | elif self.executable is not None:
|
---|
430 | self.name = os.path.splitext(os.path.basename(self.executable))[0]
|
---|
431 | if self.name[-4:] != ".app":
|
---|
432 | self.name += ".app"
|
---|
433 |
|
---|
434 | if self.executable is None:
|
---|
435 | if not self.standalone and not isFramework():
|
---|
436 | self.symlink_exec = 1
|
---|
437 | if self.python:
|
---|
438 | self.executable = self.python
|
---|
439 | else:
|
---|
440 | self.executable = sys.executable
|
---|
441 |
|
---|
442 | if self.nibname:
|
---|
443 | self.plist.NSMainNibFile = self.nibname
|
---|
444 | if not hasattr(self.plist, "NSPrincipalClass"):
|
---|
445 | self.plist.NSPrincipalClass = "NSApplication"
|
---|
446 |
|
---|
447 | if self.standalone and isFramework():
|
---|
448 | self.addPythonFramework()
|
---|
449 |
|
---|
450 | BundleBuilder.setup(self)
|
---|
451 |
|
---|
452 | self.plist.CFBundleExecutable = self.name
|
---|
453 |
|
---|
454 | if self.standalone or self.semi_standalone:
|
---|
455 | self.findDependencies()
|
---|
456 |
|
---|
457 | def preProcess(self):
|
---|
458 | resdir = "Contents/Resources"
|
---|
459 | if self.executable is not None:
|
---|
460 | if self.mainprogram is None:
|
---|
461 | execname = self.name
|
---|
462 | else:
|
---|
463 | execname = os.path.basename(self.executable)
|
---|
464 | execpath = pathjoin(self.execdir, execname)
|
---|
465 | if not self.symlink_exec:
|
---|
466 | self.files.append((self.destroot + self.executable, execpath))
|
---|
467 | self.execpath = execpath
|
---|
468 |
|
---|
469 | if self.mainprogram is not None:
|
---|
470 | mainprogram = os.path.basename(self.mainprogram)
|
---|
471 | self.files.append((self.mainprogram, pathjoin(resdir, mainprogram)))
|
---|
472 | if self.argv_emulation:
|
---|
473 | # Change the main program, and create the helper main program (which
|
---|
474 | # does argv collection and then calls the real main).
|
---|
475 | # Also update the included modules (if we're creating a standalone
|
---|
476 | # program) and the plist
|
---|
477 | realmainprogram = mainprogram
|
---|
478 | mainprogram = '__argvemulator_' + mainprogram
|
---|
479 | resdirpath = pathjoin(self.bundlepath, resdir)
|
---|
480 | mainprogrampath = pathjoin(resdirpath, mainprogram)
|
---|
481 | makedirs(resdirpath)
|
---|
482 | open(mainprogrampath, "w").write(ARGV_EMULATOR % locals())
|
---|
483 | if self.standalone or self.semi_standalone:
|
---|
484 | self.includeModules.append("argvemulator")
|
---|
485 | self.includeModules.append("os")
|
---|
486 | if not self.plist.has_key("CFBundleDocumentTypes"):
|
---|
487 | self.plist["CFBundleDocumentTypes"] = [
|
---|
488 | { "CFBundleTypeOSTypes" : [
|
---|
489 | "****",
|
---|
490 | "fold",
|
---|
491 | "disk"],
|
---|
492 | "CFBundleTypeRole": "Viewer"}]
|
---|
493 | # Write bootstrap script
|
---|
494 | executable = os.path.basename(self.executable)
|
---|
495 | execdir = pathjoin(self.bundlepath, self.execdir)
|
---|
496 | bootstrappath = pathjoin(execdir, self.name)
|
---|
497 | makedirs(execdir)
|
---|
498 | if self.standalone or self.semi_standalone:
|
---|
499 | # XXX we're screwed when the end user has deleted
|
---|
500 | # /usr/bin/python
|
---|
501 | hashbang = "/usr/bin/python"
|
---|
502 | elif self.python:
|
---|
503 | hashbang = self.python
|
---|
504 | else:
|
---|
505 | hashbang = os.path.realpath(sys.executable)
|
---|
506 | standalone = self.standalone
|
---|
507 | semi_standalone = self.semi_standalone
|
---|
508 | open(bootstrappath, "w").write(BOOTSTRAP_SCRIPT % locals())
|
---|
509 | os.chmod(bootstrappath, 0775)
|
---|
510 |
|
---|
511 | if self.iconfile is not None:
|
---|
512 | iconbase = os.path.basename(self.iconfile)
|
---|
513 | self.plist.CFBundleIconFile = iconbase
|
---|
514 | self.files.append((self.iconfile, pathjoin(resdir, iconbase)))
|
---|
515 |
|
---|
516 | def postProcess(self):
|
---|
517 | if self.standalone or self.semi_standalone:
|
---|
518 | self.addPythonModules()
|
---|
519 | if self.strip and not self.symlink:
|
---|
520 | self.stripBinaries()
|
---|
521 |
|
---|
522 | if self.symlink_exec and self.executable:
|
---|
523 | self.message("Symlinking executable %s to %s" % (self.executable,
|
---|
524 | self.execpath), 2)
|
---|
525 | dst = pathjoin(self.bundlepath, self.execpath)
|
---|
526 | makedirs(os.path.dirname(dst))
|
---|
527 | os.symlink(os.path.abspath(self.executable), dst)
|
---|
528 |
|
---|
529 | if self.missingModules or self.maybeMissingModules:
|
---|
530 | self.reportMissing()
|
---|
531 |
|
---|
532 | def addPythonFramework(self):
|
---|
533 | # If we're building a standalone app with Python.framework,
|
---|
534 | # include a minimal subset of Python.framework, *unless*
|
---|
535 | # Python.framework was specified manually in self.libs.
|
---|
536 | for lib in self.libs:
|
---|
537 | if os.path.basename(lib) == "Python.framework":
|
---|
538 | # a Python.framework was specified as a library
|
---|
539 | return
|
---|
540 |
|
---|
541 | frameworkpath = sys.exec_prefix[:sys.exec_prefix.find(
|
---|
542 | "Python.framework") + len("Python.framework")]
|
---|
543 |
|
---|
544 | version = sys.version[:3]
|
---|
545 | frameworkpath = pathjoin(frameworkpath, "Versions", version)
|
---|
546 | destbase = pathjoin("Contents", "Frameworks", "Python.framework",
|
---|
547 | "Versions", version)
|
---|
548 | for item in PYTHONFRAMEWORKGOODIES:
|
---|
549 | src = pathjoin(frameworkpath, item)
|
---|
550 | dst = pathjoin(destbase, item)
|
---|
551 | self.files.append((src, dst))
|
---|
552 |
|
---|
553 | def _getSiteCode(self):
|
---|
554 | return compile(SITE_PY % {"semi_standalone": self.semi_standalone},
|
---|
555 | "<-bundlebuilder.py->", "exec")
|
---|
556 |
|
---|
557 | def addPythonModules(self):
|
---|
558 | self.message("Adding Python modules", 1)
|
---|
559 |
|
---|
560 | if USE_ZIPIMPORT:
|
---|
561 | # Create a zip file containing all modules as pyc.
|
---|
562 | import zipfile
|
---|
563 | relpath = pathjoin("Contents", "Resources", ZIP_ARCHIVE)
|
---|
564 | abspath = pathjoin(self.bundlepath, relpath)
|
---|
565 | zf = zipfile.ZipFile(abspath, "w", zipfile.ZIP_DEFLATED)
|
---|
566 | for name, code, ispkg in self.pymodules:
|
---|
567 | self.message("Adding Python module %s" % name, 2)
|
---|
568 | path, pyc = getPycData(name, code, ispkg)
|
---|
569 | zf.writestr(path, pyc)
|
---|
570 | zf.close()
|
---|
571 | # add site.pyc
|
---|
572 | sitepath = pathjoin(self.bundlepath, "Contents", "Resources",
|
---|
573 | "site" + PYC_EXT)
|
---|
574 | writePyc(self._getSiteCode(), sitepath)
|
---|
575 | else:
|
---|
576 | # Create individual .pyc files.
|
---|
577 | for name, code, ispkg in self.pymodules:
|
---|
578 | if ispkg:
|
---|
579 | name += ".__init__"
|
---|
580 | path = name.split(".")
|
---|
581 | path = pathjoin("Contents", "Resources", *path) + PYC_EXT
|
---|
582 |
|
---|
583 | if ispkg:
|
---|
584 | self.message("Adding Python package %s" % path, 2)
|
---|
585 | else:
|
---|
586 | self.message("Adding Python module %s" % path, 2)
|
---|
587 |
|
---|
588 | abspath = pathjoin(self.bundlepath, path)
|
---|
589 | makedirs(os.path.dirname(abspath))
|
---|
590 | writePyc(code, abspath)
|
---|
591 |
|
---|
592 | def stripBinaries(self):
|
---|
593 | if not os.path.exists(STRIP_EXEC):
|
---|
594 | self.message("Error: can't strip binaries: no strip program at "
|
---|
595 | "%s" % STRIP_EXEC, 0)
|
---|
596 | else:
|
---|
597 | import stat
|
---|
598 | self.message("Stripping binaries", 1)
|
---|
599 | def walk(top):
|
---|
600 | for name in os.listdir(top):
|
---|
601 | path = pathjoin(top, name)
|
---|
602 | if os.path.islink(path):
|
---|
603 | continue
|
---|
604 | if os.path.isdir(path):
|
---|
605 | walk(path)
|
---|
606 | else:
|
---|
607 | mod = os.stat(path)[stat.ST_MODE]
|
---|
608 | if not (mod & 0100):
|
---|
609 | continue
|
---|
610 | relpath = path[len(self.bundlepath):]
|
---|
611 | self.message("Stripping %s" % relpath, 2)
|
---|
612 | inf, outf = os.popen4("%s -S \"%s\"" %
|
---|
613 | (STRIP_EXEC, path))
|
---|
614 | output = outf.read().strip()
|
---|
615 | if output:
|
---|
616 | # usually not a real problem, like when we're
|
---|
617 | # trying to strip a script
|
---|
618 | self.message("Problem stripping %s:" % relpath, 3)
|
---|
619 | self.message(output, 3)
|
---|
620 | walk(self.bundlepath)
|
---|
621 |
|
---|
622 | def findDependencies(self):
|
---|
623 | self.message("Finding module dependencies", 1)
|
---|
624 | import modulefinder
|
---|
625 | mf = modulefinder.ModuleFinder(excludes=self.excludeModules)
|
---|
626 | if USE_ZIPIMPORT:
|
---|
627 | # zipimport imports zlib, must add it manually
|
---|
628 | mf.import_hook("zlib")
|
---|
629 | # manually add our own site.py
|
---|
630 | site = mf.add_module("site")
|
---|
631 | site.__code__ = self._getSiteCode()
|
---|
632 | mf.scan_code(site.__code__, site)
|
---|
633 |
|
---|
634 | # warnings.py gets imported implicitly from C
|
---|
635 | mf.import_hook("warnings")
|
---|
636 |
|
---|
637 | includeModules = self.includeModules[:]
|
---|
638 | for name in self.includePackages:
|
---|
639 | includeModules.extend(findPackageContents(name).keys())
|
---|
640 | for name in includeModules:
|
---|
641 | try:
|
---|
642 | mf.import_hook(name)
|
---|
643 | except ImportError:
|
---|
644 | self.missingModules.append(name)
|
---|
645 |
|
---|
646 | mf.run_script(self.mainprogram)
|
---|
647 | modules = mf.modules.items()
|
---|
648 | modules.sort()
|
---|
649 | for name, mod in modules:
|
---|
650 | path = mod.__file__
|
---|
651 | if path and self.semi_standalone:
|
---|
652 | # skip the standard library
|
---|
653 | if path.startswith(LIB) and not path.startswith(SITE_PACKAGES):
|
---|
654 | continue
|
---|
655 | if path and mod.__code__ is None:
|
---|
656 | # C extension
|
---|
657 | filename = os.path.basename(path)
|
---|
658 | pathitems = name.split(".")[:-1] + [filename]
|
---|
659 | dstpath = pathjoin(*pathitems)
|
---|
660 | if USE_ZIPIMPORT:
|
---|
661 | if name != "zlib":
|
---|
662 | # neatly pack all extension modules in a subdirectory,
|
---|
663 | # except zlib, since it's necessary for bootstrapping.
|
---|
664 | dstpath = pathjoin("ExtensionModules", dstpath)
|
---|
665 | # Python modules are stored in a Zip archive, but put
|
---|
666 | # extensions in Contents/Resources/. Add a tiny "loader"
|
---|
667 | # program in the Zip archive. Due to Thomas Heller.
|
---|
668 | source = EXT_LOADER % {"name": name, "filename": dstpath}
|
---|
669 | code = compile(source, "<dynloader for %s>" % name, "exec")
|
---|
670 | mod.__code__ = code
|
---|
671 | self.files.append((path, pathjoin("Contents", "Resources", dstpath)))
|
---|
672 | if mod.__code__ is not None:
|
---|
673 | ispkg = mod.__path__ is not None
|
---|
674 | if not USE_ZIPIMPORT or name != "site":
|
---|
675 | # Our site.py is doing the bootstrapping, so we must
|
---|
676 | # include a real .pyc file if USE_ZIPIMPORT is True.
|
---|
677 | self.pymodules.append((name, mod.__code__, ispkg))
|
---|
678 |
|
---|
679 | if hasattr(mf, "any_missing_maybe"):
|
---|
680 | missing, maybe = mf.any_missing_maybe()
|
---|
681 | else:
|
---|
682 | missing = mf.any_missing()
|
---|
683 | maybe = []
|
---|
684 | self.missingModules.extend(missing)
|
---|
685 | self.maybeMissingModules.extend(maybe)
|
---|
686 |
|
---|
687 | def reportMissing(self):
|
---|
688 | missing = [name for name in self.missingModules
|
---|
689 | if name not in MAYMISS_MODULES]
|
---|
690 | if self.maybeMissingModules:
|
---|
691 | maybe = self.maybeMissingModules
|
---|
692 | else:
|
---|
693 | maybe = [name for name in missing if "." in name]
|
---|
694 | missing = [name for name in missing if "." not in name]
|
---|
695 | missing.sort()
|
---|
696 | maybe.sort()
|
---|
697 | if maybe:
|
---|
698 | self.message("Warning: couldn't find the following submodules:", 1)
|
---|
699 | self.message(" (Note that these could be false alarms -- "
|
---|
700 | "it's not always", 1)
|
---|
701 | self.message(" possible to distinguish between \"from package "
|
---|
702 | "import submodule\" ", 1)
|
---|
703 | self.message(" and \"from package import name\")", 1)
|
---|
704 | for name in maybe:
|
---|
705 | self.message(" ? " + name, 1)
|
---|
706 | if missing:
|
---|
707 | self.message("Warning: couldn't find the following modules:", 1)
|
---|
708 | for name in missing:
|
---|
709 | self.message(" ? " + name, 1)
|
---|
710 |
|
---|
711 | def report(self):
|
---|
712 | # XXX something decent
|
---|
713 | import pprint
|
---|
714 | pprint.pprint(self.__dict__)
|
---|
715 | if self.standalone or self.semi_standalone:
|
---|
716 | self.reportMissing()
|
---|
717 |
|
---|
718 | #
|
---|
719 | # Utilities.
|
---|
720 | #
|
---|
721 |
|
---|
722 | SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()]
|
---|
723 | identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$")
|
---|
724 |
|
---|
725 | def findPackageContents(name, searchpath=None):
|
---|
726 | head = name.split(".")[-1]
|
---|
727 | if identifierRE.match(head) is None:
|
---|
728 | return {}
|
---|
729 | try:
|
---|
730 | fp, path, (ext, mode, tp) = imp.find_module(head, searchpath)
|
---|
731 | except ImportError:
|
---|
732 | return {}
|
---|
733 | modules = {name: None}
|
---|
734 | if tp == imp.PKG_DIRECTORY and path:
|
---|
735 | files = os.listdir(path)
|
---|
736 | for sub in files:
|
---|
737 | sub, ext = os.path.splitext(sub)
|
---|
738 | fullname = name + "." + sub
|
---|
739 | if sub != "__init__" and fullname not in modules:
|
---|
740 | modules.update(findPackageContents(fullname, [path]))
|
---|
741 | return modules
|
---|
742 |
|
---|
743 | def writePyc(code, path):
|
---|
744 | f = open(path, "wb")
|
---|
745 | f.write(MAGIC)
|
---|
746 | f.write("\0" * 4) # don't bother about a time stamp
|
---|
747 | marshal.dump(code, f)
|
---|
748 | f.close()
|
---|
749 |
|
---|
750 | def copy(src, dst, mkdirs=0):
|
---|
751 | """Copy a file or a directory."""
|
---|
752 | if mkdirs:
|
---|
753 | makedirs(os.path.dirname(dst))
|
---|
754 | if os.path.isdir(src):
|
---|
755 | shutil.copytree(src, dst, symlinks=1)
|
---|
756 | else:
|
---|
757 | shutil.copy2(src, dst)
|
---|
758 |
|
---|
759 | def copytodir(src, dstdir):
|
---|
760 | """Copy a file or a directory to an existing directory."""
|
---|
761 | dst = pathjoin(dstdir, os.path.basename(src))
|
---|
762 | copy(src, dst)
|
---|
763 |
|
---|
764 | def makedirs(dir):
|
---|
765 | """Make all directories leading up to 'dir' including the leaf
|
---|
766 | directory. Don't moan if any path element already exists."""
|
---|
767 | try:
|
---|
768 | os.makedirs(dir)
|
---|
769 | except OSError, why:
|
---|
770 | if why.errno != errno.EEXIST:
|
---|
771 | raise
|
---|
772 |
|
---|
773 | def symlink(src, dst, mkdirs=0):
|
---|
774 | """Copy a file or a directory."""
|
---|
775 | if not os.path.exists(src):
|
---|
776 | raise IOError, "No such file or directory: '%s'" % src
|
---|
777 | if mkdirs:
|
---|
778 | makedirs(os.path.dirname(dst))
|
---|
779 | os.symlink(os.path.abspath(src), dst)
|
---|
780 |
|
---|
781 | def pathjoin(*args):
|
---|
782 | """Safe wrapper for os.path.join: asserts that all but the first
|
---|
783 | argument are relative paths."""
|
---|
784 | for seg in args[1:]:
|
---|
785 | assert seg[0] != "/"
|
---|
786 | return os.path.join(*args)
|
---|
787 |
|
---|
788 |
|
---|
789 | cmdline_doc = """\
|
---|
790 | Usage:
|
---|
791 | python bundlebuilder.py [options] command
|
---|
792 | python mybuildscript.py [options] command
|
---|
793 |
|
---|
794 | Commands:
|
---|
795 | build build the application
|
---|
796 | report print a report
|
---|
797 |
|
---|
798 | Options:
|
---|
799 | -b, --builddir=DIR the build directory; defaults to "build"
|
---|
800 | -n, --name=NAME application name
|
---|
801 | -r, --resource=FILE extra file or folder to be copied to Resources
|
---|
802 | -f, --file=SRC:DST extra file or folder to be copied into the bundle;
|
---|
803 | DST must be a path relative to the bundle root
|
---|
804 | -e, --executable=FILE the executable to be used
|
---|
805 | -m, --mainprogram=FILE the Python main program
|
---|
806 | -a, --argv add a wrapper main program to create sys.argv
|
---|
807 | -p, --plist=FILE .plist file (default: generate one)
|
---|
808 | --nib=NAME main nib name
|
---|
809 | -c, --creator=CCCC 4-char creator code (default: '????')
|
---|
810 | --iconfile=FILE filename of the icon (an .icns file) to be used
|
---|
811 | as the Finder icon
|
---|
812 | --bundle-id=ID the CFBundleIdentifier, in reverse-dns format
|
---|
813 | (eg. org.python.BuildApplet; this is used for
|
---|
814 | the preferences file name)
|
---|
815 | -l, --link symlink files/folder instead of copying them
|
---|
816 | --link-exec symlink the executable instead of copying it
|
---|
817 | --standalone build a standalone application, which is fully
|
---|
818 | independent of a Python installation
|
---|
819 | --semi-standalone build a standalone application, which depends on
|
---|
820 | an installed Python, yet includes all third-party
|
---|
821 | modules.
|
---|
822 | --python=FILE Python to use in #! line in stead of current Python
|
---|
823 | --lib=FILE shared library or framework to be copied into
|
---|
824 | the bundle
|
---|
825 | -x, --exclude=MODULE exclude module (with --(semi-)standalone)
|
---|
826 | -i, --include=MODULE include module (with --(semi-)standalone)
|
---|
827 | --package=PACKAGE include a whole package (with --(semi-)standalone)
|
---|
828 | --strip strip binaries (remove debug info)
|
---|
829 | -v, --verbose increase verbosity level
|
---|
830 | -q, --quiet decrease verbosity level
|
---|
831 | -h, --help print this message
|
---|
832 | """
|
---|
833 |
|
---|
834 | def usage(msg=None):
|
---|
835 | if msg:
|
---|
836 | print msg
|
---|
837 | print cmdline_doc
|
---|
838 | sys.exit(1)
|
---|
839 |
|
---|
840 | def main(builder=None):
|
---|
841 | if builder is None:
|
---|
842 | builder = AppBuilder(verbosity=1)
|
---|
843 |
|
---|
844 | shortopts = "b:n:r:f:e:m:c:p:lx:i:hvqa"
|
---|
845 | longopts = ("builddir=", "name=", "resource=", "file=", "executable=",
|
---|
846 | "mainprogram=", "creator=", "nib=", "plist=", "link",
|
---|
847 | "link-exec", "help", "verbose", "quiet", "argv", "standalone",
|
---|
848 | "exclude=", "include=", "package=", "strip", "iconfile=",
|
---|
849 | "lib=", "python=", "semi-standalone", "bundle-id=", "destroot=")
|
---|
850 |
|
---|
851 | try:
|
---|
852 | options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
|
---|
853 | except getopt.error:
|
---|
854 | usage()
|
---|
855 |
|
---|
856 | for opt, arg in options:
|
---|
857 | if opt in ('-b', '--builddir'):
|
---|
858 | builder.builddir = arg
|
---|
859 | elif opt in ('-n', '--name'):
|
---|
860 | builder.name = arg
|
---|
861 | elif opt in ('-r', '--resource'):
|
---|
862 | builder.resources.append(os.path.normpath(arg))
|
---|
863 | elif opt in ('-f', '--file'):
|
---|
864 | srcdst = arg.split(':')
|
---|
865 | if len(srcdst) != 2:
|
---|
866 | usage("-f or --file argument must be two paths, "
|
---|
867 | "separated by a colon")
|
---|
868 | builder.files.append(srcdst)
|
---|
869 | elif opt in ('-e', '--executable'):
|
---|
870 | builder.executable = arg
|
---|
871 | elif opt in ('-m', '--mainprogram'):
|
---|
872 | builder.mainprogram = arg
|
---|
873 | elif opt in ('-a', '--argv'):
|
---|
874 | builder.argv_emulation = 1
|
---|
875 | elif opt in ('-c', '--creator'):
|
---|
876 | builder.creator = arg
|
---|
877 | elif opt == '--bundle-id':
|
---|
878 | builder.bundle_id = arg
|
---|
879 | elif opt == '--iconfile':
|
---|
880 | builder.iconfile = arg
|
---|
881 | elif opt == "--lib":
|
---|
882 | builder.libs.append(os.path.normpath(arg))
|
---|
883 | elif opt == "--nib":
|
---|
884 | builder.nibname = arg
|
---|
885 | elif opt in ('-p', '--plist'):
|
---|
886 | builder.plist = Plist.fromFile(arg)
|
---|
887 | elif opt in ('-l', '--link'):
|
---|
888 | builder.symlink = 1
|
---|
889 | elif opt == '--link-exec':
|
---|
890 | builder.symlink_exec = 1
|
---|
891 | elif opt in ('-h', '--help'):
|
---|
892 | usage()
|
---|
893 | elif opt in ('-v', '--verbose'):
|
---|
894 | builder.verbosity += 1
|
---|
895 | elif opt in ('-q', '--quiet'):
|
---|
896 | builder.verbosity -= 1
|
---|
897 | elif opt == '--standalone':
|
---|
898 | builder.standalone = 1
|
---|
899 | elif opt == '--semi-standalone':
|
---|
900 | builder.semi_standalone = 1
|
---|
901 | elif opt == '--python':
|
---|
902 | builder.python = arg
|
---|
903 | elif opt in ('-x', '--exclude'):
|
---|
904 | builder.excludeModules.append(arg)
|
---|
905 | elif opt in ('-i', '--include'):
|
---|
906 | builder.includeModules.append(arg)
|
---|
907 | elif opt == '--package':
|
---|
908 | builder.includePackages.append(arg)
|
---|
909 | elif opt == '--strip':
|
---|
910 | builder.strip = 1
|
---|
911 | elif opt == '--destroot':
|
---|
912 | builder.destroot = arg
|
---|
913 |
|
---|
914 | if len(args) != 1:
|
---|
915 | usage("Must specify one command ('build', 'report' or 'help')")
|
---|
916 | command = args[0]
|
---|
917 |
|
---|
918 | if command == "build":
|
---|
919 | builder.setup()
|
---|
920 | builder.build()
|
---|
921 | elif command == "report":
|
---|
922 | builder.setup()
|
---|
923 | builder.report()
|
---|
924 | elif command == "help":
|
---|
925 | usage()
|
---|
926 | else:
|
---|
927 | usage("Unknown command '%s'" % command)
|
---|
928 |
|
---|
929 |
|
---|
930 | def buildapp(**kwargs):
|
---|
931 | builder = AppBuilder(**kwargs)
|
---|
932 | main(builder)
|
---|
933 |
|
---|
934 |
|
---|
935 | if __name__ == "__main__":
|
---|
936 | main()
|
---|