commit 944e5634ae93a2f083dbd4916ca30f05235a65e9
parent 2d2c252e06beb84e55dcf1bb42ff80b91b552d30
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 1 Nov 2026 15:57:41 +0100
add proper build system
Diffstat:
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