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())