зеркало из https://github.com/mozilla/sccache.git
175 строки
5.3 KiB
Python
175 строки
5.3 KiB
Python
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
import os
|
|
import re
|
|
from types import StringTypes
|
|
from collections import Iterable
|
|
|
|
|
|
class Makefile(object):
|
|
'''Provides an interface for writing simple makefiles
|
|
|
|
Instances of this class are created, populated with rules, then
|
|
written.
|
|
'''
|
|
|
|
def __init__(self):
|
|
self._statements = []
|
|
|
|
def create_rule(self, targets=[]):
|
|
'''
|
|
Create a new rule in the makefile for the given targets.
|
|
Returns the corresponding Rule instance.
|
|
'''
|
|
rule = Rule(targets)
|
|
self._statements.append(rule)
|
|
return rule
|
|
|
|
def add_statement(self, statement):
|
|
'''
|
|
Add a raw statement in the makefile. Meant to be used for
|
|
simple variable assignments.
|
|
'''
|
|
self._statements.append(statement)
|
|
|
|
def dump(self, fh, removal_guard=True):
|
|
'''
|
|
Dump all the rules to the given file handle. Optionally (and by
|
|
default), add guard rules for file removals (empty rules for other
|
|
rules' dependencies)
|
|
'''
|
|
all_deps = set()
|
|
all_targets = set()
|
|
for statement in self._statements:
|
|
if isinstance(statement, Rule):
|
|
statement.dump(fh)
|
|
all_deps.update(statement.dependencies())
|
|
all_targets.update(statement.targets())
|
|
else:
|
|
fh.write('%s\n' % statement)
|
|
if removal_guard:
|
|
guard = Rule(sorted(all_deps - all_targets))
|
|
guard.dump(fh)
|
|
|
|
|
|
class _SimpleOrderedSet(object):
|
|
'''
|
|
Simple ordered set, specialized for used in Rule below only.
|
|
It doesn't expose a complete API, and normalizes path separators
|
|
at insertion.
|
|
'''
|
|
def __init__(self):
|
|
self._list = []
|
|
self._set = set()
|
|
|
|
def __nonzero__(self):
|
|
return bool(self._set)
|
|
|
|
def __iter__(self):
|
|
return iter(self._list)
|
|
|
|
def __contains__(self, key):
|
|
return key in self._set
|
|
|
|
def update(self, iterable):
|
|
def _add(iterable):
|
|
emitted = set()
|
|
for i in iterable:
|
|
i = i.replace(os.sep, '/')
|
|
if i not in self._set and i not in emitted:
|
|
yield i
|
|
emitted.add(i)
|
|
added = list(_add(iterable))
|
|
self._set.update(added)
|
|
self._list.extend(added)
|
|
|
|
|
|
class Rule(object):
|
|
'''Class handling simple rules in the form:
|
|
target1 target2 ... : dep1 dep2 ...
|
|
command1
|
|
command2
|
|
...
|
|
'''
|
|
def __init__(self, targets=[]):
|
|
self._targets = _SimpleOrderedSet()
|
|
self._dependencies = _SimpleOrderedSet()
|
|
self._commands = []
|
|
self.add_targets(targets)
|
|
|
|
def add_targets(self, targets):
|
|
'''Add additional targets to the rule.'''
|
|
assert isinstance(targets, Iterable) and not isinstance(targets, StringTypes)
|
|
self._targets.update(targets)
|
|
return self
|
|
|
|
def add_dependencies(self, deps):
|
|
'''Add dependencies to the rule.'''
|
|
assert isinstance(deps, Iterable) and not isinstance(deps, StringTypes)
|
|
self._dependencies.update(deps)
|
|
return self
|
|
|
|
def add_commands(self, commands):
|
|
'''Add commands to the rule.'''
|
|
assert isinstance(commands, Iterable) and not isinstance(commands, StringTypes)
|
|
self._commands.extend(commands)
|
|
return self
|
|
|
|
def targets(self):
|
|
'''Return an iterator on the rule targets.'''
|
|
# Ensure the returned iterator is actually just that, an iterator.
|
|
# Avoids caller fiddling with the set itself.
|
|
return iter(self._targets)
|
|
|
|
def dependencies(self):
|
|
'''Return an iterator on the rule dependencies.'''
|
|
return iter(d for d in self._dependencies if not d in self._targets)
|
|
|
|
def commands(self):
|
|
'''Return an iterator on the rule commands.'''
|
|
return iter(self._commands)
|
|
|
|
def dump(self, fh):
|
|
'''
|
|
Dump the rule to the given file handle.
|
|
'''
|
|
if not self._targets:
|
|
return
|
|
fh.write('%s:' % ' '.join(self._targets))
|
|
if self._dependencies:
|
|
fh.write(' %s' % ' '.join(self.dependencies()))
|
|
fh.write('\n')
|
|
for cmd in self._commands:
|
|
fh.write('\t%s\n' % cmd)
|
|
|
|
|
|
# colon followed by anything except a slash (Windows path detection)
|
|
_depfilesplitter = re.compile(r':(?![\\/])')
|
|
|
|
|
|
def read_dep_makefile(fh):
|
|
"""
|
|
Read the file handler containing a dep makefile (simple makefile only
|
|
containing dependencies) and returns an iterator of the corresponding Rules
|
|
it contains. Ignores removal guard rules.
|
|
"""
|
|
|
|
rule = ''
|
|
for line in fh.readlines():
|
|
assert not line.startswith('\t')
|
|
line = line.strip()
|
|
if line.endswith('\\'):
|
|
rule += line[:-1]
|
|
else:
|
|
rule += line
|
|
split_rule = _depfilesplitter.split(rule, 1)
|
|
if len(split_rule) > 1 and split_rule[1].strip():
|
|
yield Rule(split_rule[0].strip().split()) \
|
|
.add_dependencies(split_rule[1].strip().split())
|
|
rule = ''
|
|
|
|
if rule:
|
|
raise Exception('Makefile finishes with a backslash. Expected more input.')
|