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