comparison 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
comparison
equal deleted inserted replaced
376:1e951e71d3f1 377:9667d78ba79e
2 This module defines tasks and a runner for these tasks. Tasks can 2 This module defines tasks and a runner for these tasks. Tasks can
3 have dependencies and it can be determined if they need to be run. 3 have dependencies and it can be determined if they need to be run.
4 """ 4 """
5 5
6 import logging 6 import logging
7 import re
8 import os
9 import glob
10
11
12 task_map = {}
13 def register_task(name):
14 """ Decorator that registers a task class """
15 def f(cls):
16 task_map[name] = cls
17 return cls
18 return f
19
7 20
8 class TaskError(Exception): 21 class TaskError(Exception):
22 """ When a task fails, this exception is raised """
9 def __init__(self, msg): 23 def __init__(self, msg):
10 self.msg = msg 24 self.msg = msg
11 25
12 26
27 class Project:
28 """ A project contains a set of named targets that can depend upon
29 eachother """
30 def __init__(self, name):
31 self.name = name
32 self.targets = {}
33 self.properties = {}
34 self.macro_regex = re.compile('\$\{([^\}]+)\}')
35
36 def set_property(self, name, value):
37 self.properties[name] = value
38
39 def get_property(self, name):
40 if name not in self.properties:
41 raise TaskError('Property "{}" not found'.format(name))
42 return self.properties[name]
43
44 def add_target(self, t):
45 if t.name in self.targets:
46 raise TaskError("Duplicate target '{}'".format(t.name))
47 self.targets[t.name] = t
48
49 def get_target(self, target_name):
50 if target_name not in self.targets:
51 raise TaskError('target "{}" not found'.format(target_name))
52 return self.targets[target_name]
53
54 def expand_macros(self, txt):
55 """ Replace all macros in txt with the correct properties """
56 while True:
57 mo = self.macro_regex.search(txt)
58 if not mo:
59 break
60 propname = mo.group(1)
61 propval = self.get_property(propname)
62 txt = txt[:mo.start()] + propval + txt[mo.end():]
63 return txt
64
65 def dfs(self, target_name, state):
66 state.add(target_name)
67 target = self.get_target(target_name)
68 for dep in target.dependencies:
69 if dep in state:
70 raise TaskError('Dependency loop detected {} -> {}'.format(target_name, dep))
71 self.dfs(dep, state)
72
73 def check_target(self, target_name):
74 state = set()
75 self.dfs(target_name, state)
76
77 def dependencies(self, target_name):
78 assert type(target_name) is str
79 target = self.get_target(target_name)
80 cdst = list(self.dependencies(dep) for dep in target.dependencies)
81 cdst.append(target.dependencies)
82 return set.union(*cdst)
83
84
85 class Target:
86 """ Defines a target that has a name and a list of tasks to execute """
87 def __init__(self, name, project):
88 self.name = name
89 self.project = project
90 self.tasks = []
91 self.dependencies = set()
92
93 def add_task(self, task):
94 self.tasks.append(task)
95
96 def add_dependency(self, target_name):
97 """ Add another task as a dependency for this task """
98 self.dependencies.add(target_name)
99
100 def __gt__(self, other):
101 return other.name in self.project.dependencies(self.name)
102
103 def __repr__(self):
104 return 'Target "{}"'.format(self.name)
105
106
13 class Task: 107 class Task:
14 """ Task that can run, and depend on other tasks """ 108 """ Task that can run, and depend on other tasks """
15 def __init__(self, name): 109 def __init__(self, target, kwargs, sub_elements=[]):
16 self.name = name 110 self.logger = logging.getLogger('task')
17 self.completed = False 111 self.target = target
18 self.dependencies = set() 112 self.name = self.__class__.__name__
19 self.duration = 1 113 self.arguments = kwargs
114 self.subs = sub_elements
115
116 def get_argument(self, name):
117 if name not in self.arguments:
118 raise TaskError('attribute "{}" not specified'.format(name))
119 return self.arguments[name]
120
121 def get_property(self, name):
122 return self.target.project.get_property(name)
123
124 def relpath(self, filename):
125 basedir = self.get_property('basedir')
126 return os.path.join(basedir, filename)
127
128 def open_file_set(self, s):
129 """ Creates a list of open file handles. s can be one of these:
130 - A string like "a.c3"
131 - A string like "*.c3"
132 - A string like "a.c3;src/*.c3"
133 """
134 assert type(s) is str
135 fns = []
136 for part in s.split(';'):
137 fns += glob.glob(self.relpath(part))
138 return fns
20 139
21 def run(self): 140 def run(self):
22 raise NotImplementedError("Implement this abstract method!") 141 raise NotImplementedError("Implement this abstract method!")
23 142
24 def fire(self):
25 """ Wrapper around run that marks the task as done """
26 assert all(t.completed for t in self.dependencies)
27 self.run()
28 self.completed = True
29
30 def add_dependency(self, task):
31 """ Add another task as a dependency for this task """
32 if task is self:
33 raise TaskError('Can not add dependency on task itself!')
34 if self in task.down_stream_tasks:
35 raise TaskError('Can not introduce circular task')
36 self.dependencies.add(task)
37 return task
38
39 @property
40 def down_stream_tasks(self):
41 """ Return a set of all tasks that follow this task """
42 # TODO: is this upstream or downstream???
43 cdst = list(dep.down_stream_tasks for dep in self.dependencies)
44 cdst.append(self.dependencies)
45 return set.union(*cdst)
46
47 def __gt__(self, other):
48 return other in self.down_stream_tasks
49
50 def __repr__(self): 143 def __repr__(self):
51 return 'Task "{}"'.format(self.name) 144 return 'Task "{}"'.format(self.name)
52
53
54 class EmptyTask(Task):
55 """ Basic task that does nothing """
56 def run(self):
57 pass
58 145
59 146
60 class TaskRunner: 147 class TaskRunner:
61 """ Basic task runner that can run some tasks in sequence """ 148 """ Basic task runner that can run some tasks in sequence """
62 def __init__(self): 149 def __init__(self):
63 self.logger = logging.getLogger('taskrunner') 150 self.logger = logging.getLogger('taskrunner')
64 self.task_list = []
65 151
66 def add_task(self, task): 152 def run(self, project, targets=[]):
67 self.task_list.append(task) 153 """ Try to run a project """
154 # Determine what targets to run:
155 if targets:
156 target_list = targets
157 else:
158 if project.default:
159 target_list = [project.default]
160 else:
161 target_list = []
68 162
69 @property 163 try:
70 def total_duration(self): 164 if not target_list:
71 return sum(t.duration for t in self.task_list) 165 self.logger.info('Done!')
166 return 0
72 167
73 def run_tasks(self): 168 # Check for loops:
74 # First sort tasks: 169 for target in target_list:
75 self.task_list.sort() 170 project.check_target(target)
76 171
77 # Run tasks: 172 # Calculate all dependencies:
78 passed_time = 0.0 173 target_list = set.union(*[project.dependencies(t) for t in target_list]).union(set(target_list))
79 total_time = self.total_duration 174 # Lookup actual targets:
80 try: 175 target_list = [project.get_target(target_name) for target_name in target_list]
81 for t in self.task_list: 176 target_list.sort()
82 self.report_progress(passed_time / total_time, t.name) 177
83 t.fire() 178 self.logger.info('Target sequence: {}'.format(target_list))
84 passed_time += t.duration 179
180 # Run tasks:
181 for target in target_list:
182 self.logger.info('Target {}'.format(target.name))
183 for task in target.tasks:
184 if type(task) is tuple:
185 tname, props = task
186 for arg in props:
187 props[arg] = project.expand_macros(props[arg])
188 task = task_map[tname](target, props)
189 self.logger.info('Running {}'.format(task))
190 task.run()
191 else:
192 raise Exception()
193 self.logger.info('Done!')
85 except TaskError as e: 194 except TaskError as e:
86 self.logger.error(str(e.msg)) 195 self.logger.error(str(e.msg))
87 return 1 196 return 1
88 self.report_progress(1, 'OK')
89 return 0 197 return 0
90 198
91 def display(self):
92 """ Display task how they would be run """
93 for task in self.task_list:
94 print(task)
95
96 def report_progress(self, percentage, text):
97 self.logger.info('[{:3.1%}] {}'.format(percentage, text))