talerbuildconfig.py (18335B)
1 # This file is part of TALER 2 # (C) 2019 GNUnet e.V. 3 # 4 # Authors: 5 # Author: ng0 <ng0@taler.net> 6 # Author: Florian Dold <dold@taler.net> 7 # 8 # Permission to use, copy, modify, and/or distribute this software for any 9 # purpose with or without fee is hereby granted. 10 # 11 # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 12 # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 13 # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE 14 # LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES 15 # OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, 16 # WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 17 # ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 18 # THIS SOFTWARE. 19 # 20 # SPDX-License-Identifier: 0BSD 21 22 import sys 23 24 if not (sys.version_info.major == 3 and sys.version_info.minor >= 7): 25 print("This script requires Python 3.7 or higher!") 26 print("You are using Python {}.{}.".format(sys.version_info.major, sys.version_info.minor)) 27 sys.exit(1) 28 29 from abc import ABC 30 import argparse 31 import os 32 import sys 33 import shlex 34 import shutil 35 import logging 36 import subprocess 37 from dataclasses import dataclass 38 import semver 39 from pathlib import Path 40 41 """ 42 This module aims to replicate a small GNU Coding Standards 43 configure script, tailored at projects in GNU Taler. We hope it 44 can be of use outside of GNU Taler, hence it is dedicated to the 45 public domain ('0BSD'). 46 It takes a couple of arguments on the commandline equivalent to 47 configure by autotools, in addition some environment variables 48 xan take precedence over the switches. In the absence of switches, 49 /usr/local is assumed as the PREFIX. 50 When all data from tests are gathered, it generates a config.mk 51 Makefile fragment, which is the processed by a Makefile (usually) in 52 GNU Make format. 53 """ 54 55 # Should be incremented each time we add some functionality 56 serialversion = 2 57 58 59 # TODO: We need a smallest version argument. 60 61 class Tool(ABC): 62 def args(self, parser): 63 ... 64 65 def check(self, buildconfig): 66 ... 67 68 class Plugin(ABC): 69 def args(self, parser): 70 ... 71 72 class BuildConfig: 73 def __init__(self): 74 # Pairs of (key, value) for config.mk variables 75 self.make_variables = [] 76 self.tools = [] 77 self.tool_results = {} 78 self.plugins = [] 79 self.args = None 80 self.prefix_enabled = False 81 self.configmk_enabled = False 82 self.configmk_dotfile = False 83 84 def add_tool(self, tool): 85 """Deprecated. Prefer the 'use' method.""" 86 if isinstance(tool, Tool): 87 self.tools.append(tool) 88 else: 89 raise Exception("Not a 'Tool' instance: " + repr(tool)) 90 91 def use(self, plugin): 92 if isinstance(plugin, Plugin): 93 self.plugins.append(plugin) 94 elif isinstance(plugin, Tool): 95 self.tools.append(plugin) 96 else: 97 raise Exception("Not a 'Plugin' or 'Tool' instance: " + repr(plugin)) 98 99 def _set_tool(self, name, value, version=None): 100 self.tool_results[name] = (value, version) 101 102 def enable_prefix(self): 103 """If enabled, process the --prefix argument.""" 104 self.prefix_enabled = True 105 106 def _warn(self, msg): 107 print("Warning", msg) 108 109 def _error(self, msg): 110 print("Error", msg) 111 112 def enable_configmk(self, dotfile=False): 113 """If enabled, output the config.mk makefile fragment.""" 114 self.configmk_enabled = True 115 self.configmk_dotfile = dotfile 116 117 def run(self): 118 parser = argparse.ArgumentParser() 119 if self.prefix_enabled: 120 parser.add_argument( 121 "--prefix", 122 type=str, 123 default="/usr/local", 124 help="Directory prefix for installation", 125 ) 126 for tool in self.tools: 127 tool.args(parser) 128 129 for plugin in self.plugins: 130 plugin.args(parser) 131 132 args = self.args = parser.parse_args() 133 134 for plugin in self.plugins: 135 res = plugin.run(self) 136 137 for tool in self.tools: 138 res = tool.check(self) 139 if not res: 140 print(f"Error: tool '{tool.name}' not available") 141 if hasattr(tool, "hint"): 142 print(f"Hint: {tool.hint}") 143 sys.exit(1) 144 if hasattr(tool, "version_spec"): 145 sv = semver.SimpleSpec(tool.version_spec) 146 path, version = self.tool_results[tool.name] 147 if not sv.match(semver.Version(version)): 148 print(f"Error: Tool '{tool.name}' has version '{version}', but we require '{tool.version_spec}'") 149 sys.exit(1) 150 151 152 for tool in self.tools: 153 path, version = self.tool_results[tool.name] 154 if version is None: 155 print(f"found {tool.name} as {path}") 156 else: 157 print(f"found {tool.name} as {path} (version {version})") 158 159 if self.configmk_enabled: 160 if self.configmk_dotfile: 161 d = Path(".") 162 cf = d / ".config.mk" 163 else: 164 d = Path(os.environ.get("TALERBUILDSYSTEMDIR", ".")) 165 cf = d / "config.mk" 166 d.mkdir(parents=True, exist_ok=True) 167 print(f"writing {cf}") 168 with open(cf, "w") as f: 169 f.write("# this makefile fragment is autogenerated by configure.py\n") 170 if self.prefix_enabled: 171 f.write(f"prefix = {args.prefix}\n") 172 for tool in self.tools: 173 path, version = self.tool_results[tool.name] 174 f.write(f"{tool.name} = {path}\n") 175 for plugin in self.plugins: 176 d = plugin.get_configmk(self) 177 for k, v in d.items(): 178 f.write(f"{k} = {v}\n") 179 180 181 def existence(name): 182 return shutil.which(name) is not None 183 184 185 class Option(Plugin): 186 187 def __init__(self, optname, help, required=True, default=None): 188 self.optname = optname 189 self.help = help 190 self.default = default 191 self.required = required 192 self._arg = None 193 194 def args(self, parser): 195 parser.add_argument("--" + self.optname, action="store") 196 197 def run(self, buildconfig): 198 arg = getattr(buildconfig.args, self.optname) 199 if arg is None: 200 if self.required: 201 print(f"required option '--{self.optname}' missing") 202 sys.exit(1) 203 else: 204 arg = self.default 205 self._arg = arg 206 207 def get_configmk(self, buildconfig): 208 key = "opt_" + self.optname 209 return {"opt_" + self.optname: self._arg} 210 211 212 class YarnTool(Tool): 213 name = "yarn" 214 description = "The yarn package manager for node" 215 216 def args(self, parser): 217 parser.add_argument("--with-yarn", action="store") 218 219 def check(self, buildconfig): 220 yarn_arg = buildconfig.args.with_yarn 221 if yarn_arg is not None: 222 buildconfig._set_tool("yarn", yarn_arg) 223 return True 224 if existence("yarn"): 225 p1 = subprocess.run( 226 ["yarn", "help"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE 227 ) 228 if "No such file or directory" in p1.stdout.decode("utf-8"): 229 if existence("cmdtest"): 230 buildconfig._warn( 231 "cmdtest is installed, this can lead to known issues with yarn." 232 ) 233 buildconfig._error( 234 "You seem to have the wrong kind of 'yarn' installed.\n" 235 "Please remove the conflicting binary before proceeding" 236 ) 237 return False 238 yarn_version = tool_version("yarn --version") 239 buildconfig._set_tool("yarn", "yarn", yarn_version) 240 return True 241 elif existence("yarnpkg"): 242 yarn_version = tool_version("yarnpkg --version") 243 buildconfig._set_tool("yarn", "yarnpkg", yarn_version) 244 return True 245 return False 246 247 248 def tool_version(name): 249 return subprocess.getstatusoutput(name)[1] 250 251 252 class EmscriptenTool: 253 def args(self, parser): 254 pass 255 256 def check(self, buildconfig): 257 if existence("emcc"): 258 emscripten_version = tool_version("emcc --version") 259 buildconfig._set_tool("emcc", "emcc", emscripten_version) 260 return True 261 return False 262 263 class PyToxTool(Tool): 264 name ="tox" 265 266 def args(self, parser): 267 parser.add_argument( 268 "--with-tox", type=str, help="name of the tox executable" 269 ) 270 271 def check(self, buildconfig): 272 # No suffix. Would probably be cheaper to do this in 273 # the dict as well. We also need to check the python 274 # version it was build against (TODO). 275 if existence("tox"): 276 import tox 277 mypytox_version = tox.__version__ 278 buildconfig._set_tool("tox", "tox", mypytox_version) 279 return True 280 else: 281 # Has suffix, try suffix. We know the names in advance, 282 # so use a dictionary and iterate over it. Use enough names 283 # to safe updating this for another couple of years. 284 version_dict = { 285 "3.0": "tox-3.0", 286 "3.1": "tox-3.1", 287 "3.2": "tox-3.2", 288 "3.3": "tox-3.3", 289 "3.4": "tox-3.4", 290 "3.5": "tox-3.5", 291 "3.6": "tox-3.6", 292 "3.7": "tox-3.7", 293 "3.8": "tox-3.8", 294 "3.9": "tox-3.9", 295 "4.0": "tox-4.0", 296 } 297 for key, value in version_dict.items(): 298 if existence(value): 299 # FIXME: This version reporting is slightly off 300 # FIXME: and only maps to the suffix. 301 import tox 302 mypytox_version = tox.__version__ 303 buildconfig._set_tool("tox", value, mypytox_version) 304 return True 305 306 307 class YapfTool(Tool): 308 name ="yapf" 309 310 def args(self, parser): 311 parser.add_argument( 312 "--with-yapf", type=str, help="name of the yapf executable" 313 ) 314 315 def check(self, buildconfig): 316 # No suffix. Would probably be cheaper to do this in 317 # the dict as well. We also need to check the python 318 # version it was build against (TODO). 319 if existence("yapf"): 320 import yapf 321 myyapf_version = yapf.__version__ 322 buildconfig._set_tool("yapf", "yapf", myyapf_version) 323 return True 324 else: 325 # Has suffix, try suffix. We know the names in advance, 326 # so use a dictionary and iterate over it. Use enough names 327 # to safe updating this for another couple of years. 328 version_dict = { 329 "3.0": "yapf3.0", 330 "3.1": "yapf3.1", 331 "3.2": "yapf3.2", 332 "3.3": "yapf3.3", 333 "3.4": "yapf3.4", 334 "3.5": "yapf3.5", 335 "3.6": "yapf3.6", 336 "3.7": "yapf3.7", 337 "3.8": "yapf3.8", 338 "3.9": "yapf3.9", 339 "4.0": "yapf4.0", 340 "4.1": "yapf4.1", 341 "4.2": "yapf4.2", 342 "4.3": "yapf4.3", 343 "4.4": "yapf4.4", 344 "4.5": "yapf4.5", 345 "4.6": "yapf4.6", 346 "4.7": "yapf4.7", 347 "4.8": "yapf4.8", 348 "4.9": "yapf4.9", 349 "5.0": "yapf5.0", 350 "5.1": "yapf5.1", 351 } 352 for key, value in version_dict.items(): 353 if existence(value): 354 # FIXME: This version reporting is slightly off 355 # FIXME: and only maps to the suffix. 356 import yapf 357 myyapf_version = yapf.__version__ 358 buildconfig._set_tool("yapf", value, myyapf_version) 359 return True 360 361 362 class PyBabelTool(Tool): 363 name = "pybabel" 364 365 def args(self, parser): 366 parser.add_argument( 367 "--with-pybabel", type=str, help="name of the pybabel executable" 368 ) 369 370 def check(self, buildconfig): 371 # No suffix. Would probably be cheaper to do this in 372 # the dict as well. We also need to check the python 373 # version it was build against (TODO). 374 if existence("pybabel"): 375 import babel 376 pybabel_version = babel.__version__ 377 buildconfig._set_tool("pybabel", "pybabel", pybabel_version) 378 return True 379 else: 380 # Has suffix, try suffix. We know the names in advance, 381 # so use a dictionary and iterate over it. Use enough names 382 # to safe updating this for another couple of years. 383 # 384 # Food for thought: If we only accept python 3.7 or higher, 385 # is checking pybabel + pybabel-3.[0-9]* too much and could 386 # be broken down to pybabel + pybabel-3.7 and later names? 387 version_dict = { 388 "3.0": "pybabel-3.0", 389 "3.1": "pybabel-3.1", 390 "3.2": "pybabel-3.2", 391 "3.3": "pybabel-3.3", 392 "3.4": "pybabel-3.4", 393 "3.5": "pybabel-3.5", 394 "3.6": "pybabel-3.6", 395 "3.7": "pybabel-3.7", 396 "3.8": "pybabel-3.8", 397 "3.9": "pybabel-3.9", 398 "4.0": "pybabel-4.0", 399 } 400 for key, value in version_dict.items(): 401 if existence(value): 402 # FIXME: This version reporting is slightly off 403 # FIXME: and only maps to the suffix. 404 pybabel_version = key 405 buildconfig._set_tool("pybabel", value, pybabel_version) 406 return True 407 408 409 class PythonTool(Tool): 410 # This exists in addition to the files in sh, so that 411 # the Makefiles can use this value instead. 412 name = "python" 413 414 def args(self, parser): 415 parser.add_argument( 416 "--with-python", type=str, help="name of the python executable" 417 ) 418 419 def check(self, buildconfig): 420 # No suffix. Would probably be cheaper to do this in 421 # the dict as well. We need at least version 3.7. 422 if existence("python") and (shlex.split(subprocess.getstatusoutput("python --version")[1])[1] >= '3.7'): 423 # python might not be python3. It might not even be 424 # python 3.x. 425 python_version = shlex.split(subprocess.getstatusoutput("python --version")[1])[1] 426 if python_version >= '3.7': 427 buildconfig._set_tool("python", "python", python_version) 428 return True 429 else: 430 # Has suffix, try suffix. We know the names in advance, 431 # so use a dictionary and iterate over it. Use enough names 432 # to safe updating this for another couple of years. 433 # 434 # Food for thought: If we only accept python 3.7 or higher, 435 # is checking pybabel + pybabel-3.[0-9]* too much and could 436 # be broken down to pybabel + pybabel-3.7 and later names? 437 version_dict = { 438 "3.7": "python3.7", 439 "3.8": "python3.8", 440 "3.9": "python3.9", 441 "3.10": "python3.10", 442 "3.11": "python3.11", 443 "3.12": "python3.12", 444 "3.13": "python3.13", 445 "3.14": "python3.14", 446 } 447 for key, value in version_dict.items(): 448 if existence(value): 449 python3_version = key 450 buildconfig._set_tool("python", value, python3_version) 451 return True 452 453 454 # TODO: Make this really optional, not use a hack ("true"). 455 class BrowserTool(Tool): 456 name = "browser" 457 458 def args(self, parser): 459 parser.add_argument( 460 "--with-browser", type=str, help="name of your webbrowser executable" 461 ) 462 463 def check(self, buildconfig): 464 browser_dict = { 465 "ice": "icecat", 466 "ff": "firefox", 467 "chg": "chrome", 468 "ch": "chromium", 469 "o": "opera", 470 "t": "true" 471 } 472 if "BROWSER" in os.environ: 473 buildconfig._set_tool("browser", os.environ["BROWSER"]) 474 return True 475 for value in browser_dict.values(): 476 if existence(value): 477 buildconfig._set_tool("browser", value) 478 return True 479 480 481 class NodeJsTool(Tool): 482 name = "node" 483 hint = "If you are using Ubuntu Linux or Debian Linux, try installing the\nnode-legacy package or symlink node to nodejs." 484 485 def __init__(self, version_spec): 486 self.version_spec = version_spec 487 488 def args(self, parser): 489 pass 490 491 def check(self, buildconfig): 492 if not existence("node"): 493 return False 494 if ( 495 subprocess.getstatusoutput( 496 "node -p 'process.exit((/v([0-9]+)/.exec(process.version)[1] >= 4) ? 0 : 5)'" 497 )[1] 498 != "" 499 ): 500 buildconfig._warn("your node version is too old, use Node 4.x or newer") 501 return False 502 node_version = tool_version("node --version").lstrip("v") 503 buildconfig._set_tool("node", "node", version=node_version) 504 return True 505 506 class GenericTool(Tool): 507 def __init__(self, name, hint=None, version_arg="-v"): 508 self.name = name 509 if hint is not None: 510 self.hint = hint 511 self.version_arg = version_arg 512 513 def args(self, parser): 514 pass 515 516 def check(self, buildconfig): 517 if not existence(self.name): 518 return False 519 vers = tool_version(f"{self.name} {self.version_arg}") 520 buildconfig._set_tool(self.name, self.name, version=vers) 521 return True 522 523 524 class PosixTool(Tool): 525 def __init__(self, name): 526 self.name = name 527 528 def args(self, parser): 529 pass 530 531 def check(self, buildconfig): 532 found = existence(self.name) 533 if found: 534 buildconfig._set_tool(self.name, self.name) 535 return True 536 return False