Modules | Files | Inheritance Tree | Inheritance Graph | Name Index | Config
File: Synopsis/Core/Executor.py
    1| """
    2| Executors are the implementation of the various actions. The actual Action
    3| objects themselves just contain the data needed to perform the actions, and
    4| are minimal on actual code so that they can be easily serialized. The code and
    5| data needed for the execution of an Action is implemented in the matching
    6| Executor class.
    7| """
    8| 
    9| import string, re, os, stat, sys, statcache
   10| 
   11| from Action import ActionVisitor
   12| from Synopsis.Core import Util
   13| import AST
   14| 
   15| try: import gc
   16| except ImportError: gc = None
   17| 
   18| 
   19| class Executor:
   20|     """Base class for executor classes, defining the common interface between
   21|     each executor instance."""
   22|     def get_output_names(self):
   23|         """Returns a list of (name, timestamp) tuples, representing the output
   24|         from this executor. The names must be given to get_output in turn to
   25|         retrieve the AST objects, and the timestamp may be used for build
   26|         control."""
   27|         pass
   28| 
   29|     def prepare_output(self, name, keep):
   30|         """Prepares an AST object for returning. For most objects, this does
   31|         nothing. In the case of a cacher, this causes it to process each input
   32|         in turn and store the results to disk. This is as opposed to keeping
   33|         each previous input in memory while the next is parsed!
   34|         Returns the AST if keep is set, else None."""
   35|         if keep: return get_output(name)
   36| 
   37|     def get_output(self, name):
   38|         """Returns the AST object for the given name. Name must one returned
   39|         from the 'get_output_names' method."""
   40|         pass
   41| 
   42|     
   43| class ExecutorCreator (ActionVisitor):
   44|     """Creates Executor instances for Action objects"""
   45|     def __init__(self, project, verbose=0):
   46|         self.__project = project
   47|         self.verbose = verbose or project.verbose()
   48| 
   49|     def project(self):
   50|         """Returns the project for this creator"""
   51|         return self.__project
   52| 
   53|     def create(self, action):
   54|         """Creates an executor for the given action"""
   55|         self.__executor = None
   56|         action.accept(self)
   57|         return self.__executor
   58| 
   59|     def visitAction(self, action):
   60|         """This is an unknown or incomplete Action: ignore."""
   61|         print "Warning: Unknown action '%s'"%action.name()
   62| 
   63|     def visitSource(self, action):
   64|         self.__executor = SourceExecutor(self, action)
   65|     def visitParser(self, action):
   66|         self.__executor = ParserExecutor(self, action)
   67|     def visitLinker(self, action):
   68|         self.__executor = LinkerExecutor(self, action)
   69|     def visitCacher(self, action):
   70|         self.__executor = CacherExecutor(self, action)
   71|     def visitFormat(self, action):
   72|         self.__executor = FormatExecutor(self, action)
   73| 
   74| class SourceExecutor (Executor):
   75|     glob_cache = {}
   76| 
   77|     def __init__(self, executor, action):
   78|         self.__executor = executor
   79|         self.__project = executor.project()
   80|         self.__action = action
   81| 
   82|     def compile_glob(self, globstr):
   83|         """Returns a compiled regular expression for the given glob string. A
   84|         glob string is something like "*.?pp" which gets translated into
   85|         "^.*\..pp$". Compiled regular expressions are cached in a class
   86|         variable"""
   87|         if self.glob_cache.has_key(globstr):
   88|             return self.glob_cache[globstr]
   89|         glob = string.replace(globstr, '.', '\.')
   90|         glob = string.replace(glob, '?', '.')
   91|         glob = string.replace(glob, '*', '.*')
   92|         glob = re.compile('^%s$'%glob)
   93|         self.glob_cache[globstr] = glob
   94|         return glob
   95|     def get_output_names(self):
   96|         """Expands the paths into a list of filenames, and return those"""
   97|         # Use an 'open' list which contains 3-tuples of 'recurse?', 'path' and
   98|         # 'glob'
   99|         def path_to_tuple(path_obj):
  100|             if path_obj.type == 'Simple':
  101|                if path_obj.dir.find('/') == -1:
  102|                    return (0, '.', path_obj.dir)
  103|                return (0,)+os.path.split(path_obj.dir)
  104|             elif path_obj.type == 'Dir':
  105|                return (0, path_obj.dir, path_obj.glob)
  106|          else:
  107|                return (1, path_obj.dir, path_obj.glob)
  108| 
  109|         names = []
  110|         for rule in self.__action.rules():
  111|             if rule.type == 'Simple':
  112|                # Add the specified files if they exist
  113|                for file in rule.files:
  114|                try:
  115|                       filepath = os.path.abspath(file)
  116|                       stats = os.stat(filepath)
  117|                       if stat.S_ISREG(stats[stat.ST_MODE]):
  118|                           names.append((file, stats[stat.ST_MTIME]))
  119|                    except OSError, e:
  120|                       print "Warning:",e
  121|             elif rule.type == 'Glob':
  122|                glob = self.compile_glob(rule.glob)
  123|                dirs = list(rule.dirs)
  124|                while len(dirs):
  125|                    dir = dirs.pop(0)
  126|                    # Get list of files in this dir
  127|                    for file in os.listdir(dir):
  128|                # Stat file
  129|                       filepath = os.path.join(dir, file)
  130|                       stats = os.stat(filepath)
  131|                       if stat.S_ISDIR(stats[stat.ST_MODE]) and rule.recursive:
  132|                           # Add to list of dirs to check
  133|                          dirs.append(filepath)
  134|                       elif stat.S_ISREG(stats[stat.ST_MODE]):
  135|                           # Check if matches glob
  136|                         if glob.match(file):
  137|                              # Strip any "./" from the start of the name
  138|                              if len(filepath) > 2 and filepath[:2] == "./":
  139|                              filepath = filepath[2:]
  140|                              names.append((filepath, stats[stat.ST_MTIME]))
  141|             elif rule.type == 'Exclude':
  142|                glob = self.compile_glob(rule.glob)
  143|                old_names = names
  144|                names = []
  145|                for name in old_names:
  146|                    # Only re-add ones that don't match
  147|                    if not glob.match(name[0]):
  148|                       names.append(name)
  149|         
  150|         return names
  151|     def get_output(self, name):
  152|         """Raises an exception, since the SourceAction is only used to
  153|         identify files -- the loading is done by the parsers themselves"""
  154|         raise 'ParseError', "SourceAction doesn't support get_output method."
  155| 
  156| class ParserExecutor (Executor):
  157|     """Parses the input files given by its input SourceActions"""
  158|     def __init__(self, executor, action):
  159|         self.__executor = executor
  160|         self.__project = executor.project()
  161|         self.__action = action
  162|         self.__name_map = {}
  163|         self.__is_multi = None
  164| 
  165|     def is_multi(self):
  166|         """Returns true if this parser parses multiple source files at once.
  167|         This is determined by the parser type and config options."""
  168|         if self.__is_multi is not None: return self.__is_multi
  169|         config = self.__action.config()
  170|         module = config.name
  171|         if module == "C++":
  172|             if hasattr(config, 'multiple_files'):
  173|                self.__is_multi = config.multiple_files
  174|          else:
  175|                self.__is_multi = 0
  176|         else:
  177|             self.__is_multi = 0
  178|         return self.__is_multi
  179| 
  180|     def get_output_names(self):
  181|         """Returns the names from all connected SourceActions, and caches
  182|         which source action they came from"""
  183|         names = []
  184|         # for each input source action...
  185|         for source_action in self.__action.inputs():
  186|             source = self.__executor.create(source_action)
  187|             source_names = source.get_output_names()
  188|             names.extend(source_names)
  189|             for name, timestamp in source_names:
  190|                self.__name_map[name] = source
  191|         # Check multi-file
  192|         if self.is_multi():
  193|             # Only return first name
  194|             return names[0:1]
  195|         return names
  196| 
  197|     def get_output(self, name):
  198|         if self.__executor.verbose:
  199|             print self.__action.name()+": Parsing "+name
  200|             sys.stdout.flush()
  201|         config = self.__action.config()
  202|         parser = self.get_parser()
  203|         # Do the parse
  204|         extra_files = None
  205|         if self.is_multi():
  206|             # Find all source files
  207|             extra_files = self.__name_map.keys()
  208|         ast = parser.parse(name, extra_files, [], config)
  209|         # Return the parsed AST
  210|         return ast
  211| 
  212|     def get_parser(self):
  213|         """Returns the parser module, using the module name stored in the
  214|         Action object. If the module cannot be loaded, this method will raise
  215|         an exception."""
  216|         module = self.__action.config().name
  217|         try:
  218|             parser = Util._import("Synopsis.Parser." + module)
  219|         except ImportError:
  220|             # TODO: invent some exception to pass up
  221|             sys.stderr.write(cmdname + ": Could not import parser `" + name + "'\n")
  222|             sys.exit(1)
  223|         return parser
  224| 
  225| 
  226|         
  227| class LinkerExecutor (Executor):
  228|     def __init__(self, executor, action):
  229|         self.__executor = executor
  230|         self.__project = executor.project()
  231|         self.__action = action
  232|         self.__inputs = {}
  233|         self.__names = {}
  234|     def get_output_names(self):
  235|         """Links multiple ASTs together, and/or performs other manipulative
  236|         actions on a single AST."""
  237|         # Figure out the output name
  238|         myname = self.__action.name()
  239|         if not myname: myname = 'LinkerOutput'
  240|         myname = myname.replace(' ', '_')
  241|         # Figure out the timestamp
  242|         ts = 0
  243|         for input in self.__action.inputs():
  244|             exec_obj = self.__executor.create(input)
  245|             self.__inputs[input] = exec_obj
  246|             names = exec_obj.get_output_names()
  247|             self.__names[input] = names
  248|             for name, timestamp in names:
  249|                if timestamp > ts:
  250|                   ts = timestamp
  251|         return [ (myname, ts) ]
  252| 
  253|     def get_output(self, name):
  254|         # Get input AST(s), probably from a cacher, source or other linker
  255|         # Prepare the inputs
  256|         for input in self.__action.inputs():
  257|             exec_obj = self.__inputs[input]
  258|             names = self.__names[input]
  259|             for iname, timestamp in names:
  260|                exec_obj.prepare_output(iname, 0)
  261|         # Merge the inputs into one AST
  262|         if self.__executor.verbose:
  263|             print self.__action.name()+": Linking "+name
  264|             sys.stdout.flush()
  265|         ast = AST.AST()
  266|         for input in self.__action.inputs():
  267|             exec_obj = self.__inputs[input]
  268|             names = self.__names[input]
  269|             for iname, timestamp in names:
  270|                input_ast = exec_obj.get_output(iname)
  271|                ast.merge(input_ast)
  272|         # Pass merged AST to linker
  273|         module = self.get_linker()
  274|         module.resolve([], ast, self.__action.config())
  275|         # Return linked AST
  276|         return ast
  277| 
  278|     def get_linker(self):
  279|         """Returns the linker module, using the module name stored in the
  280|         Action object. If the module cannot be loaded, this method will raise
  281|         an exception."""
  282|         module = self.__action.config().name
  283|         try:
  284|             linker = Util._import("Synopsis.Linker." + module)
  285|         except ImportError:
  286|             # TODO: invent some exception to pass up
  287|             sys.stderr.write(cmdname + ": Could not import linker `" + name + "'\n")
  288|             sys.exit(1)
  289|         return linker
  290| 
  291| 
  292| class CacherExecutor (Executor):
  293|     def __init__(self, executor, action):
  294|         self.__executor = executor
  295|         self.__project = executor.project()
  296|         self.__action = action
  297|         self.__execs = {}
  298|         self.__timestamps = {}
  299|         self.__input_map = {}
  300|         self.__names = []
  301|     def get_output_names(self):
  302|         action = self.__action
  303|         if action.file:
  304|             # Find file
  305|             stats = os.stat(action.file)
  306|             return action.file, stats[stat.ST_MTIME]
  307|         names = self.__names
  308|         # TODO: add logic here to check timestamps, etc
  309|         for input in action.inputs():
  310|             exec_obj = self.__executor.create(input)
  311|             self.__execs[input] = exec_obj
  312|             in_names = exec_obj.get_output_names()
  313|             names.extend(in_names)
  314|             # Remember which input for each name
  315|             for name, timestamp in in_names:
  316|                self.__input_map[name] = exec_obj
  317|                self.__timestamps[name] = timestamp
  318|         return names
  319|     def get_cache_filename(self, name):
  320|         """Returns the filename of the cache for the input with the given
  321|         name"""
  322|         jname = str(name)
  323|         if jname[0] == '/': jname = jname[1:]
  324|         cache_filename = os.path.join(self.__action.dir, jname)
  325|         if cache_filename[-4:] != ".syn":
  326|             cache_filename = cache_filename + ".syn"
  327|         return cache_filename
  328|     def _get_timestamp(self, filename):
  329|         """Returns the timestamp of the given file, or 0 if not found"""
  330|         try:
  331|             stats = statcache.stat(filename)
  332|             return stats[stat.ST_MTIME]
  333|         except OSError:
  334|             # NB: will catch any type of error caused by the stat call, not
  335|             # just Not Found
  336|             return 0
  337| 
  338|     def _is_up_to_date(self, name, cache_filename):
  339|         """Returns true if the input 'name' in file 'cache_filename' is up to
  340|         date. Checks all dependencies"""
  341|         # Check timestamp on cache
  342|         cache_ts = self._get_timestamp(cache_filename)
  343|         if cache_ts == 0 or cache_ts < self.__timestamps[name]:
  344|             # Cache doesn't exist or is older than file
  345|             return 0
  346|         # Load the deps from the file to check that they are all okay
  347|         try:
  348|             deps = AST.load_deps(cache_filename)
  349|         except:
  350|             # Hopefully wrong file version - must create anew
  351|             msg = sys.exc_info()[1]
  352|             print "Warning: Forcing rebuild due to error (%s)"%msg
  353|             return 0
  354|         # Decide basename to use. Must end in a /
  355|         basename = None
  356|         if hasattr(self.__action, 'basename'):
  357|             basename = self.__action.basename
  358|             if len(basename) and basename[-1] != '/':
  359|                basename = basename + '/'
  360|         # Check each dep
  361|         for filename, timestamp in deps:
  362|             # Must match exactly (eg: installing headers from a
  363|             # tarball/package gives files their original timestamp, which may
  364|             # be earlier than the timestamp we last saw!
  365|             if basename and filename[0] != '/':
  366|                # Presume need to prepend basename
  367|                filename = basename + filename
  368|             if timestamp != self._get_timestamp(filename):
  369|                return 0
  370|         # All deps checked out okay!
  371|         return 1
  372| 
  373|     def prepare_output(self, name, keep):
  374|         """Prepares the output, which means that it parses it, saves it to
  375|         disk, and forgets about it. If keep is set, return the AST anyway"""
  376|         action = self.__action
  377|         # Check if is a single-file loader (not cache)
  378|         if action.file: return
  379|         cache_filename = self.get_cache_filename(name)
  380|         if self._is_up_to_date(name, cache_filename):
  381|           return
  382|         # Need to regenerate. Find input
  383|         exec_obj = self.__input_map[name]
  384|         ast = exec_obj.get_output(name)
  385|         # Save to cache file
  386|         try:
  387|             # Create dir for file
  388|             dir = os.path.dirname(cache_filename)
  389|             if not os.path.exists(dir):
  390|                print "Warning: creating directory",dir
  391|                os.makedirs(dir)
  392|             AST.save(cache_filename, ast)
  393|         except:
  394|             exc, msg = sys.exc_info()[0:2]
  395|             print "Warning: %s: %s"%(exc, msg)
  396|         if keep: return ast
  397|         elif gc:
  398|             # Try to free up mem
  399|             ast = None
  400|             #gc.set_debug(gc.DEBUG_STATS)
  401|             gc.collect()
  402| 
  403|     def get_output(self, name):
  404|         """Gets the output"""
  405|         action = self.__action
  406|         # Check if is a single-file loader (not cache)
  407|         if action.file:
  408|             return AST.load(action.file)
  409|         # Double-check preparedness (may generate output)
  410|         ast = self.prepare_output(name, 1)
  411|         if ast: return ast
  412|         # Should now be able to just load from cache file
  413|         return AST.load(self.get_cache_filename(name))
  414|         
  415| class FormatExecutor (Executor):
  416|     """Formats the input AST given by its single input"""
  417|     def __init__(self, executor, action):
  418|         self.__executor = executor
  419|         self.__project = executor.project()
  420|         self.__action = action
  421|         self.__input_exec = None
  422| 
  423|     def get_output_names(self):
  424|         inputs = self.__action.inputs()
  425|         if len(inputs) != 1:
  426|             raise 'Error', 'Formatter takes exactly one input AST'
  427|         self.__input_exec = self.__executor.create(inputs[0])
  428|         names = self.__input_exec.get_output_names()
  429|         if len(names) != 1:
  430|             raise 'Error', 'Formatter takes exactly one input AST'
  431|         return names
  432| 
  433|     def get_output(self, name):
  434|         # Get input AST, probably from a cache or linker
  435|         ast = self.__input_exec.get_output(name)
  436|         module = self.__action.config().name
  437|         # Pass AST to formatter
  438|         if self.__executor.verbose:
  439|             print self.__action.name()+": Formatting "+name
  440|             sys.stdout.flush()
  441|         try:
  442|             formatter = Util._import("Synopsis.Formatter." + module)
  443|         except ImportError:
  444|             sys.stderr.write(cmdname + ": Could not import formatter `" + module + "'\n")
  445|             sys.exit(1)
  446|         formatter.format([], ast, self.__action.config())
  447|         # Finalize AST (incl. maybe write to disk with timestamp info)
  448|         return None
  449|     
  450|