1import itertools
2import textwrap
3from collections import deque, namedtuple
4
5from medikit.structs import Script
6from medikit.utils import get_override_warning_banner
7
8MakefileTarget = namedtuple("MakefileTarget", ["deps", "rule", "doc"])
9
10
11class Makefile(object):
12    @property
13    def targets(self):
14        for key in self._target_order:
15            yield key, self._target_values[key]
16
17    @property
18    def environ(self):
19        return self._env_values
20
21    def __init__(self):
22        self._env_order, self._env_values, self._env_assignment_operators = deque(), {}, {}
23        self._target_order, self._target_values = deque(), {}
24        self.hidden = set()
25        self.phony = set()
26        self.header = []
27
28    def __delitem__(self, key):
29        self._env_order.remove(key)
30        del self._env_values[key]
31
32    def __contains__(self, item):
33        return item in self._env_values
34
35    def __getitem__(self, item):
36        return self._env_values[item]
37
38    def __setitem__(self, key, value):
39        self._env_values[key] = value
40        if not key in self._env_order:
41            self._env_order.append(key)
42
43    def __iter__(self):
44        for key in self._env_order:
45            yield key, self._env_values[key]
46
47    def __len__(self):
48        return len(self._env_order)
49
50    def __str__(self):
51        content = [get_override_warning_banner(), ""] + self.header + [""]
52
53        if len(self):
54            for k, v in self:
55                v = textwrap.dedent(str(v)).strip()
56                v = v.replace("\n", " \\\n" + " " * (len(k) + 4))
57                content.append("{} {} {}".format(k, self._env_assignment_operators.get(k, "?="), v))
58            content.append("")
59
60        if len(self.phony):
61            content.append(".PHONY: " + " ".join(sorted(self.phony)))
62            content.append("")
63
64        for target, details in self.targets:
65            deps, rule, doc = details
66
67            if hasattr(rule, "render"):
68                content += list(rule.render(target, deps, doc))
69            else:
70                content.append(
71                    "{}: {}  {} {}".format(
72                        target,
73                        " ".join(deps),
74                        "#" if target in self.hidden else "##",
75                        doc.replace("\n", " ") if doc else "",
76                    ).strip()
77                )
78
79                script = textwrap.dedent(str(rule)).strip()
80
81                for line in script.split("\n"):
82                    content.append("\t" + line)
83
84            content.append("")
85
86        return "\n".join(content)
87
88    def keys(self):
89        return list(self._env_order)
90
91    def add_target(self, target, rule, *, deps=None, phony=False, first=False, doc=None, hidden=False):
92        if target in self._target_order:
93            raise RuntimeError("Duplicate definition for make target «{}».".format(target))
94
95        if isinstance(rule, str):
96            rule = Script(rule)
97
98        self._target_values[target] = MakefileTarget(
99            deps=tuple(deps) if deps else tuple(), rule=rule, doc=textwrap.dedent(doc or "").strip()
100        )
101
102        self._target_order.appendleft(target) if first else self._target_order.append(target)
103
104        if phony:
105            self.phony.add(target)
106
107        if hidden:
108            self.hidden.add(target)
109
110    def get_clean_target(self):
111        if not self.has_target("clean"):
112            self.add_target("clean", CleanScript(), phony=True, doc="""Cleans up the working copy.""")
113        return self.get_target("clean")
114
115    def add_install_target(self, extra=None):
116        if extra:
117            target = "install-" + extra
118            doc = "Installs the project (with " + extra + " dependencies)."
119        else:
120            target = "install"
121            doc = "Installs the project."
122
123        if not self.has_target(target):
124            self.add_target(target, InstallScript(), phony=True, doc=doc)
125
126        clean_target = self.get_clean_target()
127        marker = ".medikit/" + target
128        if not marker in clean_target.remove:
129            clean_target.remove.append(marker)
130        return self.get_target(target)
131
132    def get_target(self, target):
133        return self._target_values[target][1]
134
135    def has_target(self, target):
136        return target in self._target_values
137
138    def set_deps(self, target, deps=None):
139        self._target_values[target] = (deps or list(), self._target_values[target][1], self._target_values[target][2])
140        return self
141
142    def set_script(self, target, script):
143        self._target_values[target] = (self._target_values[target][0], script, self._target_values[target][2])
144        return self
145
146    def set_assignment_operator(self, key, value):
147        assert value in ("?=", "=", "+=", ":=", "::=", "!="), "Invalid operator"
148        self._env_assignment_operators[key] = value
149
150    def setleft(self, key, value):
151        self._env_values[key] = value
152        if key in self._env_order:
153            self._env_order.remove(key)
154        self._env_order.appendleft(key)
155
156    def updateleft(self, *lst):
157        for key, value in reversed(lst):
158            self.setleft(key, value)
159
160
161class InstallScript(Script):
162    def __init__(self, script=None):
163        super(InstallScript, self).__init__(script)
164
165        self.before_install = []
166        self.install = self.script
167        self.after_install = []
168
169        self.deps = []
170
171    def __iter__(self):
172        yield '@if [ -z "$(QUICK)" ]; then \\'
173        for line in map(
174            lambda x: "    {} ; \\".format(x), itertools.chain(self.before_install, self.install, self.after_install)
175        ):
176            yield line
177        yield "fi"
178
179    def render(self, name, deps, doc):
180        tab = "\t"
181        yield "{name}: .medikit/{name} {deps}  ## {doc}".format(name=name, deps=" ".join(deps), doc=doc)
182        yield ".medikit/{name}: {deps}".format(name=name, deps=" ".join(sorted(set(self.deps))))
183        yield tab + "$(eval target := $(shell echo $@ | rev | cut -d/ -f1 | rev))"
184        yield "ifeq ($(filter quick,$(MAKECMDGOALS)),quick)"
185        yield tab + r'@printf "Skipping \033[36m%s\033[0m because of \033[36mquick\033[0m target.\n" $(target)'
186        yield "else ifneq ($(QUICK),)"
187        yield tab + r'@printf "Skipping \033[36m%s\033[0m because \033[36m$$QUICK\033[0m is not empty.\n" $(target)'
188        yield "else"
189        yield tab + r'@printf "Applying \033[36m%s\033[0m target...\n" $(target)'
190        for line in itertools.chain(self.before_install, self.install, self.after_install):
191            yield tab + line
192        yield tab + "@mkdir -p .medikit; touch $@"
193        yield "endif"
194
195
196class CleanScript(Script):
197    # We should not clean .medikit subdirectories here, as it will deny releases.
198    # TODO: move into python feature
199    remove = ["build", "dist", "*.egg-info"]
200
201    def __iter__(self):
202        # cleanup build directories
203        yield "rm -rf {}".format(" ".join(self.remove))
204        # cleanup python bytecode -
205        yield "find . -name __pycache__ -type d | xargs rm -rf"
206