commit 5a3b66c27505e5938deb9a1b2f523861b771605c
parent e45345f475c905f56bb2448765f91d6e701cd608
Author: Antoine A <>
Date: Tue, 29 Sep 2026 22:25:27 +0200
common: sync with latest build logic of taler-rust
Diffstat:
9 files changed, 157 insertions(+), 317 deletions(-)
diff --git a/build-system/archive.py b/build-system/archive.py
@@ -1,169 +0,0 @@
-#!/usr/bin/env python3
-
-# This file has been placed in the public domain.
-
-"""Create a source archive containing this repository and its submodules."""
-
-from __future__ import annotations
-
-import argparse
-import io
-import os
-import subprocess
-import sys
-import tarfile
-from pathlib import Path
-
-
-def run_git(repo: Path, *args: str, input_data: bytes | None = None) -> bytes:
- try:
- return subprocess.run(
- ["git", *args],
- cwd=repo,
- input=input_data,
- check=True,
- stdout=subprocess.PIPE,
- ).stdout
- except subprocess.CalledProcessError as exc:
- command = " ".join(("git", *args))
- raise RuntimeError(f"'{command}' failed in {repo}") from exc
-
-
-def tracked_paths(repo: Path) -> list[Path]:
- output = run_git(repo, "ls-files", "-z", "--cached")
- return [
- Path(os.fsdecode(item)) for item in output.rstrip(b"\0").split(b"\0") if item
- ]
-
-
-def submodule_paths(repo: Path) -> set[Path]:
- gitmodules = repo / ".gitmodules"
- if not gitmodules.exists():
- return set()
- result = subprocess.run(
- [
- "git",
- "config",
- "-f",
- ".gitmodules",
- "--get-regexp",
- r"^submodule\..*\.path$",
- ],
- cwd=repo,
- check=False,
- stdout=subprocess.PIPE,
- text=True,
- )
- if result.returncode not in (0, 1):
- raise RuntimeError(f"could not read submodules from {gitmodules}")
- return {Path(line.split(maxsplit=1)[1]) for line in result.stdout.splitlines()}
-
-
-def export_ignored(repo: Path, paths: list[Path]) -> set[Path]:
- if not paths:
- return set()
- query = b"\0".join(os.fsencode(path) for path in paths) + b"\0"
- output = run_git(
- repo, "check-attr", "-z", "--stdin", "export-ignore", input_data=query
- )
- fields = output.rstrip(b"\0").split(b"\0")
- ignored: set[Path] = set()
- for index in range(0, len(fields), 3):
- if fields[index + 2] == b"set":
- ignored.add(Path(os.fsdecode(fields[index])))
- return ignored
-
-
-def repository_entries(repo: Path) -> list[tuple[Path, Path]]:
- """Return (filesystem path, archive-relative path) pairs for one repository."""
- submodules = submodule_paths(repo)
- candidates: list[tuple[Path, Path]] = []
-
- for relative in tracked_paths(repo):
- if relative not in submodules:
- candidates.append((repo / relative, relative))
-
- for submodule in sorted(submodules):
- submodule_dir = repo / submodule
- if not (submodule_dir / ".git").exists():
- raise RuntimeError(
- f"submodule '{submodule}' is not initialized; run './bootstrap' first"
- )
- for source, relative in repository_entries(submodule_dir):
- candidates.append((source, submodule / relative))
-
- ignored = export_ignored(repo, [relative for _, relative in candidates])
- return [
- (source, relative) for source, relative in candidates if relative not in ignored
- ]
-
-
-def archive_prefix(output: Path) -> Path:
- name = output.name
- for suffix in (".tar.gz", ".tgz"):
- if name.endswith(suffix):
- prefix = name[: -len(suffix)]
- if prefix:
- return Path(prefix)
- break
- raise ValueError("output file must end in .tar.gz or .tgz")
-
-
-def parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- "--include",
- action="append",
- default=[],
- metavar="FILE",
- help="include an untracked or export-ignored file (may be repeated)",
- )
- parser.add_argument("output", type=Path, help="output .tar.gz file")
- return parser.parse_args()
-
-
-def main() -> int:
- args = parse_args()
- root = Path(run_git(Path.cwd(), "rev-parse", "--show-toplevel").decode().strip())
- output = args.output.resolve()
- prefix = archive_prefix(output)
- entries = repository_entries(root)
- version = run_git(root, "describe", "--tags", "--always", "--abbrev=8").strip()
- if not version or any(character in version for character in (b"\n", b"\r", b"\0")):
- raise ValueError("program version must be a nonempty single line")
- version += b"\n"
-
- included: list[tuple[Path, Path]] = []
- for name in args.include:
- relative = Path(name)
- if relative.is_absolute() or ".." in relative.parts:
- raise ValueError(
- f"included path must be relative to the repository: {name}"
- )
- source = (root / relative).absolute()
- if not source.is_file():
- raise FileNotFoundError(f"included file does not exist: {name}")
- included.append((source, relative))
-
- with tarfile.open(output, "w:gz") as archive:
- stamp = tarfile.TarInfo(str(prefix / ".version"))
- stamp.size = len(version)
- stamp.mode = 0o644
- stamp.mtime = int(run_git(root, "log", "-1", "--format=%ct"))
- archive.addfile(stamp, io.BytesIO(version))
- for source, relative in [*included, *entries]:
- # A stale stamp in the checkout must not override the Git version.
- if relative == Path(".version"):
- continue
- archive.add(source, arcname=prefix / relative, recursive=False)
-
- print(f"created {output}")
- return 0
-
-
-if __name__ == "__main__":
- try:
- sys.exit(main())
- except (OSError, RuntimeError, ValueError) as exc:
- print(f"archive: {exc}", file=sys.stderr)
- sys.exit(1)
diff --git a/build-system/configure.py b/build-system/configure.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+
+"""Configure the taler-rust installation prefix and Rust toolchain."""
+
+from __future__ import annotations
+
+import argparse
+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 point of the check is to report an unusable toolchain here,
+# once, instead of as a wall of parse errors in the middle of the build.
+#
+# 1.85 edition 2024 / resolver "3" (Cargo.toml)
+# 1.86 Vec::pop_if (common/failure-injection)
+# 1.88 let chains (used throughout)
+# 1.93 std::fmt::from_fn (taler-common, taler-api, taler-wise)
+#
+# Debian trixie ships 1.85, which is too old; trixie-backports and testing both
+# ship a new enough rustc. Keep this in step with `rust-version` in Cargo.toml.
+RUST_MIN_VERSION = "1.93"
+BUILD_VARIABLE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*=.*", re.DOTALL)
+
+
+def version_tuple(version: str) -> tuple[int, int, int]:
+ parts = [int(part) for part in version.split(".")[:3]]
+ return tuple(parts + [0] * (3 - len(parts)))
+
+
+def tool_version(path: str) -> str | None:
+ try:
+ output = subprocess.run(
+ [path, "--version"],
+ check=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ ).stdout
+ except (OSError, subprocess.SubprocessError):
+ return None
+ found = re.search(r"\d+(?:\.\d+)+", output)
+ return found.group(0) if found else None
+
+
+def find_tool(name: str) -> tuple[str, str | None]:
+ requested = os.environ.get(name.upper()) or name
+ path = shutil.which(requested)
+ if path is None:
+ print(f"configure: error: tool '{name}' not available", file=sys.stderr)
+ print(
+ f"configure: hint: install '{name}' or point {name.upper()}= at it",
+ file=sys.stderr,
+ )
+ raise SystemExit(1)
+ return path, tool_version(path)
+
+
+def parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ usage="./configure [--prefix=DIR] [VARIABLE=VALUE]...",
+ description="Configure the installation prefix and Rust toolchain.",
+ )
+ parser.add_argument("--prefix", default="/usr/local", help="installation prefix")
+ 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:
+ if not BUILD_VARIABLE.fullmatch(argument):
+ parser.error(f"unrecognized argument: {argument}")
+ variable = argument.partition("=")[0]
+ print(
+ f"configure: WARNING: unsupported variable '{variable}' is ignored",
+ file=sys.stderr,
+ )
+ return args
+
+
+def main() -> int:
+ args = parse_args()
+ cargo_path, cargo_version = find_tool("cargo")
+ rustc_path, rustc_version = find_tool("rustc")
+
+ for name, path, version in (
+ ("cargo", cargo_path, cargo_version),
+ ("rustc", rustc_path, rustc_version),
+ ):
+ suffix = f" (version {version})" if version else ""
+ print(f"found {name} as {path}{suffix}")
+
+ 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,
+ )
+
+ config = (
+ "# This makefile fragment is generated by configure.\n"
+ f"prefix = {args.prefix}\n"
+ f"cargo = {cargo_path}\n"
+ f"rustc = {rustc_path}\n"
+ )
+ Path(".config.mk").write_text(config, encoding="utf-8")
+ print("writing .config.mk")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/build-system/configure.sh b/build-system/configure.sh
@@ -1,67 +0,0 @@
-#!/bin/sh
-
-# This file has been placed in the public domain.
-
-set -eu
-
-prefix=/usr/local
-
-usage() {
- cat <<EOF
-Usage: ./configure [--prefix=DIR] [VARIABLE=VALUE]...
-
-Configure the installation prefix (default: /usr/local).
-Unsupported build variables are ignored with a warning.
-EOF
-}
-
-while test $# -gt 0; do
- case $1 in
- --prefix=*)
- prefix=${1#*=}
- ;;
- --prefix)
- if test $# -lt 2; then
- echo "configure: option '--prefix' requires an argument" >&2
- exit 2
- fi
- prefix=$2
- shift
- ;;
- -h|--help)
- usage
- exit 0
- ;;
- *=*)
- variable=${1%%=*}
- case $variable in
- ""|[!A-Za-z_]*|*[!A-Za-z0-9_]*)
- echo "configure: unrecognized option '$1'" >&2
- echo "Try './configure --help' for more information." >&2
- exit 2
- ;;
- *)
- echo "configure: WARNING: unsupported variable '$variable' is ignored" >&2
- ;;
- esac
- ;;
- *)
- echo "configure: unrecognized option '$1'" >&2
- echo "Try './configure --help' for more information." >&2
- exit 2
- ;;
- esac
- shift
-done
-
-if test -z "$prefix"; then
- echo "configure: installation prefix must not be empty" >&2
- exit 2
-fi
-
-cat >.config.mk <<EOF
-# This makefile fragment is generated by configure.
-prefix = $prefix
-EOF
-
-echo "configured installation prefix: $prefix"
diff --git a/build-system/version.gradle b/build-system/version.gradle
@@ -1,42 +0,0 @@
-// This file is in the public domain.
-
-// Applied by the root project, independently of the Kotlin build plugins.
-def git = { List<String> arguments ->
- def result = providers.exec {
- workingDir rootDir
- commandLine(['git'] + arguments)
- environment = System.getenv().findAll { name, value ->
- !(name in ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR'])
- }
- ignoreExitValue = true
- }
- if (result.result.get().exitValue != 0) {
- throw new GradleException("git ${arguments.join(' ')}: ${result.standardError.asText.get().trim()}")
- }
- result.standardOutput.asText.get().trim()
-}
-
-String resolvedVersion
-try {
- // Do not describe an enclosing repository when building a source archive.
- if (!new File(rootDir, '.git').exists()) {
- throw new GradleException('source tree has no Git metadata')
- }
- if (new File(git(['rev-parse', '--show-toplevel'])).canonicalFile != rootDir.canonicalFile) {
- throw new GradleException('Git metadata belongs to another source tree')
- }
- resolvedVersion = git(['describe', '--tags', '--always', '--abbrev=8'])
-} catch (Exception gitError) {
- def stamp = new File(rootDir, '.version')
- if (!stamp.isFile()) {
- throw new GradleException(
- "Cannot determine program version: ${gitError.message}. Source archives must contain ${stamp}",
- gitError
- )
- }
- resolvedVersion = stamp.getText('UTF-8').trim()
-}
-if (!resolvedVersion || resolvedVersion.contains('\n') || resolvedVersion.contains('\r') || resolvedVersion.contains('\u0000')) {
- throw new GradleException('Program version must be a nonempty single line')
-}
-version = resolvedVersion
diff --git a/contrib/ci/jobs/4-deb/version.sh b/contrib/ci/jobs/4-deb/version.sh
@@ -1,6 +0,0 @@
-#!/bin/sh
-# This file is in the public domain.
-# Compatibility entry point, also usable in generated job directories.
-set -eu
-repo_dir=$(git rev-parse --show-toplevel)
-exec "$repo_dir/contrib/ci/version.sh" "$@"
diff --git a/libeufin-ebics/src/ws.rs b/libeufin-ebics/src/ws.rs
@@ -258,7 +258,7 @@ pub async fn listen_for_notification(
#[cfg(test)]
mod test {
- use std::{assert_matches, fmt::Debug, fs::Permissions, os::unix::fs::PermissionsExt as _};
+ use std::{fmt::Debug, fs::Permissions, os::unix::fs::PermissionsExt as _};
use axum::{
extract::{
@@ -433,9 +433,9 @@ mod test {
// Check message number and type
assert!(count <= 3);
if count == 3 {
- assert_matches!(msg, WssNotification::GeneralInfo { .. })
+ assert!(matches!(msg, WssNotification::GeneralInfo { .. }))
} else {
- assert_matches!(msg, WssNotification::NewData { .. })
+ assert!(matches!(msg, WssNotification::NewData { .. }))
}
})
.await
diff --git a/libeufin-nexus/src/db/initiated.rs b/libeufin-nexus/src/db/initiated.rs
@@ -465,7 +465,7 @@ pub async fn tx_status_update(
#[cfg(test)]
mod test {
- use std::{assert_matches, str::FromStr as _};
+ use std::str::FromStr as _;
use jiff::{Span, Timestamp, civil::Date};
use libeufin_ebics::{ebics::rand_ebics_id, iso20022::model::Tx};
@@ -621,10 +621,10 @@ mod test {
.unwrap();
// Create a test batch with three transactions
for id in ["TX", "TX_SETTLED"] {
- assert_matches!(
+ assert!(matches!(
gen_initiate(db, id, "lol").await,
PaymentInitiationResult::Success(_)
- );
+ ));
}
batch_initiated(db, &Timestamp::now(), "BATCH", false)
.await
@@ -632,19 +632,19 @@ mod test {
// Create witness transactions and batch
for id in ["WITNESS_1", "WITNESS_2"] {
- assert_matches!(
+ assert!(matches!(
gen_initiate(db, id, "lol").await,
PaymentInitiationResult::Success(_)
- );
+ ));
}
batch_initiated(db, &Timestamp::now(), "BATCH_WITNESS", false)
.await
.unwrap();
for id in ["WITNESS_3", "WITNESS_4"] {
- assert_matches!(
+ assert!(matches!(
gen_initiate(db, id, "lol").await,
PaymentInitiationResult::Success(_)
- );
+ ));
}
// Check everything is unsubmitted
sqlx::query(
@@ -859,10 +859,10 @@ mod test {
let (_, db) = db_setup().await;
let now = Timestamp::now();
for i in 0..6 {
- assert_matches!(
+ assert!(matches!(
gen_initiate(&db, format!("PAY{i}"), "").await,
PaymentInitiationResult::Success(_)
- );
+ ));
batch_initiated(&db, &now, &rand_ebics_id(), false)
.await
.unwrap();
diff --git a/libeufin-nexus/src/db/payment.rs b/libeufin-nexus/src/db/payment.rs
@@ -307,8 +307,6 @@ pub async fn register_in_malformed(
#[cfg(test)]
mod test {
- use std::assert_matches;
-
use jiff::Timestamp;
use libeufin_ebics::{
ebics::rand_ebics_id,
@@ -348,10 +346,10 @@ mod test {
format!("{} https://exchange.com/", ShortHashCode::rand()),
] {
let payment = gen_out_pay(subject.clone());
- assert_matches!(
+ assert!(matches!(
gen_initiate(&db, payment.id.e2e_id.clone().unwrap(), subject).await,
PaymentInitiationResult::Success(_)
- );
+ ));
let first = register_outgoing(&db, &payment).await.unwrap();
assert_eq!(
first,
@@ -462,10 +460,10 @@ mod test {
format!("{wtid} https://exchange.com/"),
format!("{wtid} https://exchange.com/"),
] {
- assert_matches!(
+ assert!(matches!(
gen_initiate(&db, rand_ebics_id(), subject).await,
PaymentInitiationResult::Success(_)
- );
+ ));
}
batch_initiated(&db, &Timestamp::now(), "BATCH", false)
.await
@@ -550,7 +548,8 @@ mod test {
)
.await
.unwrap();
- assert_matches!(
+ assert!(
+ matches!(
res,
IncomingBounceRegistrationResult::Success(InResult {
new: true,
@@ -558,7 +557,7 @@ mod test {
completed: false,
pending: false,
ref bounce_id
- }) if bounce_id.as_ref() == Some(&id),
+ }) if bounce_id.as_ref() == Some(&id)),
"{res:?}"
);
// Idempotent
@@ -572,15 +571,17 @@ mod test {
)
.await
.unwrap();
- assert_matches!(
- res,
- IncomingBounceRegistrationResult::Success(InResult {
- new: false,
- id: _,
- completed: false,
- pending: false,
- ref bounce_id
- }) if bounce_id.as_ref() == Some(&id),
+ assert!(
+ matches!(
+ res,
+ IncomingBounceRegistrationResult::Success(InResult {
+ new: false,
+ id: _,
+ completed: false,
+ pending: false,
+ ref bounce_id
+ }) if bounce_id.as_ref() == Some(&id),
+ ),
"{res:?}"
);
diff --git a/testbench/src/integration.rs b/testbench/src/integration.rs
@@ -17,7 +17,7 @@
* <http://www.gnu.org/licenses/>
*/
-use std::{assert_matches, time::Duration};
+use std::time::Duration;
use anyhow::anyhow;
use axum::http::Method;
@@ -482,7 +482,9 @@ async fn conversion() {
.assert_ok_json::<IncomingHistory>()
.incoming_transactions
.remove(0);
- assert_matches!(tx, IncomingBankTransaction::Reserve { amount, reserve_pub, .. } if amount == converted && reserve_pub == key);
+ assert!(
+ matches!(tx, IncomingBankTransaction::Reserve { amount, reserve_pub, .. } if amount == converted && reserve_pub == key)
+ );
}
// Cashout
@@ -536,7 +538,9 @@ async fn conversion() {
.assert_ok_json::<IncomingHistory>()
.incoming_transactions
.remove(0);
- assert_matches!(tx, IncomingBankTransaction::Reserve { amount, reserve_pub, .. } if amount == converted && reserve_pub == key);
+ assert!(
+ matches!(tx, IncomingBankTransaction::Reserve { amount, reserve_pub, .. } if amount == converted && reserve_pub == key)
+ );
// Bounce
ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")
@@ -587,7 +591,9 @@ async fn conversion() {
.assert_ok_json::<IncomingHistory>()
.incoming_transactions
.remove(0);
- assert_matches!(tx, IncomingBankTransaction::Reserve { amount, reserve_pub, .. } if amount == converted && reserve_pub == key);
+ assert!(
+ matches!(tx, IncomingBankTransaction::Reserve { amount, reserve_pub, .. } if amount == converted && reserve_pub == key)
+ );
// Bounce
ctx.posta("/accounts/exchange/taler-wire-gateway/transfer")