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