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