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.
215 lines
7.8 KiB
Bash
Executable File
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}"
|