Mercurial > lcfOS
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)) |