1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import 6 7import os 8import re 9from types import StringTypes 10from collections import Iterable 11 12 13class Makefile(object): 14 '''Provides an interface for writing simple makefiles 15 16 Instances of this class are created, populated with rules, then 17 written. 18 ''' 19 20 def __init__(self): 21 self._statements = [] 22 23 def create_rule(self, targets=[]): 24 ''' 25 Create a new rule in the makefile for the given targets. 26 Returns the corresponding Rule instance. 27 ''' 28 rule = Rule(targets) 29 self._statements.append(rule) 30 return rule 31 32 def add_statement(self, statement): 33 ''' 34 Add a raw statement in the makefile. Meant to be used for 35 simple variable assignments. 36 ''' 37 self._statements.append(statement) 38 39 def dump(self, fh, removal_guard=True): 40 ''' 41 Dump all the rules to the given file handle. Optionally (and by 42 default), add guard rules for file removals (empty rules for other 43 rules' dependencies) 44 ''' 45 all_deps = set() 46 all_targets = set() 47 for statement in self._statements: 48 if isinstance(statement, Rule): 49 statement.dump(fh) 50 all_deps.update(statement.dependencies()) 51 all_targets.update(statement.targets()) 52 else: 53 fh.write('%s\n' % statement) 54 if removal_guard: 55 guard = Rule(sorted(all_deps - all_targets)) 56 guard.dump(fh) 57 58 59class _SimpleOrderedSet(object): 60 ''' 61 Simple ordered set, specialized for used in Rule below only. 62 It doesn't expose a complete API, and normalizes path separators 63 at insertion. 64 ''' 65 def __init__(self): 66 self._list = [] 67 self._set = set() 68 69 def __nonzero__(self): 70 return bool(self._set) 71 72 def __iter__(self): 73 return iter(self._list) 74 75 def __contains__(self, key): 76 return key in self._set 77 78 def update(self, iterable): 79 def _add(iterable): 80 emitted = set() 81 for i in iterable: 82 i = i.replace(os.sep, '/') 83 if i not in self._set and i not in emitted: 84 yield i 85 emitted.add(i) 86 added = list(_add(iterable)) 87 self._set.update(added) 88 self._list.extend(added) 89 90 91class Rule(object): 92 '''Class handling simple rules in the form: 93 target1 target2 ... : dep1 dep2 ... 94 command1 95 command2 96 ... 97 ''' 98 def __init__(self, targets=[]): 99 self._targets = _SimpleOrderedSet() 100 self._dependencies = _SimpleOrderedSet() 101 self._commands = [] 102 self.add_targets(targets) 103 104 def add_targets(self, targets): 105 '''Add additional targets to the rule.''' 106 assert isinstance(targets, Iterable) and not isinstance(targets, StringTypes) 107 self._targets.update(targets) 108 return self 109 110 def add_dependencies(self, deps): 111 '''Add dependencies to the rule.''' 112 assert isinstance(deps, Iterable) and not isinstance(deps, StringTypes) 113 self._dependencies.update(deps) 114 return self 115 116 def add_commands(self, commands): 117 '''Add commands to the rule.''' 118 assert isinstance(commands, Iterable) and not isinstance(commands, StringTypes) 119 self._commands.extend(commands) 120 return self 121 122 def targets(self): 123 '''Return an iterator on the rule targets.''' 124 # Ensure the returned iterator is actually just that, an iterator. 125 # Avoids caller fiddling with the set itself. 126 return iter(self._targets) 127 128 def dependencies(self): 129 '''Return an iterator on the rule dependencies.''' 130 return iter(d for d in self._dependencies if not d in self._targets) 131 132 def commands(self): 133 '''Return an iterator on the rule commands.''' 134 return iter(self._commands) 135 136 def dump(self, fh): 137 ''' 138 Dump the rule to the given file handle. 139 ''' 140 if not self._targets: 141 return 142 fh.write('%s:' % ' '.join(self._targets)) 143 if self._dependencies: 144 fh.write(' %s' % ' '.join(self.dependencies())) 145 fh.write('\n') 146 for cmd in self._commands: 147 fh.write('\t%s\n' % cmd) 148 149 150# colon followed by anything except a slash (Windows path detection) 151_depfilesplitter = re.compile(r':(?![\\/])') 152 153 154def read_dep_makefile(fh): 155 """ 156 Read the file handler containing a dep makefile (simple makefile only 157 containing dependencies) and returns an iterator of the corresponding Rules 158 it contains. Ignores removal guard rules. 159 """ 160 161 rule = '' 162 for line in fh.readlines(): 163 assert not line.startswith('\t') 164 line = line.strip() 165 if line.endswith('\\'): 166 rule += line[:-1] 167 else: 168 rule += line 169 split_rule = _depfilesplitter.split(rule, 1) 170 if len(split_rule) > 1 and split_rule[1].strip(): 171 yield Rule(split_rule[0].strip().split()) \ 172 .add_dependencies(split_rule[1].strip().split()) 173 rule = '' 174 175 if rule: 176 raise Exception('Makefile finishes with a backslash. Expected more input.') 177 178def write_dep_makefile(fh, target, deps): 179 ''' 180 Write a Makefile containing only target's dependencies to the file handle 181 specified. 182 ''' 183 mk = Makefile() 184 rule = mk.create_rule(targets=[target]) 185 rule.add_dependencies(deps) 186 mk.dump(fh, removal_guard=True) 187