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