diff python/ppci/tasks.py @ 377:9667d78ba79e

Switched to xml for project description
author Windel Bouwman
date Fri, 11 Apr 2014 15:47:50 +0200
parents 3bb7dcfe5529
children 6ae782a085e0
line wrap: on
line diff
--- a/python/ppci/tasks.py	Tue Mar 25 19:36:51 2014 +0100
+++ b/python/ppci/tasks.py	Fri Apr 11 15:47:50 2014 +0200
@@ -4,94 +4,195 @@
 """
 
 import logging
+import re
+import os
+import glob
+
+
+task_map = {}
+def register_task(name):
+    """ Decorator that registers a task class """
+    def f(cls):
+        task_map[name] = cls
+        return cls
+    return f
+
 
 class TaskError(Exception):
+    """ When a task fails, this exception is raised """
     def __init__(self, msg):
         self.msg = msg
 
 
-class Task:
-    """ Task that can run, and depend on other tasks """
+class Project:
+    """ A project contains a set of named targets that can depend upon 
+        eachother """
     def __init__(self, name):
         self.name = name
-        self.completed = False
+        self.targets = {}
+        self.properties = {}
+        self.macro_regex = re.compile('\$\{([^\}]+)\}')
+
+    def set_property(self, name, value):
+        self.properties[name] = value
+
+    def get_property(self, name):
+        if name not in self.properties:
+            raise TaskError('Property "{}" not found'.format(name))
+        return self.properties[name]
+
+    def add_target(self, t):
+        if t.name in self.targets:
+            raise TaskError("Duplicate target '{}'".format(t.name))
+        self.targets[t.name] = t
+
+    def get_target(self, target_name):
+        if target_name not in self.targets:
+            raise TaskError('target "{}" not found'.format(target_name))
+        return self.targets[target_name]
+
+    def expand_macros(self, txt):
+        """ Replace all macros in txt with the correct properties """
+        while True:
+            mo = self.macro_regex.search(txt)
+            if not mo:
+                break
+            propname = mo.group(1)
+            propval = self.get_property(propname)
+            txt = txt[:mo.start()] + propval + txt[mo.end():]
+        return txt
+
+    def dfs(self, target_name, state):
+        state.add(target_name)
+        target = self.get_target(target_name)
+        for dep in target.dependencies:
+            if dep in state:
+                raise TaskError('Dependency loop detected {} -> {}'.format(target_name, dep))
+            self.dfs(dep, state)
+
+    def check_target(self, target_name):
+        state = set()
+        self.dfs(target_name, state)
+
+    def dependencies(self, target_name):
+        assert type(target_name) is str
+        target = self.get_target(target_name)
+        cdst = list(self.dependencies(dep) for dep in target.dependencies)
+        cdst.append(target.dependencies)
+        return set.union(*cdst)
+
+
+class Target:
+    """ Defines a target that has a name and a list of tasks to execute """
+    def __init__(self, name, project):
+        self.name = name
+        self.project = project
+        self.tasks = []
         self.dependencies = set()
-        self.duration = 1
+
+    def add_task(self, task):
+        self.tasks.append(task)
+
+    def add_dependency(self, target_name):
+        """ Add another task as a dependency for this task """
+        self.dependencies.add(target_name)
+
+    def __gt__(self, other):
+        return other.name in self.project.dependencies(self.name)
+
+    def __repr__(self):
+        return 'Target "{}"'.format(self.name)
+
+
+class Task:
+    """ Task that can run, and depend on other tasks """
+    def __init__(self, target, kwargs, sub_elements=[]):
+        self.logger = logging.getLogger('task')
+        self.target = target
+        self.name = self.__class__.__name__
+        self.arguments = kwargs
+        self.subs = sub_elements
+
+    def get_argument(self, name):
+        if name not in self.arguments:
+            raise TaskError('attribute "{}" not specified'.format(name))
+        return self.arguments[name]
+
+    def get_property(self, name):
+        return self.target.project.get_property(name)
+
+    def relpath(self, filename):
+        basedir = self.get_property('basedir')
+        return os.path.join(basedir, filename)
+
+    def open_file_set(self, s):
+        """ Creates a list of open file handles. s can be one of these:
+            - A string like "a.c3"
+            - A string like "*.c3"
+            - A string like "a.c3;src/*.c3"
+        """
+        assert type(s) is str
+        fns = []
+        for part in s.split(';'):
+            fns += glob.glob(self.relpath(part))
+        return fns
 
     def run(self):
         raise NotImplementedError("Implement this abstract method!")
 
-    def fire(self):
-        """ Wrapper around run that marks the task as done """
-        assert all(t.completed for t in self.dependencies)
-        self.run()
-        self.completed = True
-
-    def add_dependency(self, task):
-        """ Add another task as a dependency for this task """
-        if task is self:
-            raise TaskError('Can not add dependency on task itself!')
-        if self in task.down_stream_tasks:
-            raise TaskError('Can not introduce circular task')
-        self.dependencies.add(task)
-        return task
-
-    @property
-    def down_stream_tasks(self):
-        """ Return a set of all tasks that follow this task """
-        # TODO: is this upstream or downstream???
-        cdst = list(dep.down_stream_tasks for dep in self.dependencies)
-        cdst.append(self.dependencies)
-        return set.union(*cdst)
-
-    def __gt__(self, other):
-        return other in self.down_stream_tasks
-
     def __repr__(self):
         return 'Task "{}"'.format(self.name)
 
 
-class EmptyTask(Task):
-    """ Basic task that does nothing """
-    def run(self):
-        pass
-
-
 class TaskRunner:
     """ Basic task runner that can run some tasks in sequence """
     def __init__(self):
         self.logger = logging.getLogger('taskrunner')
-        self.task_list = []
 
-    def add_task(self, task):
-        self.task_list.append(task)
+    def run(self, project, targets=[]):
+        """ Try to run a project """
+        # Determine what targets to run:
+        if targets:
+            target_list = targets
+        else:
+            if project.default:
+                target_list = [project.default]
+            else:
+                target_list = []
 
-    @property
-    def total_duration(self):
-        return sum(t.duration for t in self.task_list)
+        try:
+            if not target_list:
+                self.logger.info('Done!')
+                return 0
+
+            # Check for loops:
+            for target in target_list:
+                project.check_target(target)
 
-    def run_tasks(self):
-        # First sort tasks:
-        self.task_list.sort()
+            # Calculate all dependencies:
+            target_list = set.union(*[project.dependencies(t) for t in target_list]).union(set(target_list))
+            # Lookup actual targets:
+            target_list = [project.get_target(target_name) for target_name in target_list]
+            target_list.sort()
+
+            self.logger.info('Target sequence: {}'.format(target_list))
 
-        # Run tasks:
-        passed_time = 0.0
-        total_time = self.total_duration
-        try:
-            for t in self.task_list:
-                self.report_progress(passed_time / total_time, t.name)
-                t.fire()
-                passed_time += t.duration
+            # Run tasks:
+            for target in target_list:
+                self.logger.info('Target {}'.format(target.name))
+                for task in target.tasks:
+                    if type(task) is tuple:
+                        tname, props = task
+                        for arg in props:
+                            props[arg] = project.expand_macros(props[arg])
+                        task = task_map[tname](target, props)
+                        self.logger.info('Running {}'.format(task))
+                        task.run()
+                    else:
+                        raise Exception()
+            self.logger.info('Done!')
         except TaskError as e:
             self.logger.error(str(e.msg))
             return 1
-        self.report_progress(1, 'OK')
         return 0
 
-    def display(self):
-        """ Display task how they would be run """
-        for task in self.task_list:
-            print(task)
-
-    def report_progress(self, percentage, text):
-        self.logger.info('[{:3.1%}] {}'.format(percentage, text))