Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 1 | #! /ufs/guido/bin/sgi/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 | # - It's highly non-portable, since it knows about paths and libraries |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 12 | # (there's a customization section though, and it knows how to |
| 13 | # distinguish an SGI from a Sun SPARC system -- adding knowledge |
| 14 | # about more systems is left as an exercise for the reader). |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 15 | # - You need to have the Python source tree lying around as well as |
| 16 | # the "libpython.a" used to generate the Python binary. |
| 17 | # - For scripts that use many modules it generates absurdly large |
| 18 | # files (frozen.c and config.o as well as the final binary), |
| 19 | # and is consequently rather slow. |
| 20 | # |
| 21 | # Caveats: |
| 22 | # - The search for modules sometimes finds modules that are never |
| 23 | # actually imported since the code importing them is never executed. |
| 24 | # - If an imported module isn't found, you get a warning but the |
| 25 | # process of freezing continues. The binary will fail if it |
| 26 | # actually tries to import one of these modules. |
| 27 | # - This often happens with the module 'mac', which module 'os' tries |
| 28 | # to import (to determine whether it is running on a Macintosh). |
| 29 | # You can ignore the warning about this. |
| 30 | # - If the program dynamically reads or generates Python code and |
| 31 | # executes it, this code may reference built-in or library modules |
| 32 | # that aren't present in the frozen binary, and this will fail. |
| 33 | # - Your program may be using external data files, e.g. compiled |
| 34 | # forms definitions (*.fd). These aren't incorporated. Since |
| 35 | # sys.path in the resulting binary only contains '.', if your |
| 36 | # program searches its data files along sys.path (as the 'flp' |
| 37 | # modules does to find its forms definitions), you may need to |
| 38 | # change the program to extend the search path or instruct its users |
| 39 | # to set the environment variable PYTHONPATH to point to your data |
| 40 | # files. |
| 41 | # |
| 42 | # Usage hints: |
| 43 | # - If you have a bunch of scripts that you want to freeze, instead |
| 44 | # of freezing each of them separately, you might consider writing |
| 45 | # a tiny main script that looks at sys.argv[0] and then imports |
| 46 | # the corresponding module. You can then make links to the |
| 47 | # frozen binary named after the various scripts you support. |
| 48 | # Pass the additional scripts as arguments after the main script. |
| 49 | # A minimal script to do this is the following. |
| 50 | # import sys, posixpath |
| 51 | # exec('import ' + posixpath.basename(sys.argv[0]) + '\n') |
| 52 | |
| 53 | |
| 54 | import os |
| 55 | import sys |
| 56 | import regex |
| 57 | import getopt |
| 58 | import regsub |
| 59 | import string |
| 60 | import marshal |
| 61 | |
| 62 | # Function to join two pathnames with a slash in between |
| 63 | j = os.path.join |
| 64 | |
| 65 | ################################## |
| 66 | # START OF CONFIGURATION SECTION # |
| 67 | ################################## |
| 68 | |
| 69 | # Attempt to guess machine architecture |
| 70 | if os.path.exists('/usr/lib/libgl_s'): ARCH = 'sgi' |
| 71 | else: ARCH = 'sun4' |
| 72 | |
| 73 | # Site parametrizations (change to match your site) |
| 74 | CC = 'cc' # C compiler |
| 75 | TOP = '/ufs/guido/src' # Parent of all source trees |
| 76 | PYTHON = j(TOP, 'python') # Top of the Python source tree |
| 77 | SRC = j(PYTHON, 'src') # Python source directory |
| 78 | BLD = j(PYTHON, 'build.' + ARCH) # Python build directory |
| 79 | #BLD = SRC # Use this if you build in SRC |
| 80 | |
| 81 | # Other packages (change to match your site) |
| 82 | DL = j(TOP, 'dl') # Top of the dl source tree |
| 83 | DL_DLD = j(TOP, 'dl-dld') # The dl-dld source directory |
| 84 | DLD = j(TOP, 'dld-3.2.3') # The dld source directory |
| 85 | FORMS = j(TOP, 'forms') # Top of the FORMS source tree |
| 86 | STDWIN = j(TOP, 'stdwin') # Top of the STDWIN source tree |
| 87 | READLINE = j(TOP, 'readline.' + ARCH) # Top of the GNU Readline source tree |
| 88 | SUN_X11 = '/usr/local/X11R5/lib/libX11.a' |
| 89 | |
| 90 | # File names (usually no need to change) |
| 91 | LIBP = j(BLD, 'libpython.a') # Main Python library |
| 92 | CONFIG = j(SRC, 'config.c') # Configuration source file |
| 93 | FMAIN = j(SRC, 'frozenmain.c') # Special main source file |
| 94 | |
| 95 | # Libraries needed when linking. First tuple item is built-in module |
| 96 | # for which it is needed (or '*' for always), rest are ld arguments. |
| 97 | # There is a separate list per architecture. |
| 98 | libdeps_sgi = [ \ |
| 99 | ('stdwin', j(STDWIN, 'Build/' + ARCH + '/x11/lib/lib.a')), \ |
| 100 | ('fl', j(FORMS, 'FORMS/libforms.a'), '-lfm_s'), \ |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 101 | ('*', j(READLINE, 'libreadline.a'), '-ltermcap'), \ |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 102 | ('al', '-laudio'), \ |
| 103 | ('sv', '-lsvideo', '-lXext'), \ |
| 104 | ('cd', '-lcdaudio', '-lds'), \ |
| 105 | ('cl', '-lcl'), \ |
| 106 | ('imgfile', '-limage', '-lgutil', '-lm'), \ |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 107 | ('mpz', '/ufs/jh/src/gmp-1.2/libgmp.a'), \ |
| 108 | ('md5', '/ufs/jh/src/md5/md5.o'), \ |
| 109 | ('*', '-lsun'), \ |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 110 | ('*', j(DL, 'libdl.a'), '-lmld'), \ |
| 111 | ('*', '-lmpc'), \ |
| 112 | ('fm', '-lfm_s'), \ |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 113 | ('gl', '-lgl_s', '-lX11_s'), \ |
| 114 | ('stdwin', '-lX11_s'), \ |
| 115 | ('*', '-lm'), \ |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 116 | ('*', '-lc_s'), \ |
| 117 | ] |
| 118 | libdeps_sun4 = [ \ |
| 119 | ('*', '-Bstatic'), \ |
| 120 | ('stdwin', j(STDWIN, 'Build/' + ARCH + '/x11/lib/lib.a')), \ |
| 121 | ('*', j(READLINE, 'libreadline.a')), \ |
| 122 | ('*', '-lm'), \ |
| 123 | ('*', j(DL_DLD,'libdl.a'), j(DLD,'libdld.a')), \ |
| 124 | ('*', SUN_X11), \ |
| 125 | ('*', '-ltermcap'), \ |
| 126 | ('*', '-lc'), \ |
| 127 | ] |
| 128 | libdeps = eval('libdeps_' + ARCH) |
| 129 | |
| 130 | ################################ |
| 131 | # END OF CONFIGURATION SECTION # |
| 132 | ################################ |
| 133 | |
| 134 | # Exception used when scanfile fails |
| 135 | NoSuchFile = 'NoSuchFile' |
| 136 | |
| 137 | # Global options |
| 138 | quiet = 0 # -q |
| 139 | verbose = 0 # -v |
| 140 | noexec = 0 # -n |
| 141 | nowrite = 0 # -N |
| 142 | ofile = 'a.out' # -o file |
| 143 | |
| 144 | # Main program -- argument parsing etc. |
| 145 | def main(): |
| 146 | global quiet, verbose, noexec, nowrite, ofile |
| 147 | try: |
| 148 | opts, args = getopt.getopt(sys.argv[1:], 'nNo:qv') |
| 149 | except getopt.error, msg: |
| 150 | usage(str(msg)) |
| 151 | sys.exit(2) |
| 152 | for o, a in opts: |
| 153 | if o == '-n': noexec = 1 |
| 154 | if o == '-N': nowrite = 1 |
| 155 | if o == '-o': ofile = a |
| 156 | if o == '-q': verbose = 0; quiet = 1 |
| 157 | if o == '-v': verbose = verbose + 1; quiet = 0 |
| 158 | if len(args) < 1: |
| 159 | usage('please pass at least one file argument') |
| 160 | sys.exit(2) |
| 161 | process(args[0], args[1:]) |
| 162 | |
| 163 | # Print usage message to stderr |
| 164 | def usage(*msgs): |
| 165 | sys.stdout = sys.stderr |
| 166 | for msg in msgs: print msg |
| 167 | print 'Usage: freeze [options] scriptfile [modulefile ...]' |
| 168 | print '-n : generate the files but don\'t compile and link' |
| 169 | print '-N : don\'t write frozen.c (do compile unless -n given)' |
| 170 | print '-o file : binary output file (default a.out)' |
| 171 | print '-q : quiet (no messages at all except errors)' |
| 172 | print '-v : verbose (lots of extra messages)' |
| 173 | |
| 174 | # Process the script file |
| 175 | def process(filename, addmodules): |
| 176 | global noexec |
| 177 | # |
| 178 | if not quiet: print 'Computing needed modules ...' |
| 179 | todo = {} |
| 180 | todo['__main__'] = filename |
| 181 | for name in addmodules: |
| 182 | mod = os.path.basename(name) |
| 183 | if mod[-3:] == '.py': mod = mod[:-3] |
| 184 | todo[mod] = name |
| 185 | try: |
| 186 | dict = closure(todo) |
| 187 | except NoSuchFile, filename: |
| 188 | sys.stderr.write('Can\'t open file %s\n' % filename) |
| 189 | sys.exit(1) |
| 190 | # |
| 191 | mods = dict.keys() |
| 192 | mods.sort() |
| 193 | # |
| 194 | if verbose: |
| 195 | print '%-15s %s' % ('Module', 'Filename') |
| 196 | for mod in mods: |
| 197 | print '%-15s %s' % (`mod`, dict[mod]) |
| 198 | # |
| 199 | if not quiet: print 'Looking for dynamically linked modules ...' |
| 200 | dlmodules = [] |
| 201 | objs = [] |
| 202 | libs = [] |
| 203 | for mod in mods: |
| 204 | if dict[mod][-2:] == '.o': |
| 205 | if verbose: print 'Found', mod, dict[mod] |
| 206 | dlmodules.append(mod) |
| 207 | objs.append(dict[mod]) |
| 208 | libsname = dict[mod][:-2] + '.libs' |
| 209 | try: |
| 210 | f = open(libsname, 'r') |
| 211 | except IOError: |
| 212 | f = None |
| 213 | if f: |
| 214 | libtext = f.read() |
| 215 | f.close() |
| 216 | for lib in string.split(libtext): |
| 217 | if lib in libs: libs.remove(lib) |
| 218 | libs.append(lib) |
| 219 | # |
| 220 | if not nowrite: |
| 221 | if not quiet: print 'Writing frozen.c ...' |
| 222 | writefrozen('frozen.c', dict) |
| 223 | else: |
| 224 | if not quiet: print 'NOT writing frozen.c ...' |
| 225 | # |
| 226 | if not dlmodules: |
| 227 | config = CONFIG |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 228 | if not quiet: print 'Using existing', config, '...' |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 229 | else: |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 230 | config = 'tmpconfig.c' |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 231 | if nowrite: |
| 232 | if not quiet: print 'NOT writing config.c ...' |
| 233 | else: |
| 234 | if not quiet: |
| 235 | print 'Writing config.c with dl modules ...' |
| 236 | f = open(CONFIG, 'r') |
| 237 | g = open(config, 'w') |
| 238 | m1 = regex.compile('-- ADDMODULE MARKER 1 --') |
| 239 | m2 = regex.compile('-- ADDMODULE MARKER 2 --') |
| 240 | while 1: |
| 241 | line = f.readline() |
| 242 | if not line: break |
| 243 | g.write(line) |
| 244 | if m1.search(line) >= 0: |
| 245 | if verbose: print 'Marker 1 ...' |
| 246 | for mod in dlmodules: |
| 247 | g.write('extern void init' + \ |
| 248 | mod + '();\n') |
| 249 | if m2.search(line) >= 0: |
| 250 | if verbose: print 'Marker 2 ...' |
| 251 | for mod in dlmodules: |
| 252 | g.write('{"' + mod + \ |
| 253 | '", init' + mod + '},\n') |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 254 | g.close() |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 255 | # |
Guido van Rossum | db392b9 | 1993-06-05 18:03:53 +0000 | [diff] [blame] | 256 | if not quiet: |
| 257 | if noexec: print 'Generating compilation commands ...' |
| 258 | else: print 'Starting compilation ...' |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 259 | defs = ['-DUSE_FROZEN', '-DPYTHONPATH=\'"."\''] |
| 260 | for mod in dict.keys(): |
| 261 | if dict[mod] == '<builtin>' and mod <> 'sys': |
| 262 | defs.append('-DUSE_' + string.upper(mod)) |
| 263 | # |
| 264 | incs = ['-I.', '-I' + SRC] |
| 265 | if dict.has_key('stdwin'): |
| 266 | incs.append('-I' + j(STDWIN, 'H')) |
| 267 | # |
| 268 | srcs = [config, FMAIN] |
| 269 | # |
| 270 | libs.append(LIBP) |
| 271 | for item in libdeps: |
| 272 | m = item[0] |
| 273 | if m == '*' or dict.has_key(m): |
| 274 | for l in item[1:]: |
| 275 | if l in libs: libs.remove(l) |
| 276 | libs.append(l) |
| 277 | # |
| 278 | sts = 0 |
| 279 | # |
| 280 | cmd = CC + ' -c' |
| 281 | cmd = cmd + ' ' + string.join(defs) |
| 282 | cmd = cmd + ' ' + string.join(incs) |
| 283 | cmd = cmd + ' ' + string.join(srcs) |
| 284 | print cmd |
| 285 | # |
| 286 | if not noexec: |
| 287 | sts = os.system(cmd) |
| 288 | if sts: |
| 289 | print 'Exit status', sts, '-- turning on -n' |
| 290 | noexec = 1 |
| 291 | # |
| 292 | for s in srcs: |
| 293 | s = os.path.basename(s) |
| 294 | if s[-2:] == '.c': s = s[:-2] |
| 295 | o = s + '.o' |
| 296 | objs.insert(0, o) |
| 297 | # |
| 298 | cmd = CC |
| 299 | cmd = cmd + ' ' + string.join(objs) |
| 300 | cmd = cmd + ' ' + string.join(libs) |
| 301 | cmd = cmd + ' -o ' + ofile |
| 302 | print cmd |
| 303 | # |
| 304 | if not noexec: |
| 305 | sts = os.system(cmd) |
| 306 | if sts: |
| 307 | print 'Exit status', sts |
| 308 | else: |
| 309 | print 'Done.' |
| 310 | # |
Guido van Rossum | 032d394 | 1994-01-07 10:55:55 +0000 | [diff] [blame^] | 311 | if not quiet and not noexec and sts == 0: |
| 312 | print 'Note: consider this:'; print '\tstrip', ofile |
Guido van Rossum | 5146ea3 | 1993-04-01 20:45:45 +0000 | [diff] [blame] | 313 | # |
| 314 | sys.exit(sts) |
| 315 | |
| 316 | |
| 317 | # Generate code for a given module |
| 318 | def makecode(filename): |
| 319 | if filename[-2:] == '.o': |
| 320 | return None |
| 321 | try: |
| 322 | f = open(filename, 'r') |
| 323 | except IOError: |
| 324 | return None |
| 325 | if verbose: print 'Making code from', filename, '...' |
| 326 | text = f.read() |
| 327 | code = compile(text, filename, 'exec') |
| 328 | f.close() |
| 329 | return marshal.dumps(code) |
| 330 | |
| 331 | |
| 332 | # Write the C source file containing the frozen Python code |
| 333 | def writefrozen(filename, dict): |
| 334 | f = open(filename, 'w') |
| 335 | codelist = [] |
| 336 | for mod in dict.keys(): |
| 337 | codestring = makecode(dict[mod]) |
| 338 | if codestring is not None: |
| 339 | codelist.append((mod, codestring)) |
| 340 | write = sys.stdout.write |
| 341 | save_stdout = sys.stdout |
| 342 | try: |
| 343 | sys.stdout = f |
| 344 | for mod, codestring in codelist: |
| 345 | if verbose: |
| 346 | write('Writing initializer for %s\n'%mod) |
| 347 | print 'static char M_' + mod + '[' + \ |
| 348 | str(len(codestring)) + '+1] = {' |
| 349 | for i in range(0, len(codestring), 16): |
| 350 | for c in codestring[i:i+16]: |
| 351 | print str(ord(c)) + ',', |
| 352 | print |
| 353 | print '};' |
| 354 | print 'struct frozen {' |
| 355 | print ' char *name;' |
| 356 | print ' char *code;' |
| 357 | print ' int size;' |
| 358 | print '} frozen_modules[] = {' |
| 359 | for mod, codestring in codelist: |
| 360 | print ' {"' + mod + '",', |
| 361 | print 'M_' + mod + ',', |
| 362 | print str(len(codestring)) + '},' |
| 363 | print ' {0, 0, 0} /* sentinel */' |
| 364 | print '};' |
| 365 | finally: |
| 366 | sys.stdout = save_stdout |
| 367 | f.close() |
| 368 | |
| 369 | |
| 370 | # Determine the names and filenames of the modules imported by the |
| 371 | # script, recursively. This is done by scanning for lines containing |
| 372 | # import statements. (The scanning has only superficial knowledge of |
| 373 | # Python syntax and no knowledge of semantics, so in theory the result |
| 374 | # may be incorrect -- however this is quite unlikely if you don't |
| 375 | # intentionally obscure your Python code.) |
| 376 | |
| 377 | # Compute the closure of scanfile() -- special first file because of script |
| 378 | def closure(todo): |
| 379 | done = {} |
| 380 | while todo: |
| 381 | newtodo = {} |
| 382 | for modname in todo.keys(): |
| 383 | if not done.has_key(modname): |
| 384 | filename = todo[modname] |
| 385 | if filename is None: |
| 386 | filename = findmodule(modname) |
| 387 | done[modname] = filename |
| 388 | if filename in ('<builtin>', '<unknown>'): |
| 389 | continue |
| 390 | modules = scanfile(filename) |
| 391 | for m in modules: |
| 392 | if not done.has_key(m): |
| 393 | newtodo[m] = None |
| 394 | todo = newtodo |
| 395 | return done |
| 396 | |
| 397 | # Scan a file looking for import statements |
| 398 | importstr = '\(^\|:\)[ \t]*import[ \t]+\([a-zA-Z0-9_, \t]+\)' |
| 399 | fromstr = '\(^\|:\)[ \t]*from[ \t]+\([a-zA-Z0-9_]+\)[ \t]+import[ \t]+' |
| 400 | isimport = regex.compile(importstr) |
| 401 | isfrom = regex.compile(fromstr) |
| 402 | def scanfile(filename): |
| 403 | allmodules = {} |
| 404 | try: |
| 405 | f = open(filename, 'r') |
| 406 | except IOError, msg: |
| 407 | raise NoSuchFile, filename |
| 408 | while 1: |
| 409 | line = f.readline() |
| 410 | if not line: break # EOF |
| 411 | while line[-2:] == '\\\n': # Continuation line |
| 412 | line = line[:-2] + ' ' |
| 413 | line = line + f.readline() |
| 414 | if isimport.search(line) >= 0: |
| 415 | rawmodules = isimport.group(2) |
| 416 | modules = string.splitfields(rawmodules, ',') |
| 417 | for i in range(len(modules)): |
| 418 | modules[i] = string.strip(modules[i]) |
| 419 | elif isfrom.search(line) >= 0: |
| 420 | modules = [isfrom.group(2)] |
| 421 | else: |
| 422 | continue |
| 423 | for mod in modules: |
| 424 | allmodules[mod] = None |
| 425 | f.close() |
| 426 | return allmodules.keys() |
| 427 | |
| 428 | # Find the file containing a module, given its name; None if not found |
| 429 | builtins = sys.builtin_module_names + ['sys'] |
| 430 | def findmodule(modname): |
| 431 | if modname in builtins: return '<builtin>' |
| 432 | for dirname in sys.path: |
| 433 | dlfullname = os.path.join(dirname, modname + 'module.o') |
| 434 | try: |
| 435 | f = open(dlfullname, 'r') |
| 436 | except IOError: |
| 437 | f = None |
| 438 | if f: |
| 439 | f.close() |
| 440 | return dlfullname |
| 441 | fullname = os.path.join(dirname, modname + '.py') |
| 442 | try: |
| 443 | f = open(fullname, 'r') |
| 444 | except IOError: |
| 445 | continue |
| 446 | f.close() |
| 447 | return fullname |
| 448 | if not quiet: |
| 449 | sys.stderr.write('Warning: module %s not found\n' % modname) |
| 450 | return '<unknown>' |
| 451 | |
| 452 | |
| 453 | # Call the main program |
| 454 | main() |