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
234 lines
8.9 KiB
Bash
234 lines
8.9 KiB
Bash
# shellcheck shell=bash
|
|
# Shared behaviour for every per-course run.sh. Sourced, never executed.
|
|
#
|
|
# A course run.sh is a stub: it declares what kind of project it is, then calls
|
|
# course_main. The logic lives here once, because 44 hand-copied runners is
|
|
# precisely how gates and scripts in this fleet have drifted apart before.
|
|
#
|
|
# Contract (see ../../run.sh):
|
|
# --probe report whether this could run HERE, without running it
|
|
# exit 0 runnable | 78 blocked | 79 nothing to run
|
|
# (no args) actually run it
|
|
#
|
|
# A stub calls:
|
|
# course_main --kind <kind> [--entry <path>] [--note <text>] -- "$@"
|
|
# Values are passed as arguments rather than set as globals so that a stub has
|
|
# no assignments a linter has to be told to ignore.
|
|
|
|
readonly COURSE_EXIT_BLOCKED=78
|
|
readonly COURSE_EXIT_NO_CODE=79
|
|
|
|
COURSE_ROOT="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[1]}")")" && pwd)"
|
|
readonly COURSE_ROOT
|
|
|
|
# --- helpers -----------------------------------------------------------------
|
|
|
|
_have() { command -v "$1" > /dev/null 2>&1; }
|
|
|
|
# Report "blocked" with the reason, which is the whole point of the status
|
|
# table: "needs X" is useful, "failed" is not.
|
|
_blocked() {
|
|
printf '%s\n' "$1"
|
|
exit "$COURSE_EXIT_BLOCKED"
|
|
}
|
|
|
|
_no_code() {
|
|
printf '%s\n' "$1"
|
|
exit "$COURSE_EXIT_NO_CODE"
|
|
}
|
|
|
|
_runnable() { printf '%s\n' "$1"; exit 0; }
|
|
|
|
# First tool present wins; otherwise blocked naming all the candidates.
|
|
_require_any() {
|
|
local label="$1"; shift
|
|
local tool
|
|
for tool in "$@"; do
|
|
if _have "$tool"; then
|
|
printf '%s' "$tool"
|
|
return 0
|
|
fi
|
|
done
|
|
_blocked "needs $label (none of: $*)"
|
|
}
|
|
|
|
# Probe-side variant. _require_any prints the tool it found, so probes have to
|
|
# redirect its stdout -- which also swallowed the failure REASON and turned
|
|
# every blocked probe into "unknown". This one returns a status and prints
|
|
# only on failure.
|
|
_probe_any() {
|
|
local label="$1"; shift
|
|
local tool
|
|
for tool in "$@"; do
|
|
_have "$tool" && return 0
|
|
done
|
|
_blocked "needs $label (none of: $*)"
|
|
}
|
|
|
|
_first_file() {
|
|
find "$COURSE_ROOT" -name "$1" -not -path '*/node_modules/*' -print -quit 2>/dev/null
|
|
}
|
|
|
|
# --- probes ------------------------------------------------------------------
|
|
|
|
_probe() {
|
|
case "$COURSE_KIND" in
|
|
none) _no_code "${COURSE_NOTE:-no executable code in this directory}" ;;
|
|
tex)
|
|
_probe_any "a LaTeX toolchain" latexmk pdflatex xelatex
|
|
_runnable "builds ${COURSE_ENTRY:-the report} with latexmk"
|
|
;;
|
|
python)
|
|
_have python3 || _blocked "needs python3"
|
|
[[ -n "$(_first_file '*.py')" ]] || _no_code "no .py files found"
|
|
_runnable "python3 ${COURSE_ENTRY:-the entry script}"
|
|
;;
|
|
node)
|
|
_have node || _blocked "needs node"
|
|
_have npm || _blocked "needs npm"
|
|
_runnable "npm install && npm start"
|
|
;;
|
|
maven)
|
|
_have mvn || _blocked "needs maven"
|
|
_have java || _blocked "needs a JDK"
|
|
_runnable "mvn package"
|
|
;;
|
|
make)
|
|
_have make || _blocked "needs make"
|
|
_probe_any "a C compiler" gcc cc clang
|
|
_runnable "make"
|
|
;;
|
|
dotnet)
|
|
_have dotnet || _blocked "needs the .NET SDK"
|
|
_runnable "dotnet build"
|
|
;;
|
|
unity)
|
|
_have unity-editor || _have unityhub \
|
|
|| _blocked "needs the Unity editor (GUI, not installable on a CI runner)"
|
|
_runnable "opens in the Unity editor"
|
|
;;
|
|
matlab)
|
|
_probe_any "MATLAB or Octave" matlab octave
|
|
_runnable "runs the .m scripts"
|
|
;;
|
|
cxx)
|
|
_probe_any "a C++ compiler" g++ clang++
|
|
[[ -n "$(_first_file '*.cpp')" ]] || _no_code "no .cpp files found"
|
|
_runnable "compiles each standalone .cpp"
|
|
;;
|
|
notebook)
|
|
_have jupyter || _blocked "needs jupyter"
|
|
_runnable "jupyter nbconvert --execute"
|
|
;;
|
|
compose)
|
|
_have docker || _blocked "needs docker"
|
|
docker info > /dev/null 2>&1 || _blocked "docker is installed but the daemon is not running"
|
|
_runnable "docker compose up"
|
|
;;
|
|
*) _blocked "unknown COURSE_KIND '${COURSE_KIND:-unset}'" ;;
|
|
esac
|
|
}
|
|
|
|
# --- runners -----------------------------------------------------------------
|
|
|
|
_run() {
|
|
case "$COURSE_KIND" in
|
|
none) _no_code "${COURSE_NOTE:-nothing to run}" ;;
|
|
tex)
|
|
local tex="${COURSE_ENTRY:-}"
|
|
[[ -n "$tex" ]] || tex="$(_first_file '*.tex')"
|
|
[[ -n "$tex" ]] || _no_code "no .tex found"
|
|
local tool
|
|
tool="$(_require_any 'a LaTeX toolchain' latexmk pdflatex xelatex)"
|
|
# Build into build/, never beside the source. A submitted report.pdf
|
|
# is a tracked deliverable, and a rebuild whose output name happens
|
|
# to match it would silently overwrite what was handed in.
|
|
local dir base
|
|
dir="$(dirname "$tex")"
|
|
base="$(basename "$tex")"
|
|
if [[ $tool == latexmk ]]; then
|
|
( cd "$dir" && latexmk -pdf -interaction=nonstopmode -outdir=build "$base" )
|
|
else
|
|
( cd "$dir" && mkdir -p build \
|
|
&& "$tool" -interaction=nonstopmode -output-directory=build "$base" )
|
|
fi
|
|
printf 'output in %s/build\n' "$dir"
|
|
;;
|
|
python)
|
|
local entry="${COURSE_ENTRY:-}"
|
|
[[ -n "$entry" ]] || entry="$(_first_file 'main.py')"
|
|
[[ -n "$entry" ]] || _no_code "no entry point; see this directory's README"
|
|
( cd "$(dirname "$entry")" && python3 "$(basename "$entry")" )
|
|
;;
|
|
node) ( cd "${COURSE_ENTRY:-$COURSE_ROOT}" && npm install && npm start ) ;;
|
|
maven) ( cd "${COURSE_ENTRY:-$COURSE_ROOT}" && mvn -q package ) ;;
|
|
make) ( cd "${COURSE_ENTRY:-$COURSE_ROOT}" && make ) ;;
|
|
dotnet) ( cd "${COURSE_ENTRY:-$COURSE_ROOT}" && dotnet build ) ;;
|
|
compose) ( cd "${COURSE_ENTRY:-$COURSE_ROOT}" && docker compose up ) ;;
|
|
unity) _blocked "open ${COURSE_ROOT} in the Unity editor; there is no headless path" ;;
|
|
cxx)
|
|
# Each .cpp here is a self-contained assignment with its own main(),
|
|
# so they are compiled individually rather than linked together.
|
|
local cxx src rel out
|
|
cxx="$(_require_any 'a C++ compiler' g++ clang++)"
|
|
# Binaries go to build/, which is gitignored -- compiling in place
|
|
# drops untracked executables next to the sources, and an
|
|
# extensionless binary is not something .gitignore catches by name.
|
|
mkdir -p "$COURSE_ROOT/build"
|
|
while IFS= read -r src; do
|
|
rel="${src#"$COURSE_ROOT"/}"
|
|
out="$COURSE_ROOT/build/$(basename "${rel%.cpp}")"
|
|
# Some files here are exam-answer fragments with no main(), so
|
|
# they cannot be linked. Compile those to an object instead --
|
|
# that still type-checks them, which is the point.
|
|
if grep -qE '\bint[[:space:]]+main[[:space:]]*\(' "$src"; then
|
|
printf 'compiling %s\n' "$rel"
|
|
"$cxx" -O2 -o "$out" "$src"
|
|
else
|
|
printf 'checking %s (no main; -c only)\n' "$rel"
|
|
"$cxx" -O2 -c -o "${out}.o" "$src"
|
|
fi
|
|
done < <(find "$COURSE_ROOT" -name '*.cpp' -not -path '*/node_modules/*' -not -path '*/build/*' | sort)
|
|
printf 'binaries in %s/build\n' "$COURSE_ROOT"
|
|
;;
|
|
notebook)
|
|
_have jupyter || _blocked "needs jupyter"
|
|
local nb
|
|
while IFS= read -r nb; do
|
|
jupyter nbconvert --to notebook --execute --inplace "$nb"
|
|
done < <(find "$COURSE_ROOT" -name '*.ipynb' -not -path '*/.ipynb_checkpoints/*' | sort)
|
|
;;
|
|
matlab)
|
|
local tool
|
|
tool="$(_require_any 'MATLAB or Octave' matlab octave)"
|
|
printf 'run the .m files in %s with %s\n' "$COURSE_ROOT" "$tool"
|
|
;;
|
|
*) printf 'unknown COURSE_KIND: %s\n' "${COURSE_KIND:-unset}" >&2; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
course_main() {
|
|
COURSE_KIND=""
|
|
COURSE_ENTRY=""
|
|
COURSE_NOTE=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--kind) COURSE_KIND="$2"; shift 2 ;;
|
|
--entry) COURSE_ENTRY="$2"; shift 2 ;;
|
|
--note) COURSE_NOTE="$2"; shift 2 ;;
|
|
--) shift; break ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
|
|
case "${1:-}" in
|
|
--probe) _probe ;;
|
|
-h | --help)
|
|
printf 'usage: %s [--probe]\n' "$(basename "$0")"
|
|
printf ' --probe report runnable/blocked/no-code without running\n'
|
|
;;
|
|
"") _run ;;
|
|
*) printf 'unknown option: %s\n' "$1" >&2; return 1 ;;
|
|
esac
|
|
}
|