1#! /usr/bin/env python 2 3"""\ 4bundlebuilder.py -- Tools to assemble MacOS X (application) bundles. 5 6This module contains two classes to build so called "bundles" for 7MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass 8specialized in building application bundles. 9 10[Bundle|App]Builder objects are instantiated with a bunch of keyword 11arguments, and have a build() method that will do all the work. See 12the class doc strings for a description of the constructor arguments. 13 14The 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 19Where "buildapp.py" is a user-supplied setup.py-like script following 20this 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 31from warnings import warnpy3k 32warnpy3k("In 3.x, the bundlebuilder module is removed.", stacklevel=2) 33 34import sys 35import os, errno, shutil 36import imp, marshal 37import re 38from copy import deepcopy 39import getopt 40from plistlib import Plist 41from types import FunctionType as function 42 43class BundleBuilderError(Exception): pass 44 45 46class 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 71class 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 232if __debug__: 233 PYC_EXT = ".pyc" 234else: 235 PYC_EXT = ".pyo" 236 237MAGIC = imp.get_magic() 238USE_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. 242SITE_PY = """\ 243import sys 244if not %(semi_standalone)s: 245 del sys.path[1:] # sys.path[0] is Contents/Resources/ 246""" 247 248ZIP_ARCHIVE = "Modules.zip" 249SITE_PY_ZIP = SITE_PY + ("sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE) 250 251def 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# 261EXT_LOADER = """\ 262def __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() 273del __load 274""" 275 276MAYMISS_MODULES = ['os2', 'nt', 'ntpath', 'dos', 'dospath', 277 'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize', 278 'org.python.core', 'riscos', 'riscosenviron', 'riscospath' 279] 280 281STRIP_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# 294BOOTSTRAP_SCRIPT = """\ 295#!%(hashbang)s 296 297import sys, os 298execdir = os.path.dirname(sys.argv[0]) 299executable = os.path.join(execdir, "%(executable)s") 300resdir = os.path.join(os.path.dirname(execdir), "Resources") 301libdir = os.path.join(os.path.dirname(execdir), "Frameworks") 302mainprogram = os.path.join(resdir, "%(mainprogram)s") 303 304if %(optimize)s: 305 sys.argv.insert(1, '-O') 306 307sys.argv.insert(1, mainprogram) 308if %(standalone)s or %(semi_standalone)s: 309 os.environ["PYTHONPATH"] = resdir 310 if %(standalone)s: 311 os.environ["PYTHONHOME"] = resdir 312else: 313 pypath = os.getenv("PYTHONPATH", "") 314 if pypath: 315 pypath = ":" + pypath 316 os.environ["PYTHONPATH"] = resdir + pypath 317 318os.environ["PYTHONEXECUTABLE"] = executable 319os.environ["DYLD_LIBRARY_PATH"] = libdir 320os.environ["DYLD_FRAMEWORK_PATH"] = libdir 321os.execve(executable, sys.argv, os.environ) 322""" 323 324 325# 326# Optional wrapper that converts "dropped files" into sys.argv values. 327# 328ARGV_EMULATOR = """\ 329import argvemulator, os 330 331argvemulator.ArgvCollector().mainloop() 332execfile(os.path.join(os.path.split(__file__)[0], "%(realmainprogram)s")) 333""" 334 335# 336# When building a standalone app with Python.framework, we need to copy 337# a subset from Python.framework to the bundle. The following list 338# specifies exactly what items we'll copy. 339# 340PYTHONFRAMEWORKGOODIES = [ 341 "Python", # the Python core library 342 "Resources/English.lproj", 343 "Resources/Info.plist", 344] 345 346def isFramework(): 347 return sys.exec_prefix.find("Python.framework") > 0 348 349 350LIB = os.path.join(sys.prefix, "lib", "python" + sys.version[:3]) 351SITE_PACKAGES = os.path.join(LIB, "site-packages") 352 353 354class AppBuilder(BundleBuilder): 355 356 use_zipimport = USE_ZIPIMPORT 357 358 # Override type of the bundle. 359 type = "APPL" 360 361 # platform, name of the subfolder of Contents that contains the executable. 362 platform = "MacOS" 363 364 # A Python main program. If this argument is given, the main 365 # executable in the bundle will be a small wrapper that invokes 366 # the main program. (XXX Discuss why.) 367 mainprogram = None 368 369 # The main executable. If a Python main program is specified 370 # the executable will be copied to Resources and be invoked 371 # by the wrapper program mentioned above. Otherwise it will 372 # simply be used as the main executable. 373 executable = None 374 375 # The name of the main nib, for Cocoa apps. *Must* be specified 376 # when building a Cocoa app. 377 nibname = None 378 379 # The name of the icon file to be copied to Resources and used for 380 # the Finder icon. 381 iconfile = None 382 383 # Symlink the executable instead of copying it. 384 symlink_exec = 0 385 386 # If True, build standalone app. 387 standalone = 0 388 389 # If True, build semi-standalone app (only includes third-party modules). 390 semi_standalone = 0 391 392 # If set, use this for #! lines in stead of sys.executable 393 python = None 394 395 # If True, add a real main program that emulates sys.argv before calling 396 # mainprogram 397 argv_emulation = 0 398 399 # The following attributes are only used when building a standalone app. 400 401 # Exclude these modules. 402 excludeModules = [] 403 404 # Include these modules. 405 includeModules = [] 406 407 # Include these packages. 408 includePackages = [] 409 410 # Strip binaries from debug info. 411 strip = 0 412 413 # Found Python modules: [(name, codeobject, ispkg), ...] 414 pymodules = [] 415 416 # Modules that modulefinder couldn't find: 417 missingModules = [] 418 maybeMissingModules = [] 419 420 def setup(self): 421 if ((self.standalone or self.semi_standalone) 422 and self.mainprogram is None): 423 raise BundleBuilderError, ("must specify 'mainprogram' when " 424 "building a standalone application.") 425 if self.mainprogram is None and self.executable is None: 426 raise BundleBuilderError, ("must specify either or both of " 427 "'executable' and 'mainprogram'") 428 429 self.execdir = pathjoin("Contents", self.platform) 430 431 if self.name is not None: 432 pass 433 elif self.mainprogram is not None: 434 self.name = os.path.splitext(os.path.basename(self.mainprogram))[0] 435 elif self.executable is not None: 436 self.name = os.path.splitext(os.path.basename(self.executable))[0] 437 if self.name[-4:] != ".app": 438 self.name += ".app" 439 440 if self.executable is None: 441 if not self.standalone and not isFramework(): 442 self.symlink_exec = 1 443 if self.python: 444 self.executable = self.python 445 else: 446 self.executable = sys.executable 447 448 if self.nibname: 449 self.plist.NSMainNibFile = self.nibname 450 if not hasattr(self.plist, "NSPrincipalClass"): 451 self.plist.NSPrincipalClass = "NSApplication" 452 453 if self.standalone and isFramework(): 454 self.addPythonFramework() 455 456 BundleBuilder.setup(self) 457 458 self.plist.CFBundleExecutable = self.name 459 460 if self.standalone or self.semi_standalone: 461 self.findDependencies() 462 463 def preProcess(self): 464 resdir = "Contents/Resources" 465 if self.executable is not None: 466 if self.mainprogram is None: 467 execname = self.name 468 else: 469 execname = os.path.basename(self.executable) 470 execpath = pathjoin(self.execdir, execname) 471 if not self.symlink_exec: 472 self.files.append((self.destroot + self.executable, execpath)) 473 self.execpath = execpath 474 475 if self.mainprogram is not None: 476 mainprogram = os.path.basename(self.mainprogram) 477 self.files.append((self.mainprogram, pathjoin(resdir, mainprogram))) 478 if self.argv_emulation: 479 # Change the main program, and create the helper main program (which 480 # does argv collection and then calls the real main). 481 # Also update the included modules (if we're creating a standalone 482 # program) and the plist 483 realmainprogram = mainprogram 484 mainprogram = '__argvemulator_' + mainprogram 485 resdirpath = pathjoin(self.bundlepath, resdir) 486 mainprogrampath = pathjoin(resdirpath, mainprogram) 487 makedirs(resdirpath) 488 open(mainprogrampath, "w").write(ARGV_EMULATOR % locals()) 489 if self.standalone or self.semi_standalone: 490 self.includeModules.append("argvemulator") 491 self.includeModules.append("os") 492 if "CFBundleDocumentTypes" not in self.plist: 493 self.plist["CFBundleDocumentTypes"] = [ 494 { "CFBundleTypeOSTypes" : [ 495 "****", 496 "fold", 497 "disk"], 498 "CFBundleTypeRole": "Viewer"}] 499 # Write bootstrap script 500 executable = os.path.basename(self.executable) 501 execdir = pathjoin(self.bundlepath, self.execdir) 502 bootstrappath = pathjoin(execdir, self.name) 503 makedirs(execdir) 504 if self.standalone or self.semi_standalone: 505 # XXX we're screwed when the end user has deleted 506 # /usr/bin/python 507 hashbang = "/usr/bin/python" 508 elif self.python: 509 hashbang = self.python 510 else: 511 hashbang = os.path.realpath(sys.executable) 512 standalone = self.standalone 513 semi_standalone = self.semi_standalone 514 optimize = sys.flags.optimize 515 open(bootstrappath, "w").write(BOOTSTRAP_SCRIPT % locals()) 516 os.chmod(bootstrappath, 0775) 517 518 if self.iconfile is not None: 519 iconbase = os.path.basename(self.iconfile) 520 self.plist.CFBundleIconFile = iconbase 521 self.files.append((self.iconfile, pathjoin(resdir, iconbase))) 522 523 def postProcess(self): 524 if self.standalone or self.semi_standalone: 525 self.addPythonModules() 526 if self.strip and not self.symlink: 527 self.stripBinaries() 528 529 if self.symlink_exec and self.executable: 530 self.message("Symlinking executable %s to %s" % (self.executable, 531 self.execpath), 2) 532 dst = pathjoin(self.bundlepath, self.execpath) 533 makedirs(os.path.dirname(dst)) 534 os.symlink(os.path.abspath(self.executable), dst) 535 536 if self.missingModules or self.maybeMissingModules: 537 self.reportMissing() 538 539 def addPythonFramework(self): 540 # If we're building a standalone app with Python.framework, 541 # include a minimal subset of Python.framework, *unless* 542 # Python.framework was specified manually in self.libs. 543 for lib in self.libs: 544 if os.path.basename(lib) == "Python.framework": 545 # a Python.framework was specified as a library 546 return 547 548 frameworkpath = sys.exec_prefix[:sys.exec_prefix.find( 549 "Python.framework") + len("Python.framework")] 550 551 version = sys.version[:3] 552 frameworkpath = pathjoin(frameworkpath, "Versions", version) 553 destbase = pathjoin("Contents", "Frameworks", "Python.framework", 554 "Versions", version) 555 for item in PYTHONFRAMEWORKGOODIES: 556 src = pathjoin(frameworkpath, item) 557 dst = pathjoin(destbase, item) 558 self.files.append((src, dst)) 559 560 def _getSiteCode(self): 561 if self.use_zipimport: 562 return compile(SITE_PY % {"semi_standalone": self.semi_standalone}, 563 "<-bundlebuilder.py->", "exec") 564 565 def addPythonModules(self): 566 self.message("Adding Python modules", 1) 567 568 if self.use_zipimport: 569 # Create a zip file containing all modules as pyc. 570 import zipfile 571 relpath = pathjoin("Contents", "Resources", ZIP_ARCHIVE) 572 abspath = pathjoin(self.bundlepath, relpath) 573 zf = zipfile.ZipFile(abspath, "w", zipfile.ZIP_DEFLATED) 574 for name, code, ispkg in self.pymodules: 575 self.message("Adding Python module %s" % name, 2) 576 path, pyc = getPycData(name, code, ispkg) 577 zf.writestr(path, pyc) 578 zf.close() 579 # add site.pyc 580 sitepath = pathjoin(self.bundlepath, "Contents", "Resources", 581 "site" + PYC_EXT) 582 writePyc(self._getSiteCode(), sitepath) 583 else: 584 # Create individual .pyc files. 585 for name, code, ispkg in self.pymodules: 586 if ispkg: 587 name += ".__init__" 588 path = name.split(".") 589 path = pathjoin("Contents", "Resources", *path) + PYC_EXT 590 591 if ispkg: 592 self.message("Adding Python package %s" % path, 2) 593 else: 594 self.message("Adding Python module %s" % path, 2) 595 596 abspath = pathjoin(self.bundlepath, path) 597 makedirs(os.path.dirname(abspath)) 598 writePyc(code, abspath) 599 600 def stripBinaries(self): 601 if not os.path.exists(STRIP_EXEC): 602 self.message("Error: can't strip binaries: no strip program at " 603 "%s" % STRIP_EXEC, 0) 604 else: 605 import stat 606 self.message("Stripping binaries", 1) 607 def walk(top): 608 for name in os.listdir(top): 609 path = pathjoin(top, name) 610 if os.path.islink(path): 611 continue 612 if os.path.isdir(path): 613 walk(path) 614 else: 615 mod = os.stat(path)[stat.ST_MODE] 616 if not (mod & 0100): 617 continue 618 relpath = path[len(self.bundlepath):] 619 self.message("Stripping %s" % relpath, 2) 620 inf, outf = os.popen4("%s -S \"%s\"" % 621 (STRIP_EXEC, path)) 622 output = outf.read().strip() 623 if output: 624 # usually not a real problem, like when we're 625 # trying to strip a script 626 self.message("Problem stripping %s:" % relpath, 3) 627 self.message(output, 3) 628 walk(self.bundlepath) 629 630 def findDependencies(self): 631 self.message("Finding module dependencies", 1) 632 import modulefinder 633 mf = modulefinder.ModuleFinder(excludes=self.excludeModules) 634 if self.use_zipimport: 635 # zipimport imports zlib, must add it manually 636 mf.import_hook("zlib") 637 # manually add our own site.py 638 site = mf.add_module("site") 639 site.__code__ = self._getSiteCode() 640 mf.scan_code(site.__code__, site) 641 642 # warnings.py gets imported implicitly from C 643 mf.import_hook("warnings") 644 645 includeModules = self.includeModules[:] 646 for name in self.includePackages: 647 includeModules.extend(findPackageContents(name).keys()) 648 for name in includeModules: 649 try: 650 mf.import_hook(name) 651 except ImportError: 652 self.missingModules.append(name) 653 654 mf.run_script(self.mainprogram) 655 modules = mf.modules.items() 656 modules.sort() 657 for name, mod in modules: 658 path = mod.__file__ 659 if path and self.semi_standalone: 660 # skip the standard library 661 if path.startswith(LIB) and not path.startswith(SITE_PACKAGES): 662 continue 663 if path and mod.__code__ is None: 664 # C extension 665 filename = os.path.basename(path) 666 pathitems = name.split(".")[:-1] + [filename] 667 dstpath = pathjoin(*pathitems) 668 if self.use_zipimport: 669 if name != "zlib": 670 # neatly pack all extension modules in a subdirectory, 671 # except zlib, since it's necessary for bootstrapping. 672 dstpath = pathjoin("ExtensionModules", dstpath) 673 # Python modules are stored in a Zip archive, but put 674 # extensions in Contents/Resources/. Add a tiny "loader" 675 # program in the Zip archive. Due to Thomas Heller. 676 source = EXT_LOADER % {"name": name, "filename": dstpath} 677 code = compile(source, "<dynloader for %s>" % name, "exec") 678 mod.__code__ = code 679 self.files.append((path, pathjoin("Contents", "Resources", dstpath))) 680 if mod.__code__ is not None: 681 ispkg = mod.__path__ is not None 682 if not self.use_zipimport or name != "site": 683 # Our site.py is doing the bootstrapping, so we must 684 # include a real .pyc file if self.use_zipimport is True. 685 self.pymodules.append((name, mod.__code__, ispkg)) 686 687 if hasattr(mf, "any_missing_maybe"): 688 missing, maybe = mf.any_missing_maybe() 689 else: 690 missing = mf.any_missing() 691 maybe = [] 692 self.missingModules.extend(missing) 693 self.maybeMissingModules.extend(maybe) 694 695 def reportMissing(self): 696 missing = [name for name in self.missingModules 697 if name not in MAYMISS_MODULES] 698 if self.maybeMissingModules: 699 maybe = self.maybeMissingModules 700 else: 701 maybe = [name for name in missing if "." in name] 702 missing = [name for name in missing if "." not in name] 703 missing.sort() 704 maybe.sort() 705 if maybe: 706 self.message("Warning: couldn't find the following submodules:", 1) 707 self.message(" (Note that these could be false alarms -- " 708 "it's not always", 1) 709 self.message(" possible to distinguish between \"from package " 710 "import submodule\" ", 1) 711 self.message(" and \"from package import name\")", 1) 712 for name in maybe: 713 self.message(" ? " + name, 1) 714 if missing: 715 self.message("Warning: couldn't find the following modules:", 1) 716 for name in missing: 717 self.message(" ? " + name, 1) 718 719 def report(self): 720 # XXX something decent 721 import pprint 722 pprint.pprint(self.__dict__) 723 if self.standalone or self.semi_standalone: 724 self.reportMissing() 725 726# 727# Utilities. 728# 729 730SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()] 731identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$") 732 733def findPackageContents(name, searchpath=None): 734 head = name.split(".")[-1] 735 if identifierRE.match(head) is None: 736 return {} 737 try: 738 fp, path, (ext, mode, tp) = imp.find_module(head, searchpath) 739 except ImportError: 740 return {} 741 modules = {name: None} 742 if tp == imp.PKG_DIRECTORY and path: 743 files = os.listdir(path) 744 for sub in files: 745 sub, ext = os.path.splitext(sub) 746 fullname = name + "." + sub 747 if sub != "__init__" and fullname not in modules: 748 modules.update(findPackageContents(fullname, [path])) 749 return modules 750 751def writePyc(code, path): 752 f = open(path, "wb") 753 f.write(MAGIC) 754 f.write("\0" * 4) # don't bother about a time stamp 755 marshal.dump(code, f) 756 f.close() 757 758def copy(src, dst, mkdirs=0): 759 """Copy a file or a directory.""" 760 if mkdirs: 761 makedirs(os.path.dirname(dst)) 762 if os.path.isdir(src): 763 shutil.copytree(src, dst, symlinks=1) 764 else: 765 shutil.copy2(src, dst) 766 767def copytodir(src, dstdir): 768 """Copy a file or a directory to an existing directory.""" 769 dst = pathjoin(dstdir, os.path.basename(src)) 770 copy(src, dst) 771 772def makedirs(dir): 773 """Make all directories leading up to 'dir' including the leaf 774 directory. Don't moan if any path element already exists.""" 775 try: 776 os.makedirs(dir) 777 except OSError, why: 778 if why.errno != errno.EEXIST: 779 raise 780 781def symlink(src, dst, mkdirs=0): 782 """Copy a file or a directory.""" 783 if not os.path.exists(src): 784 raise IOError, "No such file or directory: '%s'" % src 785 if mkdirs: 786 makedirs(os.path.dirname(dst)) 787 os.symlink(os.path.abspath(src), dst) 788 789def pathjoin(*args): 790 """Safe wrapper for os.path.join: asserts that all but the first 791 argument are relative paths.""" 792 for seg in args[1:]: 793 assert seg[0] != "/" 794 return os.path.join(*args) 795 796 797cmdline_doc = """\ 798Usage: 799 python bundlebuilder.py [options] command 800 python mybuildscript.py [options] command 801 802Commands: 803 build build the application 804 report print a report 805 806Options: 807 -b, --builddir=DIR the build directory; defaults to "build" 808 -n, --name=NAME application name 809 -r, --resource=FILE extra file or folder to be copied to Resources 810 -f, --file=SRC:DST extra file or folder to be copied into the bundle; 811 DST must be a path relative to the bundle root 812 -e, --executable=FILE the executable to be used 813 -m, --mainprogram=FILE the Python main program 814 -a, --argv add a wrapper main program to create sys.argv 815 -p, --plist=FILE .plist file (default: generate one) 816 --nib=NAME main nib name 817 -c, --creator=CCCC 4-char creator code (default: '????') 818 --iconfile=FILE filename of the icon (an .icns file) to be used 819 as the Finder icon 820 --bundle-id=ID the CFBundleIdentifier, in reverse-dns format 821 (eg. org.python.BuildApplet; this is used for 822 the preferences file name) 823 -l, --link symlink files/folder instead of copying them 824 --link-exec symlink the executable instead of copying it 825 --standalone build a standalone application, which is fully 826 independent of a Python installation 827 --semi-standalone build a standalone application, which depends on 828 an installed Python, yet includes all third-party 829 modules. 830 --no-zipimport Do not copy code into a zip file 831 --python=FILE Python to use in #! line in stead of current Python 832 --lib=FILE shared library or framework to be copied into 833 the bundle 834 -x, --exclude=MODULE exclude module (with --(semi-)standalone) 835 -i, --include=MODULE include module (with --(semi-)standalone) 836 --package=PACKAGE include a whole package (with --(semi-)standalone) 837 --strip strip binaries (remove debug info) 838 -v, --verbose increase verbosity level 839 -q, --quiet decrease verbosity level 840 -h, --help print this message 841""" 842 843def usage(msg=None): 844 if msg: 845 print msg 846 print cmdline_doc 847 sys.exit(1) 848 849def main(builder=None): 850 if builder is None: 851 builder = AppBuilder(verbosity=1) 852 853 shortopts = "b:n:r:f:e:m:c:p:lx:i:hvqa" 854 longopts = ("builddir=", "name=", "resource=", "file=", "executable=", 855 "mainprogram=", "creator=", "nib=", "plist=", "link", 856 "link-exec", "help", "verbose", "quiet", "argv", "standalone", 857 "exclude=", "include=", "package=", "strip", "iconfile=", 858 "lib=", "python=", "semi-standalone", "bundle-id=", "destroot=" 859 "no-zipimport" 860 ) 861 862 try: 863 options, args = getopt.getopt(sys.argv[1:], shortopts, longopts) 864 except getopt.error: 865 usage() 866 867 for opt, arg in options: 868 if opt in ('-b', '--builddir'): 869 builder.builddir = arg 870 elif opt in ('-n', '--name'): 871 builder.name = arg 872 elif opt in ('-r', '--resource'): 873 builder.resources.append(os.path.normpath(arg)) 874 elif opt in ('-f', '--file'): 875 srcdst = arg.split(':') 876 if len(srcdst) != 2: 877 usage("-f or --file argument must be two paths, " 878 "separated by a colon") 879 builder.files.append(srcdst) 880 elif opt in ('-e', '--executable'): 881 builder.executable = arg 882 elif opt in ('-m', '--mainprogram'): 883 builder.mainprogram = arg 884 elif opt in ('-a', '--argv'): 885 builder.argv_emulation = 1 886 elif opt in ('-c', '--creator'): 887 builder.creator = arg 888 elif opt == '--bundle-id': 889 builder.bundle_id = arg 890 elif opt == '--iconfile': 891 builder.iconfile = arg 892 elif opt == "--lib": 893 builder.libs.append(os.path.normpath(arg)) 894 elif opt == "--nib": 895 builder.nibname = arg 896 elif opt in ('-p', '--plist'): 897 builder.plist = Plist.fromFile(arg) 898 elif opt in ('-l', '--link'): 899 builder.symlink = 1 900 elif opt == '--link-exec': 901 builder.symlink_exec = 1 902 elif opt in ('-h', '--help'): 903 usage() 904 elif opt in ('-v', '--verbose'): 905 builder.verbosity += 1 906 elif opt in ('-q', '--quiet'): 907 builder.verbosity -= 1 908 elif opt == '--standalone': 909 builder.standalone = 1 910 elif opt == '--semi-standalone': 911 builder.semi_standalone = 1 912 elif opt == '--python': 913 builder.python = arg 914 elif opt in ('-x', '--exclude'): 915 builder.excludeModules.append(arg) 916 elif opt in ('-i', '--include'): 917 builder.includeModules.append(arg) 918 elif opt == '--package': 919 builder.includePackages.append(arg) 920 elif opt == '--strip': 921 builder.strip = 1 922 elif opt == '--destroot': 923 builder.destroot = arg 924 elif opt == '--no-zipimport': 925 builder.use_zipimport = False 926 927 if len(args) != 1: 928 usage("Must specify one command ('build', 'report' or 'help')") 929 command = args[0] 930 931 if command == "build": 932 builder.setup() 933 builder.build() 934 elif command == "report": 935 builder.setup() 936 builder.report() 937 elif command == "help": 938 usage() 939 else: 940 usage("Unknown command '%s'" % command) 941 942 943def buildapp(**kwargs): 944 builder = AppBuilder(**kwargs) 945 main(builder) 946 947 948if __name__ == "__main__": 949 main() 950