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