1 #! /usr/bin/env python 2 3 """\ 4 bundlebuilder.py -- Tools to assemble MacOS X (application) bundles. 5 6 This module contains two classes to build so called "bundles" for 7 MacOS X. BundleBuilder is a general tool, AppBuilder is a subclass 8 specialized in building application bundles. 9 10 [Bundle|App]Builder objects are instantiated with a bunch of keyword 11 arguments, and have a build() method that will do all the work. See 12 the class doc strings for a description of the constructor arguments. 13 14 The module contains a main program that can be used in two ways: 15 16 % python bundlebuilder.py [options] build 17 % python buildapp.py [options] build 18 19 Where "buildapp.py" is a user-supplied setup.py-like script following 20 this model: 21 22 from bundlebuilder import buildapp 23 buildapp(<lots-of-keyword-args>) 24 25 """ 26 27 28 __all__ = ["BundleBuilder", "BundleBuilderError", "AppBuilder", "buildapp"] 29 30 31 from warnings import warnpy3k 32 warnpy3k("In 3.x, the bundlebuilder module is removed.", stacklevel=2) 33 34 import sys 35 import os, errno, shutil 36 import imp, marshal 37 import re 38 from copy import deepcopy 39 import getopt 40 from plistlib import Plist 41 from types import FunctionType as function 42 43 class BundleBuilderError(Exception): pass 44 45 46 class Defaults: 47 48 """Class attributes that don't start with an underscore and are 49 not functions or classmethods are (deep)copied to self.__dict__. 50 This allows for mutable default values. 51 """ 52 53 def __init__(self, **kwargs): 54 defaults = self._getDefaults() 55 defaults.update(kwargs) 56 self.__dict__.update(defaults) 57 58 def _getDefaults(cls): 59 defaults = {} 60 for base in cls.__bases__: 61 if hasattr(base, "_getDefaults"): 62 defaults.update(base._getDefaults()) 63 for name, value in cls.__dict__.items(): 64 if name[0] != "_" and not isinstance(value, 65 (function, classmethod)): 66 defaults[name] = deepcopy(value) 67 return defaults 68 _getDefaults = classmethod(_getDefaults) 69 70 71 class BundleBuilder(Defaults): 72 73 """BundleBuilder is a barebones class for assembling bundles. It 74 knows nothing about executables or icons, it only copies files 75 and creates the PkgInfo and Info.plist files. 76 """ 77 78 # (Note that Defaults.__init__ (deep)copies these values to 79 # instance variables. Mutable defaults are therefore safe.) 80 81 # Name of the bundle, with or without extension. 82 name = None 83 84 # The property list ("plist") 85 plist = Plist(CFBundleDevelopmentRegion = "English", 86 CFBundleInfoDictionaryVersion = "6.0") 87 88 # The type of the bundle. 89 type = "BNDL" 90 # The creator code of the bundle. 91 creator = None 92 93 # the CFBundleIdentifier (this is used for the preferences file name) 94 bundle_id = None 95 96 # List of files that have to be copied to <bundle>/Contents/Resources. 97 resources = [] 98 99 # List of (src, dest) tuples; dest should be a path relative to the bundle 100 # (eg. "Contents/Resources/MyStuff/SomeFile.ext). 101 files = [] 102 103 # List of shared libraries (dylibs, Frameworks) to bundle with the app 104 # will be placed in Contents/Frameworks 105 libs = [] 106 107 # Directory where the bundle will be assembled. 108 builddir = "build" 109 110 # Make symlinks instead copying files. This is handy during debugging, but 111 # makes the bundle non-distributable. 112 symlink = 0 113 114 # Verbosity level. 115 verbosity = 1 116 117 # Destination root directory 118 destroot = "" 119 120 def setup(self): 121 # XXX rethink self.name munging, this is brittle. 122 self.name, ext = os.path.splitext(self.name) 123 if not ext: 124 ext = ".bundle" 125 bundleextension = ext 126 # misc (derived) attributes 127 self.bundlepath = pathjoin(self.builddir, self.name + bundleextension) 128 129 plist = self.plist 130 plist.CFBundleName = self.name 131 plist.CFBundlePackageType = self.type 132 if self.creator is None: 133 if hasattr(plist, "CFBundleSignature"): 134 self.creator = plist.CFBundleSignature 135 else: 136 self.creator = "????" 137 plist.CFBundleSignature = self.creator 138 if self.bundle_id: 139 plist.CFBundleIdentifier = self.bundle_id 140 elif not hasattr(plist, "CFBundleIdentifier"): 141 plist.CFBundleIdentifier = self.name 142 143 def build(self): 144 """Build the bundle.""" 145 builddir = self.builddir 146 if builddir and not os.path.exists(builddir): 147 os.mkdir(builddir) 148 self.message("Building %s" % repr(self.bundlepath), 1) 149 if os.path.exists(self.bundlepath): 150 shutil.rmtree(self.bundlepath) 151 if os.path.exists(self.bundlepath + '~'): 152 shutil.rmtree(self.bundlepath + '~') 153 bp = self.bundlepath 154 155 # Create the app bundle in a temporary location and then 156 # rename the completed bundle. This way the Finder will 157 # never see an incomplete bundle (where it might pick up 158 # and cache the wrong meta data) 159 self.bundlepath = bp + '~' 160 try: 161 os.mkdir(self.bundlepath) 162 self.preProcess() 163 self._copyFiles() 164 self._addMetaFiles() 165 self.postProcess() 166 os.rename(self.bundlepath, bp) 167 finally: 168 self.bundlepath = bp 169 self.message("Done.", 1) 170 171 def preProcess(self): 172 """Hook for subclasses.""" 173 pass 174 def postProcess(self): 175 """Hook for subclasses.""" 176 pass 177 178 def _addMetaFiles(self): 179 contents = pathjoin(self.bundlepath, "Contents") 180 makedirs(contents) 181 # 182 # Write Contents/PkgInfo 183 assert len(self.type) == len(self.creator) == 4, \ 184 "type and creator must be 4-byte strings." 185 pkginfo = pathjoin(contents, "PkgInfo") 186 f = open(pkginfo, "wb") 187 f.write(self.type + self.creator) 188 f.close() 189 # 190 # Write Contents/Info.plist 191 infoplist = pathjoin(contents, "Info.plist") 192 self.plist.write(infoplist) 193 194 def _copyFiles(self): 195 files = self.files[:] 196 for path in self.resources: 197 files.append((path, pathjoin("Contents", "Resources", 198 os.path.basename(path)))) 199 for path in self.libs: 200 files.append((path, pathjoin("Contents", "Frameworks", 201 os.path.basename(path)))) 202 if self.symlink: 203 self.message("Making symbolic links", 1) 204 msg = "Making symlink from" 205 else: 206 self.message("Copying files", 1) 207 msg = "Copying" 208 files.sort() 209 for src, dst in files: 210 if os.path.isdir(src): 211 self.message("%s %s/ to %s/" % (msg, src, dst), 2) 212 else: 213 self.message("%s %s to %s" % (msg, src, dst), 2) 214 dst = pathjoin(self.bundlepath, dst) 215 if self.symlink: 216 symlink(src, dst, mkdirs=1) 217 else: 218 copy(src, dst, mkdirs=1) 219 220 def message(self, msg, level=0): 221 if level <= self.verbosity: 222 indent = "" 223 if level > 1: 224 indent = (level - 1) * " " 225 sys.stderr.write(indent + msg + "\n") 226 227 def report(self): 228 # XXX something decent 229 pass 230 231 232 if __debug__: 233 PYC_EXT = ".pyc" 234 else: 235 PYC_EXT = ".pyo" 236 237 MAGIC = imp.get_magic() 238 USE_ZIPIMPORT = "zipimport" in sys.builtin_module_names 239 240 # For standalone apps, we have our own minimal site.py. We don't need 241 # all the cruft of the real site.py. 242 SITE_PY = """\ 243 import sys 244 if not %(semi_standalone)s: 245 del sys.path[1:] # sys.path[0] is Contents/Resources/ 246 """ 247 248 ZIP_ARCHIVE = "Modules.zip" 249 SITE_PY_ZIP = SITE_PY + ("sys.path.append(sys.path[0] + '/%s')\n" % ZIP_ARCHIVE) 250 251 def getPycData(fullname, code, ispkg): 252 if ispkg: 253 fullname += ".__init__" 254 path = fullname.replace(".", os.sep) + PYC_EXT 255 return path, MAGIC + '\0\0\0\0' + marshal.dumps(code) 256 257 # 258 # Extension modules can't be in the modules zip archive, so a placeholder 259 # is added instead, that loads the extension from a specified location. 260 # 261 EXT_LOADER = """\ 262 def __load(): 263 import imp, sys, os 264 for p in sys.path: 265 path = os.path.join(p, "%(filename)s") 266 if os.path.exists(path): 267 break 268 else: 269 assert 0, "file not found: %(filename)s" 270 mod = imp.load_dynamic("%(name)s", path) 271 272 __load() 273 del __load 274 """ 275 276 MAYMISS_MODULES = ['os2', 'nt', 'ntpath', 'dos', 'dospath', 277 'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize', 278 'org.python.core', 'riscos', 'riscosenviron', 'riscospath' 279 ] 280 281 STRIP_EXEC = "/usr/bin/strip" 282 283 # 284 # We're using a stock interpreter to run the app, yet we need 285 # a way to pass the Python main program to the interpreter. The 286 # bootstrapping script fires up the interpreter with the right 287 # arguments. os.execve() is used as OSX doesn't like us to 288 # start a real new process. Also, the executable name must match 289 # the CFBundleExecutable value in the Info.plist, so we lie 290 # deliberately with argv[0]. The actual Python executable is 291 # passed in an environment variable so we can "repair" 292 # sys.executable later. 293 # 294 BOOTSTRAP_SCRIPT = """\ 295 #!%(hashbang)s 296 297 import sys, os 298 execdir = os.path.dirname(sys.argv[0]) 299 executable = os.path.join(execdir, "%(executable)s") 300 resdir = os.path.join(os.path.dirname(execdir), "Resources") 301 libdir = os.path.join(os.path.dirname(execdir), "Frameworks") 302 mainprogram = os.path.join(resdir, "%(mainprogram)s") 303 304 if %(optimize)s: 305 sys.argv.insert(1, '-O') 306 307 sys.argv.insert(1, mainprogram) 308 if %(standalone)s or %(semi_standalone)s: 309 os.environ["PYTHONPATH"] = resdir 310 if %(standalone)s: 311 os.environ["PYTHONHOME"] = resdir 312 else: 313 pypath = os.getenv("PYTHONPATH", "") 314 if pypath: 315 pypath = ":" + pypath 316 os.environ["PYTHONPATH"] = resdir + pypath 317 318 os.environ["PYTHONEXECUTABLE"] = executable 319 os.environ["DYLD_LIBRARY_PATH"] = libdir 320 os.environ["DYLD_FRAMEWORK_PATH"] = libdir 321 os.execve(executable, sys.argv, os.environ) 322 """ 323 324 325 # 326 # Optional wrapper that converts "dropped files" into sys.argv values. 327 # 328 ARGV_EMULATOR = """\ 329 import argvemulator, os 330 331 argvemulator.ArgvCollector().mainloop() 332 execfile(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 # 340 PYTHONFRAMEWORKGOODIES = [ 341 "Python", # the Python core library 342 "Resources/English.lproj", 343 "Resources/Info.plist", 344 ] 345 346 def isFramework(): 347 return sys.exec_prefix.find("Python.framework") > 0 348 349 350 LIB = os.path.join(sys.prefix, "lib", "python" + sys.version[:3]) 351 SITE_PACKAGES = os.path.join(LIB, "site-packages") 352 353 354 class 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 730 SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()] 731 identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$") 732 733 def 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 751 def 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 758 def 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 767 def 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 772 def 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 781 def 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 789 def 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 797 cmdline_doc = """\ 798 Usage: 799 python bundlebuilder.py [options] command 800 python mybuildscript.py [options] command 801 802 Commands: 803 build build the application 804 report print a report 805 806 Options: 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 843 def usage(msg=None): 844 if msg: 845 print msg 846 print cmdline_doc 847 sys.exit(1) 848 849 def 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 943 def buildapp(**kwargs): 944 builder = AppBuilder(**kwargs) 945 main(builder) 946 947 948 if __name__ == "__main__": 949 main() 950