Files
dtourolle 84fade99ec Put the developer docs under docs/dev and index the folder for users first
docs/ had 26 developer documents flat beside the manual, and the two
audiences are very differently sized: most readers want the manual and
the gesture reference, a few want the register, the designs and the
measurements. The manual and gestures.md stay at the top; everything for
someone changing the code moves to docs/dev/, and the two documents that
name their own successors — the v0.1 milestone and the UI-refinement plan
— go to docs/dev/archive/ rather than being deleted, since both are still
cited. docs/README.md is the index, users first.

Every reference follows: code comments, Cargo manifests, the workflows,
the pre-commit hook, the bench and traceability tools (which locate the
repo root by docs/dev/requirements.md now), packaging, the Docker READMEs,
CLAUDE.md, CONTRIBUTING.md and the README. The matrix links one level
deeper and is regenerated. Links out of the moved documents into the tree
gain a level; a link checker over every Markdown file finds none broken.
2026-09-20 21:16:03 +02:00

215 lines
7.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Run what .gitea/workflows/ runs, on this machine.
#
# ./tools/ci-local.sh # every job
# ./tools/ci-local.sh desktop # one job
# ./tools/ci-local.sh desktop layering # several
# ./tools/ci-local.sh --list
#
# Jobs: desktop, android, layering, traceability.
#
# Fidelity and its limits. The desktop, layering and traceability jobs run
# natively rather than inside catthehacker/ubuntu:act-latest, which is honest
# only because the host toolchain is pinned to the same 1.92.0 CI installs —
# the check below refuses to run otherwise, since a clippy lint set that drifts
# between here and CI makes a green local run worthless. What this cannot catch
# is a missing *system* library: CI apt-installs libfontconfig and libxkbcommon
# into a minimal image, so a build that only succeeds here because the
# development box has some other -dev package will still break CI. The android
# job does run in its container, because nothing about a cross-compile
# reproduces natively.
#
# Also unmodelled: actions/checkout (this runs against the working tree, dirty
# or not — that is the point) and actions/cache (the host cargo cache stands in,
# which makes repeat runs far faster than CI's).
set -uo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${REPO}"
RUST_PIN="1.92.0"
ALL_JOBS=(desktop android layering traceability)
if [[ "${1:-}" == "--list" ]]; then
printf '%s\n' "${ALL_JOBS[@]}"
exit 0
fi
if [[ $# -gt 0 ]]; then
JOBS=("$@")
for j in "${JOBS[@]}"; do
if [[ ! " ${ALL_JOBS[*]} " == *" ${j} "* ]]; then
echo "error: unknown job '${j}'; try --list" >&2
exit 2
fi
done
else
JOBS=("${ALL_JOBS[@]}")
fi
# Colour only when a terminal is watching; a redirected log stays plain.
if [[ -t 1 ]]; then
B=$'\e[1m'; R=$'\e[31m'; G=$'\e[32m'; Y=$'\e[33m'; Z=$'\e[0m'
else
B=''; R=''; G=''; Y=''; Z=''
fi
RESULTS=()
FAILED=0
step() { printf '\n%s==> %s%s\n' "${B}" "$*" "${Z}"; }
# A step's failure is recorded and the job carries on, the way separate CI
# steps would not — but one local run that reports every problem beats four
# runs each finding the next one.
record() {
local name="$1" code="$2"
if [[ "${code}" -eq 0 ]]; then
RESULTS+=("${G}pass${Z} ${name}")
else
RESULTS+=("${R}FAIL${Z} ${name}")
FAILED=1
fi
}
run() {
local name="$1"; shift
step "${name}: $*"
"$@"
record "${name}" "$?"
}
# CI pins the toolchain so an unrelated push cannot fail on a compiler that
# moved under it. A local run on a different rustc is measuring a different
# thing, and silently so.
check_toolchain() {
local have
have="$(rustc --version | awk '{print $2}')"
if [[ "${have}" != "${RUST_PIN}" ]]; then
printf '%swarning: rustc %s, CI pins %s — clippy and fmt may disagree%s\n' \
"${Y}" "${have}" "${RUST_PIN}" "${Z}" >&2
fi
}
job_desktop() {
check_toolchain
run "desktop/fmt" cargo fmt --all -- --check
run "desktop/clippy" cargo clippy --workspace --all-targets -- -D warnings
run "desktop/test" cargo test --workspace
run "desktop/build" cargo build --workspace --release
}
# The one job that genuinely needs the container: it is a cross-compile, and
# the NDK, the linker and MIN_API all live in the image.
job_android() {
if ! command -v podman >/dev/null 2>&1 && ! command -v docker >/dev/null 2>&1; then
RESULTS+=("${Y}skip${Z} android (no podman or docker)")
return
fi
step "android: cross-compile core"
./docker/android/build.sh \
cargo check -p dr-types -p dr-gpu -p dr-sync --target aarch64-linux-android
record "android/check" "$?"
step "android: verify minimum API level"
./docker/android/build.sh cargo ndk -t arm64-v8a build -p dr-gpu --release
record "android/ndk-build" "$?"
# Asserted, not reported — and looked up under the target triple.
#
# The workflow this mirrors searches the whole target dir for the first
# `*.so` and reads its .comment. Under target-android/debug/deps sit the
# host proc-macro libraries, which are x86-64 ELF built by the Debian gcc,
# so the step happily prints a comment section that says nothing about
# Android and passes. A wrong MIN_API is invisible until a device refuses
# to install, which is exactly the failure the step exists to catch, so it
# is worth failing on here.
local triple_dir so api
triple_dir="${XDG_CACHE_HOME:-${HOME}/.cache}/darkroom-android/target/aarch64-linux-android/release"
so="$(find "${triple_dir}" -maxdepth 1 -name '*.so' -print -quit 2>/dev/null)"
if [[ -z "${so}" ]]; then
echo "no aarch64 .so under ${triple_dir}"
record "android/min-api" 1
return
fi
# MIN_API is the Dockerfile's, read rather than repeated: two copies of the
# number would drift and the drift is the bug being checked for.
local min_api
min_api="$(sed -n 's/^ARG MIN_API=\([0-9]*\).*/\1/p' docker/android/Dockerfile)"
# Both sides must be non-empty before they are compared: two failed parses
# would otherwise satisfy `"" == ""` and report a pass, which is the same
# silent success this check was rewritten to remove.
if [[ -z "${min_api}" ]]; then
echo "FAIL: no ARG MIN_API= in docker/android/Dockerfile"
record "android/min-api" 1
return
fi
echo "checking ${so}"
file "${so}"
api="$(file "${so}" | sed -n 's/.*for Android \([0-9]*\).*/\1/p')"
if [[ -n "${api}" && "${api}" == "${min_api}" ]]; then
record "android/min-api" 0
else
echo "FAIL: linked for Android '${api:-unknown}', expected ${min_api}"
record "android/min-api" 1
fi
}
# ARCH §6.5a. Builds nothing: `cargo tree` only resolves the graph.
job_layering() {
step "layering: core crates must not depend on the UI"
local failed=0
for crate in dr-types dr-gpu dr-sync; do
if cargo tree -p "${crate}" -e normal 2>/dev/null | grep -qE '\bslint\b|\bi-slint'; then
echo "FAIL: ${crate} depends on Slint (ARCH §6.5a)"
failed=1
else
echo "ok: ${crate}"
fi
done
record "layering" "${failed}"
}
job_traceability() {
run "traceability/self-test" cargo test -p traceability
run "traceability/gate" cargo run -q -p traceability -- check
# Compared against the working tree, not against HEAD.
#
# CI asks `git diff --quiet docs/dev/traceability.md` after regenerating, which
# is right there and wrong here: CI starts from a clean checkout, so the
# only diff it can see is the one regeneration introduced. Locally the file
# is usually already modified for honest reasons — an uncommitted feature
# brings new TRACES tags with it — and the git form would then report the
# matrix as stale on every run, including the runs where it is perfectly
# up to date. Snapshotting first asks the question CI means to ask: does
# regenerating change anything?
step "traceability: matrix is up to date"
local before
before="$(mktemp)"
cp docs/dev/traceability.md "${before}"
cargo run -q -p traceability -- report
if diff -q "${before}" docs/dev/traceability.md >/dev/null; then
record "traceability/matrix" 0
else
echo "docs/dev/traceability.md was stale; regenerating changed it:"
diff --stat "${before}" docs/dev/traceability.md 2>/dev/null \
|| diff "${before}" docs/dev/traceability.md | head -20
record "traceability/matrix" 1
fi
rm -f "${before}"
}
for job in "${JOBS[@]}"; do
printf '\n%s──────── %s ────────%s\n' "${B}" "${job}" "${Z}"
"job_${job}"
done
printf '\n%s──────── summary ────────%s\n' "${B}" "${Z}"
printf '%s\n' "${RESULTS[@]}"
exit "${FAILED}"