#!/usr/bin/python # python script that compiles files and directories to python bytecode, # possibly with optimizations # TODO: # Distinguish between files and directories # Accept the optimization flag # Enforce command option rules import py_compile import compileall import sys import os import re import getopt import traceback usage = r""" pycc : Python byteCode Compiler (pronounced "pie-sees") (C) Andrew Kesterson 2006, usage: pycc src_files ... options: -L dir : Add the following directory to the sys.path; similar to the -L directive in gcc -l lib : Specifically import the given module before compiling any modules (successive -l directives are processed in the order received); similar to -l directive in gcc. The argument should be in standard python 'import' notation. "from xxx import y' should be enclosed in quotes. Otherwise provide 'xxx' alone for the module name to import -O : Enable the python compiler's optimizations; default output will have a .pyo extension instead of a .pyc. NOTE: FOR SOME REASON, this option doesn't work on single files. Only directories. This is a limitation of the python compiler libraries, not this program. -o : When compiling a single file, specify the output filename. When compiling a directory or multiple files, this directive has no effect. -r : Recurse beyond the first level of directories given on the command line, so that for example passing src/dir on the command line would compile src/dir/src1.py and also src/dir/subdir/src2.py. src_files This is a list of single files and directories. Directories are not recursed into (beyond the first level) unless the -r option is specified. -E : Stop compilation of all source files when one source file has an error (otherwise, an error message is printed to stderr and compilation continues to the next file). Error messages are printed regardless of this flag. -d n : Tells the compiler to descend up to (at maximum) n levels of directories when running recursively -F : Force the generation of code, even when the .pyc/.pyo timestamps are up to date -v : Be verbose (errors are always printed regardless) -h : display this help """ shortopts = "L:l:Oo:rd:EvhF" class ArgChecker: def printErr(self, msg): if ( not msg.endswith("\n") ): msg += "\n" sys.__stdout__.write(msg) def importPaths (self, importStr): fromRE = re.compile(r"""^from\s+([\w.]+)\s+import\s+(\w+).*$""") importRE = re.compile(r"""^import\s+([.\w]+).*$""") fromparts = fromRE.findall(importStr) importparts = importRE.findall(importStr) try: if ( (len(fromparts) != 1 or len(fromparts[0]) != 2 ) and len(importparts) == 0): self.printErr("Bad option: -l %s : invalid syntax : not enough arguments!" % (importStr)) self.printErr("%s : %s" % (fromparts, importparts)) return False elif ( len(fromparts) == 1 and len(fromparts[0]) == 2 ): __import__(fromparts[0][1], globals(), None, fromparts[0][0].split(".")) return True elif ( len(importparts) == 1): __import__(importparts[0][0], globals(), None, None) return True return False except Exception, e: self.printErr("Import error:%s: %s" % (importStr, str(e))) return False def checkArgs (self, argc, argv): global shortopts global usage compiler = PYCompiler() opts, args = getopt.getopt(argv, shortopts) # self.printErr("%s : %s" % (opts, args)) errors = False if ( len(args) == 0 ): self.printErr(usage) return None for file in args: if ( os.path.exists(file) ): if ( os.path.isfile(file) ): compiler.queueFile(file) elif ( os.path.isdir(file) ): compiler.queueDir(file) else: self.printErr("Bad Option: Unable to determine type of file %s" % file) errors = True else: self.printErr("Bad Option: File or directory does not exist %s" % file) errors = True for opt in opts: if ( opt[0] == "-L" ): # add library path if ( os.path.exists(opt[1]) and os.path.isdir(opt[1]) ): sys.path.append(opt[1]) else: self.printErr("Bad option: -L %s : Path is not a directory or is nonexistant" % opt[1]) errors = True elif ( opt[0] == "-l" ): # specifically import the given module before continuing if ( not self.importPaths(opt[1]) ): errors = True elif ( opt[0] == "-O" ) : compiler.optimize(True) elif ( opt[0] == "-o" ) : if ( len(compiler.files) == 1 and len(compiler.dirs) == 0 ): compiler.ofile = opt[1] else: if ( compiler.verbose ): self.printErr("Ignoring option -o with multiple files or directories...") elif ( opt[0] == "-r" ): compiler.recursive(True) elif ( opt[0] == "-E" ): compiler.stopOnError(True) elif ( opt[0] == "-v" ): compiler.setVerbose(True) elif ( opt[0] == "-F" ): compiler.forceGen(True) elif ( opt[0] == "-h" ): self.printErr("HELP FOUND") return False elif ( opt[0] == "-d" ): try: compiler.setMaxDepth(int(opt[1])) except Exception, e: self.printErr("Bad Option: %s : must pass an integer" % opt[0]) else: self.printErr(usage) errors = True if ( not errors ): return compiler else: #self.printErr("%s : %s" % (opts, args)) return None class PYCompiler : def __init__ (self): self.files = [] self.dirs = [] self.opt = False self.recurse = False self.stopErr = False self.verbose = 0 self.depth = 10 self.force = 0 self.ofile = "" def setVerbose (self, opt): if ( opt ): self.verbose = 1 else: self.verbose = 0 def forceGen (self, opt): if ( opt ): self.force = 1 else: self.force = 0 def setMaxDepth (self, depth): self.depth = depth def printErr(self, msg): sys.__stdout__.write(msg) if ( (not msg.endswith("\n")) ): sys.__stdout__.write("\n") def queueFile (self, fname): self.files.append(fname) def queueDir (self, dirname): self.dirs.append(dirname) def printCompileErr (self, e): #self.printErr("printErr called with exception %s" % str(e)) efile = e.file etype = e.exc_type_name if ( len(e.exc_value) > 1 ) : eline = e.exc_value[1][1] ecode = e.exc_value[1][3] else: eline = -1 ecode = "(no code given)" edesc = e.exc_value[0] msg = "%s:%d: %s : %s : %s" % (efile, eline, etype, edesc, str(ecode)) self.printErr(msg) def compile (self): for fname in self.files: ofile = "" if ( len(self.files) == 1 and len(self.dirs) == 0 and self.ofile) : ofile = self.ofile else: ofile = fname + "c" if ( not self.__compile_file(fname, ofile) ): if ( self.stopErr or ( len(self.files) + len(self.dirs)) == 1): return False for dirname in self.dirs : if ( not os.path.walk(dirname, self.__compile_dir, None) ): if ( self.stopErr ): return False return True def __compile_file (self, fname, ofile = None): try: if ( self.force == 1 and len(self.files) == 1 and len(self.dirs) == 0): if ( os.path.exists(ofile) and os.path.isfile(ofile) ): try: os.remove(ofile) except OSError, e: if ( self.verbose ): self.printErr("%s : Failed to remove original - ignoring ... " % fname) else: if ( self.verbose ): self.printErr("%s : Original output does not exist - ignoring ... " % fname) #self.printErr("%s -> %s ..." % (fname, ofile)) py_compile.compile(fname, ofile, None, True) if ( (fname in self.files) and self.verbose ): # don't print this if we're being called from __compile_dir self.printErr("%s -> %s ... OK" % (fname, ofile)) return True except py_compile.PyCompileError, e: self.printCompileErr(e) if ( fname in self.files ): self.printErr("%s -> %s ... FAILED" % (fname, ofile)) return False def __compile_dir (self, arg, dirname, fnames): try: for fname in fnames: if ( not (fname.endswith("py") or fname.endswith("PY")) ): continue tocompile = os.path.join(dirname, fname) if ( os.path.isdir(tocompile) ): os.path.walk(dirname, self.__compile_dir, None) outfile = tocompile+"c" if ( not self.__compile_file(tocompile, outfile) ): self.printErr("%s -> %s ... FAILED" % (tocompile, outfile)) if ( self.stopErr ): return False else: continue else: if ( self.verbose ): self.printErr("%s -> %s ... OK" % (tocompile, outfile)) return True except py_compile.PyCompileError, e: self.printCompileErr(e) return False def optimize (self, opt): self.opt = opt def recursive (self, opt): self.recurse = opt def stopOnError (self, opt): self.stopErr = opt def main (argc, argv): checker = ArgChecker() compiler = checker.checkArgs(argc, argv) if ( not compiler ): sys.exit(1) else: if ( not compiler.compile() ): sys.exit(2) else: sys.exit(0) if ( __name__ == "__main__" ): main(len(sys.argv)-1, sys.argv[1:])