# 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 collections.abc import Iterable import six 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. """ targets = list(targets) for target in targets: assert isinstance(target, six.text_type) 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. """ assert isinstance(statement, six.text_type) 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 __bool__(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, six.string_types ) targets = list(targets) for target in targets: assert isinstance(target, six.text_type) self._targets.update(targets) return self def add_dependencies(self, deps): """Add dependencies to the rule.""" assert isinstance(deps, Iterable) and not isinstance(deps, six.string_types) deps = list(deps) for dep in deps: assert isinstance(dep, six.text_type) self._dependencies.update(deps) return self def add_commands(self, commands): """Add commands to the rule.""" assert isinstance(commands, Iterable) and not isinstance( commands, six.string_types ) commands = list(commands) for command in commands: assert isinstance(command, six.text_type) 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 d not 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(): line = six.ensure_text(line) 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.") def write_dep_makefile(fh, target, deps): """ Write a Makefile containing only target's dependencies to the file handle specified. """ mk = Makefile() rule = mk.create_rule(targets=[target]) rule.add_dependencies(deps) mk.dump(fh, removal_guard=True)