Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit cef3288

Browse files
committed
added support for building standalone applications
- requires modulefinder.py to be on sys.path - does *not* work for Python.framework (yet), only for static builds
1 parent d32047f commit cef3288

1 file changed

Lines changed: 259 additions & 14 deletions

File tree

Mac/Lib/bundlebuilder.py

Lines changed: 259 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,23 @@
2424
2525
"""
2626

27-
#
28-
# XXX Todo:
29-
# - modulefinder support to build standalone apps
30-
# - consider turning this into a distutils extension
31-
#
3227

33-
__all__ = ["BundleBuilder", "AppBuilder", "buildapp"]
28+
__all__ = ["BundleBuilder", "BundleBuilderError", "AppBuilder", "buildapp"]
3429

3530

3631
import sys
3732
import os, errno, shutil
33+
import imp, marshal
34+
import re
3835
from copy import deepcopy
3936
import getopt
4037
from plistlib import Plist
4138
from types import FunctionType as function
4239

4340

41+
class BundleBuilderError(Exception): pass
42+
43+
4444
class Defaults:
4545

4646
"""Class attributes that don't start with an underscore and are
@@ -176,6 +176,7 @@ def _copyFiles(self):
176176
else:
177177
self.message("Copying files", 1)
178178
msg = "Copying"
179+
files.sort()
179180
for src, dst in files:
180181
if os.path.isdir(src):
181182
self.message("%s %s/ to %s/" % (msg, src, dst), 2)
@@ -200,7 +201,42 @@ def report(self):
200201
pprint.pprint(self.__dict__)
201202

202203

203-
mainWrapperTemplate = """\
204+
205+
if __debug__:
206+
PYC_EXT = ".pyc"
207+
else:
208+
PYC_EXT = ".pyo"
209+
210+
MAGIC = imp.get_magic()
211+
USE_FROZEN = hasattr(imp, "set_frozenmodules")
212+
213+
# For standalone apps, we have our own minimal site.py. We don't need
214+
# all the cruft of the real site.py.
215+
SITE_PY = """\
216+
import sys
217+
del sys.path[1:] # sys.path[0] is Contents/Resources/
218+
"""
219+
220+
if USE_FROZEN:
221+
FROZEN_ARCHIVE = "FrozenModules.marshal"
222+
SITE_PY += """\
223+
# bootstrapping
224+
import imp, marshal
225+
f = open(sys.path[0] + "/%s", "rb")
226+
imp.set_frozenmodules(marshal.load(f))
227+
f.close()
228+
""" % FROZEN_ARCHIVE
229+
230+
SITE_CO = compile(SITE_PY, "<-bundlebuilder->", "exec")
231+
232+
MAYMISS_MODULES = ['mac', 'os2', 'nt', 'ntpath', 'dos', 'dospath',
233+
'win32api', 'ce', '_winreg', 'nturl2path', 'sitecustomize',
234+
'org.python.core', 'riscos', 'riscosenviron', 'riscospath'
235+
]
236+
237+
STRIP_EXEC = "/usr/bin/strip"
238+
239+
EXECVE_WRAPPER = """\
204240
#!/usr/bin/env python
205241
206242
import os
@@ -211,14 +247,14 @@ def report(self):
211247
assert os.path.exists(mainprogram)
212248
argv.insert(1, mainprogram)
213249
os.environ["PYTHONPATH"] = resources
214-
%(setpythonhome)s
215250
%(setexecutable)s
216251
os.execve(executable, argv, os.environ)
217252
"""
218253

219254
setExecutableTemplate = """executable = os.path.join(resources, "%s")"""
220255
pythonhomeSnippet = """os.environ["home"] = resources"""
221256

257+
222258
class AppBuilder(BundleBuilder):
223259

224260
# A Python main program. If this argument is given, the main
@@ -239,9 +275,41 @@ class AppBuilder(BundleBuilder):
239275
# Symlink the executable instead of copying it.
240276
symlink_exec = 0
241277

278+
# If True, build standalone app.
279+
standalone = 0
280+
281+
# The following attributes are only used when building a standalone app.
282+
283+
# Exclude these modules.
284+
excludeModules = []
285+
286+
# Include these modules.
287+
includeModules = []
288+
289+
# Include these packages.
290+
includePackages = []
291+
292+
# Strip binaries.
293+
strip = 0
294+
295+
# Found C extension modules: [(name, path), ...]
296+
extensions = []
297+
298+
# Found Python modules: [(name, codeobject, ispkg), ...]
299+
pymodules = []
300+
301+
# Modules that modulefinder couldn't find:
302+
missingModules = []
303+
304+
# List of all binaries (executables or shared libs), for stripping purposes
305+
binaries = []
306+
242307
def setup(self):
308+
if self.standalone and self.mainprogram is None:
309+
raise BundleBuilderError, ("must specify 'mainprogram' when "
310+
"building a standalone application.")
243311
if self.mainprogram is None and self.executable is None:
244-
raise TypeError, ("must specify either or both of "
312+
raise BundleBuilderError, ("must specify either or both of "
245313
"'executable' and 'mainprogram'")
246314

247315
if self.name is not None:
@@ -262,41 +330,201 @@ def setup(self):
262330

263331
self.plist.CFBundleExecutable = self.name
264332

333+
if self.standalone:
334+
if self.executable is None: # *assert* that it is None?
335+
self.executable = sys.executable
336+
self.findDependencies()
337+
265338
def preProcess(self):
266-
resdir = pathjoin("Contents", "Resources")
339+
resdir = "Contents/Resources"
267340
if self.executable is not None:
268341
if self.mainprogram is None:
269342
execpath = pathjoin(self.execdir, self.name)
270343
else:
271344
execpath = pathjoin(resdir, os.path.basename(self.executable))
272345
if not self.symlink_exec:
273346
self.files.append((self.executable, execpath))
347+
self.binaries.append(execpath)
274348
self.execpath = execpath
275349
# For execve wrapper
276350
setexecutable = setExecutableTemplate % os.path.basename(self.executable)
277351
else:
278352
setexecutable = "" # XXX for locals() call
279353

280354
if self.mainprogram is not None:
281-
setpythonhome = "" # pythonhomeSnippet if we're making a standalone app
282355
mainname = os.path.basename(self.mainprogram)
283356
self.files.append((self.mainprogram, pathjoin(resdir, mainname)))
284357
# Create execve wrapper
285358
mainprogram = self.mainprogram # XXX for locals() call
286359
execdir = pathjoin(self.bundlepath, self.execdir)
287360
mainwrapperpath = pathjoin(execdir, self.name)
288361
makedirs(execdir)
289-
open(mainwrapperpath, "w").write(mainWrapperTemplate % locals())
362+
open(mainwrapperpath, "w").write(EXECVE_WRAPPER % locals())
290363
os.chmod(mainwrapperpath, 0777)
291364

292365
def postProcess(self):
366+
self.addPythonModules()
367+
if self.strip and not self.symlink:
368+
self.stripBinaries()
369+
293370
if self.symlink_exec and self.executable:
294371
self.message("Symlinking executable %s to %s" % (self.executable,
295372
self.execpath), 2)
296373
dst = pathjoin(self.bundlepath, self.execpath)
297374
makedirs(os.path.dirname(dst))
298375
os.symlink(os.path.abspath(self.executable), dst)
299376

377+
if self.missingModules:
378+
self.reportMissing()
379+
380+
def addPythonModules(self):
381+
self.message("Adding Python modules", 1)
382+
pymodules = self.pymodules
383+
384+
if USE_FROZEN:
385+
# This anticipates the acceptance of this patch:
386+
# http://www.python.org/sf/642578
387+
# Create a file containing all modules, frozen.
388+
frozenmodules = []
389+
for name, code, ispkg in pymodules:
390+
if ispkg:
391+
self.message("Adding Python package %s" % name, 2)
392+
else:
393+
self.message("Adding Python module %s" % name, 2)
394+
frozenmodules.append((name, marshal.dumps(code), ispkg))
395+
frozenmodules = tuple(frozenmodules)
396+
relpath = "Contents/Resources/" + FROZEN_ARCHIVE
397+
abspath = pathjoin(self.bundlepath, relpath)
398+
f = open(abspath, "wb")
399+
marshal.dump(frozenmodules, f)
400+
f.close()
401+
# add site.pyc
402+
sitepath = pathjoin(self.bundlepath, "Contents/Resources/site" + PYC_EXT)
403+
writePyc(SITE_CO, sitepath)
404+
else:
405+
# Create individual .pyc files.
406+
for name, code, ispkg in pymodules:
407+
if ispkg:
408+
name += ".__init__"
409+
path = name.split(".")
410+
path = pathjoin("Contents/Resources/", *path) + PYC_EXT
411+
412+
if ispkg:
413+
self.message("Adding Python package %s" % path, 2)
414+
else:
415+
self.message("Adding Python module %s" % path, 2)
416+
417+
abspath = pathjoin(self.bundlepath, path)
418+
makedirs(os.path.dirname(abspath))
419+
writePyc(code, abspath)
420+
421+
def stripBinaries(self):
422+
if not os.path.exists(STRIP_EXEC):
423+
self.message("Error: can't strip binaries: no strip program at "
424+
"%s" % STRIP_EXEC, 0)
425+
else:
426+
self.message("Stripping binaries", 1)
427+
for relpath in self.binaries:
428+
self.message("Stripping %s" % relpath, 2)
429+
abspath = pathjoin(self.bundlepath, relpath)
430+
assert not os.path.islink(abspath)
431+
rv = os.system("%s -S \"%s\"" % (STRIP_EXEC, abspath))
432+
433+
def findDependencies(self):
434+
self.message("Finding module dependencies", 1)
435+
import modulefinder
436+
mf = modulefinder.ModuleFinder(excludes=self.excludeModules)
437+
# manually add our own site.py
438+
site = mf.add_module("site")
439+
site.__code__ = SITE_CO
440+
mf.scan_code(SITE_CO, site)
441+
442+
includeModules = self.includeModules[:]
443+
for name in self.includePackages:
444+
includeModules.extend(findPackageContents(name).keys())
445+
for name in includeModules:
446+
try:
447+
mf.import_hook(name)
448+
except ImportError:
449+
self.missingModules.append(name)
450+
451+
452+
mf.run_script(self.mainprogram)
453+
modules = mf.modules.items()
454+
modules.sort()
455+
for name, mod in modules:
456+
if mod.__file__ and mod.__code__ is None:
457+
# C extension
458+
path = mod.__file__
459+
ext = os.path.splitext(path)[1]
460+
if USE_FROZEN: # "proper" freezing
461+
# rename extensions that are submodules of packages to
462+
# <packagename>.<modulename>.<ext>
463+
dstpath = "Contents/Resources/" + name + ext
464+
else:
465+
dstpath = name.split(".")
466+
dstpath = pathjoin("Contents/Resources/", *dstpath) + ext
467+
self.files.append((path, dstpath))
468+
self.extensions.append((name, path, dstpath))
469+
self.binaries.append(dstpath)
470+
elif mod.__code__ is not None:
471+
ispkg = mod.__path__ is not None
472+
if not USE_FROZEN or name != "site":
473+
# Our site.py is doing the bootstrapping, so we must
474+
# include a real .pyc file if USE_FROZEN is True.
475+
self.pymodules.append((name, mod.__code__, ispkg))
476+
477+
self.missingModules.extend(mf.any_missing())
478+
479+
def reportMissing(self):
480+
missing = [name for name in self.missingModules
481+
if name not in MAYMISS_MODULES]
482+
missingsub = [name for name in missing if "." in name]
483+
missing = [name for name in missing if "." not in name]
484+
missing.sort()
485+
missingsub.sort()
486+
if missing:
487+
self.message("Warning: couldn't find the following modules:", 1)
488+
self.message(" " + ", ".join(missing))
489+
if missingsub:
490+
self.message("Warning: couldn't find the following submodules "
491+
"(but it's probably OK since modulefinder can't distinguish "
492+
"between from \"module import submodule\" and "
493+
"\"from module import name\"):", 1)
494+
self.message(" " + ", ".join(missingsub))
495+
496+
#
497+
# Utilities.
498+
#
499+
500+
SUFFIXES = [_suf for _suf, _mode, _tp in imp.get_suffixes()]
501+
identifierRE = re.compile(r"[_a-zA-z][_a-zA-Z0-9]*$")
502+
503+
def findPackageContents(name, searchpath=None):
504+
head = name.split(".")[-1]
505+
if identifierRE.match(head) is None:
506+
return {}
507+
try:
508+
fp, path, (ext, mode, tp) = imp.find_module(head, searchpath)
509+
except ImportError:
510+
return {}
511+
modules = {name: None}
512+
if tp == imp.PKG_DIRECTORY and path:
513+
files = os.listdir(path)
514+
for sub in files:
515+
sub, ext = os.path.splitext(sub)
516+
fullname = name + "." + sub
517+
if sub != "__init__" and fullname not in modules:
518+
modules.update(findPackageContents(fullname, [path]))
519+
return modules
520+
521+
def writePyc(code, path):
522+
f = open(path, "wb")
523+
f.write("\0" * 8) # don't bother about a time stamp
524+
marshal.dump(code, f)
525+
f.seek(0, 0)
526+
f.write(MAGIC)
527+
f.close()
300528

301529
def copy(src, dst, mkdirs=0):
302530
"""Copy a file or a directory."""
@@ -355,6 +583,12 @@ def pathjoin(*args):
355583
-c, --creator=CCCC 4-char creator code (default: '????')
356584
-l, --link symlink files/folder instead of copying them
357585
--link-exec symlink the executable instead of copying it
586+
--standalone build a standalone application, which is fully
587+
independent of a Python installation
588+
-x, --exclude=MODULE exclude module (with --standalone)
589+
-i, --include=MODULE include module (with --standalone)
590+
--package=PACKAGE include a whole package (with --standalone)
591+
--strip strip binaries (remove debug info)
358592
-v, --verbose increase verbosity level
359593
-q, --quiet decrease verbosity level
360594
-h, --help print this message
@@ -370,10 +604,11 @@ def main(builder=None):
370604
if builder is None:
371605
builder = AppBuilder(verbosity=1)
372606

373-
shortopts = "b:n:r:e:m:c:p:lhvq"
607+
shortopts = "b:n:r:e:m:c:p:lx:i:hvq"
374608
longopts = ("builddir=", "name=", "resource=", "executable=",
375609
"mainprogram=", "creator=", "nib=", "plist=", "link",
376-
"link-exec", "help", "verbose", "quiet")
610+
"link-exec", "help", "verbose", "quiet", "standalone",
611+
"exclude=", "include=", "package=", "strip")
377612

378613
try:
379614
options, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
@@ -407,6 +642,16 @@ def main(builder=None):
407642
builder.verbosity += 1
408643
elif opt in ('-q', '--quiet'):
409644
builder.verbosity -= 1
645+
elif opt == '--standalone':
646+
builder.standalone = 1
647+
elif opt in ('-x', '--exclude'):
648+
builder.excludeModules.append(arg)
649+
elif opt in ('-i', '--include'):
650+
builder.includeModules.append(arg)
651+
elif opt == '--package':
652+
builder.includePackages.append(arg)
653+
elif opt == '--strip':
654+
builder.strip = 1
410655

411656
if len(args) != 1:
412657
usage("Must specify one command ('build', 'report' or 'help')")

0 commit comments

Comments
 (0)