mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 18:07:14 +01:00
Two invariants had been established by hand and left ungated, which is the
"a remedy that only works if someone remembers" pattern:
- scripts/check_ignore_consistency.sh fails the commit if any TRACKED file
is also gitignored. .binary-allowlist used to *instruct* the reader to
re-run that check; now it is enforced. Verified both ways -- passes on the
tree, and fails when an ignored file is force-added.
DOCS-runnability.md now really has two columns. Each environment writes only
its own gates/runnability-<env>.tsv and the doc renders from both, so a CI run
cannot clobber the local results. The ci tsv is gitignored: committing a copy
generated here would label this machine's capabilities as the runner's.
Rendering it surfaced a classic bug -- awk's NR==FNR idiom silently swallows
the SECOND file when the first is empty, so the table came out with a header
and no rows. It compares FILENAME instead.
runnability.yml deliberately does NOT diff against a committed baseline. A
status is a property of the machine, so `runnable` locally and `blocked` on a
runner is correct rather than drift, and diffing would make the workflow red
by construction. It fails on `error` instead -- a runner that crashes or
reports an unknown kind.
Then five more courses were actually run, because EADS alone had produced four
real bugs and one sample proves nothing:
- ECOAR and EPFU build (nasm + 32-bit gcc, and gcc respectively).
- ECOTE's 17 test modules could not run AT ALL. Its package is named `code/`,
which shadows the standard library: pytest's debugging plugin imports pdb,
pdb imports code, and it got this directory instead, so the suite died
with an INTERNALERROR before collecting a single test. Renamed to
translator/ with all 17 imports updated -- 26 tests now pass.
- A LaTeX report builds. That one exposed a hazard rather than a bug:
latexmk wrote its PDF beside the source, and in a course where the built
name matches the submitted one it would have silently overwritten a
tracked deliverable. Both the LaTeX and C++ runners now build into a
gitignored build/, and rebuilding ECOGR leaves `git status` empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
171 lines
6.3 KiB
Bash
Executable File
171 lines
6.3 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# ============================================================================
|
|
# run.sh -- drive this coursework archive.
|
|
#
|
|
# ./run.sh check run the full local gate (pre-commit, all files)
|
|
# ./run.sh install alias for ./install.sh
|
|
# ./run.sh status probe every course dir, rewrite DOCS-runnability.md
|
|
# ./run.sh run <dir> actually execute one course project
|
|
# ./run.sh sync-vendored show the mirrors that gates/vendored.txt feeds
|
|
#
|
|
# `status` PROBES: it asks each directory whether its toolchain is present and
|
|
# never executes the project, so it takes seconds and is safe in CI. Use
|
|
# `run <dir>` to actually run something. Those are very different commands and
|
|
# conflating them is how a "status check" turns into 44 builds.
|
|
# ============================================================================
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
readonly SCRIPT_DIR
|
|
readonly RUNNABILITY_DOC="${SCRIPT_DIR}/DOCS-runnability.md"
|
|
|
|
# Exit codes a per-directory run.sh uses to report itself.
|
|
readonly EXIT_BLOCKED=78
|
|
readonly EXIT_NO_CODE=79
|
|
|
|
log() { printf '==> %s\n' "$*"; }
|
|
err() { printf 'error: %s\n' "$*" >&2; }
|
|
|
|
usage() {
|
|
grep -E '^# \./run\.sh' "${BASH_SOURCE[0]}" | sed 's/^# //'
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
course_dirs() {
|
|
find "${SCRIPT_DIR}/Programming" "${SCRIPT_DIR}/NotProgramming" \
|
|
-mindepth 1 -maxdepth 1 -type d | sort
|
|
}
|
|
|
|
cmd_check() {
|
|
log "running the full gate"
|
|
( cd "$SCRIPT_DIR" && pre-commit run --all-files )
|
|
}
|
|
|
|
cmd_install() { exec "${SCRIPT_DIR}/install.sh" "$@"; }
|
|
|
|
# A runner opts in either by handling --probe itself, or by delegating to the
|
|
# shared library (which does). Anything else predates this harness and is left
|
|
# strictly alone.
|
|
_supports_probe() {
|
|
grep -qF -- '--probe' "$1" || grep -qF 'course_lib.sh' "$1"
|
|
}
|
|
|
|
# Probe one directory. Prints "<status>\t<detail>"; never executes a project.
|
|
#
|
|
# The guard below is load-bearing. Some course dirs already ship their own
|
|
# run.sh from years ago -- Programming/PSD/run.sh installs packages with sudo
|
|
# and starts Kafka. Calling one of those with an unknown --probe flag does not
|
|
# probe anything, it RUNS the project. So a runner is only invoked if it
|
|
# actually declares --probe; anything else is reported unprobed and left alone.
|
|
# `timeout` is defence in depth for a script that declares the flag and then
|
|
# blocks on input anyway.
|
|
probe_one() {
|
|
local dir="$1" runner="$1/run.sh"
|
|
if [[ -x "$runner" ]] && _supports_probe "$runner"; then
|
|
local out status
|
|
out="$(timeout 20s "$runner" --probe 2> /dev/null)" && status=0 || status=$?
|
|
case "$status" in
|
|
0) printf 'runnable\t%s\n' "${out:-ok}" ;;
|
|
"$EXIT_BLOCKED") printf 'blocked\t%s\n' "${out:-unknown}" ;;
|
|
"$EXIT_NO_CODE") printf 'no-code\t%s\n' "${out:-unknown}" ;;
|
|
*) printf 'error\t%s\n' "probe exited $status" ;;
|
|
esac
|
|
return 0
|
|
fi
|
|
if [[ -x "$runner" ]]; then
|
|
printf 'unprobed\tpre-existing run.sh, no --probe support\n'
|
|
else
|
|
printf 'unprobed\tno run.sh yet\n'
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
local env_label="${CI:+ci}"
|
|
env_label="${env_label:-local}"
|
|
local store="${SCRIPT_DIR}/gates/runnability-${env_label}.tsv"
|
|
log "probing course directories (${env_label})"
|
|
|
|
local dir rel line
|
|
: > "$store"
|
|
while IFS= read -r dir; do
|
|
rel="${dir#"${SCRIPT_DIR}"/}"
|
|
line="$(probe_one "$dir")"
|
|
printf '%s\t%s\n' "$rel" "$line" >> "$store"
|
|
done < <(course_dirs)
|
|
|
|
# Each environment writes only its OWN file, then the doc is rendered from
|
|
# both. Writing a single shared column would mean a CI run silently
|
|
# overwrote the local results with its own -- and the whole point of
|
|
# recording per environment is that "runnable here, blocked on a runner"
|
|
# is the normal case rather than drift.
|
|
write_runnability_doc
|
|
log "wrote ${RUNNABILITY_DOC##*/}"
|
|
awk -F'\t' '{ n[$2]++ } END { for (k in n) printf " %-10s %d\n", k, n[k] }' "$store"
|
|
}
|
|
|
|
write_runnability_doc() {
|
|
local local_tsv="${SCRIPT_DIR}/gates/runnability-local.tsv"
|
|
local ci_tsv="${SCRIPT_DIR}/gates/runnability-ci.tsv"
|
|
[[ -f "$local_tsv" ]] || : > "$local_tsv"
|
|
[[ -f "$ci_tsv" ]] || : > "$ci_tsv"
|
|
|
|
{
|
|
cat <<'MD'
|
|
# Runnability
|
|
|
|
Generated by `./run.sh status`. Do not edit by hand.
|
|
|
|
Status is recorded PER ENVIRONMENT, from `gates/runnability-local.tsv` and
|
|
`gates/runnability-ci.tsv`. A directory that is `runnable` locally and
|
|
`blocked` in CI is the normal case, not drift: the runner has no MATLAB, no
|
|
Unity and no .NET SDK. Each environment rewrites only its own file, so a CI
|
|
run cannot clobber the local column.
|
|
|
|
`-` means that environment has not probed yet.
|
|
|
|
MD
|
|
printf '| Course | local | ci | Detail (local) |\n|---|---|---|---|\n'
|
|
# Compare FILENAME rather than using the NR==FNR idiom: with an EMPTY
|
|
# first file awk never reads it, so NR==FNR stays true for every row
|
|
# of the second file and the whole table comes out blank.
|
|
awk -F'\t' -v cifile="$ci_tsv" '
|
|
FILENAME == cifile { ci[$1] = $2; next }
|
|
{ printf "| `%s` | %s | %s | %s |\n", $1, $2, ($1 in ci ? ci[$1] : "-"), $3 }
|
|
' "$ci_tsv" "$local_tsv"
|
|
} > "$RUNNABILITY_DOC"
|
|
}
|
|
|
|
cmd_run() {
|
|
local target="${1:-}"
|
|
[[ -n "$target" ]] || { err "run needs a directory"; usage 1; }
|
|
local runner="${SCRIPT_DIR}/${target}/run.sh"
|
|
[[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; }
|
|
log "running $target"
|
|
"$runner"
|
|
}
|
|
|
|
cmd_sync_vendored() {
|
|
log "gates/vendored.txt declares:"
|
|
grep -vE '^\s*#|^\s*$' "${SCRIPT_DIR}/gates/vendored.txt" | sed 's/^/ /'
|
|
printf '\nMirrored into:\n'
|
|
printf ' ~/utils/file_length/_tables.py VENDORED_SUBPATHS["WUT_Computer_Science"]\n'
|
|
printf ' pyproject.toml [tool.ruff] exclude (Milestone 4)\n'
|
|
printf ' pyproject.toml [tool.coverage.run] omit (Milestone 4)\n'
|
|
}
|
|
|
|
main() {
|
|
case "${1:-}" in
|
|
check) shift; cmd_check "$@" ;;
|
|
install) shift; cmd_install "$@" ;;
|
|
status) shift; cmd_status "$@" ;;
|
|
run) shift; cmd_run "$@" ;;
|
|
sync-vendored) shift; cmd_sync_vendored "$@" ;;
|
|
-h | --help | "") usage ;;
|
|
*) err "unknown command: $1"; usage 1 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|