kych

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

commit 944e5634ae93a2f083dbd4916ca30f05235a65e9
parent 2d2c252e06beb84e55dcf1bb42ff80b91b552d30
Author: Christian Grothoff <christian@grothoff.org>
Date:   Sun,  1 Nov 2026 15:57:41 +0100

add proper build system

Diffstat:
M.gitignore | 12++++++++++++
AMakefile | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MREADME | 42++++++++++++++++++++++++++++++++++++++++++
Aconfigure | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rdebian/config.d/00-kych-defaults.conf -> contrib/config.d/00-kych-defaults.conf | 0
Mdebian/README.source | 7++++++-
Mdebian/kych.install | 2+-
Ascripts/dist.sh | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 854 insertions(+), 2 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -19,6 +19,18 @@ target/ # Version stamp written by scripts/get_version.sh .version +# Written by ./configure +/.config.mk + +# Man pages rendered from documentation/taler-docs/manpages by "make man" +/doc/man/ + +# The verifier jar staged by "make verifier" +/swiyu-verifier/jar/ + +# Source tarball written by "make dist" +/kych-*.tar.gz + # Debian build artifacts debian/cargo/ debian/man/ diff --git a/Makefile b/Makefile @@ -0,0 +1,300 @@ +# This Makefile has been placed under the public domain + +# Written by ./configure. The leading '-' keeps it optional, and it is +# included BEFORE the '?=' defaults below, so a configured tree wins while a +# plain 'make' in an unconfigured tree still builds with whatever is on $PATH. +-include .config.mk + +# Defaults for an unconfigured tree. No rustup required either way; any cargo +# works, including an explicit override: +# make build cargo=/opt/rust/bin/cargo +cargo ?= cargo +prefix ?= /usr/local + +# Optional tools. Empty means "not available": the man pages are then neither +# built nor installed, and clippy is skipped by 'make check'. ./configure +# fills these in; see its output for what it found. +sphinx_build ?= +cargo_clippy ?= no + +# The SWIYU verifier is a second, foreign upstream (Java/Maven) in a submodule. +# It is built only when ./configure said it could be -- see --enable-verifier. +enable_verifier ?= no +mvn ?= +java_home ?= + +# Absolute DESTDIR, or the empty string when DESTDIR is unset/empty. +abs_destdir = $(abspath $(DESTDIR)) + +bin_dir = $(abs_destdir)$(prefix)/bin +share_dir = $(abs_destdir)$(prefix)/share +man_dir = $(share_dir)/man +# $DATADIR as the configuration parser computes it: <prefix>/share/<project>, +# with the project name "kych" (src/config.rs: ConfigSource::new("kych", ...)). +# SQL_DIR defaults to "$DATADIR/sql", so a tree installed here needs no +# configuration at all to find its schema. +kych_data = $(share_dir)/kych +verifier_data = $(share_dir)/swiyu-verifier + +# The crate root is a subdirectory, not the top of the tree, and it has one +# path dependency which cargo builds along with it. +crate = kych_oauth2_gateway +vendored = taler-config +release = $(crate)/target/release + +# The verifier's upstream checkout, and where its jar is staged under a name +# that does not carry the version -- so that neither the install rules nor the +# systemd unit change with every upstream release. +verifier_src = swiyu-verifier/upstream +verifier_jar = swiyu-verifier/jar/swiyu-verifier.jar + +# -DskipTests: upstream's own suite needs Testcontainers, hence a container +# runtime. -Dpgpverify.skip=true: upstream binds the pgpverify plugin to the +# 'validate' phase, which checks every dependency's PGP signature against +# public keyservers; it is upstream's documented escape hatch, and without it +# the build needs keys.openpgp.org to be reachable. +MAVEN_FLAGS ?= -B -DskipTests -Dpgpverify.skip=true + +# Man pages: generated here from the reStructuredText under +# documentation/taler-docs/manpages, which is the single source shared with the +# taler-docs repository. A source tarball ships them pre-built in $(man_build), +# so a build from a tarball needs no sphinx. +man_build = doc/man +man_sources = $(wildcard documentation/taler-docs/manpages/*.rst) +# Keep these in step with man_pages in documentation/sphinx-man/conf.py. +kych_man1 = kych-oauth2-gateway.1 kych-client-management.1 kych-dbinit.1 \ + kych-dbconfig.1 +kych_man5 = kych.conf.5 +verifier_man1 = swiyu-verifier.1 swiyu-verifier-dbconfig.1 + +all: build + +.PHONY: help +help: + @echo 'Targets:' + @echo ' build build the gateway, the man pages and (if enabled) the verifier' + @echo ' check run the test suites and, if available, clippy' + @echo ' install install into $$(prefix), honouring $$(DESTDIR)' + @echo ' installcheck smoke-test what install put in place' + @echo ' dist build the kych-<version>.tar.gz source tarball' + @echo ' distcheck build, test and install a tree unpacked from that tarball' + @echo ' deb build the Debian packages' + @echo ' doc cargo doc' + @echo ' fmt cargo fmt' + @echo ' clean remove build products' + @echo ' distclean also remove ./configure output' + @echo + @echo "prefix = $(prefix), verifier = $(enable_verifier)" + +# ---------------------------------------------------------------- build + +.PHONY: build +build: gateway man +ifeq ($(strip $(enable_verifier)),yes) +build: verifier +endif + +.PHONY: gateway +gateway: + $(cargo) build --release --manifest-path $(crate)/Cargo.toml + +# The stamp exists because sphinx-build has no single output file to depend on +# and there are seven of them; it keeps a second 'make' from re-running sphinx. +man_stamp = $(man_build)/.sphinx-stamp + +ifeq ($(strip $(sphinx_build)),) +.PHONY: man +man: + @if [ -d $(man_build) ]; then \ + echo "man: using the pre-built pages in $(man_build)"; \ + else \ + echo "man: no sphinx-build, so no man pages will be built or installed;" \ + "install python3-sphinx and re-run ./configure" >&2; \ + fi +else +.PHONY: man +man: $(man_stamp) + +# Warnings about pages missing from a toctree are expected: the toctree they +# belong to lives in the taler-docs repository. See the comment in conf.py. +$(man_stamp): $(man_sources) documentation/sphinx-man/conf.py + $(sphinx_build) -b man -q -c documentation/sphinx-man \ + documentation/taler-docs/manpages $(man_build) + @touch $@ +endif + +.PHONY: verifier +verifier: $(verifier_jar) + +# A plain 'git clone' does not bring the submodule, and neither does a build +# from a tree where ./bootstrap was never run; say so rather than letting Maven +# complain about a missing pom. +$(verifier_jar): + @test -f $(verifier_src)/pom.xml || { \ + echo "$(verifier_src) is empty: run ./bootstrap" >&2; exit 1; } + @test -n "$(strip $(mvn))" || { \ + echo "no maven: re-run ./configure --enable-verifier for the reason" >&2; \ + exit 1; } + cd $(verifier_src) && JAVA_HOME=$(java_home) $(mvn) $(MAVEN_FLAGS) package + @mkdir -p $(dir $@) +# Exactly one jar, and not the '.jar.original' that the Spring Boot plugin +# leaves beside it: a glob matching both would stage the wrong one. + cp $(verifier_src)/verifier-application/target/swiyu-verifier-application-*.jar $@ + +# ---------------------------------------------------------------- check + +.PHONY: check +check: +# The two integration suites in $(crate)/tests/ return Ok(()) early when +# DATABASE_URL is unset or unreachable, printing only to stderr -- so a green +# run here does not mean the database layer was exercised. Point it at a +# throwaway database: the tests write to the live schema. + @test -n "$${DATABASE_URL:-}" || echo \ + "check: DATABASE_URL is unset, so the integration tests will skip themselves" >&2 + $(cargo) test --manifest-path $(crate)/Cargo.toml +# Not covered by the line above: the vendored crate has its own suite, and it +# is the quickest check that a re-vendored taler-config still behaves. + $(cargo) test --manifest-path $(vendored)/Cargo.toml +ifeq ($(strip $(cargo_clippy)),yes) + $(cargo) clippy --manifest-path $(crate)/Cargo.toml --all-targets +endif + +# ---------------------------------------------------------------- install + +.PHONY: install +install: install-gateway install-man install-verifier + +.PHONY: install-gateway +install-gateway: gateway + install -D -t $(bin_dir) $(release)/kych-oauth2-gateway + install -D -t $(bin_dir) $(release)/kych-client-management + install -D -t $(bin_dir) $(release)/kych-dbinit +# Creates the role and the database; kych-dbinit then applies the SQL patches. + install -D -t $(bin_dir) contrib/kych-dbconfig +# The daemon serves /js from a path relative to its working directory +# (ServeDir::new("js") in main.rs), so it has to be started from a directory +# that has this one below it -- $(kych_data) for an installed tree. + install -m 644 -D -t $(kych_data)/js $(crate)/js/qrcode.min.js + install -m 644 -D -t $(kych_data)/sql $(crate)/oauth2_gatewaydb/versioning.sql + install -m 644 -D -t $(kych_data)/sql $(crate)/oauth2_gatewaydb/oauth2gw-*.sql +# Deliberately NOT in sql/: kych-dbinit would take drop.sql for a patch file, +# and dropping the schema is not a migration. + install -m 644 -D -t $(kych_data) $(crate)/oauth2_gatewaydb/drop.sql + install -m 644 -D -t $(kych_data) $(crate)/kych.conf.example +# Read before the administrator's own configuration file on every start, which +# therefore wins. These are the options that have no built-in default, so +# without this file the daemon refuses to start; the Debian package installs +# the very same file. Note that the parser does not sort config.d -- keep it +# at one file. + install -m 644 -D -t $(kych_data)/config.d contrib/config.d/00-kych-defaults.conf + +# $(1): page names, $(2): manual section. Every line is continued, so that +# this expands to a single recipe line and therefore runs in a single shell. +# A page that was never built is skipped with a warning rather than failing the +# install: that is the case for a tarball unpacked on a host without sphinx. +define install_man_pages + @set -eu; for page in $(1); do \ + if [ -f $(man_build)/$$page ]; then \ + install -m 644 -D $(man_build)/$$page $(man_dir)/man$(2)/$$page; \ + else \ + echo "install: $(man_build)/$$page was not built; skipping" >&2; \ + fi; \ + done +endef + +.PHONY: install-man +install-man: man + $(call install_man_pages,$(kych_man1),1) + $(call install_man_pages,$(kych_man5),5) + +.PHONY: install-verifier +ifeq ($(strip $(enable_verifier)),yes) +install-verifier: verifier man + install -m 644 -D $(verifier_jar) $(verifier_data)/swiyu-verifier.jar + install -D -t $(bin_dir) contrib/swiyu-verifier + install -D -t $(bin_dir) contrib/swiyu-verifier-dbconfig +# Not trusted by the JVM's default store; see swiyu-verifier/README for +# building a truststore that holds it. + install -m 644 -D -t $(verifier_data)/certs $(verifier_src)/certs/root_ca_vi.crt + install -m 644 -D -t $(verifier_data) debian/signing-key.yml.example + install -m 644 -D -t $(verifier_data) swiyu-verifier/application-kych.yml.template + install -m 644 -D -t $(verifier_data) swiyu-verifier/client_metadata.json + $(call install_man_pages,$(verifier_man1),1) +# contrib/swiyu-verifier looks for its jar, configuration and truststore under +# the FHS paths the Debian package uses, so outside a /usr prefix it has to be +# told where they are; swiyu-verifier/README covers running the jar directly. + @test "$(prefix)" = /usr || echo \ + "install: contrib/swiyu-verifier carries /usr/share paths; see swiyu-verifier/README" >&2 +else +install-verifier: + @echo "install: the SWIYU verifier is not enabled; skipping it" +endif + +.PHONY: installcheck +installcheck: + @set -eu; status=0; \ + for binary in kych-oauth2-gateway kych-client-management kych-dbinit; do \ + if "$(bin_dir)/$$binary" --help >/dev/null 2>&1; then \ + echo "installcheck: $$binary ok"; \ + else \ + echo "installcheck: $(bin_dir)/$$binary is missing or does not run" >&2; \ + status=1; \ + fi; \ + done; \ + for data in js/qrcode.min.js sql/versioning.sql; do \ + if [ -f "$(kych_data)/$$data" ]; then \ + echo "installcheck: $$data ok"; \ + else \ + echo "installcheck: $(kych_data)/$$data is missing" >&2; status=1; \ + fi; \ + done; \ + exit $$status + +# ---------------------------------------------------------------- dist + +.PHONY: dist +# The man pages are built first so that the tarball carries them and a build +# from it needs no sphinx. +dist: man + ./scripts/dist.sh + +.PHONY: distcheck +distcheck: dist + ./scripts/dist.sh --check + +.PHONY: deb +deb: + ./debian/stamp-version.sh + dpkg-buildpackage -b -uc -us + +.PHONY: doc +doc: + $(cargo) doc --manifest-path $(crate)/Cargo.toml + +.PHONY: fmt +fmt: + $(cargo) fmt --manifest-path $(crate)/Cargo.toml + +# ---------------------------------------------------------------- clean + +.PHONY: clean +clean: + $(cargo) clean --manifest-path $(crate)/Cargo.toml + $(cargo) clean --manifest-path $(vendored)/Cargo.toml + rm -rf $(man_build) $(dir $(verifier_jar)) +# doc/ and swiyu-verifier/jar/ hold nothing but generated files, so take the +# now-empty directories with them; '|| true' because they may not exist. + @rmdir doc 2>/dev/null || true +# Only when the submodule is checked out: 'clean' has to work on a tree where +# it is not. Upstream's own build products only; the Maven repository is a +# cache outside this tree and not ours to delete. + @test ! -f $(verifier_src)/pom.xml || rm -rf \ + $(verifier_src)/verifier-application/target $(verifier_src)/verifier-service/target + +.PHONY: distclean +distclean: clean + rm -f .config.mk .version + +.PHONY: maintainer-clean +maintainer-clean: distclean + rm -f kych-*.tar.gz diff --git a/README b/README @@ -71,6 +71,48 @@ Standards SWIYU Swiss e-ID Trust Infrastructure +Building +-------- + +The tree builds with the usual sequence. ./bootstrap is only needed in a git +checkout -- a source tarball is ready to configure as unpacked: + + ./bootstrap # fetch the verifier submodule + ./configure --prefix=$HOME/target # default prefix: /usr/local + make + make check + make install + +./configure reports the toolchain it found and writes .config.mk; "make help" +lists every target. A plain "make" without ./configure also works and uses +whatever cargo is on $PATH. + +Two parts of the build are optional, and ./configure says so rather than +failing late: + + * python3-sphinx renders the man pages from + documentation/taler-docs/manpages. Without it they are simply not built; + a source tarball ships them pre-built, so building from a tarball never + needs sphinx. + + * The SWIYU verifier needs maven and a JDK 25, and its submodule checked + out. It is built when all three are present and skipped otherwise; pass + --enable-verifier to turn a missing one into a configure error, or + --disable-verifier to leave it out deliberately. + +"make dist" writes kych-<version>.tar.gz: the tree as of HEAD, plus the +verifier submodule, the pre-built man pages and a .version file, so that a +build from it needs neither git nor sphinx. "make distcheck" unpacks that +tarball and runs the whole sequence above inside it. + +Note that "make check" runs the unit tests always, but the two integration +suites in kych_oauth2_gateway/tests/ skip themselves unless DATABASE_URL names +a reachable PostgreSQL. They write to the live schema, so point them at a +throwaway database: + + DATABASE_URL=postgres:///kych_test make check + + Getting Started --------------- diff --git a/configure b/configure @@ -0,0 +1,321 @@ +#!/usr/bin/env python3 + +"""Configure the KyCH installation prefix and build toolchain. + +Writes .config.mk, the makefile fragment that the top-level Makefile includes. +The point of this script is to resolve the toolchain once, here, and to report +an unusable or incomplete one as a single clear message instead of as a wall of +errors in the middle of a build. + +Nothing here is required to build: the Makefile falls back to whatever is on +$PATH, so a plain "make" in an unconfigured tree still works. What ./configure +adds is the prefix, the version checks, and the decision whether to build the +SWIYU verifier. +""" + +from __future__ import annotations + +import argparse +import glob +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys + + +# Oldest Rust release that can build this tree. Keep the reason for the number +# next to it -- the number is only useful if the next person can tell whether +# it is still true. +# +# 1.85 edition 2024 (kych_oauth2_gateway/Cargo.toml) +# 1.85 async closures (main.rs: "async |cfg|") +# 1.88 let chains (taler-config/src/config.rs:466) +# +# Note that taler-config declares rust-version = "1.85" while using a let +# chain; 1.88 is the honest floor for the two crates together. That +# declaration is upstream's to fix (see taler-config/README) -- do not patch +# the vendored copy to make this comment shorter. +RUST_MIN_VERSION = "1.88" + +# The SWIYU verifier's jar is built for Java 25 as of its 5.0.0 release (Spring +# Boot 4.1); debian/rules names the same JDK explicitly and for the same +# reason -- a host whose default javac is older fails the build with a +# release-version error rather than anything informative. +JAVA_MIN_VERSION = 25 + +# Where a Debian JDK lands. Checked before $PATH so that a host with several +# JDKs installed and an older one as the default still configures cleanly. +# Tried in order, first match wins. The last, unversioned pattern is there +# only so that an installed-but-too-old JDK is reported as such, rather than as +# "no JDK found". +JDK_GLOBS = ( + f"/usr/lib/jvm/java-{JAVA_MIN_VERSION}-openjdk-*", + f"/usr/lib/jvm/java-{JAVA_MIN_VERSION}-*", + "/usr/lib/jvm/*", +) + +# The verifier's own source, a git submodule pinned to an upstream release tag. +# Absent in a plain "git clone", hence ./bootstrap. +VERIFIER_POM = Path("swiyu-verifier/upstream/pom.xml") + +# VARIABLE=VALUE arguments. The ones that mean something are listed here and +# have the same effect as the environment variable of that name; anything else +# is reported and ignored, as in the sibling repositories' configure. +TOOL_VARIABLES = ("CARGO", "RUSTC", "SPHINXBUILD", "MVN", "JAVA_HOME") +BUILD_VARIABLE = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)=(.*)", re.DOTALL) + + +def version_tuple(version: str) -> tuple[int, ...]: + parts = [int(part) for part in re.findall(r"\d+", version)[:3]] + return tuple(parts + [0] * (3 - len(parts))) + + +def tool_version(path: str, *args: str) -> str | None: + """Run "path --version" (or whatever args say) and pick out the number.""" + try: + completed = subprocess.run( + [path, *(args or ("--version",))], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ) + except (OSError, subprocess.SubprocessError): + return None + found = re.search(r"\d+(?:\.\d+)+", completed.stdout) + return found.group(0) if found else None + + +def find_tool(name: str, env: str, *version_args: str) -> tuple[str | None, str | None]: + """Locate a tool, honouring an override in the environment. + + Returns (path, version); path is None when the tool is not installed. The + caller decides whether that is fatal -- cargo is, sphinx-build is not. + """ + requested = os.environ.get(env) or name + path = shutil.which(requested) + if path is None: + return None, None + return path, tool_version(path, *version_args) + + +def require_tool(name: str, env: str) -> tuple[str, str | None]: + path, version = find_tool(name, env) + if path is None: + print(f"configure: error: tool '{name}' not available", file=sys.stderr) + print( + f"configure: hint: install '{name}' or point {env}= at it", + file=sys.stderr, + ) + raise SystemExit(1) + return path, version + + +def report(name: str, path: str | None, version: str | None, note: str = "") -> None: + if path is None: + print(f"checking for {name}... no{note}") + return + suffix = f" (version {version})" if version else "" + print(f"checking for {name}... {path}{suffix}{note}") + + +def find_jdk() -> tuple[str | None, str | None]: + """Find a JDK new enough for the verifier, as a JAVA_HOME. + + An explicit JAVA_HOME wins, then a versioned JDK in the usual Debian + location, then whatever javac is on $PATH. + """ + explicit = os.environ.get("JAVA_HOME") + if explicit: + javac = Path(explicit) / "bin" / "javac" + if javac.is_file() and os.access(javac, os.X_OK): + return explicit, tool_version(str(javac)) + print( + f"configure: WARNING: JAVA_HOME={explicit} has no usable bin/javac", + file=sys.stderr, + ) + + for pattern in JDK_GLOBS: + for home in sorted(glob.glob(pattern)): + javac = Path(home) / "bin" / "javac" + if javac.is_file() and os.access(javac, os.X_OK): + return home, tool_version(str(javac)) + + javac = shutil.which("javac") + if javac is not None: + # The JDK home is the parent of the directory holding javac, except + # when javac is a symlink into the JDK (as on Debian, where /usr/bin + # would otherwise be reported as JAVA_HOME). + resolved = Path(javac).resolve() + return str(resolved.parent.parent), tool_version(str(resolved)) + + return None, None + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + usage="./configure [--prefix=DIR] [--enable-verifier|--disable-verifier]" + " [VARIABLE=VALUE]...", + description="Configure the installation prefix and the build toolchain.", + epilog="Recognised variables: " + ", ".join(TOOL_VARIABLES), + ) + parser.add_argument( + "--prefix", + default="/usr/local", + metavar="DIR", + help="installation prefix (default: /usr/local)", + ) + # Three-valued on purpose: "auto" builds the verifier when it can, and the + # explicit forms are there so that a build host can insist either way -- + # --enable-verifier turns a missing JDK into a configure error rather than + # a package that quietly lacks the jar. + parser.add_argument( + "--enable-verifier", + dest="verifier", + action="store_const", + const="yes", + default="auto", + help="build the SWIYU verifier; fail now if its toolchain is missing", + ) + parser.add_argument( + "--disable-verifier", + dest="verifier", + action="store_const", + const="no", + help="do not build the SWIYU verifier (gateway only)", + ) + parser.add_argument("build_variables", nargs="*", metavar="VARIABLE=VALUE") + + args = parser.parse_args() + if not args.prefix: + parser.error("installation prefix must not be empty") + + for argument in args.build_variables: + matched = BUILD_VARIABLE.fullmatch(argument) + if not matched: + parser.error(f"unrecognized argument: {argument}") + variable, value = matched.group(1), matched.group(2) + if variable in TOOL_VARIABLES: + os.environ[variable] = value + else: + print( + f"configure: WARNING: unsupported variable '{variable}' is ignored", + file=sys.stderr, + ) + return args + + +def configure_verifier(requested: str) -> tuple[bool, str | None, str | None]: + """Decide whether the verifier is built, and with which toolchain.""" + if requested == "no": + print("checking whether to build the SWIYU verifier... no (--disable-verifier)") + return False, None, None + + mvn_path, mvn_version = find_tool("mvn", "MVN") + report("mvn", mvn_path, mvn_version) + java_home, java_version = find_jdk() + report(f"a JDK >= {JAVA_MIN_VERSION}", java_home, java_version) + + missing = [] + if not VERIFIER_POM.is_file(): + missing.append(f"{VERIFIER_POM.parent} is not checked out (run ./bootstrap)") + if mvn_path is None: + missing.append("maven is not installed") + if java_home is None: + missing.append(f"no JDK found (needs openjdk-{JAVA_MIN_VERSION}-jdk-headless)") + elif java_version and version_tuple(java_version)[0] < JAVA_MIN_VERSION: + missing.append( + f"javac {java_version} is older than {JAVA_MIN_VERSION}" + f" (needs openjdk-{JAVA_MIN_VERSION}-jdk-headless)" + ) + + if not missing: + print("checking whether to build the SWIYU verifier... yes") + return True, mvn_path, java_home + + if requested == "yes": + print("checking whether to build the SWIYU verifier... error", file=sys.stderr) + for reason in missing: + print(f"configure: error: {reason}", file=sys.stderr) + raise SystemExit(1) + + print("checking whether to build the SWIYU verifier... no") + for reason in missing: + print(f"configure: {reason}") + print( + "configure: only the gateway will be built;" + " pass --enable-verifier to make this an error" + ) + return False, mvn_path, java_home + + +def main() -> int: + # Line-buffer stdout so that the "checking for ..." progress and the + # diagnostics on stderr stay interleaved in the right order when the output + # is redirected to a file or a build log. + sys.stdout.reconfigure(line_buffering=True) + + args = parse_args() + + cargo_path, cargo_version = require_tool("cargo", "CARGO") + rustc_path, rustc_version = require_tool("rustc", "RUSTC") + report("cargo", cargo_path, cargo_version) + report("rustc", rustc_path, rustc_version) + + if rustc_version and version_tuple(rustc_version) < version_tuple(RUST_MIN_VERSION): + print( + f"configure: WARNING: rustc {rustc_version} is older than" + f" {RUST_MIN_VERSION}; this tree will not compile with it." + " On Debian stable: apt install -t trixie-backports rustc cargo", + file=sys.stderr, + ) + + # Optional: without it the man pages are not built. A source tarball + # ships them pre-built, so this is only a real requirement for a build + # from git. + sphinx_path, sphinx_version = find_tool("sphinx-build", "SPHINXBUILD") + report("sphinx-build", sphinx_path, sphinx_version) + if sphinx_path is None: + print( + "configure: sphinx-build not found: man pages will not be built" + " (install python3-sphinx)" + ) + + # Optional: "make check" runs it when it is there. Debian ships it in the + # rustc package, but a minimal rustup toolchain does not have it. + clippy = subprocess.run( + [cargo_path, "clippy", "--version"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=False, + ) + has_clippy = clippy.returncode == 0 + print(f"checking for cargo clippy... {'yes' if has_clippy else 'no'}") + + build_verifier, mvn_path, java_home = configure_verifier(args.verifier) + + config = [ + "# This makefile fragment is generated by ./configure. Do not edit it;", + "# re-run ./configure instead.", + f"prefix = {args.prefix}", + f"cargo = {cargo_path}", + f"rustc = {rustc_path}", + f"sphinx_build = {sphinx_path or ''}", + f"cargo_clippy = {'yes' if has_clippy else 'no'}", + f"enable_verifier = {'yes' if build_verifier else 'no'}", + f"mvn = {mvn_path or ''}", + f"java_home = {java_home or ''}", + "", + ] + Path(".config.mk").write_text("\n".join(config), encoding="utf-8") + + print() + print(f"writing .config.mk: prefix = {args.prefix}") + print("run 'make' to build, then 'make check' and 'make install'") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/debian/config.d/00-kych-defaults.conf b/contrib/config.d/00-kych-defaults.conf diff --git a/debian/README.source b/debian/README.source @@ -14,7 +14,12 @@ produced: dpkg-buildpackage -b -uc -us The submodule step is not optional: without it debian/rules stops with -"swiyu-verifier/upstream is empty". +"swiyu-verifier/upstream is empty". "make deb" at the top of the tree runs +the last two steps for you. + +A tarball from "make dist" can be built the same way, and needs no .git to do +it: the tarball carries the submodule and a .version file, which is what +stamp-version.sh reads when git is not available. The second step rewrites debian/changelog with the version reported by scripts/get_version.sh -- the same "git describe --tags" logic the other GNU diff --git a/debian/kych.install b/debian/kych.install @@ -28,7 +28,7 @@ kych_oauth2_gateway/oauth2_gatewaydb/drop.sql usr/share/kych/ # Read before the conffile on every start, and replaced without asking on # upgrade -- which is why the defaults live here and not in /etc. -debian/config.d/00-kych-defaults.conf usr/share/kych/config.d/ +contrib/config.d/00-kych-defaults.conf usr/share/kych/config.d/ # Installed to /etc, hence registered as a conffile automatically. debian/etc/kych/kych.conf etc/kych/ diff --git a/scripts/dist.sh b/scripts/dist.sh @@ -0,0 +1,172 @@ +#!/bin/sh +# Build the source tarball, or check one. Driven by "make dist" and +# "make distcheck"; see those targets in the top-level Makefile. +# +# ./scripts/dist.sh write ./kych-<version>.tar.gz +# ./scripts/dist.sh --check unpack that tarball and build, test and +# install the tree it contains +# +# The tarball has to carry three things that a plain "git archive" does not: +# +# * the swiyu-verifier/upstream submodule, which is a separate upstream and +# is not part of the superproject's tree object; +# * .version, so that a build from the tarball does not need git (this is +# what scripts/get_version.sh reads when there is no .git); +# * the pre-built man pages, so that a build from the tarball does not need +# sphinx either. +# +set -eu + +top_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +cd "$top_dir" + +# Keep this in step with debian/stamp-version.sh, which derives the Debian +# version the same way: scripts/get_version.sh is "git describe --tags" with +# the leading "v" stripped, and falls back to the commit count when no tag is +# reachable from HEAD. +resolve_version() { + version=$(./scripts/get_version.sh) + case "$version" in + [0-9]*) ;; + *) + if [ -e .git ]; then + version="0.0.0-$(git rev-list --count HEAD)-g$(git rev-parse --short=8 HEAD)" + # get_version.sh cached the bogus value; do not ship it. + rm -f .version + else + echo "$0: no git tag and no git repository; cannot derive a version" >&2 + exit 1 + fi + ;; + esac + echo "$version" +} + +make_dist() { + if [ ! -e .git ]; then + echo "$0: not a git checkout; 'make dist' builds the tarball from git" >&2 + exit 1 + fi + + version=$(resolve_version) + tarball="kych-$version.tar.gz" + dist_dir="kych-$version" + + # The tarball is built from HEAD, not from the working tree, so anything + # uncommitted silently will not be in it. That is the usual surprise with + # git-archive-based dist rules, hence the warning. + if ! git diff-index --quiet HEAD -- 2>/dev/null; then + echo "$0: WARNING: the working tree has uncommitted changes;" \ + "the tarball is built from HEAD and will not contain them" >&2 + fi + + # Without it the tarball cannot build the swiyu-verifier package, which is + # half of what this source tree produces. + if [ ! -f swiyu-verifier/upstream/pom.xml ]; then + echo "$0: swiyu-verifier/upstream is not checked out: run ./bootstrap" >&2 + exit 1 + fi + + staging=$(mktemp -d) + trap 'rm -rf "$staging"' EXIT INT TERM + mkdir "$staging/$dist_dir" + + echo "dist: exporting HEAD" + git archive --format=tar HEAD | tar -x -C "$staging/$dist_dir" + + # $sm_path is the submodule's path relative to the top of the tree, which + # is exactly the prefix it needs inside the tarball. + echo "dist: exporting submodules" + export KYCH_DIST_ROOT="$staging/$dist_dir" + git submodule foreach --quiet \ + 'git archive --format=tar --prefix="$sm_path/" HEAD | + tar -x -C "$KYCH_DIST_ROOT"' + + echo "$version" > "$staging/$dist_dir/.version" + + # Only the pages themselves: the output directory also holds sphinx's + # .doctrees cache and the stamp file the Makefile uses. + pages=$(find doc/man -maxdepth 1 -name '*.[15]' -print 2>/dev/null || true) + if [ -n "$pages" ]; then + echo "dist: including the pre-built man pages" + mkdir -p "$staging/$dist_dir/doc/man" + for page in $pages; do + cp "$page" "$staging/$dist_dir/doc/man/" + done + else + echo "$0: WARNING: no man pages in doc/man; a build from the tarball" \ + "will need python3-sphinx. Run 'make man' first." >&2 + fi + + # Reproducible-ish: a fixed owner and sort order, and the commit's own + # timestamp, so that two dist runs from the same commit agree. + rm -f "$tarball" + tar -c -z -f "$tarball" \ + --numeric-owner --owner=0 --group=0 --sort=name \ + --mtime="@$(git log -1 --format=%ct)" \ + -C "$staging" "$dist_dir" + + rm -rf "$staging" + trap - EXIT INT TERM + echo "dist: wrote $tarball ($(du -h "$tarball" | cut -f1))" +} + +check_dist() { + version=$(resolve_version) + tarball="kych-$version.tar.gz" + dist_dir="kych-$version" + + if [ ! -f "$tarball" ]; then + echo "$0: $tarball does not exist; run 'make dist' first" >&2 + exit 1 + fi + + # The verifier is a 300 MB Maven download and a Java build of a foreign + # upstream; checking the tarball does not need it, and a build host without + # a JDK 25 could not do it anyway. Set this to "yes" to include it. + verifier_flag=--disable-verifier + if [ "${KYCH_DISTCHECK_VERIFIER:-no}" = yes ]; then + verifier_flag=--enable-verifier + fi + + work=$(mktemp -d) + trap 'rm -rf "$work"' EXIT INT TERM + + echo "distcheck: unpacking $tarball" + tar -x -z -f "$tarball" -C "$work" + test -d "$work/$dist_dir" || { + echo "$0: $tarball does not unpack into $dist_dir" >&2 + exit 1 + } + + # Built from a tree with no .git, which is the point: it proves the tarball + # is self-contained, down to the version in .version. + test ! -e "$work/$dist_dir/.git" || { + echo "$0: the tarball contains a .git directory" >&2 + exit 1 + } + + inst="$work/_inst" + echo "distcheck: configure, build, check, install" + ( + cd "$work/$dist_dir" + ./configure --prefix="$inst" "$verifier_flag" + make + make check + make install + make installcheck + ) + + rm -rf "$work" + trap - EXIT INT TERM + echo "distcheck: $tarball is complete and builds, tests and installs cleanly" +} + +case "${1:---dist}" in + --dist) make_dist ;; + --check) check_dist ;; + *) + echo "usage: $0 [--dist|--check]" >&2 + exit 1 + ;; +esac