WUT_Computer_Science/scripts/course_exec.sh
Krzysztof kuhy Rudnicki 42fa8228b0
Some checks failed
md-naming / markdown naming convention (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
runnability / runnability (push) Has been cancelled
Keep going past a broken target, and run what is already built
Four follow-ups to the previous commit, three of them cases where the new
runner still did the thing it was written to stop doing.

A build that was already up to date ran nothing. `make` says "up to date" on
the second invocation, no executable changes mtime, the before/after snapshot
diff is empty -- and the runner printed a line and stopped, which is exactly
the complaint that started this. It now falls back to running the executables
that are there. Proved with a scratch course whose makefile has a real file
target, because both make courses in this repo rebuild unconditionally and
neither can reach the branch.

One broken target aborted the rest. `run <dir> all` stopped at the first
failure under set -e, so ELAC's second document was never attempted. Failures
are collected per target and reported together at the end instead.

An index was ignored when a course had one target: `run ECOAR 5` silently ran
target 1. Selections are validated before the single-target shortcut now.

And "ECOAR has 1 targets" read like a bug in the tool.

That last fix then falsified a claim in TODO-refactor-remaining.md. It said
ELAC's projectA and AIS's report/final/ver1 "fail with latexmk exit 12" and
were broken documents. With build/ deleted first, both courses build every
document and exit 0. The exit 12 reproduced once against pre-existing build
state and never from clean; the note now says that, and says which part is
inference rather than re-verified, because the original failing state is gone.

All 44 probes still produce a byte-identical runnability-local.tsv.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-29 11:04:09 +02:00

130 lines
4.8 KiB
Bash

# shellcheck shell=bash
# Running what the build produced. Sourced by course_lib.sh, never run.
#
# Building is not running. `./run.sh run EPFU` used to print one `gcc` line and
# stop, leaving you to find `a.out` yourself -- reported by kuhy on 2026-08-29:
# "I run run.sh and I have no idea where the actual program is and how to run
# it". Every kind that produces an executable now executes it and says so.
#
# Interactive by default: you typed `run`, so you are at the keyboard, and
# EPFU's penguins and ECOAR's C both read stdin. Under --batch stdin is
# /dev/null and a timeout applies, so an unattended run cannot wedge. --probe
# never reaches this file at all.
COURSE_EXEC_FAILURES=0
readonly COURSE_TIMEOUT_EXIT=124
_batch_timeout() { printf '%s' "${COURSE_BATCH_TIMEOUT:-60}"; }
_exec_note() {
local rc="$1" what="$2"
if [[ "$rc" -eq 0 ]]; then
return 0
fi
COURSE_EXEC_FAILURES=$((COURSE_EXEC_FAILURES + 1))
if [[ "$rc" -eq "$COURSE_TIMEOUT_EXIT" && -n "${COURSE_BATCH:-}" ]]; then
# Not a failure of the program: --batch gives stdin as /dev/null, and
# an interactive one is supposed to sit there. Say which it was.
printf ' %s: still running after %ss, stopped (batch mode)\n' \
"$what" "$(_batch_timeout)" >&2
else
printf ' %s: exited %s\n' "$what" "$rc" >&2
fi
}
# Run one built artifact, from its own directory so relative data files resolve.
_exec_built() {
local exe="$1" label rc=0
label="$(_target_label "$exe")"
printf '\n--> running %s\n' "$label"
if [[ -n "${COURSE_BATCH:-}" ]]; then
( cd "$(dirname "$exe")" \
&& timeout "$(_batch_timeout)" "$exe" < /dev/null ) || rc=$?
else
( cd "$(dirname "$exe")" && "$exe" ) || rc=$?
fi
_exec_note "$rc" "$label"
}
# Every executable file under a directory, with its mtime, so that "what did
# the build just produce" can be answered by comparing two snapshots. A
# makefile need not say what it writes, and EPFU's does not: `gcc ... main.c`
# with no -o leaves an a.out that nothing mentions.
_executable_snapshot() {
find "$1" -type f -executable \
-not -name '*.sh' -not -name '*.py' -not -name '*.pl' \
-not -path '*/.git/*' -not -path '*/.venv/*' \
-printf '%p\t%T@\n' 2>/dev/null | sort
}
# Run whatever appeared or changed between two snapshots.
_exec_new_binaries() {
local before="$1" after="$2" what="$3"
local new_paths=()
local line path
while IFS= read -r line; do
path="${line%%$'\t'*}"
new_paths+=("$path")
done < <(comm -13 <(printf '%s\n' "$before") <(printf '%s\n' "$after"))
# Nothing new does not mean nothing to run: make says "up to date" on the
# second invocation, and running nothing then is the very complaint this
# file exists to fix. Fall back to whatever executables are there.
if [[ ${#new_paths[@]} -eq 0 ]]; then
while IFS= read -r line; do
[[ -n "$line" ]] || continue
new_paths+=("${line%%$'\t'*}")
done < <(printf '%s\n' "$after")
if [[ ${#new_paths[@]} -eq 0 ]]; then
printf 'nothing to run after %s (it produces no binary)\n' "$what"
return 0
fi
printf 'already up to date after %s; running what is there\n' "$what"
fi
local exe
for exe in "${new_paths[@]}"; do
_exec_built "$exe"
done
}
# `make` in one directory, then run what it produced.
_make_and_run() {
local dir="$1" before after
before="$(_executable_snapshot "$dir")"
( cd "$dir" && make )
after="$(_executable_snapshot "$dir")"
_exec_new_binaries "$before" "$after" "make in $(_target_label "$dir")"
}
# A jar is not self-describing either: pick the one maven just wrote, skipping
# the sources/javadoc siblings that are not runnable.
_run_jar() {
local dir="$1" jar
jar="$(find "$dir" -path '*/target/*.jar' \
-not -name '*-sources.jar' -not -name '*-javadoc.jar' -print -quit 2>/dev/null)"
if [[ -z "$jar" ]]; then
printf 'built, but no runnable jar under %s/target\n' "$(_target_label "$dir")"
return 0
fi
local rc=0
printf '\n--> running %s\n' "$(_target_label "$jar")"
if [[ -n "${COURSE_BATCH:-}" ]]; then
( cd "$dir" && timeout "$(_batch_timeout)" java -jar "$jar" < /dev/null ) || rc=$?
else
( cd "$dir" && java -jar "$jar" ) || rc=$?
fi
_exec_note "$rc" "$(_target_label "$jar")"
}
# A loose C program with no makefile of its own. Same deal as the cxx kind:
# compile into the gitignored build/, then actually run it.
_compile_and_run_c() {
local src="$1" cc out
cc="$(_require_any 'a C compiler' gcc cc clang)"
mkdir -p "$COURSE_ROOT/build"
out="$COURSE_ROOT/build/$(basename "${src%.c}")"
printf 'compiling %s\n' "$(_target_label "$src")"
"$cc" -O2 -Wall -Wextra -o "$out" "$src"
_exec_built "$out"
}