From ab8503f11de99b030368d24faf7c8788de9eb84e Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 14 Oct 2019 22:03:24 +0530 Subject: build scripts as library --- .gitignore | 1 + README | 15 ++- bootstrap.template | 14 +++ configure.py | 226 ----------------------------------------- configure.py.template | 20 ++++ talerbuildconfig.py | 275 ++++++++++++++++++++++++++++++++++++++++++++++++++ testconfigure.py | 13 +++ 7 files changed, 337 insertions(+), 227 deletions(-) create mode 100755 bootstrap.template delete mode 100644 configure.py create mode 100755 configure.py.template create mode 100644 talerbuildconfig.py create mode 100644 testconfigure.py diff --git a/.gitignore b/.gitignore index aee2e4c..ed077ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ config.mk +__pycache__ diff --git a/README b/README index fce232a..2b2e5a5 100644 --- a/README +++ b/README @@ -1 +1,14 @@ -shared build-system files for (some) parts of Taler. +Shared build-system files for (some) parts of Taler. + +A repository using these build-system files should be structured as follows: + + +- bootstrap (copied/adjusted from bootstrap.template) +- build-system (directory containing build system "stuff") +--| configure.py (copied/adjusted from bootstrap.template) +--| taler-build-scripts (git submodule of taler-build-scripts) +--| Makefile + +Makefile and configure.py can also be placed directly into the root of the +repository. However, this might lead to errors when "make" can be invoked +before bootstrap and configure has been done. diff --git a/bootstrap.template b/bootstrap.template new file mode 100755 index 0000000..82700e1 --- /dev/null +++ b/bootstrap.template @@ -0,0 +1,14 @@ +#!/bin/sh + +# Bootstrap the repository. Used when the repository is checked out from git. +# When using the source tarball, running this script is not necessary. + +set -eu + +if ! git --version >/dev/null; then + echo "git not installed" + exit 1 +fi + +git submodule update --init +cp build-system/taler-build-scripts/configure ./configure diff --git a/configure.py b/configure.py deleted file mode 100644 index 4406560..0000000 --- a/configure.py +++ /dev/null @@ -1,226 +0,0 @@ -# This file is part of TALER -# (C) 2019 GNUnet e.V. -# -# Authors: -# Author: ng0 -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE -# LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES -# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, -# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, -# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -# THIS SOFTWARE. -# -# SPDX-License-Identifier: 0BSD - -import argparse -import os -import sys -import logging -from distutils.spawn import find_executable -import subprocess - -""" -This application aims to replicate a small GNU Coding Standards -configure script, taylored at projects in GNU Taler. We hope it -can be of use outside of GNU Taler, hence it is dedicated to the -public domain ('0BSD'). -It takes a couple of arguments on the commandline equivalent to -configure by autotools, in addition some environment variables -xan take precedence over the switches. In the absence of switches, -/usr/local is assumed as the PREFIX. -When all data from tests are gathered, it generates a config.mk -Makefile fragement, which is the processed by a Makefile (usually) in -GNU Make format. -""" - - -def existence(name): - return find_executable(name) is not None - - -def tool_version(name): - return subprocess.getstatusoutput(name)[1] - - -def tool_emscripten(): - if existence('emcc'): - emscripten_version = tool_version('emcc --version') - return f"emscripten version {emscripten_version} found" - else: - return f"emscripten compiler not found" - - -def tool_pybabel(): - # No suffix. Would probably be cheaper to do this in - # the dict as well. - if existence('pybabel'): - return 'pybabel' - else: - # Has suffix, try suffix. We know the names in advance, - # so use a dictionary and iterate over it. Use enough names - # to safe updating this for another couple of years. - # - # Food for thought: If we only accept python 3.7 or higher, - # is checking pybabel + pybabel-3.[0-9]* too much and could - # be broken down to pybabel + pybabel-3.7 and later names? - version_dict = { - '3.0': 'pybabel-3.0', - '3.1': 'pybabel-3.1', - '3.2': 'pybabel-3.2', - '3.3': 'pybabel-3.3', - '3.4': 'pybabel-3.4', - '3.5': 'pybabel-3.5', - '3.6': 'pybabel-3.6', - '3.7': 'pybabel-3.7', - '3.8': 'pybabel-3.8', - '3.9': 'pybabel-3.9', - '4.0': 'pybabel-4.0', - } - for value in version_dict.values(): - if existence(value): - return value - - -def tool_browser(): - # TODO: read xdg-open value first. - browser_dict = { - 'ice': 'icecat', - 'ff': 'firefox', - 'chg': 'chrome', - 'ch': 'chromium', - 'o': 'opera' - } - if 'BROWSER' in os.environ: - return os.environ.get('BROWSER') - else: - for value in browser_dict.values(): - if existence(value): - return value - - -def tool_node(): - if existence('node') is None: - sys.exit( - 'Error: node executable not found.\nIf you are using Ubuntu Linux or Debian Linux, try installing the\nnode-legacy package or symlink node to nodejs.' - ) - else: - if subprocess.getstatusoutput( - "node -p 'process.exit(!(/v([0-9]+)/.exec(process.version)[1] >= 4))'" - )[1] is not '': - sys.exit('Your node version is too old, use Node 4.x or newer') - else: - node_version = tool_version("node --version") - return f"Using Node version {node_version}" - - -def tool_yarn(): - if existence('yarn'): - p1 = subprocess.run(['yarn', 'help'], - stderr=subprocess.STDOUT, - stdout=subprocess.PIPE) - if 'No such file or directory' in p1.stdout.decode('utf-8'): - if existence('cmdtest'): - print( - 'WARNING: cmdtest is installed, this can lead\nto know issues with yarn.' - ) - sys.exit( - 'ERROR: You seem to have the wrong kind of "yarn" installed, please remove the\nconflicting binary before continuing!' - ) - return 'yarn' - elif existence('yarnpkg'): - return 'yarnpkg' - else: - sys.exit( - 'ERROR: yarn missing. See https://yarnpkg.com/en/docs/install\n' - ) - - -def tool_posix(): - messages = [] - - tool_find = existence('find') - if tool_find is None: - messages.append('prerequisite find(1) not found.') - - tool_xargs = existence('xargs') - if tool_xargs is None: - messages.append('prerequisite xargs(1) not found.') - - tool_msgmerge = existence('msgmerge') - if tool_msgmerge is None: - messages.append('prerequisite msgmerge(1) not found.') - - return messages - - -def main(): - logging.basicConfig(level=logging.DEBUG) - logger = logging.getLogger(__name__) - - parser = argparse.ArgumentParser() - parser.add_argument( - '-p', - '--prefix', - type=str, - default='/usr/local', - help='Directory prefix for installation' - ) - parser.add_argument( - '-yarn', '--with-yarn', type=str, help='name of yarn executable' - ) - parser.add_argument( - '-browser', - '--with-browser', - type=str, - help='name of your webbrowser executable' - ) - parser.add_argument( - '-pybabel', - '--with-pybabel', - type=str, - help='name of your pybabel executable' - ) - args = parser.parse_args() - if 'DEBUG' in os.environ: - logger.debug('%s', args) - - # get PREFIX - if 'PREFIX' in os.environ: - p_myprefix = os.environ.get('PREFIX') - if p_myprefix is not None and os.path.isdir(p_myprefix) is True: - myprefix = p_myprefix - elif args.prefix is not '/usr/local': - myprefix = args.prefix - else: - myprefix = parser.get_default('prefix') - - # get yarn executable - if args.with_yarn is not None: - yarnexe = args.with_yarn - else: - yarnexe = str(tool_yarn()) - - mybrowser = tool_browser() - mypybabel = tool_pybabel() - f = open('config.mk', 'w+') - f.writelines([ - '# This mk fragment is autogenerated by configure.py\n', - f'prefix={myprefix}\n', f'yarnexe={yarnexe}\n', - f'RUN_BROWSER={mybrowser}\n', f'pybabel={mypybabel}\n' - ]) - f.close() - print(tool_node()) - print(tool_emscripten()) - posixlist = tool_posix() - for msg in posixlist: - print(posixlist[msg]) - - -if __name__ == "__main__": - main() diff --git a/configure.py.template b/configure.py.template new file mode 100755 index 0000000..f55fec1 --- /dev/null +++ b/configure.py.template @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import sys +from pathlib import Path + +base_dir = Path(__file__, "../build-system/taler-build-scripts").resolve() +if not base_dir.exists(): + print( + f"build system directory ({base_dir}) missing", file=sys.stderr + ) + sys.exit(1) +sys.path.insert(0, base_dir) + +from talerbuildconfig import * + +b = BuildConfig() +b.enable_prefix() +b.enable_configmk() +b.add_tool(PosixTool("find")) +b.run() diff --git a/talerbuildconfig.py b/talerbuildconfig.py new file mode 100644 index 0000000..95f0fa2 --- /dev/null +++ b/talerbuildconfig.py @@ -0,0 +1,275 @@ +# This file is part of TALER +# (C) 2019 GNUnet e.V. +# +# Authors: +# Author: ng0 +# Author: Florian Dold +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE +# LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES +# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +# THIS SOFTWARE. +# +# SPDX-License-Identifier: 0BSD + +from abc import ABC +import argparse +import os +import sys +import logging +from distutils.spawn import find_executable +import subprocess +from dataclasses import dataclass + +""" +This module aims to replicate a small GNU Coding Standards +configure script, taylored at projects in GNU Taler. We hope it +can be of use outside of GNU Taler, hence it is dedicated to the +public domain ('0BSD'). +It takes a couple of arguments on the commandline equivalent to +configure by autotools, in addition some environment variables +xan take precedence over the switches. In the absence of switches, +/usr/local is assumed as the PREFIX. +When all data from tests are gathered, it generates a config.mk +Makefile fragement, which is the processed by a Makefile (usually) in +GNU Make format. +""" + + +class Tool(ABC): + def args(self): + ... + + def check(self, buildconfig): + ... + + +class BuildConfig: + def __init__(self): + # Pairs of (key, value) for config.mk variables + self.make_variables = [] + self.tools = [] + self.tool_results = {} + self.args = None + self.prefix_enabled = False + self.configmk_enabled = False + + def add_tool(self, tool): + if isinstance(tool, Tool): + self.tools.append(tool) + else: + raise Exception("Not a tool instance: " + repr(tool)) + + def _set_tool(self, name, value, version=None): + self.tool_results[name] = (value, version) + + def enable_prefix(self): + """If enabled, process the --prefix argument.""" + self.prefix_enabled = True + + def enable_configmk(self): + """If enabled, output the config.mk makefile fragment.""" + self.configmk_enabled = True + + def run(self): + parser = argparse.ArgumentParser() + if self.prefix_enabled: + parser.add_argument( + "--prefix", + type=str, + default="/usr/local", + help="Directory prefix for installation", + ) + for tool in self.tools: + tool.args(parser) + args = self.args = parser.parse_args() + + for tool in self.tools: + res = tool.check(self) + if not res: + print(f"Error: tool {tool.name} not available") + if hasattr(tool, "hint"): + print(f"Hint: {tool.hint}") + + for tool in self.tools: + path, version = self.tool_results[tool.name] + if version is None: + print(f"found {tool.name} as {path}") + else: + print(f"found {tool.name} as {path} (version {version})") + + if self.configmk_enabled: + with open("config.mk", "w") as f: + f.write("# this makefile fragment is autogenerated by configure.py\n") + if self.prefix_enabled: + f.write(f"prefix = {args.prefix}\n") + for tool in self.tools: + path, version = self.tool_results[tool.name] + f.write(f"{tool.name} = {path}\n") + + +def existence(name): + return find_executable(name) is not None + + +class YarnTool(Tool): + name = "yarn" + description = "The yarn package manager for node" + + def args(self, parser): + parser.add_argument("--with-yarn", action="store") + + def check(self, buildconfig): + yarn_arg = buildconfig.args.with_yarn + if yarn_arg is not None: + buildconfig._set_tool("yarn", yarn_arg) + return True + if existence("yarn"): + p1 = subprocess.run( + ["yarn", "help"], stderr=subprocess.STDOUT, stdout=subprocess.PIPE + ) + if "No such file or directory" in p1.stdout.decode("utf-8"): + if existence("cmdtest"): + buildconfig._warn( + "cmdtest is installed, this can lead to known issues with yarn." + ) + buildconfig._error( + "You seem to have the wrong kind of 'yarn' installed.\n" + "Please remove the conflicting binary before proceeding" + ) + return False + yarn_version = tool_version("yarn --version") + buildconfig._set_tool("yarn", "yarn", yarn_version) + return True + elif existence("yarnpkg"): + yarn_version = tool_version("yarnpkg --version") + buildconfig._set_tool("yarn", "yarnpkg", yarn_version) + return True + return False + + +def tool_version(name): + return subprocess.getstatusoutput(name)[1] + + +class EmscriptenTool: + def args(self, parser): + pass + + def check(self, buildconfig): + if existence("emcc"): + emscripten_version = tool_version("emcc --version") + buildconfig._set_tool("emcc", "emcc", emscripten_version) + return True + return False + + +class PyBabelTool(Tool): + name = "pybabel" + + def args(self, parser): + parser.add_argument( + "--with-pybabel", type=str, help="name of the pybabel executable" + ) + + def check(self, buildconfig): + # No suffix. Would probably be cheaper to do this in + # the dict as well. + if existence("pybabel"): + buildconfig._set_tool("pybabel", "pybabel") + return True + else: + # Has suffix, try suffix. We know the names in advance, + # so use a dictionary and iterate over it. Use enough names + # to safe updating this for another couple of years. + # + # Food for thought: If we only accept python 3.7 or higher, + # is checking pybabel + pybabel-3.[0-9]* too much and could + # be broken down to pybabel + pybabel-3.7 and later names? + version_dict = { + "3.0": "pybabel-3.0", + "3.1": "pybabel-3.1", + "3.2": "pybabel-3.2", + "3.3": "pybabel-3.3", + "3.4": "pybabel-3.4", + "3.5": "pybabel-3.5", + "3.6": "pybabel-3.6", + "3.7": "pybabel-3.7", + "3.8": "pybabel-3.8", + "3.9": "pybabel-3.9", + "4.0": "pybabel-4.0", + } + for value in version_dict.values(): + if existence(value): + buildconfig._set_tool("pybabel", value) + return True + + +class BrowserTool(Tool): + name = "browser" + + def args(self, parser): + parser.add_argument( + "--with-browser", type=str, help="name of your webbrowser executable" + ) + + def check(self, buildconfig): + browser_dict = { + "ice": "icecat", + "ff": "firefox", + "chg": "chrome", + "ch": "chromium", + "o": "opera", + } + if "BROWSER" in os.environ: + buildconfig._set_tool("browser", os.environ["BROWSER"]) + return True + for value in browser_dict.values(): + if existence(value): + buildconfig._set_tool("browser", value) + return True + + +class NodeJsTool(Tool): + name = "node" + hint = "If you are using Ubuntu Linux or Debian Linux, try installing the\nnode-legacy package or symlink node to nodejs." + + def args(self, parser): + pass + + def check(self, buildconfig): + if existence("node") is None: + return False + if ( + subprocess.getstatusoutput( + "node -p 'process.exit(!(/v([0-9]+)/.exec(process.version)[1] >= 4))'" + )[1] + is not "" + ): + buildconfig._warn("your node version is too old, use Node 4.x or newer") + return False + node_version = tool_version("node --version") + buildconfig._set_tool("node", "node", version=node_version) + return True + + +class PosixTool(Tool): + def __init__(self, name): + self.name = name + + def args(self, parser): + pass + + def check(self, buildconfig): + found = existence("find") + if found: + buildconfig._set_tool(self.name, self.name) + return True + return False diff --git a/testconfigure.py b/testconfigure.py new file mode 100644 index 0000000..af7e85a --- /dev/null +++ b/testconfigure.py @@ -0,0 +1,13 @@ +from talerbuildconfig import * + +b = BuildConfig() +b.enable_prefix() +b.enable_configmk() +b.add_tool(YarnTool()) +b.add_tool(BrowserTool()) +b.add_tool(PyBabelTool()) +b.add_tool(NodeJsTool()) +b.add_tool(PosixTool("find")) +b.add_tool(PosixTool("xargs")) +b.add_tool(PosixTool("msgmerge")) +b.run() -- cgit v1.2.3