kych

OAuth 2.0 API for Swiyu to enable Taler integration of Swiyu for KYC (experimental)
Log | Files | Refs | Submodules | README | LICENSE

configure (12242B)


      1 #!/usr/bin/env python3
      2 
      3 """Configure the KyCH installation prefix and build toolchain.
      4 
      5 Writes .config.mk, the makefile fragment that the top-level Makefile includes.
      6 The point of this script is to resolve the toolchain once, here, and to report
      7 an unusable or incomplete one as a single clear message instead of as a wall of
      8 errors in the middle of a build.
      9 
     10 Nothing here is required to build: the Makefile falls back to whatever is on
     11 $PATH, so a plain "make" in an unconfigured tree still works.  What ./configure
     12 adds is the prefix, the version checks, and the decision whether to build the
     13 SWIYU verifier.
     14 """
     15 
     16 from __future__ import annotations
     17 
     18 import argparse
     19 import glob
     20 import os
     21 from pathlib import Path
     22 import re
     23 import shutil
     24 import subprocess
     25 import sys
     26 
     27 
     28 # Oldest Rust release that can build this tree.  Keep the reason for the number
     29 # next to it -- the number is only useful if the next person can tell whether
     30 # it is still true.
     31 #
     32 #   1.85  edition 2024                      (kych_oauth2_gateway/Cargo.toml)
     33 #   1.85  async closures                    (main.rs: "async |cfg|")
     34 #   1.88  let chains                        (taler-config/src/config.rs:466)
     35 #
     36 # Note that taler-config declares rust-version = "1.85" while using a let
     37 # chain; 1.88 is the honest floor for the two crates together.  That
     38 # declaration is upstream's to fix (see taler-config/README) -- do not patch
     39 # the vendored copy to make this comment shorter.
     40 RUST_MIN_VERSION = "1.88"
     41 
     42 # The SWIYU verifier's jar is built for Java 25 as of its 5.0.0 release (Spring
     43 # Boot 4.1); debian/rules names the same JDK explicitly and for the same
     44 # reason -- a host whose default javac is older fails the build with a
     45 # release-version error rather than anything informative.
     46 JAVA_MIN_VERSION = 25
     47 
     48 # Where a Debian JDK lands.  Checked before $PATH so that a host with several
     49 # JDKs installed and an older one as the default still configures cleanly.
     50 # Tried in order, first match wins.  The last, unversioned pattern is there
     51 # only so that an installed-but-too-old JDK is reported as such, rather than as
     52 # "no JDK found".
     53 JDK_GLOBS = (
     54     f"/usr/lib/jvm/java-{JAVA_MIN_VERSION}-openjdk-*",
     55     f"/usr/lib/jvm/java-{JAVA_MIN_VERSION}-*",
     56     "/usr/lib/jvm/*",
     57 )
     58 
     59 # The verifier's own source, a git submodule pinned to an upstream release tag.
     60 # Absent in a plain "git clone", hence ./bootstrap.
     61 VERIFIER_POM = Path("swiyu-verifier/upstream/pom.xml")
     62 
     63 # VARIABLE=VALUE arguments.  The ones that mean something are listed here and
     64 # have the same effect as the environment variable of that name; anything else
     65 # is reported and ignored, as in the sibling repositories' configure.
     66 TOOL_VARIABLES = ("CARGO", "RUSTC", "SPHINXBUILD", "MVN", "JAVA_HOME")
     67 BUILD_VARIABLE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)=(.*)", re.DOTALL)
     68 
     69 
     70 def version_tuple(version: str) -> tuple[int, ...]:
     71     parts = [int(part) for part in re.findall(r"\d+", version)[:3]]
     72     return tuple(parts + [0] * (3 - len(parts)))
     73 
     74 
     75 def tool_version(path: str, *args: str) -> str | None:
     76     """Run "path --version" (or whatever args say) and pick out the number."""
     77     try:
     78         completed = subprocess.run(
     79             [path, *(args or ("--version",))],
     80             check=True,
     81             stdout=subprocess.PIPE,
     82             stderr=subprocess.STDOUT,
     83             text=True,
     84         )
     85     except (OSError, subprocess.SubprocessError):
     86         return None
     87     found = re.search(r"\d+(?:\.\d+)+", completed.stdout)
     88     return found.group(0) if found else None
     89 
     90 
     91 def find_tool(name: str, env: str, *version_args: str) -> tuple[str | None, str | None]:
     92     """Locate a tool, honouring an override in the environment.
     93 
     94     Returns (path, version); path is None when the tool is not installed.  The
     95     caller decides whether that is fatal -- cargo is, sphinx-build is not.
     96     """
     97     requested = os.environ.get(env) or name
     98     path = shutil.which(requested)
     99     if path is None:
    100         return None, None
    101     return path, tool_version(path, *version_args)
    102 
    103 
    104 def require_tool(name: str, env: str) -> tuple[str, str | None]:
    105     path, version = find_tool(name, env)
    106     if path is None:
    107         print(f"configure: error: tool '{name}' not available", file=sys.stderr)
    108         print(
    109             f"configure: hint: install '{name}' or point {env}= at it",
    110             file=sys.stderr,
    111         )
    112         raise SystemExit(1)
    113     return path, version
    114 
    115 
    116 def report(name: str, path: str | None, version: str | None, note: str = "") -> None:
    117     if path is None:
    118         print(f"checking for {name}... no{note}")
    119         return
    120     suffix = f" (version {version})" if version else ""
    121     print(f"checking for {name}... {path}{suffix}{note}")
    122 
    123 
    124 def find_jdk() -> tuple[str | None, str | None]:
    125     """Find a JDK new enough for the verifier, as a JAVA_HOME.
    126 
    127     An explicit JAVA_HOME wins, then a versioned JDK in the usual Debian
    128     location, then whatever javac is on $PATH.
    129     """
    130     explicit = os.environ.get("JAVA_HOME")
    131     if explicit:
    132         javac = Path(explicit) / "bin" / "javac"
    133         if javac.is_file() and os.access(javac, os.X_OK):
    134             return explicit, tool_version(str(javac))
    135         print(
    136             f"configure: WARNING: JAVA_HOME={explicit} has no usable bin/javac",
    137             file=sys.stderr,
    138         )
    139 
    140     for pattern in JDK_GLOBS:
    141         for home in sorted(glob.glob(pattern)):
    142             javac = Path(home) / "bin" / "javac"
    143             if javac.is_file() and os.access(javac, os.X_OK):
    144                 return home, tool_version(str(javac))
    145 
    146     javac = shutil.which("javac")
    147     if javac is not None:
    148         # The JDK home is the parent of the directory holding javac, except
    149         # when javac is a symlink into the JDK (as on Debian, where /usr/bin
    150         # would otherwise be reported as JAVA_HOME).
    151         resolved = Path(javac).resolve()
    152         return str(resolved.parent.parent), tool_version(str(resolved))
    153 
    154     return None, None
    155 
    156 
    157 def parse_args() -> argparse.Namespace:
    158     parser = argparse.ArgumentParser(
    159         usage="./configure [--prefix=DIR] [--enable-verifier|--disable-verifier]"
    160         " [VARIABLE=VALUE]...",
    161         description="Configure the installation prefix and the build toolchain.",
    162         epilog="Recognised variables: " + ", ".join(TOOL_VARIABLES),
    163     )
    164     parser.add_argument(
    165         "--prefix",
    166         default="/usr/local",
    167         metavar="DIR",
    168         help="installation prefix (default: /usr/local)",
    169     )
    170     # Three-valued on purpose: "auto" builds the verifier when it can, and the
    171     # explicit forms are there so that a build host can insist either way --
    172     # --enable-verifier turns a missing JDK into a configure error rather than
    173     # a package that quietly lacks the jar.
    174     parser.add_argument(
    175         "--enable-verifier",
    176         dest="verifier",
    177         action="store_const",
    178         const="yes",
    179         default="auto",
    180         help="build the SWIYU verifier; fail now if its toolchain is missing",
    181     )
    182     parser.add_argument(
    183         "--disable-verifier",
    184         dest="verifier",
    185         action="store_const",
    186         const="no",
    187         help="do not build the SWIYU verifier (gateway only)",
    188     )
    189     parser.add_argument("build_variables", nargs="*", metavar="VARIABLE=VALUE")
    190 
    191     args = parser.parse_args()
    192     if not args.prefix:
    193         parser.error("installation prefix must not be empty")
    194 
    195     for argument in args.build_variables:
    196         matched = BUILD_VARIABLE.fullmatch(argument)
    197         if not matched:
    198             parser.error(f"unrecognized argument: {argument}")
    199         variable, value = matched.group(1), matched.group(2)
    200         if variable in TOOL_VARIABLES:
    201             os.environ[variable] = value
    202         else:
    203             print(
    204                 f"configure: WARNING: unsupported variable '{variable}' is ignored",
    205                 file=sys.stderr,
    206             )
    207     return args
    208 
    209 
    210 def configure_verifier(requested: str) -> tuple[bool, str | None, str | None]:
    211     """Decide whether the verifier is built, and with which toolchain."""
    212     if requested == "no":
    213         print("checking whether to build the SWIYU verifier... no (--disable-verifier)")
    214         return False, None, None
    215 
    216     mvn_path, mvn_version = find_tool("mvn", "MVN")
    217     report("mvn", mvn_path, mvn_version)
    218     java_home, java_version = find_jdk()
    219     report(f"a JDK >= {JAVA_MIN_VERSION}", java_home, java_version)
    220 
    221     missing = []
    222     if not VERIFIER_POM.is_file():
    223         missing.append(f"{VERIFIER_POM.parent} is not checked out (run ./bootstrap)")
    224     if mvn_path is None:
    225         missing.append("maven is not installed")
    226     if java_home is None:
    227         missing.append(f"no JDK found (needs openjdk-{JAVA_MIN_VERSION}-jdk-headless)")
    228     elif java_version and version_tuple(java_version)[0] < JAVA_MIN_VERSION:
    229         missing.append(
    230             f"javac {java_version} is older than {JAVA_MIN_VERSION}"
    231             f" (needs openjdk-{JAVA_MIN_VERSION}-jdk-headless)"
    232         )
    233 
    234     if not missing:
    235         print("checking whether to build the SWIYU verifier... yes")
    236         return True, mvn_path, java_home
    237 
    238     if requested == "yes":
    239         print("checking whether to build the SWIYU verifier... error", file=sys.stderr)
    240         for reason in missing:
    241             print(f"configure: error: {reason}", file=sys.stderr)
    242         raise SystemExit(1)
    243 
    244     print("checking whether to build the SWIYU verifier... no")
    245     for reason in missing:
    246         print(f"configure: {reason}")
    247     print(
    248         "configure: only the gateway will be built;"
    249         " pass --enable-verifier to make this an error"
    250     )
    251     return False, mvn_path, java_home
    252 
    253 
    254 def main() -> int:
    255     # Line-buffer stdout so that the "checking for ..." progress and the
    256     # diagnostics on stderr stay interleaved in the right order when the output
    257     # is redirected to a file or a build log.
    258     sys.stdout.reconfigure(line_buffering=True)
    259 
    260     args = parse_args()
    261 
    262     cargo_path, cargo_version = require_tool("cargo", "CARGO")
    263     rustc_path, rustc_version = require_tool("rustc", "RUSTC")
    264     report("cargo", cargo_path, cargo_version)
    265     report("rustc", rustc_path, rustc_version)
    266 
    267     if rustc_version and version_tuple(rustc_version) < version_tuple(RUST_MIN_VERSION):
    268         print(
    269             f"configure: WARNING: rustc {rustc_version} is older than"
    270             f" {RUST_MIN_VERSION}; this tree will not compile with it."
    271             " On Debian stable: apt install -t trixie-backports rustc cargo",
    272             file=sys.stderr,
    273         )
    274 
    275     # Optional: without it the man pages are not built.  A source tarball
    276     # ships them pre-built, so this is only a real requirement for a build
    277     # from git.
    278     sphinx_path, sphinx_version = find_tool("sphinx-build", "SPHINXBUILD")
    279     report("sphinx-build", sphinx_path, sphinx_version)
    280     if sphinx_path is None:
    281         print(
    282             "configure: sphinx-build not found: man pages will not be built"
    283             " (install python3-sphinx)"
    284         )
    285 
    286     # Optional: "make check" runs it when it is there.  Debian ships it in the
    287     # rustc package, but a minimal rustup toolchain does not have it.
    288     clippy = subprocess.run(
    289         [cargo_path, "clippy", "--version"],
    290         stdout=subprocess.DEVNULL,
    291         stderr=subprocess.DEVNULL,
    292         check=False,
    293     )
    294     has_clippy = clippy.returncode == 0
    295     print(f"checking for cargo clippy... {'yes' if has_clippy else 'no'}")
    296 
    297     build_verifier, mvn_path, java_home = configure_verifier(args.verifier)
    298 
    299     config = [
    300         "# This makefile fragment is generated by ./configure.  Do not edit it;",
    301         "# re-run ./configure instead.",
    302         f"prefix = {args.prefix}",
    303         f"cargo = {cargo_path}",
    304         f"rustc = {rustc_path}",
    305         f"sphinx_build = {sphinx_path or ''}",
    306         f"cargo_clippy = {'yes' if has_clippy else 'no'}",
    307         f"enable_verifier = {'yes' if build_verifier else 'no'}",
    308         f"mvn = {mvn_path or ''}",
    309         f"java_home = {java_home or ''}",
    310         "",
    311     ]
    312     Path(".config.mk").write_text("\n".join(config), encoding="utf-8")
    313 
    314     print()
    315     print(f"writing .config.mk: prefix = {args.prefix}")
    316     print("run 'make' to build, then 'make check' and 'make install'")
    317     return 0
    318 
    319 
    320 if __name__ == "__main__":
    321     sys.exit(main())