blob: 52285a6d65bc3438a23700856df4dc33447123cb [file] [log] [blame]
Guido van Rossumdbaf3321994-10-03 10:25:54 +00001#! /usr/local/bin/python
2
3# Given a Python script, create a binary that runs the script.
4# The binary is 100% independent of Python libraries and binaries.
5# It will not contain any Python source code -- only "compiled" Python
6# (as initialized static variables containing marshalled code objects).
7# It even does the right thing for dynamically loaded modules!
8# The module search path of the binary is set to the current directory.
9#
10# Some problems remain:
11# - You need to have the Python source tree lying around as well as
12# the various libraries used to generate the Python binary.
13# - For scripts that use many modules it generates absurdly large
14# files (frozen.c and config.o as well as the final binary),
15# and is consequently rather slow.
16#
17# Caveats:
18# - The search for modules sometimes finds modules that are never
19# actually imported since the code importing them is never executed.
20# - If an imported module isn't found, you get a warning but the
21# process of freezing continues. The binary will fail if it
22# actually tries to import one of these modules.
23# - This often happens with the module 'mac', which module 'os' tries
24# to import (to determine whether it is running on a Macintosh).
25# You can ignore the warning about this.
26# - If the program dynamically reads or generates Python code and
27# executes it, this code may reference built-in or library modules
28# that aren't present in the frozen binary, and this will fail.
29# - Your program may be using external data files, e.g. compiled
30# forms definitions (*.fd). These aren't incorporated. By default,
31# the sys.path in the resulting binary is only '.' (but you can override
32# that with the -P option).
33#
34# Usage hints:
35# - If you have a bunch of scripts that you want to freeze, instead
36# of freezing each of them separately, you might consider writing
37# a tiny main script that looks at sys.argv[0] and then imports
38# the corresponding module. You can then make links to the
39# frozen binary named after the various scripts you support.
40# Pass the additional scripts as arguments after the main script.
41# A minimal script to do this is the following.
42# import sys, posixpath
43# exec('import ' + posixpath.basename(sys.argv[0]) + '\n')
44#
45# Mods by Jack, August 94:
46# - Removed all static configuration stuff. Now, Setup and Makefile files
47# are parsed to obtain the linking info for the libraries. You have to
48# supply the -B option, though.
49# - Added -P (set sys.path) and -I/-D/-L/-l options (passed on to cc and
50# ld).
51
52import os
53import sys
54import regex
55import getopt
56import regsub
57import string
58import marshal
59
60# Exception used when scanfile fails
61NoSuchFile = 'NoSuchFile'
62
63# Global options
64builddir = '' # -B dir
65quiet = 0 # -q
66verbose = 0 # -v
67noexec = 0 # -n
68nowrite = 0 # -N
69ofile = 'a.out' # -o file
70path = '\'"."\'' # -P path
71
72cc_options = [] # Collects cc options
73ld_options = [] # Collects ld options
74module_libraries = {} # ld options for each module
75global_libraries = [] # Libraries we always need
76include_path = '' # Include path, from Makefile
77lib_path = '' # and lib path, ditto
78compiler = 'cc' # and compiler
79
80# Main program -- argument parsing etc.
81def main():
82 global quiet, verbose, noexec, nowrite, ofile, builddir, path
83 try:
84 opts, args = getopt.getopt(sys.argv[1:], 'B:nNo:P:qvI:D:L:l:')
85 except getopt.error, msg:
86 usage(str(msg))
87 sys.exit(2)
88 for o, a in opts:
89 if o == '-B': builddir = a
90 if o == '-n': noexec = 1
91 if o == '-N': nowrite = 1
92 if o == '-o': ofile = a
93 if o == '-P':
94 if '"' in a:
95 usage('sorry, cannot have " in -P option')
96 sys.exit(2)
97 path = `'"' + a + '"'`
98 if o == '-q': verbose = 0; quiet = 1
99 if o == '-v': verbose = verbose + 1; quiet = 0
100 if o in ('-I', '-D'): cc_options.append(o+a)
101 if o in ('-L', '-l'): ld_options.append(o+a)
102 if not builddir:
103 usage('sorry, you have to pass a -B option')
104 sys.exit(2)
105 if len(args) < 1:
106 usage('please pass at least one file argument')
107 sys.exit(2)
108 process(args[0], args[1:])
109
110# Print usage message to stderr
111def usage(*msgs):
112 sys.stdout = sys.stderr
113 for msg in msgs: print msg
114 print 'Usage: freeze [options] scriptfile [modulefile ...]'
115 print '-B dir : name of python build dir (no default)'
116 print '-n : generate the files but don\'t compile and link'
117 print '-N : don\'t write frozen.c (do compile unless -n given)'
118 print '-o file : binary output file (default a.out)'
119 print '-P path : set sys.path for program (default ".")'
120 print '-q : quiet (no messages at all except errors)'
121 print '-v : verbose (lots of extra messages)'
122 print '-D and -I options are passed to cc, -L and -l to ld'
123
124# Process the script file
125def process(filename, addmodules):
126 global noexec
127 #
128 if not quiet: print 'Computing needed modules ...'
129 todo = {}
130 todo['__main__'] = filename
131 for name in addmodules:
132 mod = os.path.basename(name)
133 if mod[-3:] == '.py': mod = mod[:-3]
134 todo[mod] = name
135 try:
136 dict = closure(todo)
137 except NoSuchFile, filename:
138 sys.stderr.write('Can\'t open file %s\n' % filename)
139 sys.exit(1)
140 #
141 mods = dict.keys()
142 mods.sort()
143 #
144 if verbose:
145 print '%-15s %s' % ('Module', 'Filename')
146 for mod in mods:
147 print '%-15s %s' % (`mod`, dict[mod])
148 #
149 if not quiet: print 'Looking for dynamically linked modules ...'
150 dlmodules = []
151 objs = []
152 libs = []
153 for mod in mods:
154 if dict[mod][-2:] == '.o':
155 if verbose: print 'Found', mod, dict[mod]
156 dlmodules.append(mod)
157 objs.append(dict[mod])
158 libsname = dict[mod][:-2] + '.libs'
159 try:
160 f = open(libsname, 'r')
161 except IOError:
162 f = None
163 if f:
164 libtext = f.read()
165 f.close()
166 for lib in string.split(libtext):
167 if lib in libs: libs.remove(lib)
168 libs.append(lib)
169 #
170 if not nowrite:
171 if not quiet: print 'Writing frozen.c ...'
172 writefrozen('frozen.c', dict)
173 else:
174 if not quiet: print 'NOT writing frozen.c ...'
175 #
176 if not quiet:
177 print 'Deducing compile/link options from', builddir
178 #
179 # Parse the config info
180 #
181 parse(builddir)
182 CONFIG_IN = lib_path + '/config.c.in'
183 FMAIN = lib_path + '/frozenmain.c'
184 CC = compiler
185 #
186## if not dlmodules:
187 if 0:
188 config = CONFIG
189 if not quiet: print 'Using existing', config, '...'
190 else:
191 config = 'tmpconfig.c'
192 if nowrite:
193 if not quiet: print 'NOT writing config.c ...'
194 else:
195 if not quiet:
196 print 'Writing config.c with dl modules ...'
197 f = open(CONFIG_IN, 'r')
198 g = open(config, 'w')
199 m1 = regex.compile('-- ADDMODULE MARKER 1 --')
200 m2 = regex.compile('-- ADDMODULE MARKER 2 --')
201 builtinmodules = []
202 stdmodules = ('sys', '__main__', '__builtin__',
203 'marshal')
204 for mod in dict.keys():
205 if dict[mod] == '<builtin>' and \
206 mod not in stdmodules:
207 builtinmodules.append(mod)
208 todomodules = builtinmodules + dlmodules
209 while 1:
210 line = f.readline()
211 if not line: break
212 g.write(line)
213 if m1.search(line) >= 0:
214 if verbose: print 'Marker 1 ...'
215 for mod in todomodules:
216 g.write('extern void init' + \
217 mod + '();\n')
218 if m2.search(line) >= 0:
219 if verbose: print 'Marker 2 ...'
220 for mod in todomodules:
221 g.write('{"' + mod + \
222 '", init' + mod + '},\n')
223 g.close()
224 #
225 if not quiet:
226 if noexec: print 'Generating compilation commands ...'
227 else: print 'Starting compilation ...'
228 defs = ['-DNO_MAIN', '-DUSE_FROZEN']
229 defs.append('-DPYTHONPATH='+path)
230 #
231 incs = ['-I.', '-I' + include_path]
232# if dict.has_key('stdwin'):
233# incs.append('-I' + j(STDWIN, 'H'))
234 #
235 srcs = [config, FMAIN]
236 #
237 modlibs = module_libraries
238
239 for mod in dict.keys():
240 if modlibs.has_key(mod):
241 libs = libs + modlibs[mod]
242
243 libs = libs + global_libraries
244 #
245 # remove dups:
246 # XXXX Not well tested...
247 nskip = 0
248 newlibs = []
249 while libs:
250 l = libs[0]
251 del libs[0]
252 if l[:2] == '-L' and l in newlibs:
253 nskip = nskip + 1
254 continue
255 if (l[:2] == '-l' or l[-2:] == '.a') and l in libs:
256 nskip = nskip + 1
257 continue
258 newlibs.append(l)
259 libs = newlibs
260 if nskip and not quiet:
261 print 'Removed %d duplicate libraries'%nskip
262 #
263 sts = 0
264 #
265 cmd = CC + ' -c'
266 if cc_options:
267 cmd = cmd + ' ' + string.join(cc_options)
268 cmd = cmd + ' ' + string.join(defs)
269 cmd = cmd + ' ' + string.join(incs)
270 cmd = cmd + ' ' + string.join(srcs)
271 print cmd
272 #
273 if not noexec:
274 sts = os.system(cmd)
275 if sts:
276 print 'Exit status', sts, '-- turning on -n'
277 noexec = 1
278 #
279 for s in srcs:
280 s = os.path.basename(s)
281 if s[-2:] == '.c': s = s[:-2]
282 o = s + '.o'
283 objs.insert(0, o)
284 #
285 cmd = CC
286 cmd = cmd + ' ' + string.join(objs)
287 cmd = cmd + ' ' + string.join(libs)
288 if ld_options:
289 cmd = cmd + ' ' + string.join(ld_options)
290 cmd = cmd + ' -o ' + ofile
291 print cmd
292 #
293 if not noexec:
294 sts = os.system(cmd)
295 if sts:
296 print 'Exit status', sts
297 else:
298 print 'Done.'
299 #
300 if not quiet and not noexec and sts == 0:
301 print 'Note: consider this:'; print '\tstrip', ofile
302 #
303 sys.exit(sts)
304
305
306# Generate code for a given module
307def makecode(filename):
308 if filename[-2:] == '.o':
309 return None
310 try:
311 f = open(filename, 'r')
312 except IOError:
313 return None
314 if verbose: print 'Making code from', filename, '...'
315 text = f.read()
316 code = compile(text, filename, 'exec')
317 f.close()
318 return marshal.dumps(code)
319
320
321# Write the C source file containing the frozen Python code
322def writefrozen(filename, dict):
323 f = open(filename, 'w')
324 codelist = []
325 for mod in dict.keys():
326 codestring = makecode(dict[mod])
327 if codestring is not None:
328 codelist.append((mod, codestring))
329 write = sys.stdout.write
330 save_stdout = sys.stdout
331 try:
332 sys.stdout = f
333 for mod, codestring in codelist:
334 if verbose:
335 write('Writing initializer for %s\n'%mod)
336 print 'static unsigned char M_' + mod + '[' + \
337 str(len(codestring)) + '+1] = {'
338 for i in range(0, len(codestring), 16):
339 for c in codestring[i:i+16]:
340 print str(ord(c)) + ',',
341 print
342 print '};'
343 print 'struct frozen {'
344 print ' char *name;'
345 print ' unsigned char *code;'
346 print ' int size;'
347 print '} frozen_modules[] = {'
348 for mod, codestring in codelist:
349 print ' {"' + mod + '",',
350 print 'M_' + mod + ',',
351 print str(len(codestring)) + '},'
352 print ' {0, 0, 0} /* sentinel */'
353 print '};'
354 finally:
355 sys.stdout = save_stdout
356 f.close()
357
358
359# Determine the names and filenames of the modules imported by the
360# script, recursively. This is done by scanning for lines containing
361# import statements. (The scanning has only superficial knowledge of
362# Python syntax and no knowledge of semantics, so in theory the result
363# may be incorrect -- however this is quite unlikely if you don't
364# intentionally obscure your Python code.)
365
366# Compute the closure of scanfile() -- special first file because of script
367def closure(todo):
368 done = {}
369 while todo:
370 newtodo = {}
371 for modname in todo.keys():
372 if not done.has_key(modname):
373 filename = todo[modname]
374 if filename is None:
375 filename = findmodule(modname)
376 done[modname] = filename
377 if filename in ('<builtin>', '<unknown>'):
378 continue
379 modules = scanfile(filename)
380 for m in modules:
381 if not done.has_key(m):
382 newtodo[m] = None
383 todo = newtodo
384 return done
385
386# Scan a file looking for import statements
387importstr = '\(^\|:\)[ \t]*import[ \t]+\([a-zA-Z0-9_, \t]+\)'
388fromstr = '\(^\|:\)[ \t]*from[ \t]+\([a-zA-Z0-9_]+\)[ \t]+import[ \t]+'
389isimport = regex.compile(importstr)
390isfrom = regex.compile(fromstr)
391def scanfile(filename):
392 allmodules = {}
393 try:
394 f = open(filename, 'r')
395 except IOError, msg:
396 raise NoSuchFile, filename
397 while 1:
398 line = f.readline()
399 if not line: break # EOF
400 while line[-2:] == '\\\n': # Continuation line
401 line = line[:-2] + ' '
402 line = line + f.readline()
403 if isimport.search(line) >= 0:
404 rawmodules = isimport.group(2)
405 modules = string.splitfields(rawmodules, ',')
406 for i in range(len(modules)):
407 modules[i] = string.strip(modules[i])
408 elif isfrom.search(line) >= 0:
409 modules = [isfrom.group(2)]
410 else:
411 continue
412 for mod in modules:
413 allmodules[mod] = None
414 f.close()
415 return allmodules.keys()
416
417# Find the file containing a module, given its name; None if not found
418builtins = sys.builtin_module_names + ['sys']
419def findmodule(modname):
420 if modname in builtins: return '<builtin>'
421 for dirname in sys.path:
422 dlfullname = os.path.join(dirname, modname + 'module.o')
423 try:
424 f = open(dlfullname, 'r')
425 except IOError:
426 f = None
427 if f:
428 f.close()
429 return dlfullname
430 fullname = os.path.join(dirname, modname + '.py')
431 try:
432 f = open(fullname, 'r')
433 except IOError:
434 continue
435 f.close()
436 return fullname
437 if not quiet:
438 sys.stderr.write('Warning: module %s not found\n' % modname)
439 return '<unknown>'
440#
441# Parse a setup file. Returns two dictionaries, one containing variables
442# defined with their values and one containing module definitions
443#
444def parse_setup(fp):
445 modules = {}
446 variables = {}
447 for line in fp.readlines():
448 if '#' in line: # Strip comments
449 line = string.splitfields(line, '#')[0]
450 line = string.strip(line[:-1]) # Strip whitespace
451 if not line:
452 continue
453 words = string.split(line)
454 if '=' in words[0]:
455 #
456 # equal sign before first space. Definition
457 #
458 pos = string.index(line, '=')
459 name = line[:pos]
460 value = string.strip(line[pos+1:])
461 variables[name] = value
462 else:
463 modules[words[0]] = words[1:]
464 return modules, variables
465#
466# Parse a makefile. Returns a list of the variables defined.
467#
468def parse_makefile(fp):
469 variables = {}
470 for line in fp.readlines():
471 if '#' in line: # Strip comments
472 line = string.splitfields(line, '#')[0]
473 if not line:
474 continue
475 if line[0] in string.whitespace:
476 continue
477 line = string.strip(line[:-1]) # Strip whitespace
478 if not line:
479 continue
480 if '=' in string.splitfields(line, ':')[0]:
481 #
482 # equal sign before first colon. Definition
483 #
484 pos = string.index(line, '=')
485 name = line[:pos]
486 value = string.strip(line[pos+1:])
487 variables[name] = value
488 return variables
489
490#
491# Recursively add loader options from Setup files in extension
492# directories.
493#
494def add_extension_directory(name, isinstalldir):
495 if verbose:
496 print 'Adding extension directory', name
497 fp = open(name + '/Setup', 'r')
498 modules, variables = parse_setup(fp)
499 #
500 # Locate all new modules and remember the ld flags needed for them
501 #
502 for m in modules.keys():
503 if module_libraries.has_key(m):
504 continue
505 options = modules[m]
506 if isinstalldir:
507 ld_options = []
508 else:
509 ld_options = [name + '/lib.a']
510 for o in options:
511 # ld options are all capital except DUIC and l
512 if o[:-2] == '.a':
513 ld_options.append(o)
514 elif o[0] == '-':
515 if o[1] == 'l':
516 ld_options.append(o)
517 elif o[1] in string.uppercase and not o[1] in 'DUIC':
518 ld_options.append(o)
519 module_libraries[m] = ld_options
520 #
521 # See if we have to bother with base setups
522 #
523 if variables.has_key('BASESETUP'):
524 if isinstalldir:
525 raise 'installdir has base setup'
526 setupfiles = string.split(variables['BASESETUP'])
527 for s in setupfiles:
528 if s[-6:] <> '/Setup':
529 raise 'Incorrect BASESETUP', s
530 s = s[:-6]
531 if s[0] <> '/':
532 s = name + '/' + s
533 s = os.path.normpath(s)
534 add_extension_directory(s, 0)
535#
536# Main routine for this module: given a build directory, get all
537# information needed for the linker.
538#
539def parse(dir):
540 global include_path
541 global lib_path
542 global compiler
543
544 fp = open(dir + '/Makefile', 'r')
545 #
546 # First find the global libraries and the base python
547 #
548 vars = parse_makefile(fp)
549 if vars.has_key('CC'):
550 compiler = vars['CC']
551 if not vars.has_key('installdir'):
552 raise 'No $installdir in Makefile'
553 include_path = vars['installdir'] + '/include/Py'
554 lib_path = vars['installdir'] + '/lib/python/lib'
555 global_libraries.append('-L' + lib_path)
556 global_libraries.append('-lPython')
557 global_libraries.append('-lParser')
558 global_libraries.append('-lObjects')
559 global_libraries.append('-lModules')
560 for name in ('LIBS', 'LIBM', 'LIBC'):
561 if not vars.has_key(name):
562 raise 'Missing required def in Makefile', name
563 for lib in string.split(vars[name]):
564 global_libraries.append(lib)
565 #
566 # Next, parse the modules from the base python
567 #
568 add_extension_directory(lib_path, 1)
569 #
570 # Finally, parse the modules from the extension python
571 #
572 if dir <> lib_path:
573 add_extension_directory(dir, 0)
574
575# Call the main program
576main()