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 # Directory where the bundle will be assembled.
100 # Make symlinks instead copying files. This is handy during debugging, but
101 # makes the bundle non-distributable.
108 # XXX rethink self.name munging, this is brittle.
109 self
.name
, ext
= os
.path
.splitext(self
.name
)
112 bundleextension
= ext
113 # misc (derived) attributes
114 self
.bundlepath
= pathjoin(self
.builddir
, self
.name
+ bundleextension
)
117 plist
.CFBundleName
= self
.name
118 plist
.CFBundlePackageType
= self
.type
119 if self
.creator
is None:
120 if hasattr(plist
, "CFBundleSignature"):
121 self
.creator
= plist
.CFBundleSignature
123 self
.creator
= "????"
124 plist
.CFBundleSignature
= self
.creator
125 if not hasattr(plist
, "CFBundleIdentifier"):
126 plist
.CFBundleIdentifier
= self
.name
129 """Build the bundle."""
130 builddir
= self
.builddir
131 if builddir
and not os
.path
.exists(builddir
):
133 self
.message("Building %s" % repr(self
.bundlepath
), 1)
134 if os
.path
.exists(self
.bundlepath
):
135 shutil
.rmtree(self
.bundlepath
)
136 os
.mkdir(self
.bundlepath
)
141 self
.message("Done.", 1)
143 def preProcess(self
):
144 """Hook for subclasses."""
146 def postProcess(self
):
147 """Hook for subclasses."""
150 def _addMetaFiles(self
):
151 contents
= pathjoin(self
.bundlepath
, "Contents")
154 # Write Contents/PkgInfo
155 assert len(self
.type) == len(self
.creator
) == 4, \
156 "type and creator must be 4-byte strings."
157 pkginfo
= pathjoin(contents
, "PkgInfo")
158 f
= open(pkginfo
, "wb")
159 f
.write(self
.type + self
.creator
)
162 # Write Contents/Info.plist
163 infoplist
= pathjoin(contents
, "Info.plist")
164 self
.plist
.write(infoplist
)
166 def _copyFiles(self
):
167 files
= self
.files
[:]
168 for path
in self
.resources
:
169 files
.append((path
, pathjoin("Contents", "Resources",
170 os
.path
.basename(path
))))
172 self
.message("Making symbolic links", 1)
173 msg
= "Making symlink from"
175 self
.message("Copying files", 1)
178 for src
, dst
in files
:
179 if os
.path
.isdir(src
):
180 self
.message("%s %s/ to %s/" % (msg
, src
, dst
), 2)
182 self
.message("%s %s to %s" % (msg
, src
, dst
), 2)
183 dst
= pathjoin(self
.bundlepath
, dst
)
185 symlink(src
, dst
, mkdirs
=1)
187 copy(src
, dst
, mkdirs
=1)
189 def message(self
, msg
, level
=0):
190 if level
<= self
.verbosity
:
193 indent
= (level
- 1) * " "
194 sys
.stderr
.write(indent
+ msg
+ "\n")
197 # XXX something decent
206 MAGIC
= imp
.get_magic()
207 USE_ZIPIMPORT
= "zipimport" in sys
.builtin_module_names
209 # For standalone apps, we have our own minimal site.py. We don't need
210 # all the cruft of the real site.py.
213 del sys.path[1:] # sys.path[0] is Contents/Resources/
217 ZIP_ARCHIVE
= "Modules.zip"
218 SITE_PY
+= "sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE
219 def getPycData(fullname
, code
, ispkg
):
221 fullname
+= ".__init__"
222 path
= fullname
.replace(".", os
.sep
) + PYC_EXT
223 return path
, MAGIC
+ '\0\0\0\0' + marshal
.dumps(code
)
225 SITE_CO
= compile(SITE_PY
, "<-bundlebuilder.py->", "exec")
228 # Extension modules can't be in the modules zip archive, so a placeholder
229 # is added instead, that loads the extension from a specified location.
235 path = os.path.join(p, "%(filename)s")
236 if os.path.exists(path):
239 assert 0, "file not found: %(filename)s"
240 mod = imp.load_dynamic("%(name)s", path)
246 MAYMISS_MODULES
= ['mac', 'os2', 'nt', 'ntpath', 'dos', 'dospath',
247 'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
248 'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
251 STRIP_EXEC
= "/usr/bin/strip"
254 # We're using a stock interpreter to run the app, yet we need
255 # a way to pass the Python main program to the interpreter. The
256 # bootstrapping script fires up the interpreter with the right
257 # arguments. os.execve() is used as OSX doesn't like us to
258 # start a real new process. Also, the executable name must match
259 # the CFBundleExecutable value in the Info.plist, so we lie
260 # deliberately with argv[0]. The actual Python executable is
261 # passed in an environment variable so we can "repair"
262 # sys.executable later.
264 BOOTSTRAP_SCRIPT
= """\
268 execdir = os.path.dirname(sys.argv[0])
269 executable = os.path.join(execdir, "%(executable)s")
270 resdir = os.path.join(os.path.dirname(execdir), "Resources")
271 mainprogram = os.path.join(resdir, "%(mainprogram)s")
273 sys.argv.insert(1, mainprogram)
274 os.environ["PYTHONPATH"] = resdir
275 os.environ["PYTHONEXECUTABLE"] = executable
276 os.execve(executable, sys.argv, os.environ)
281 # Optional wrapper that converts "dropped files" into sys.argv values.
284 import argvemulator, os
286 argvemulator.ArgvCollector().mainloop()
287 execfile(os.path.join(os.path.split(__file__)[0], "%(realmainprogram)s"))
291 class AppBuilder(BundleBuilder
):
293 # Override type of the bundle.
296 # platform, name of the subfolder of Contents that contains the executable.
299 # A Python main program. If this argument is given, the main
300 # executable in the bundle will be a small wrapper that invokes
301 # the main program. (XXX Discuss why.)
304 # The main executable. If a Python main program is specified
305 # the executable will be copied to Resources and be invoked
306 # by the wrapper program mentioned above. Otherwise it will
307 # simply be used as the main executable.
310 # The name of the main nib, for Cocoa apps. *Must* be specified
311 # when building a Cocoa app.
314 # The name of the icon file to be copied to Resources and used for
318 # Symlink the executable instead of copying it.
321 # If True, build standalone app.
324 # If True, add a real main program that emulates sys.argv before calling
328 # The following attributes are only used when building a standalone app.
330 # Exclude these modules.
333 # Include these modules.
336 # Include these packages.
342 # Found Python modules: [(name, codeobject, ispkg), ...]
345 # Modules that modulefinder couldn't find:
347 maybeMissingModules
= []
349 # List of all binaries (executables or shared libs), for stripping purposes
353 if self
.standalone
and self
.mainprogram
is None:
354 raise BundleBuilderError
, ("must specify 'mainprogram' when "
355 "building a standalone application.")
356 if self
.mainprogram
is None and self
.executable
is None:
357 raise BundleBuilderError
, ("must specify either or both of "
358 "'executable' and 'mainprogram'")
360 self
.execdir
= pathjoin("Contents", self
.platform
)
362 if self
.name
is not None:
364 elif self
.mainprogram
is not None:
365 self
.name
= os
.path
.splitext(os
.path
.basename(self
.mainprogram
))[0]
366 elif executable
is not None:
367 self
.name
= os
.path
.splitext(os
.path
.basename(self
.executable
))[0]
368 if self
.name
[-4:] != ".app":
371 if self
.executable
is None:
372 if not self
.standalone
:
373 self
.symlink_exec
= 1
374 self
.executable
= sys
.executable
377 self
.plist
.NSMainNibFile
= self
.nibname
378 if not hasattr(self
.plist
, "NSPrincipalClass"):
379 self
.plist
.NSPrincipalClass
= "NSApplication"
381 BundleBuilder
.setup(self
)
383 self
.plist
.CFBundleExecutable
= self
.name
386 self
.findDependencies()
388 def preProcess(self
):
389 resdir
= "Contents/Resources"
390 if self
.executable
is not None:
391 if self
.mainprogram
is None:
394 execname
= os
.path
.basename(self
.executable
)
395 execpath
= pathjoin(self
.execdir
, execname
)
396 if not self
.symlink_exec
:
397 self
.files
.append((self
.executable
, execpath
))
398 self
.binaries
.append(execpath
)
399 self
.execpath
= execpath
401 if self
.mainprogram
is not None:
402 mainprogram
= os
.path
.basename(self
.mainprogram
)
403 self
.files
.append((self
.mainprogram
, pathjoin(resdir
, mainprogram
)))
404 if self
.argv_emulation
:
405 # Change the main program, and create the helper main program (which
406 # does argv collection and then calls the real main).
407 # Also update the included modules (if we're creating a standalone
408 # program) and the plist
409 realmainprogram
= mainprogram
410 mainprogram
= '__argvemulator_' + mainprogram
411 resdirpath
= pathjoin(self
.bundlepath
, resdir
)
412 mainprogrampath
= pathjoin(resdirpath
, mainprogram
)
414 open(mainprogrampath
, "w").write(ARGV_EMULATOR
% locals())
416 self
.includeModules
.append("argvemulator")
417 self
.includeModules
.append("os")
418 if not self
.plist
.has_key("CFBundleDocumentTypes"):
419 self
.plist
["CFBundleDocumentTypes"] = [
420 { "CFBundleTypeOSTypes" : [
424 "CFBundleTypeRole": "Viewer"}]
425 # Write bootstrap script
426 executable
= os
.path
.basename(self
.executable
)
427 execdir
= pathjoin(self
.bundlepath
, self
.execdir
)
428 bootstrappath
= pathjoin(execdir
, self
.name
)
431 # XXX we're screwed when the end user has deleted
433 hashbang
= "/usr/bin/python"
435 hashbang
= sys
.executable
436 while os
.path
.islink(hashbang
):
437 hashbang
= os
.readlink(hashbang
)
438 open(bootstrappath
, "w").write(BOOTSTRAP_SCRIPT
% locals())
439 os
.chmod(bootstrappath
, 0775)
441 if self
.iconfile
is not None:
442 iconbase
= os
.path
.basename(self
.iconfile
)
443 self
.plist
.CFBundleIconFile
= iconbase
444 self
.files
.append((self
.iconfile
, pathjoin(resdir
, iconbase
)))
446 def postProcess(self
):
448 self
.addPythonModules()
449 if self
.strip
and not self
.symlink
:
452 if self
.symlink_exec
and self
.executable
:
453 self
.message("Symlinking executable %s to %s" % (self
.executable
,
455 dst
= pathjoin(self
.bundlepath
, self
.execpath
)
456 makedirs(os
.path
.dirname(dst
))
457 os
.symlink(os
.path
.abspath(self
.executable
), dst
)
459 if self
.missingModules
or self
.maybeMissingModules
:
462 def addPythonModules(self
):
463 self
.message("Adding Python modules", 1)
466 # Create a zip file containing all modules as pyc.
468 relpath
= pathjoin("Contents", "Resources", ZIP_ARCHIVE
)
469 abspath
= pathjoin(self
.bundlepath
, relpath
)
470 zf
= zipfile
.ZipFile(abspath
, "w", zipfile
.ZIP_DEFLATED
)
471 for name
, code
, ispkg
in self
.pymodules
:
472 self
.message("Adding Python module %s" % name
, 2)
473 path
, pyc
= getPycData(name
, code
, ispkg
)
474 zf
.writestr(path
, pyc
)
477 sitepath
= pathjoin(self
.bundlepath
, "Contents", "Resources",
479 writePyc(SITE_CO
, sitepath
)
481 # Create individual .pyc files.
482 for name
, code
, ispkg
in self
.pymodules
:
485 path
= name
.split(".")
486 path
= pathjoin("Contents", "Resources", *path
) + PYC_EXT
489 self
.message("Adding Python package %s" % path
, 2)
491 self
.message("Adding Python module %s" % path
, 2)
493 abspath
= pathjoin(self
.bundlepath
, path
)
494 makedirs(os
.path
.dirname(abspath
))
495 writePyc(code
, abspath
)
497 def stripBinaries(self
):
498 if not os
.path
.exists(STRIP_EXEC
):
499 self
.message("Error: can't strip binaries: no strip program at "
500 "%s" % STRIP_EXEC
, 0)
502 self
.message("Stripping binaries", 1)
503 for relpath
in self
.binaries
:
504 self
.message("Stripping %s" % relpath
, 2)
505 abspath
= pathjoin(self
.bundlepath
, relpath
)
506 assert not os
.path
.islink(abspath
)
507 rv
= os
.system("%s -S \"%s\"" % (STRIP_EXEC
, abspath
))
509 def findDependencies(self
):
510 self
.message("Finding module dependencies", 1)
512 mf
= modulefinder
.ModuleFinder(excludes
=self
.excludeModules
)
514 # zipimport imports zlib, must add it manually
515 mf
.import_hook("zlib")
516 # manually add our own site.py
517 site
= mf
.add_module("site")
518 site
.__code
__ = SITE_CO
519 mf
.scan_code(SITE_CO
, site
)
521 # warnings.py gets imported implicitly from C
522 mf
.import_hook("warnings")
524 includeModules
= self
.includeModules
[:]
525 for name
in self
.includePackages
:
526 includeModules
.extend(findPackageContents(name
).keys())
527 for name
in includeModules
:
531 self
.missingModules
.append(name
)
533 mf
.run_script(self
.mainprogram
)
534 modules
= mf
.modules
.items()
536 for name
, mod
in modules
:
537 if mod
.__file
__ and mod
.__code
__ is None:
540 filename
= os
.path
.basename(path
)
542 # Python modules are stored in a Zip archive, but put
543 # extensions in Contents/Resources/.a and add a tiny "loader"
544 # program in the Zip archive. Due to Thomas Heller.
545 dstpath
= pathjoin("Contents", "Resources", filename
)
546 source
= EXT_LOADER
% {"name": name
, "filename": filename
}
547 code
= compile(source
, "<dynloader for %s>" % name
, "exec")
551 dstpath
= name
.split(".")[:-1] + [filename
]
552 dstpath
= pathjoin("Contents", "Resources", *dstpath
)
553 self
.files
.append((path
, dstpath
))
554 self
.binaries
.append(dstpath
)
555 if mod
.__code
__ is not None:
556 ispkg
= mod
.__path
__ is not None
557 if not USE_ZIPIMPORT
or name
!= "site":
558 # Our site.py is doing the bootstrapping, so we must
559 # include a real .pyc file if USE_ZIPIMPORT is True.
560 self
.pymodules
.append((name
, mod
.__code
__, ispkg
))
562 if hasattr(mf
, "any_missing_maybe"):
563 missing
, maybe
= mf
.any_missing_maybe()
565 missing
= mf
.any_missing()
567 self
.missingModules
.extend(missing
)
568 self
.maybeMissingModules
.extend(maybe
)
570 def reportMissing(self
):
571 missing
= [name
for name
in self
.missingModules
572 if name
not in MAYMISS_MODULES
]
573 if self
.maybeMissingModules
:
574 maybe
= self
.maybeMissingModules
576 maybe
= [name
for name
in missing
if "." in name
]
577 missing
= [name
for name
in missing
if "." not in name
]
581 self
.message("Warning: couldn't find the following submodules:", 1)
582 self
.message(" (Note that these could be false alarms -- "
583 "it's not always", 1)
584 self
.message(" possible to distinguish between \"from package "
585 "import submodule\" ", 1)
586 self
.message(" and \"from package import name\")", 1)
588 self
.message(" ? " + name
, 1)
590 self
.message("Warning: couldn't find the following modules:", 1)
592 self
.message(" ? " + name
, 1)
595 # XXX something decent
597 pprint
.pprint(self
.__dict
__)
605 SUFFIXES
= [_suf
for _suf
, _mode
, _tp
in imp
.get_suffixes()]
606 identifierRE
= re
.compile(r
"[_a-zA-z][_a-zA-Z0-9]*$")
608 def findPackageContents(name
, searchpath
=None):
609 head
= name
.split(".")[-1]
610 if identifierRE
.match(head
) is None:
613 fp
, path
, (ext
, mode
, tp
) = imp
.find_module(head
, searchpath
)
616 modules
= {name
: None}
617 if tp
== imp
.PKG_DIRECTORY
and path
:
618 files
= os
.listdir(path
)
620 sub
, ext
= os
.path
.splitext(sub
)
621 fullname
= name
+ "." + sub
622 if sub
!= "__init__" and fullname
not in modules
:
623 modules
.update(findPackageContents(fullname
, [path
]))
626 def writePyc(code
, path
):
629 f
.write("\0" * 4) # don't bother about a time stamp
630 marshal
.dump(code
, f
)
633 def copy(src
, dst
, mkdirs
=0):
634 """Copy a file or a directory."""
636 makedirs(os
.path
.dirname(dst
))
637 if os
.path
.isdir(src
):
638 shutil
.copytree(src
, dst
)
640 shutil
.copy2(src
, dst
)
642 def copytodir(src
, dstdir
):
643 """Copy a file or a directory to an existing directory."""
644 dst
= pathjoin(dstdir
, os
.path
.basename(src
))
648 """Make all directories leading up to 'dir' including the leaf
649 directory. Don't moan if any path element already exists."""
653 if why
.errno
!= errno
.EEXIST
:
656 def symlink(src
, dst
, mkdirs
=0):
657 """Copy a file or a directory."""
658 if not os
.path
.exists(src
):
659 raise IOError, "No such file or directory: '%s'" % src
661 makedirs(os
.path
.dirname(dst
))
662 os
.symlink(os
.path
.abspath(src
), dst
)
665 """Safe wrapper for os.path.join: asserts that all but the first
666 argument are relative paths."""
669 return os
.path
.join(*args
)
674 python bundlebuilder.py [options] command
675 python mybuildscript.py [options] command
678 build build the application
679 report print a report
682 -b, --builddir=DIR the build directory; defaults to "build"
683 -n, --name=NAME application name
684 -r, --resource=FILE extra file or folder to be copied to Resources
685 -f, --file=SRC:DST extra file or folder to be copied into the bundle;
686 DST must be a path relative to the bundle root
687 -e, --executable=FILE the executable to be used
688 -m, --mainprogram=FILE the Python main program
689 -a, --argv add a wrapper main program to create sys.argv
690 -p, --plist=FILE .plist file (default: generate one)
691 --nib=NAME main nib name
692 -c, --creator=CCCC 4-char creator code (default: '????')
693 --iconfile=FILE filename of the icon (an .icns file) to be used
695 -l, --link symlink files/folder instead of copying them
696 --link-exec symlink the executable instead of copying it
697 --standalone build a standalone application, which is fully
698 independent of a Python installation
699 -x, --exclude=MODULE exclude module (with --standalone)
700 -i, --include=MODULE include module (with --standalone)
701 --package=PACKAGE include a whole package (with --standalone)
702 --strip strip binaries (remove debug info)
703 -v, --verbose increase verbosity level
704 -q, --quiet decrease verbosity level
705 -h, --help print this message
714 def main(builder
=None):
716 builder
= AppBuilder(verbosity
=1)
718 shortopts
= "b:n:r:f:e:m:c:p:lx:i:hvqa"
719 longopts
= ("builddir=", "name=", "resource=", "file=", "executable=",
720 "mainprogram=", "creator=", "nib=", "plist=", "link",
721 "link-exec", "help", "verbose", "quiet", "argv", "standalone",
722 "exclude=", "include=", "package=", "strip", "iconfile=")
725 options
, args
= getopt
.getopt(sys
.argv
[1:], shortopts
, longopts
)
729 for opt
, arg
in options
:
730 if opt
in ('-b', '--builddir'):
731 builder
.builddir
= arg
732 elif opt
in ('-n', '--name'):
734 elif opt
in ('-r', '--resource'):
735 builder
.resources
.append(arg
)
736 elif opt
in ('-f', '--file'):
737 srcdst
= arg
.split(':')
739 usage("-f or --file argument must be two paths, "
740 "separated by a colon")
741 builder
.files
.append(srcdst
)
742 elif opt
in ('-e', '--executable'):
743 builder
.executable
= arg
744 elif opt
in ('-m', '--mainprogram'):
745 builder
.mainprogram
= arg
746 elif opt
in ('-a', '--argv'):
747 builder
.argv_emulation
= 1
748 elif opt
in ('-c', '--creator'):
749 builder
.creator
= arg
750 elif opt
== '--iconfile':
751 builder
.iconfile
= arg
753 builder
.nibname
= arg
754 elif opt
in ('-p', '--plist'):
755 builder
.plist
= Plist
.fromFile(arg
)
756 elif opt
in ('-l', '--link'):
758 elif opt
== '--link-exec':
759 builder
.symlink_exec
= 1
760 elif opt
in ('-h', '--help'):
762 elif opt
in ('-v', '--verbose'):
763 builder
.verbosity
+= 1
764 elif opt
in ('-q', '--quiet'):
765 builder
.verbosity
-= 1
766 elif opt
== '--standalone':
767 builder
.standalone
= 1
768 elif opt
in ('-x', '--exclude'):
769 builder
.excludeModules
.append(arg
)
770 elif opt
in ('-i', '--include'):
771 builder
.includeModules
.append(arg
)
772 elif opt
== '--package':
773 builder
.includePackages
.append(arg
)
774 elif opt
== '--strip':
778 usage("Must specify one command ('build', 'report' or 'help')")
781 if command
== "build":
784 elif command
== "report":
787 elif command
== "help":
790 usage("Unknown command '%s'" % command
)
793 def buildapp(**kwargs
):
794 builder
= AppBuilder(**kwargs
)
798 if __name__
== "__main__":