Source code

Revision control

Copy as Markdown

Other Tools

# 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)