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