build-common

Shared build system code (usually as a git submodule)
Log | Files | Refs | README | LICENSE

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