4 bundlebuilder.py -- Tools to assemble MacOS X (application) bundles.
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.
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.
14 The module contains a main program that can be used in two ways:
16 % python bundlebuilder.py [options] build
17 % python buildapp.py [options] build
19 Where "buildapp.py" is a user-supplied setup.py-like script following
22 from bundlebuilder import buildapp
23 buildapp(<lots-of-keyword-args>)
28 __all__
= ["BundleBuilder", "BundleBuilderError", "AppBuilder", "buildapp"]
32 import os
, errno
, shutil
35 from copy
import deepcopy
37 from plistlib
import Plist
38 from types
import FunctionType
as function
40 class BundleBuilderError(Exception): pass
45 """Class attributes that don't start with an underscore and are
46 not functions or classmethods are (deep)copied to self.__dict__.
47 This allows for mutable default values.
50 def __init__(self
, **kwargs
):
51 defaults
= self
._getDefaults
()
52 defaults
.update(kwargs
)
53 self
.__dict
__.update(defaults
)
55 def _getDefaults(cls
):
57 for name
, value
in cls
.__dict
__.items():
58 if name
[0] != "_" and not isinstance(value
,
59 (function
, classmethod)):
60 defaults
[name
] = deepcopy(value
)
61 for base
in cls
.__bases
__:
62 if hasattr(base
, "_getDefaults"):
63 defaults
.update(base
._getDefaults
())
65 _getDefaults
= classmethod(_getDefaults
)
68 class BundleBuilder(Defaults
):
70 """BundleBuilder is a barebones class for assembling bundles. It
71 knows nothing about executables or icons, it only copies files
72 and creates the PkgInfo and Info.plist files.
75 # (Note that Defaults.__init__ (deep)copies these values to
76 # instance variables. Mutable defaults are therefore safe.)
78 # Name of the bundle, with or without extension.
81 # The property list ("plist")
82 plist
= Plist(CFBundleDevelopmentRegion
= "English",
83 CFBundleInfoDictionaryVersion
= "6.0")
85 # The type of the bundle.
87 # The creator code of the bundle.
90 # List of files that have to be copied to <bundle>/Contents/Resources.
93 # List of (src, dest) tuples; dest should be a path relative to the bundle
94 # (eg. "Contents/Resources/MyStuff/SomeFile.ext).
97 # List of shared libraries (dylibs, Frameworks) to bundle with the app
98 # will be placed in Contents/Frameworks
101 # Directory where the bundle will be assembled.
104 # Make symlinks instead copying files. This is handy during debugging, but
105 # makes the bundle non-distributable.
112 # XXX rethink self.name munging, this is brittle.
113 self
.name
, ext
= os
.path
.splitext(self
.name
)
116 bundleextension
= ext
117 # misc (derived) attributes
118 self
.bundlepath
= pathjoin(self
.builddir
, self
.name
+ bundleextension
)
121 plist
.CFBundleName
= self
.name
122 plist
.CFBundlePackageType
= self
.type
123 if self
.creator
is None:
124 if hasattr(plist
, "CFBundleSignature"):
125 self
.creator
= plist
.CFBundleSignature
127 self
.creator
= "????"
128 plist
.CFBundleSignature
= self
.creator
129 if not hasattr(plist
, "CFBundleIdentifier"):
130 plist
.CFBundleIdentifier
= self
.name
133 """Build the bundle."""
134 builddir
= self
.builddir
135 if builddir
and not os
.path
.exists(builddir
):
137 self
.message("Building %s" % repr(self
.bundlepath
), 1)
138 if os
.path
.exists(self
.bundlepath
):
139 shutil
.rmtree(self
.bundlepath
)
140 os
.mkdir(self
.bundlepath
)
145 self
.message("Done.", 1)
147 def preProcess(self
):
148 """Hook for subclasses."""
150 def postProcess(self
):
151 """Hook for subclasses."""
154 def _addMetaFiles(self
):
155 contents
= pathjoin(self
.bundlepath
, "Contents")
158 # Write Contents/PkgInfo
159 assert len(self
.type) == len(self
.creator
) == 4, \
160 "type and creator must be 4-byte strings."
161 pkginfo
= pathjoin(contents
, "PkgInfo")
162 f
= open(pkginfo
, "wb")
163 f
.write(self
.type + self
.creator
)
166 # Write Contents/Info.plist
167 infoplist
= pathjoin(contents
, "Info.plist")
168 self
.plist
.write(infoplist
)
170 def _copyFiles(self
):
171 files
= self
.files
[:]
172 for path
in self
.resources
:
173 files
.append((path
, pathjoin("Contents", "Resources",
174 os
.path
.basename(path
))))
175 for path
in self
.libs
:
176 files
.append((path
, pathjoin("Contents", "Frameworks",
177 os
.path
.basename(path
))))
179 self
.message("Making symbolic links", 1)
180 msg
= "Making symlink from"
182 self
.message("Copying files", 1)
185 for src
, dst
in files
:
186 if os
.path
.isdir(src
):
187 self
.message("%s %s/ to %s/" % (msg
, src
, dst
), 2)
189 self
.message("%s %s to %s" % (msg
, src
, dst
), 2)
190 dst
= pathjoin(self
.bundlepath
, dst
)
192 symlink(src
, dst
, mkdirs
=1)
194 copy(src
, dst
, mkdirs
=1)
196 def message(self
, msg
, level
=0):
197 if level
<= self
.verbosity
:
200 indent
= (level
- 1) * " "
201 sys
.stderr
.write(indent
+ msg
+ "\n")
204 # XXX something decent
213 MAGIC
= imp
.get_magic()
214 USE_ZIPIMPORT
= "zipimport" in sys
.builtin_module_names
216 # For standalone apps, we have our own minimal site.py. We don't need
217 # all the cruft of the real site.py.
220 del sys.path[1:] # sys.path[0] is Contents/Resources/
224 ZIP_ARCHIVE
= "Modules.zip"
225 SITE_PY
+= "sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE
226 def getPycData(fullname
, code
, ispkg
):
228 fullname
+= ".__init__"
229 path
= fullname
.replace(".", os
.sep
) + PYC_EXT
230 return path
, MAGIC
+ '\0\0\0\0' + marshal
.dumps(code
)
232 SITE_CO
= compile(SITE_PY
, "<-bundlebuilder.py->", "exec")
235 # Extension modules can't be in the modules zip archive, so a placeholder
236 # is added instead, that loads the extension from a specified location.
242 path = os.path.join(p, "%(filename)s")
243 if os.path.exists(path):
246 assert 0, "file not found: %(filename)s"
247 mod = imp.load_dynamic("%(name)s", path)
253 MAYMISS_MODULES
= ['mac', 'os2', 'nt', 'ntpath', 'dos', 'dospath',
254 'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
255 'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
258 STRIP_EXEC
= "/usr/bin/strip"
261 # We're using a stock interpreter to run the app, yet we need
262 # a way to pass the Python main program to the interpreter. The
263 # bootstrapping script fires up the interpreter with the right
264 # arguments. os.execve() is used as OSX doesn't like us to
265 # start a real new process. Also, the executable name must match
266 # the CFBundleExecutable value in the Info.plist, so we lie
267 # deliberately with argv[0]. The actual Python executable is
268 # passed in an environment variable so we can "repair"
269 # sys.executable later.
271 BOOTSTRAP_SCRIPT
= """\
275 execdir = os.path.dirname(sys.argv[0])
276 executable = os.path.join(execdir, "%(executable)s")
277 resdir = os.path.join(os.path.dirname(execdir), "Resources")
278 libdir = os.path.join(os.path.dirname(execdir), "Frameworks")
279 mainprogram = os.path.join(resdir, "%(mainprogram)s")
281 sys.argv.insert(1, mainprogram)
282 os.environ["PYTHONPATH"] = resdir
284 os.environ["PYTHONEXECUTABLE"] = executable
285 os.environ["DYLD_LIBRARY_PATH"] = libdir
286 os.execve(executable, sys.argv, os.environ)
291 # Optional wrapper that converts "dropped files" into sys.argv values.
294 import argvemulator, os
296 argvemulator.ArgvCollector().mainloop()
297 execfile(os.path.join(os.path.split(__file__)[0], "%(realmainprogram)s"))
301 class AppBuilder(BundleBuilder
):
303 # Override type of the bundle.
306 # platform, name of the subfolder of Contents that contains the executable.
309 # A Python main program. If this argument is given, the main
310 # executable in the bundle will be a small wrapper that invokes
311 # the main program. (XXX Discuss why.)
314 # The main executable. If a Python main program is specified
315 # the executable will be copied to Resources and be invoked
316 # by the wrapper program mentioned above. Otherwise it will
317 # simply be used as the main executable.
320 # The name of the main nib, for Cocoa apps. *Must* be specified
321 # when building a Cocoa app.
324 # The name of the icon file to be copied to Resources and used for
328 # Symlink the executable instead of copying it.
331 # If True, build standalone app.
334 # If True, add a real main program that emulates sys.argv before calling
338 # The following attributes are only used when building a standalone app.
340 # Exclude these modules.
343 # Include these modules.
346 # Include these packages.
352 # Found Python modules: [(name, codeobject, ispkg), ...]
355 # Modules that modulefinder couldn't find:
357 maybeMissingModules
= []
359 # List of all binaries (executables or shared libs), for stripping purposes
363 if self
.standalone
and self
.mainprogram
is None:
364 raise BundleBuilderError
, ("must specify 'mainprogram' when "
365 "building a standalone application.")
366 if self
.mainprogram
is None and self
.executable
is None:
367 raise BundleBuilderError
, ("must specify either or both of "
368 "'executable' and 'mainprogram'")
370 self
.execdir
= pathjoin("Contents", self
.platform
)
372 if self
.name
is not None:
374 elif self
.mainprogram
is not None:
375 self
.name
= os
.path
.splitext(os
.path
.basename(self
.mainprogram
))[0]
376 elif executable
is not None:
377 self
.name
= os
.path
.splitext(os
.path
.basename(self
.executable
))[0]
378 if self
.name
[-4:] != ".app":
381 if self
.executable
is None:
382 if not self
.standalone
:
383 self
.symlink_exec
= 1
384 self
.executable
= sys
.executable
387 self
.plist
.NSMainNibFile
= self
.nibname
388 if not hasattr(self
.plist
, "NSPrincipalClass"):
389 self
.plist
.NSPrincipalClass
= "NSApplication"
391 BundleBuilder
.setup(self
)
393 self
.plist
.CFBundleExecutable
= self
.name
396 self
.findDependencies()
398 def preProcess(self
):
399 resdir
= "Contents/Resources"
400 if self
.executable
is not None:
401 if self
.mainprogram
is None:
404 execname
= os
.path
.basename(self
.executable
)
405 execpath
= pathjoin(self
.execdir
, execname
)
406 if not self
.symlink_exec
:
407 self
.files
.append((self
.executable
, execpath
))
408 self
.binaries
.append(execpath
)
409 self
.execpath
= execpath
411 if self
.mainprogram
is not None:
412 mainprogram
= os
.path
.basename(self
.mainprogram
)
413 self
.files
.append((self
.mainprogram
, pathjoin(resdir
, mainprogram
)))
414 if self
.argv_emulation
:
415 # Change the main program, and create the helper main program (which
416 # does argv collection and then calls the real main).
417 # Also update the included modules (if we're creating a standalone
418 # program) and the plist
419 realmainprogram
= mainprogram
420 mainprogram
= '__argvemulator_' + mainprogram
421 resdirpath
= pathjoin(self
.bundlepath
, resdir
)
422 mainprogrampath
= pathjoin(resdirpath
, mainprogram
)
424 open(mainprogrampath
, "w").write(ARGV_EMULATOR
% locals())
426 self
.includeModules
.append("argvemulator")
427 self
.includeModules
.append("os")
428 if not self
.plist
.has_key("CFBundleDocumentTypes"):
429 self
.plist
["CFBundleDocumentTypes"] = [
430 { "CFBundleTypeOSTypes" : [
434 "CFBundleTypeRole": "Viewer"}]
435 # Write bootstrap script
436 executable
= os
.path
.basename(self
.executable
)
437 execdir
= pathjoin(self
.bundlepath
, self
.execdir
)
438 bootstrappath
= pathjoin(execdir
, self
.name
)
441 # XXX we're screwed when the end user has deleted
443 hashbang
= "/usr/bin/python"
444 pythonhome
= 'os.environ["PYTHONHOME"] = resdir'
446 hashbang
= sys
.executable
447 while os
.path
.islink(hashbang
):
448 hashbang
= os
.readlink(hashbang
)
450 open(bootstrappath
, "w").write(BOOTSTRAP_SCRIPT
% locals())
451 os
.chmod(bootstrappath
, 0775)
453 if self
.iconfile
is not None:
454 iconbase
= os
.path
.basename(self
.iconfile
)
455 self
.plist
.CFBundleIconFile
= iconbase
456 self
.files
.append((self
.iconfile
, pathjoin(resdir
, iconbase
)))
458 def postProcess(self
):
460 self
.addPythonModules()
461 if self
.strip
and not self
.symlink
:
464 if self
.symlink_exec
and self
.executable
:
465 self
.message("Symlinking executable %s to %s" % (self
.executable
,
467 dst
= pathjoin(self
.bundlepath
, self
.execpath
)
468 makedirs(os
.path
.dirname(dst
))
469 os
.symlink(os
.path
.abspath(self
.executable
), dst
)
471 if self
.missingModules
or self
.maybeMissingModules
:
474 def addPythonModules(self
):
475 self
.message("Adding Python modules", 1)
478 # Create a zip file containing all modules as pyc.
480 relpath
= pathjoin("Contents", "Resources", ZIP_ARCHIVE
)
481 abspath
= pathjoin(self
.bundlepath
, relpath
)
482 zf
= zipfile
.ZipFile(abspath
, "w", zipfile
.ZIP_DEFLATED
)
483 for name
, code
, ispkg
in self
.pymodules
:
484 self
.message("Adding Python module %s" % name
, 2)
485 path
, pyc
= getPycData(name
, code
, ispkg
)
486 zf
.writestr(path
, pyc
)
489 sitepath
= pathjoin(self
.bundlepath
, "Contents", "Resources",
491 writePyc(SITE_CO
, sitepath
)
493 # Create individual .pyc files.
494 for name
, code
, ispkg
in self
.pymodules
:
497 path
= name
.split(".")
498 path
= pathjoin("Contents", "Resources", *path
) + PYC_EXT
501 self
.message("Adding Python package %s" % path
, 2)
503 self
.message("Adding Python module %s" % path
, 2)
505 abspath
= pathjoin(self
.bundlepath
, path
)
506 makedirs(os
.path
.dirname(abspath
))
507 writePyc(code
, abspath
)
509 def stripBinaries(self
):
510 if not os
.path
.exists(STRIP_EXEC
):
511 self
.message("Error: can't strip binaries: no strip program at "
512 "%s" % STRIP_EXEC
, 0)
514 self
.message("Stripping binaries", 1)
515 for relpath
in self
.binaries
:
516 self
.message("Stripping %s" % relpath
, 2)
517 abspath
= pathjoin(self
.bundlepath
, relpath
)
518 assert not os
.path
.islink(abspath
)
519 rv
= os
.system("%s -S \"%s\"" % (STRIP_EXEC
, abspath
))
521 def findDependencies(self
):
522 self
.message("Finding module dependencies", 1)
524 mf
= modulefinder
.ModuleFinder(excludes
=self
.excludeModules
)
526 # zipimport imports zlib, must add it manually
527 mf
.import_hook("zlib")
528 # manually add our own site.py
529 site
= mf
.add_module("site")
530 site
.__code
__ = SITE_CO
531 mf
.scan_code(SITE_CO
, site
)
533 # warnings.py gets imported implicitly from C
534 mf
.import_hook("warnings")
536 includeModules
= self
.includeModules
[:]
537 for name
in self
.includePackages
:
538 includeModules
.extend(findPackageContents(name
).keys())
539 for name
in includeModules
:
543 self
.missingModules
.append(name
)
545 mf
.run_script(self
.mainprogram
)
546 modules
= mf
.modules
.items()
548 for name
, mod
in modules
:
549 if mod
.__file
__ and mod
.__code
__ is None:
552 filename
= os
.path
.basename(path
)
554 # Python modules are stored in a Zip archive, but put
555 # extensions in Contents/Resources/.a and add a tiny "loader"
556 # program in the Zip archive. Due to Thomas Heller.
557 dstpath
= pathjoin("Contents", "Resources", filename
)
558 source
= EXT_LOADER
% {"name": name, "filename": filename}
559 code
= compile(source
, "<dynloader for %s>" % name
, "exec")
563 dstpath
= name
.split(".")[:-1] + [filename
]
564 dstpath
= pathjoin("Contents", "Resources", *dstpath
)
565 self
.files
.append((path
, dstpath
))
566 self
.binaries
.append(dstpath
)
567 if mod
.__code
__ is not None:
568 ispkg
= mod
.__path
__ is not None
569 if not USE_ZIPIMPORT
or name
!= "site":
570 # Our site.py is doing the bootstrapping, so we must
571 # include a real .pyc file if USE_ZIPIMPORT is True.
572 self
.pymodules
.append((name
, mod
.__code
__, ispkg
))
574 if hasattr(mf
, "any_missing_maybe"):
575 missing
, maybe
= mf
.any_missing_maybe()
577 missing
= mf
.any_missing()
579 self
.missingModules
.extend(missing
)
580 self
.maybeMissingModules
.extend(maybe
)
582 def reportMissing(self
):
583 missing
= [name
for name
in self
.missingModules
584 if name
not in MAYMISS_MODULES
]
585 if self
.maybeMissingModules
:
586 maybe
= self
.maybeMissingModules
588 maybe
= [name
for name
in missing
if "." in name
]
589 missing
= [name
for name
in missing
if "." not in name
]
593 self
.message("Warning: couldn't find the following submodules:", 1)
594 self
.message(" (Note that these could be false alarms -- "
595 "it's not always", 1)
596 self
.message(" possible to distinguish between \"from package "
597 "import submodule\" ", 1)
598 self
.message(" and \"from package import name\")", 1)
600 self
.message(" ? " + name
, 1)
602 self
.message("Warning: couldn't find the following modules:", 1)
604 self
.message(" ? " + name
, 1)
607 # XXX something decent
609 pprint
.pprint(self
.__dict
__)
617 SUFFIXES
= [_suf
for _suf
, _mode
, _tp
in imp
.get_suffixes()]
618 identifierRE
= re
.compile(r
"[_a-zA-z][_a-zA-Z0-9]*$")
620 def findPackageContents(name
, searchpath
=None):
621 head
= name
.split(".")[-1]
622 if identifierRE
.match(head
) is None:
625 fp
, path
, (ext
, mode
, tp
) = imp
.find_module(head
, searchpath
)
628 modules
= {name: None}
629 if tp
== imp
.PKG_DIRECTORY
and path
:
630 files
= os
.listdir(path
)
632 sub
, ext
= os
.path
.splitext(sub
)
633 fullname
= name
+ "." + sub
634 if sub
!= "__init__" and fullname
not in modules
:
635 modules
.update(findPackageContents(fullname
, [path
]))
638 def writePyc(code
, path
):
641 f
.write("\0" * 4) # don't bother about a time stamp
642 marshal
.dump(code
, f
)
645 def copy(src
, dst
, mkdirs
=0):
646 """Copy a file or a directory."""
648 makedirs(os
.path
.dirname(dst
))
649 if os
.path
.isdir(src
):
650 shutil
.copytree(src
, dst
)
652 shutil
.copy2(src
, dst
)
654 def copytodir(src
, dstdir
):
655 """Copy a file or a directory to an existing directory."""
656 dst
= pathjoin(dstdir
, os
.path
.basename(src
))
660 """Make all directories leading up to 'dir' including the leaf
661 directory. Don't moan if any path element already exists."""
665 if why
.errno
!= errno
.EEXIST
:
668 def symlink(src
, dst
, mkdirs
=0):
669 """Copy a file or a directory."""
670 if not os
.path
.exists(src
):
671 raise IOError, "No such file or directory: '%s'" % src
673 makedirs(os
.path
.dirname(dst
))
674 os
.symlink(os
.path
.abspath(src
), dst
)
677 """Safe wrapper for os.path.join: asserts that all but the first
678 argument are relative paths."""
681 return os
.path
.join(*args
)
686 python bundlebuilder.py [options] command
687 python mybuildscript.py [options] command
690 build build the application
691 report print a report
694 -b, --builddir=DIR the build directory; defaults to "build"
695 -n, --name=NAME application name
696 -r, --resource=FILE extra file or folder to be copied to Resources
697 -f, --file=SRC:DST extra file or folder to be copied into the bundle;
698 DST must be a path relative to the bundle root
699 -e, --executable=FILE the executable to be used
700 -m, --mainprogram=FILE the Python main program
701 -a, --argv add a wrapper main program to create sys.argv
702 -p, --plist=FILE .plist file (default: generate one)
703 --nib=NAME main nib name
704 -c, --creator=CCCC 4-char creator code (default: '????')
705 --iconfile=FILE filename of the icon (an .icns file) to be used
707 -l, --link symlink files/folder instead of copying them
708 --link-exec symlink the executable instead of copying it
709 --standalone build a standalone application, which is fully
710 independent of a Python installation
711 --lib=FILE shared library or framework to be copied into
713 -x, --exclude=MODULE exclude module (with --standalone)
714 -i, --include=MODULE include module (with --standalone)
715 --package=PACKAGE include a whole package (with --standalone)
716 --strip strip binaries (remove debug info)
717 -v, --verbose increase verbosity level
718 -q, --quiet decrease verbosity level
719 -h, --help print this message
728 def main(builder
=None):
730 builder
= AppBuilder(verbosity
=1)
732 shortopts
= "b:n:r:f:e:m:c:p:lx:i:hvqa"
733 longopts
= ("builddir=", "name=", "resource=", "file=", "executable=",
734 "mainprogram=", "creator=", "nib=", "plist=", "link",
735 "link-exec", "help", "verbose", "quiet", "argv", "standalone",
736 "exclude=", "include=", "package=", "strip", "iconfile=",
740 options
, args
= getopt
.getopt(sys
.argv
[1:], shortopts
, longopts
)
744 for opt
, arg
in options
:
745 if opt
in ('-b', '--builddir'):
746 builder
.builddir
= arg
747 elif opt
in ('-n', '--name'):
749 elif opt
in ('-r', '--resource'):
750 builder
.resources
.append(arg
)
751 elif opt
in ('-f', '--file'):
752 srcdst
= arg
.split(':')
754 usage("-f or --file argument must be two paths, "
755 "separated by a colon")
756 builder
.files
.append(srcdst
)
757 elif opt
in ('-e', '--executable'):
758 builder
.executable
= arg
759 elif opt
in ('-m', '--mainprogram'):
760 builder
.mainprogram
= arg
761 elif opt
in ('-a', '--argv'):
762 builder
.argv_emulation
= 1
763 elif opt
in ('-c', '--creator'):
764 builder
.creator
= arg
765 elif opt
== '--iconfile':
766 builder
.iconfile
= arg
768 builder
.libs
.append(arg
)
770 builder
.nibname
= arg
771 elif opt
in ('-p', '--plist'):
772 builder
.plist
= Plist
.fromFile(arg
)
773 elif opt
in ('-l', '--link'):
775 elif opt
== '--link-exec':
776 builder
.symlink_exec
= 1
777 elif opt
in ('-h', '--help'):
779 elif opt
in ('-v', '--verbose'):
780 builder
.verbosity
+= 1
781 elif opt
in ('-q', '--quiet'):
782 builder
.verbosity
-= 1
783 elif opt
== '--standalone':
784 builder
.standalone
= 1
785 elif opt
in ('-x', '--exclude'):
786 builder
.excludeModules
.append(arg
)
787 elif opt
in ('-i', '--include'):
788 builder
.includeModules
.append(arg
)
789 elif opt
== '--package':
790 builder
.includePackages
.append(arg
)
791 elif opt
== '--strip':
795 usage("Must specify one command ('build', 'report' or 'help')")
798 if command
== "build":
801 elif command
== "report":
804 elif command
== "help":
807 usage("Unknown command '%s'" % command
)
810 def buildapp(**kwargs
):
811 builder
= AppBuilder(**kwargs
)
815 if __name__
== "__main__":