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