Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 31 additions & 3 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@
# Repository-wide Cargo defaults: the dev-build footprint (Concordat
# `rust-dev-build-footprint`) and the Rust build standard (Concordat
# `rust-build-defaults`). Cargo auto-discovers this file, so a bare
# `cargo build` gets them. See "Development build footprint" and "Rust build
# standard" in docs/developers-guide.md.
#
# The parallel `rustc` frontend and, on Linux, the `mold` linker are the
# defaults for every development, test, lint and typecheck build. Cargo
# selects a single `rustflags` source rather than merging them: a matching
# `[target.*]` table replaces `[build] rustflags` outright, and an assigned
# `RUSTFLAGS` replaces both. Every flag is therefore repeated in each source,
# and the Makefile and CI compose them into the `RUSTFLAGS` they assign.
# Coverage assigns `RUSTFLAGS` without them on purpose, so a measurement stays
# on the platform linker and the single-threaded frontend. Cranelift is not the
# development backend because the suite does not pass under it; the exception
# is recorded in the developers' guide.
[unstable]
embed-metadata = false

[profile.dev]
opt-level = 1 # default is 0; 1 shrinks a bit without killing compile times
debug = 1 # 2 is “full debuginfo”; 1 is usually enough
debug = 0 # no debug info keeps the target tree small; see the developers' guide
debug-assertions = true
overflow-checks = true
incremental = true
panic = "unwind" # keep nice backtraces in normal dev builds
incremental = false # incremental state is the largest part of a dev target tree
panic = "unwind" # keep unwinding in normal dev builds
strip = "none" # don't strip by default for dev

[build]
rustflags = ["-Zthreads=8"]

# `mold` ships for Linux only, so the linker flag lives in a Linux-only table.
# macOS and Windows fall through to `[build] rustflags` above and keep their
# platform linker.
[target.'cfg(target_os = "linux")']
rustflags = ["-Zthreads=8", "-Clink-arg=-fuse-ld=mold"]
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,12 @@ jobs:
# server with a 60 s startup timeout, and falls back to an uncached
# build, with a `sccache-fallback` warning, if the server will not start.
expect-cache: any
# The build standard (.cargo/config.toml): the parallel frontend and
# the `mold` linker. The exported RUSTFLAGS displaces the Cargo
# configuration's sources, so it restates both after `-D warnings`.
# The Whitaker lint assigns its own value and is unaffected.
install-mold: 'true'
rustflags: -D warnings -Zthreads=8 -Clink-arg=-fuse-ld=mold

- name: Install uv
id: setup-uv
Expand Down Expand Up @@ -542,7 +548,7 @@ jobs:
run: |
# Clean stale pg-embed directories that may conflict with new runs.
find target/ -maxdepth 1 -type d -name 'pg-embed-*' -exec rm -rf {} + 2>/dev/null || true
RUSTFLAGS="-D warnings" cargo nextest run --locked \
RUSTFLAGS="-D warnings -Zthreads=8 -Clink-arg=-fuse-ld=mold" cargo nextest run --locked \
--manifest-path backend/Cargo.toml \
--all-targets --all-features \
-E "$NEXTEST_EXCLUSION"
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/mutation-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,13 @@ jobs:
# it without setting it (issue #464), so a run that omits it lets
# postgresql_embedded generate a different password per process and the
# shared cluster is torn down and re-authenticated repeatedly.
# `.cargo/config.toml` links with `mold` on Linux (the build standard).
# `setup-rust` exports `RUSTFLAGS=-D warnings`, which displaces the
# configuration's `rustflags`, so the first command restores the complete
# value for cargo-mutants.
install-mold: 'true'
setup-commands: |
echo "RUSTFLAGS=-D warnings -Zthreads=8 -Clink-arg=-fuse-ld=mold" >> "$GITHUB_ENV"
echo "POSTGRESQL_VERSION==16.10.0" >> "$GITHUB_ENV"
echo "POSTGRESQL_RELEASES_URL=https://github.com/theseus-rs/postgresql-binaries" >> "$GITHUB_ENV"
echo "PG_PASSWORD=wildside_embedded_test" >> "$GITHUB_ENV"
22 changes: 18 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,21 @@ endef

RUSTFLAGS_STRICT := -D warnings
RUST_FLAGS ?= $(RUSTFLAGS_STRICT)
# The build standard (Concordat `rust-build-defaults`): the parallel `rustc`
# frontend and, on Linux, the `mold` linker. An assigned `RUSTFLAGS` replaces
# every `rustflags` source in `.cargo/config.toml`, so each gate recipe that
# assigns it restates both through `GATE_RUST_FLAGS_ENV`, keeping any inherited
# value (CI's setup-rust exports one). `RUST_FLAGS_ENV` stays free of them for
# Whitaker, which builds its lint driver on a different toolchain that must not
# inherit development flags.
BUILD_HOST_OS := $(shell uname -s)
STANDARD_RUSTFLAGS := -Zthreads=8$(if $(filter Linux,$(BUILD_HOST_OS)), -Clink-arg=-fuse-ld=mold)
# The linker flag is chosen from the host, so a gate that compiles for another
# target would get the wrong one. Refuse that rather than guess; run a
# cross-compiling build with `cargo` directly.
STANDARD_TARGET_GUARD = $(if $(strip $(CARGO_BUILD_TARGET)),$(error the build standard picks `mold` from the host; unset CARGO_BUILD_TARGET, or run cargo directly))
RUST_FLAGS_ENV := RUSTFLAGS="$(RUST_FLAGS)"
GATE_RUST_FLAGS_ENV = $(STANDARD_TARGET_GUARD)RUSTFLAGS="$${RUSTFLAGS:+$$RUSTFLAGS }$(RUST_FLAGS) $(STANDARD_RUSTFLAGS)"
RUSTDOC_FLAGS ?= --cfg docsrs -D warnings

ASYNCAPI_CLI_VERSION ?= 3.4.2
Expand Down Expand Up @@ -279,7 +293,7 @@ lint-python:
$(PYLINT) $(PYLINT_TARGETS)

lint-architecture:
$(RUST_FLAGS_ENV) cargo run -p architecture-lint --quiet
$(GATE_RUST_FLAGS_ENV) cargo run -p architecture-lint --quiet

# Lint AsyncAPI spec if present. Split to keep `lint` target concise per checkmake rules.
lint-asyncapi:
Expand Down Expand Up @@ -356,10 +370,10 @@ docs-check: deps
test: test-rust test-frontend test-workflow-contracts test-scripts test-lint-actions

test-rust: workspace-sync prepare-pg-worker
PG_EMBEDDED_WORKER=$(PG_WORKER_PATH) NEXTEST_TEST_THREADS=$(NEXTEST_TEST_THREADS) $(RUST_FLAGS_ENV) cargo nextest run --workspace --all-targets --all-features --no-fail-fast \
PG_EMBEDDED_WORKER=$(PG_WORKER_PATH) NEXTEST_TEST_THREADS=$(NEXTEST_TEST_THREADS) $(GATE_RUST_FLAGS_ENV) cargo nextest run --workspace --all-targets --all-features --no-fail-fast \
-E 'not (binary(declare_test_support_compile_fail) | binary(compile_fail_tests) | binary(sha2_digest_formatting_compile_fail))'
$(RUST_FLAGS_ENV) cargo test -p backend --test declare_test_support_compile_fail --all-features
$(RUST_FLAGS_ENV) cargo test -p backend --test sha2_digest_formatting_compile_fail --all-features
$(GATE_RUST_FLAGS_ENV) cargo test -p backend --test declare_test_support_compile_fail --all-features
$(GATE_RUST_FLAGS_ENV) cargo test -p backend --test sha2_digest_formatting_compile_fail --all-features

test-frontend: deps typecheck
pnpm run test
Expand Down
74 changes: 74 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,80 @@ Four consequences follow from the table.
where its `coverage` job is skipped because `coverage-main.yml` already
covers that branch.

## Rust build standard and development build footprint

Two Concordat rules set the estate's Rust build defaults, and
`.cargo/config.toml` carries both, so a bare `cargo build` gets them.

### Development build footprint

The `rust-dev-build-footprint` rule keeps default development builds small.
`[profile.dev]` sets `incremental = false` and `debug = 0`, so builds keep no
incremental compilation state and no debug information. The trade-off is that a
debugger sees no source-level debug information or line tables in a default
development build and every rebuild recompiles whole crates. To debug, run with
`CARGO_PROFILE_DEV_DEBUG=true`; to rebuild incrementally, set
`CARGO_INCREMENTAL=1`. `[unstable] embed-metadata = false` is the native Cargo
key for dated nightlies from 2026-07-31, so Cargo coordinates metadata output
itself. No raw `rustc` metadata flags are used.

### Rust build standard

The `rust-build-defaults` rule sets the compiler and linker defaults:

- Every `rustflags` source enables the parallel `rustc` frontend with
`-Zthreads=8`.
- The `cfg(target_os = "linux")` source also links with `mold`. macOS and
Windows keep their platform linker. CI installs `mold` through `setup-rust`'s
`install-mold` input, and through the mutation workflow's input of the same
name.

Cargo applies a single `rustflags` source rather than merging them, and an
assigned `RUSTFLAGS` replaces every source. So each source repeats the frontend
flag, and the places that assign `RUSTFLAGS` restate both flags:

- The Makefile's `lint-architecture` and `test-rust` recipes go through
`GATE_RUST_FLAGS_ENV`, which composes `-D warnings`, any inherited
`RUSTFLAGS` and the standard flags. It refuses `CARGO_BUILD_TARGET`, because
the linker flag is chosen from the host; run `cargo` directly to
cross-compile.
- The `build` job's `setup-rust` step exports the same value through its
`rustflags` input, and the Dependabot `Rust tests` step restates it. The
mutation workflow's `setup-commands` writes it to `GITHUB_ENV`, because
`setup-rust` exports `-D warnings` alone there.
- The Whitaker recipes keep `RUST_FLAGS_ENV` (`-D warnings` only), because
Whitaker builds its lint driver on a different toolchain that must not
inherit these flags. The coverage jobs also assign `-D warnings` alone, so a
measurement stays on the platform linker.

The first run after this change misses the compiler cache for every crate,
because the flags are part of the cache key, so it takes as long as a cold
build (about 87 minutes on `ubicloud-standard-4`, within the 120-minute
ceiling). `tests/workflow_contracts/build_standard_test.py` holds the
configuration, the footprint settings, the Makefile composition, the target
refusal and the CI provisioning.

### Cranelift is not the development backend

The Cranelift codegen backend is not part of this repository's standard,
because the repository's own suite does not pass under it. Measured on
2026-10-31 on the pinned `nightly-2026-10-28`, running `make test-rust` with
`CARGO_UNSTABLE_CODEGEN_BACKEND=true`,
`CARGO_PROFILE_DEV_CODEGEN_BACKEND=cranelift` and
`CARGO_PROFILE_TEST_CODEGEN_BACKEND=cranelift` and nothing else changed, a
Cranelift-compiled panic does not find its unwind handler. The run was stopped
at 1,218 of 1,849 tests: 52 tests aborted with `SIGABRT` (for example
`domain::ports::cache_key::tests::coordinate_bound_is_inclusive`,
`domain::offline_bundle_service::tests::upsert_persists_bundle_without_idempotency_key`
and the `inbound::http::offline` and `inbound::http::routes` rejection tests)
and `domain::user::tests::new_panics_when_invalid_id` failed. The same suite
passes on the LLVM backend. The pin is now `nightly-2026-10-31`; the
measurement above was taken on `nightly-2026-10-28` and has not been repeated
on the newer pin, and Dependabot moves the pin daily. Re-measure on the next
deliberate toolchain bump or by 2026-11-30, whichever is first, and adopt the
backend, through `[unstable] codegen-backend` and
`[profile.dev] codegen-backend = "cranelift"`, when the suite passes under it.

## Local Kubernetes preview

Use the repository-local Kubernetes preview when validating the backend image
Expand Down
185 changes: 185 additions & 0 deletions tests/workflow_contracts/build_standard_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
"""Contract tests for the Rust build standard and the dev-build footprint.

The Concordat ``rust-build-defaults`` rule needs the parallel ``rustc`` frontend
in every ``rustflags`` source, ``mold`` in the Linux source only, the gate
recipes that assign ``RUSTFLAGS`` to restate both, and a runner that has the
linker. The ``rust-dev-build-footprint`` rule needs the development profile to
disable incremental compilation and debug information, and the pinned nightly
to take Cargo's native ``embed-metadata = false``. Cranelift is not adopted
(see "Cranelift is not the development backend" in
``docs/developers-guide.md``), so the configuration must stay free of it.

Run via ``make test-workflow-contracts``.
"""

from __future__ import annotations

import os
import re
import subprocess # ruff: ignore[suspicious-subprocess-import] - the contract reads what `make -n` prints.
import tomllib
import typing as typ
from pathlib import Path
from shutil import which

import pytest
import workflow_inventory as inv

ROOT = Path(__file__).resolve().parents[2]
THREADS = "-Zthreads=8"
LINKER_FLAG = "-Clink-arg=-fuse-ld=mold"
LINUX_TABLE = 'cfg(target_os = "linux")'
GATE_TARGETS = ("lint-architecture", "test-rust")
STANDARD_FLAGS = f"-D warnings {THREADS} {LINKER_FLAG}"


def _config() -> dict[str, typ.Any]:
return tomllib.loads((ROOT / ".cargo" / "config.toml").read_text("utf-8"))


def test_every_rustflags_source_carries_the_parallel_frontend() -> None:
"""``[build]`` and the Linux target table both enable ``-Zthreads=8``."""
config = _config()
assert THREADS in config["build"]["rustflags"]
assert THREADS in config["target"][LINUX_TABLE]["rustflags"]


def test_the_linker_is_configured_for_linux_only() -> None:
"""The linker flag is in the Linux table and in no other source."""
config = _config()
assert LINKER_FLAG in config["target"][LINUX_TABLE]["rustflags"]
assert LINKER_FLAG not in config["build"]["rustflags"]
others = [name for name in config["target"] if name != LINUX_TABLE]
assert not others, f"unexpected target tables {others}"


def test_cranelift_stays_out_of_the_configuration() -> None:
"""The recorded exception holds while the configuration omits the backend."""
config = _config()
assert "codegen-backend" not in config.get("unstable", {})
assert "codegen-backend" not in config.get("profile", {}).get("dev", {})


def test_the_development_profile_keeps_the_footprint_settings() -> None:
"""The dev profile disables incremental state and debug information."""
dev = _config()["profile"]["dev"]
assert dev.get("incremental") is False, "profile.dev must set incremental = false"
debug = dev.get("debug")
# Python equality would also accept ``0.0``, which Cargo rejects, so test the
# type: the exact integer ``0`` or the boolean ``false``.
is_zero_integer = (
isinstance(debug, int) and not isinstance(debug, bool) and debug == 0
)
assert debug is False or is_zero_integer, "profile.dev must set debug = 0"


def test_the_configuration_disables_metadata_embedding_through_cargo() -> None:
"""The native Cargo key is set, the old key and raw rustc flags are absent."""
config = _config()
unstable = config.get("unstable", {})
assert unstable.get("embed-metadata") is False
assert "no-embed-metadata" not in unstable
flags = [*config["build"]["rustflags"], *config["target"][LINUX_TABLE]["rustflags"]]
assert not any("embed-metadata" in flag for flag in flags)


def _make_dry_run(
target: str, host_os: str, env: dict[str, str] | None = None
) -> subprocess.CompletedProcess[str]:
"""Run ``make -n`` for ``target`` as if on ``host_os``."""
environment = {k: v for k, v in os.environ.items() if k != "CARGO_BUILD_TARGET"}
environment.update(env or {})
make = which("make")
assert make is not None, "make must be installed to read the recipes"
return subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true] - a resolved `make -n` over this repository.
[make, "-n", "-s", f"BUILD_HOST_OS={host_os}", target],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
env=environment,
)


def _recipe_commands(target: str, host_os: str) -> str:
"""Return the commands ``make -n`` prints for ``target`` on ``host_os``."""
completed = _make_dry_run(target, host_os)
assert completed.returncode == 0, completed.stderr
return completed.stdout


@pytest.mark.parametrize("target", GATE_TARGETS)
def test_gate_recipes_restate_the_standard_flags_on_linux(target: str) -> None:
"""A recipe that assigns ``RUSTFLAGS`` restates the frontend and the linker."""
commands = _recipe_commands(target, "Linux")
assert "RUSTFLAGS=" in commands
assert STANDARD_FLAGS in commands


@pytest.mark.parametrize("target", GATE_TARGETS)
def test_gate_recipes_leave_the_linker_off_other_platforms(target: str) -> None:
"""Off Linux the recipe keeps the frontend flag and drops the linker."""
commands = _recipe_commands(target, "Darwin")
assert f"-D warnings {THREADS}" in commands
assert LINKER_FLAG not in commands


def test_gate_recipes_keep_an_inherited_rustflags() -> None:
"""The recipe appends to an inherited value instead of replacing it."""
commands = _recipe_commands("test-rust", "Linux")
assert re.search(r'RUSTFLAGS="\$\{RUSTFLAGS:\+\$RUSTFLAGS \}', commands)


@pytest.mark.parametrize("target", GATE_TARGETS)
def test_gate_recipes_refuse_a_cross_target_invocation(target: str) -> None:
"""The linker flag comes from the host, so a target override is refused."""
completed = _make_dry_run(
target, "Linux", env={"CARGO_BUILD_TARGET": "x86_64-pc-windows-gnu"}
)
assert completed.returncode != 0
assert "picks `mold` from the host" in completed.stderr


def test_whitaker_keeps_its_own_flag_boundary() -> None:
"""The Whitaker driver does not inherit the development flags."""
commands = _recipe_commands("lint-whitaker", "Linux")
whitaker = [line for line in commands.splitlines() if "whitaker" in line]
assert whitaker, "lint-whitaker must run Whitaker"
assert all(THREADS not in line and LINKER_FLAG not in line for line in whitaker)


def _build_job() -> dict[str, typ.Any]:
return inv.load_workflow("ci.yml")["jobs"]["build"]


def _setup_rust_step(job: dict[str, typ.Any]) -> dict[str, typ.Any]:
steps = [
step
for step in job["steps"]
if "/actions/setup-rust@" in str(step.get("uses", ""))
]
assert len(steps) == 1, "the build job must run setup-rust once"
return steps[0]


def test_the_build_job_installs_the_linker_and_exports_the_standard_flags() -> None:
"""setup-rust exports the complete flags, so cargo steps keep the standard."""
inputs = _setup_rust_step(_build_job())["with"]
assert str(inputs.get("install-mold")) == "true"
assert inputs.get("rustflags") == STANDARD_FLAGS


def test_the_dependabot_test_step_restates_the_standard_flags() -> None:
"""The step that assigns ``RUSTFLAGS`` itself carries the same flags."""
steps = [step for step in _build_job()["steps"] if step.get("name") == "Rust tests"]
assert len(steps) == 1
assert f'RUSTFLAGS="{STANDARD_FLAGS}" cargo nextest run' in steps[0]["run"]


def test_the_mutation_workflow_installs_the_linker_and_restores_the_flags() -> None:
"""``setup-rust`` exports ``-D warnings`` alone, so the flags are restored."""
job = inv.load_workflow("mutation-testing.yml")["jobs"]["mutation"]
assert str(job["with"].get("install-mold")) == "true"
assert (
f'RUSTFLAGS={STANDARD_FLAGS}" >> "$GITHUB_ENV"' in job["with"]["setup-commands"]
)
Loading
Loading