#! /usr/local/bin/python

# Given a Python script, create a binary that runs the script.
# The binary is 100% independent of Python libraries and binaries.
# It will not contain any Python source code -- only "compiled" Python
# (as initialized static variables containing marshalled code objects).
# It even does the right thing for dynamically loaded modules!
# The module search path of the binary is set to the current directory.
#
# Some problems remain:
# - It's highly non-portable, since it knows about paths and libraries
#   (there's a customization section though, and it knows how to
#   distinguish an SGI from a Sun SPARC system -- adding knowledge
#   about more systems is left as an exercise for the reader).
# - You need to have the Python source tree lying around as well as
#   the "libpython.a" used to generate the Python binary.
# - For scripts that use many modules it generates absurdly large
#   files (frozen.c and config.o as well as the final binary),
#   and is consequently rather slow.
#
# Caveats:
# - The search for modules sometimes finds modules that are never
#   actually imported since the code importing them is never executed.
# - If an imported module isn't found, you get a warning but the
#   process of freezing continues.  The binary will fail if it
#   actually tries to import one of these modules.
# - This often happens with the module 'mac', which module 'os' tries
#   to import (to determine whether it is running on a Macintosh).
#   You can ignore the warning about this.
# - If the program dynamically reads or generates Python code and
#   executes it, this code may reference built-in or library modules
#   that aren't present in the frozen binary, and this will fail.
# - Your program may be using external data files, e.g. compiled
#   forms definitions (*.fd).  These aren't incorporated.  Since
#   sys.path in the resulting binary only contains '.', if your
#   program searches its data files along sys.path (as the 'flp'
#   modules does to find its forms definitions), you may need to
#   change the program to extend the search path or instruct its users
#   to set the environment variable PYTHONPATH to point to your data
#   files.
#
# Usage hints:
# - If you have a bunch of scripts that you want to freeze, instead
#   of freezing each of them separately, you might consider writing
#   a tiny main script that looks at sys.argv[0] and then imports
#   the corresponding module.  You can then make links to the
#   frozen binary named after the various scripts you support.
#   Pass the additional scripts as arguments after the main script.
#   A minimal script to do this is the following.
#       import sys, posixpath
#       exec('import ' + posixpath.basename(sys.argv[0]) + '\n')


import os
import sys
import regex
import getopt
import regsub
import string
import marshal

# Function to join two pathnames with a slash in between
j = os.path.join

##################################
# START OF CONFIGURATION SECTION #
##################################

# Attempt to guess machine architecture
if os.path.exists('/usr/lib/libgl_s'): ARCH = 'sgi'
else: ARCH = 'sun4'

# Site parametrizations (change to match your site)
CC = 'cc'				# C compiler
TOP = '/ufs/guido/src'			# Parent of all source trees
PYTHON = j(TOP, 'python')		# Top of the Python source tree
SRC = j(PYTHON, 'src')			# Python source directory
BLD = j(PYTHON, 'build.' + ARCH)	# Python build directory
#BLD = SRC				# Use this if you build in SRC

# Other packages (change to match your site)
DL = j(TOP, 'dl')			# Top of the dl source tree
DL_DLD = j(TOP, 'dl-dld')		# The dl-dld source directory
DLD = j(TOP, 'dld-3.2.3')		# The dld source directory
FORMS = j(TOP, 'forms')			# Top of the FORMS source tree
STDWIN = j(TOP, 'stdwin')		# Top of the STDWIN source tree
READLINE = j(TOP, 'readline.' + ARCH)	# Top of the GNU Readline source tree
SUN_X11 = '/usr/local/X11R5/lib/libX11.a'

# File names (usually no need to change)
LIBP = j(BLD, 'libpython.a')		# Main Python library
CONFIG = j(SRC, 'config.c')		# Configuration source file
FMAIN = j(SRC, 'frozenmain.c')		# Special main source file

# Libraries needed when linking.  First tuple item is built-in module
# for which it is needed (or '*' for always), rest are ld arguments.
# There is a separate list per architecture.
libdeps_sgi = [ \
	  ('stdwin',	j(STDWIN, 'Build/' + ARCH + '/x11/lib/lib.a')), \
	  ('fl',	j(FORMS, 'FORMS/libforms.a'), '-lfm_s'), \
	  ('*',		j(READLINE, 'libreadline.a'), '-ltermcap'), \
	  ('al',	'-laudio'), \
	  ('sv',	'-lsvideo', '-lXext'), \
	  ('cd',	'-lcdaudio', '-lds'), \
	  ('cl',	'-lcl'), \
	  ('imgfile',	'-limage', '-lgutil', '-lm'), \
	  ('mpz',	'/ufs/jh/src/gmp-1.2/libgmp.a'), \
	  ('md5',	'/ufs/jh/src/md5/md5.o'), \
	  ('*',		'-lsun'), \
	  ('*',		j(DL, 'libdl.a'), '-lmld'), \
	  ('*',		'-lmpc'), \
	  ('fm',	'-lfm_s'), \
	  ('gl',	'-lgl_s', '-lX11_s'), \
	  ('stdwin',	'-lX11_s'), \
	  ('*',		'-lm'), \
	  ('*',		'-lc_s'), \
	  ]
libdeps_sun4 = [ \
	  ('*',		'-Bstatic'), \
	  ('stdwin',	j(STDWIN, 'Build/' + ARCH + '/x11/lib/lib.a')), \
	  ('*',		j(READLINE, 'libreadline.a')), \
	  ('*',		'-lm'), \
	  ('*',		j(DL_DLD,'libdl.a'), j(DLD,'libdld.a')), \
	  ('*',		SUN_X11), \
	  ('*',		'-ltermcap'), \
	  ('*',		'-lc'), \
	  ]
libdeps = eval('libdeps_' + ARCH)

################################
# END OF CONFIGURATION SECTION #
################################

# Exception used when scanfile fails
NoSuchFile = 'NoSuchFile'

# Global options
quiet = 0				# -q
verbose = 0				# -v
noexec = 0				# -n
nowrite = 0				# -N
ofile = 'a.out'				# -o file

# Main program -- argument parsing etc.
def main():
	global quiet, verbose, noexec, nowrite, ofile
	try:
		opts, args = getopt.getopt(sys.argv[1:], 'nNo:qv')
	except getopt.error, msg:
		usage(str(msg))
		sys.exit(2)
	for o, a in opts:
		if o == '-n': noexec = 1
		if o == '-N': nowrite = 1
		if o == '-o': ofile = a
		if o == '-q': verbose = 0; quiet = 1
		if o == '-v': verbose = verbose + 1; quiet = 0
	if len(args) < 1:
		usage('please pass at least one file argument')
		sys.exit(2)
	process(args[0], args[1:])

# Print usage message to stderr
def usage(*msgs):
	sys.stdout = sys.stderr
	for msg in msgs: print msg
	print 'Usage: freeze [options] scriptfile [modulefile ...]'
	print '-n      : generate the files but don\'t compile and link'
	print '-N      : don\'t write frozen.c (do compile unless -n given)'
	print '-o file : binary output file (default a.out)'
	print '-q      : quiet (no messages at all except errors)'
	print '-v      : verbose (lots of extra messages)'

# Process the script file
def process(filename, addmodules):
	global noexec
	#
	if not quiet: print 'Computing needed modules ...'
	todo = {}
	todo['__main__'] = filename
	for name in addmodules:
		mod = os.path.basename(name)
		if mod[-3:] == '.py': mod = mod[:-3]
		todo[mod] = name
	try:
		dict = closure(todo)
	except NoSuchFile, filename:
		sys.stderr.write('Can\'t open file %s\n' % filename)
		sys.exit(1)
	#
	mods = dict.keys()
	mods.sort()
	#
	if verbose:
		print '%-15s %s' % ('Module', 'Filename')
		for mod in mods:
			print '%-15s %s' % (`mod`, dict[mod])
	#
	if not quiet: print 'Looking for dynamically linked modules ...'
	dlmodules = []
	objs = []
	libs = []
	for mod in mods:
		if dict[mod][-2:] == '.o':
			if verbose: print 'Found', mod, dict[mod]
			dlmodules.append(mod)
			objs.append(dict[mod])
			libsname = dict[mod][:-2] + '.libs'
			try:
				f = open(libsname, 'r')
			except IOError:
				f = None
			if f:
				libtext = f.read()
				f.close()
				for lib in string.split(libtext):
					if lib in libs: libs.remove(lib)
					libs.append(lib)
	#
	if not nowrite:
		if not quiet: print 'Writing frozen.c ...'
		writefrozen('frozen.c', dict)
	else:
		if not quiet: print 'NOT writing frozen.c ...'
	#
	if not dlmodules:
		config = CONFIG
		if not quiet: print 'Using existing', config, '...'
	else:
		config = 'tmpconfig.c'
		if nowrite:
			if not quiet: print 'NOT writing config.c ...'
		else:
			if not quiet:
				print 'Writing config.c with dl modules ...'
			f = open(CONFIG, 'r')
			g = open(config, 'w')
			m1 = regex.compile('-- ADDMODULE MARKER 1 --')
			m2 = regex.compile('-- ADDMODULE MARKER 2 --')
			while 1:
				line = f.readline()
				if not line: break
				g.write(line)
				if m1.search(line) >= 0:
					if verbose: print 'Marker 1 ...'
					for mod in dlmodules:
						g.write('extern void init' + \
						  mod + '();\n')
				if m2.search(line) >= 0:
					if verbose: print 'Marker 2 ...'
					for mod in dlmodules:
						g.write('{"' + mod + \
						  '", init' + mod + '},\n')
			g.close()
	#
	if not quiet:
		if noexec: print 'Generating compilation commands ...'
		else: print 'Starting compilation ...'
	defs = ['-DUSE_FROZEN', '-DPYTHONPATH=\'"."\'']
	for mod in dict.keys():
		if dict[mod] == '<builtin>' and mod <> 'sys':
			defs.append('-DUSE_' + string.upper(mod))
	#
	incs = ['-I.', '-I' + SRC]
	if dict.has_key('stdwin'):
		incs.append('-I' + j(STDWIN, 'H'))
	#
	srcs = [config, FMAIN]
	#
	libs.append(LIBP)
	for item in libdeps:
		m = item[0]
		if m == '*' or dict.has_key(m):
			for l in item[1:]:
				if l in libs: libs.remove(l)
				libs.append(l)
	#
	sts = 0
	#
	cmd = CC + ' -c'
	cmd = cmd + ' ' + string.join(defs)
	cmd = cmd + ' ' + string.join(incs)
	cmd = cmd + ' ' + string.join(srcs)
	print cmd
	#
	if not noexec:
		sts = os.system(cmd)
		if sts:
			print 'Exit status', sts, '-- turning on -n'
			noexec = 1
	#
	for s in srcs:
		s = os.path.basename(s)
		if s[-2:] == '.c': s = s[:-2]
		o = s + '.o'
		objs.insert(0, o)
	#
	cmd = CC
	cmd = cmd + ' ' + string.join(objs)
	cmd = cmd + ' ' + string.join(libs)
	cmd = cmd + ' -o ' + ofile
	print cmd
	#
	if not noexec:
		sts = os.system(cmd)
		if sts:
			print 'Exit status', sts
		else:
			print 'Done.'
	#
	if not quiet and not noexec and sts == 0:
		print 'Note: consider this:'; print '\tstrip', ofile
	#
	sys.exit(sts)


# Generate code for a given module
def makecode(filename):
	if filename[-2:] == '.o':
		return None
	try:
		f = open(filename, 'r')
	except IOError:
		return None
	if verbose: print 'Making code from', filename, '...'
	text = f.read()
	code = compile(text, filename, 'exec')
	f.close()
	return marshal.dumps(code)


# Write the C source file containing the frozen Python code
def writefrozen(filename, dict):
	f = open(filename, 'w')
	codelist = []
	for mod in dict.keys():
		codestring = makecode(dict[mod])
		if codestring is not None:
			codelist.append((mod, codestring))
	write = sys.stdout.write
	save_stdout = sys.stdout
	try:
		sys.stdout = f
		for mod, codestring in codelist:
			if verbose:
				write('Writing initializer for %s\n'%mod)
			print 'static char M_' + mod + '[' + \
				  str(len(codestring)) + '+1] = {'
			for i in range(0, len(codestring), 16):
				for c in codestring[i:i+16]:
					print str(ord(c)) + ',',
				print
			print '};'
		print 'struct frozen {'
		print '  char *name;'
		print '  char *code;'
		print '  int size;'
		print '} frozen_modules[] = {'
		for mod, codestring in codelist:
			print '  {"' + mod + '",',
			print 'M_' + mod + ',',
			print str(len(codestring)) + '},'
		print '  {0, 0, 0} /* sentinel */'
		print '};'
	finally:
		sys.stdout = save_stdout
	f.close()


# Determine the names and filenames of the modules imported by the
# script, recursively.  This is done by scanning for lines containing
# import statements.  (The scanning has only superficial knowledge of
# Python syntax and no knowledge of semantics, so in theory the result
# may be incorrect -- however this is quite unlikely if you don't
# intentionally obscure your Python code.)

# Compute the closure of scanfile() -- special first file because of script
def closure(todo):
	done = {}
	while todo:
		newtodo = {}
		for modname in todo.keys():
			if not done.has_key(modname):
				filename = todo[modname]
				if filename is None:
					filename = findmodule(modname)
				done[modname] = filename
				if filename in ('<builtin>', '<unknown>'):
					continue
				modules = scanfile(filename)
				for m in modules:
					if not done.has_key(m):
						newtodo[m] = None
		todo = newtodo
	return done

# Scan a file looking for import statements
importstr = '\(^\|:\)[ \t]*import[ \t]+\([a-zA-Z0-9_, \t]+\)'
fromstr   = '\(^\|:\)[ \t]*from[ \t]+\([a-zA-Z0-9_]+\)[ \t]+import[ \t]+'
isimport = regex.compile(importstr)
isfrom = regex.compile(fromstr)
def scanfile(filename):
	allmodules = {}
	try:
		f = open(filename, 'r')
	except IOError, msg:
		raise NoSuchFile, filename
	while 1:
		line = f.readline()
		if not line: break # EOF
		while line[-2:] == '\\\n': # Continuation line
			line = line[:-2] + ' '
			line = line + f.readline()
		if isimport.search(line) >= 0:
			rawmodules = isimport.group(2)
			modules = string.splitfields(rawmodules, ',')
			for i in range(len(modules)):
				modules[i] = string.strip(modules[i])
		elif isfrom.search(line) >= 0:
			modules = [isfrom.group(2)]
		else:
			continue
		for mod in modules:
			allmodules[mod] = None
	f.close()
	return allmodules.keys()

# Find the file containing a module, given its name; None if not found
builtins = sys.builtin_module_names + ['sys']
def findmodule(modname):
	if modname in builtins: return '<builtin>'
	for dirname in sys.path:
		dlfullname = os.path.join(dirname, modname + 'module.o')
		try:
			f = open(dlfullname, 'r')
		except IOError:
			f = None
		if f:
			f.close()
			return dlfullname
		fullname = os.path.join(dirname, modname + '.py')
		try:
			f = open(fullname, 'r')
		except IOError:
			continue
		f.close()
		return fullname
	if not quiet:
		sys.stderr.write('Warning: module %s not found\n' % modname)
	return '<unknown>'


# Call the main program
main()
