342
|
1 """
|
|
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.
|
|
4 """
|
334
|
5
|
|
6 import logging
|
377
|
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
|
329
|
20
|
|
21 class TaskError(Exception):
|
377
|
22 """ When a task fails, this exception is raised """
|
346
|
23 def __init__(self, msg):
|
|
24 self.msg = msg
|
329
|
25
|
|
26
|
377
|
27 class Project:
|
|
28 """ A project contains a set of named targets that can depend upon
|
|
29 eachother """
|
329
|
30 def __init__(self, name):
|
|
31 self.name = name
|
377
|
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 = []
|
342
|
91 self.dependencies = set()
|
377
|
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
|
|
107 class Task:
|
|
108 """ Task that can run, and depend on other tasks """
|
|
109 def __init__(self, target, kwargs, sub_elements=[]):
|
|
110 self.logger = logging.getLogger('task')
|
|
111 self.target = target
|
|
112 self.name = self.__class__.__name__
|
|
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
|
329
|
139
|
|
140 def run(self):
|
|
141 raise NotImplementedError("Implement this abstract method!")
|
|
142
|
332
|
143 def __repr__(self):
|
|
144 return 'Task "{}"'.format(self.name)
|
|
145
|
329
|
146
|
|
147 class TaskRunner:
|
332
|
148 """ Basic task runner that can run some tasks in sequence """
|
329
|
149 def __init__(self):
|
334
|
150 self.logger = logging.getLogger('taskrunner')
|
332
|
151
|
377
|
152 def run(self, project, targets=[]):
|
|
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 = []
|
329
|
162
|
377
|
163 try:
|
|
164 if not target_list:
|
|
165 self.logger.info('Done!')
|
|
166 return 0
|
|
167
|
|
168 # Check for loops:
|
|
169 for target in target_list:
|
|
170 project.check_target(target)
|
334
|
171
|
377
|
172 # Calculate all dependencies:
|
|
173 target_list = set.union(*[project.dependencies(t) for t in target_list]).union(set(target_list))
|
|
174 # Lookup actual targets:
|
|
175 target_list = [project.get_target(target_name) for target_name in target_list]
|
|
176 target_list.sort()
|
|
177
|
|
178 self.logger.info('Target sequence: {}'.format(target_list))
|
342
|
179
|
377
|
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!')
|
329
|
194 except TaskError as e:
|
342
|
195 self.logger.error(str(e.msg))
|
329
|
196 return 1
|
|
197 return 0
|
332
|
198
|