~ubuntu-branches/ubuntu/karmic/pypy/karmic

« back to all changes in this revision

Viewing changes to lib-python/2.4.1/plat-mac/bundlebuilder.py

  • Committer: Bazaar Package Importer
  • Author(s): Alexandre Fayolle
  • Date: 2007-04-13 09:33:09 UTC
  • Revision ID: james.westby@ubuntu.com-20070413093309-yoojh4jcoocu2krz
Tags: upstream-1.0.0
ImportĀ upstreamĀ versionĀ 1.0.0

Show diffs side-by-side

added added

removed removed

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