mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 20:47:09 +01:00
Executing coursework has silently overwritten a submitted deliverable twice in
this campaign -- TRAK's outputs/output.png and SPD's results_*.png -- and both
times it was found by a later `git status`, not by the run. `course_main` now
snapshots what git sees under the course root before and after `_run`, and
fails with a diff naming the files if anything moved. Gitignored build output
does not show up there, which is why every runner already writes into build/.
Seen failing before being trusted: a stub whose `_run` appends a newline to
TRAK's config.ini exits 1 and names the file; the same stub without the append
exits 0; and a real `./run.sh run Programming/TRAK` still exits 0 after
rendering, because it writes into build/.
The notebook branch was the specific landmine. `nbconvert --execute --inplace`
rewrites the notebook it just ran, and all four notebooks here are tracked with
their outputs. It now executes to a temp --output-dir. That alone is not
enough, though: nbconvert runs the kernel with its cwd set to the notebook's
own directory, so a cell calling plt.savefig('x.png') still writes beside the
notebook. The tree check above is what actually catches that, which is why it
lives in course_main and applies to every course kind rather than to notebooks.
`_have jupyter` was also the wrong probe. jupyter on this machine is a pipx
shim whose interpreter is gone: `command -v` finds it, and running it exits
127. Both the probe and the runner now use `_works`, which runs `--version`.
`./Programming/twm_4/run.sh --probe` reports "needs jupyter" and exit 78
instead of failing confusingly mid-run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
208 lines
9.9 KiB
Bash
208 lines
9.9 KiB
Bash
# shellcheck shell=bash
|
|
# Split out of course_lib.sh, which was over this repo's 250-line cap. Sourced
|
|
# by it, never on its own -- the helpers and constants it uses live there.
|
|
#
|
|
# Answers a bare invocation: actually build or run the course.
|
|
|
|
|
|
# Use the course venv when one exists, otherwise the system interpreter.
|
|
_python_bin() {
|
|
if [[ -x "$COURSE_ROOT/.venv/bin/python" ]]; then
|
|
printf '%s' "$COURSE_ROOT/.venv/bin/python"
|
|
else
|
|
printf 'python3'
|
|
fi
|
|
}
|
|
|
|
# Dependencies are the runner's job, not the reader's. Without this, EARIN
|
|
# dies on ModuleNotFoundError for gymnasium and the "runnable" status is a lie.
|
|
_python_install_requirements() {
|
|
local reqs=()
|
|
while IFS= read -r r; do reqs+=("$r"); done < <(
|
|
find "$COURSE_ROOT" -name 'requirements*.txt' -not -path '*/.venv/*' | sort)
|
|
[[ ${#reqs[@]} -gt 0 ]] || return 0
|
|
|
|
if [[ ! -x "$COURSE_ROOT/.venv/bin/python" ]]; then
|
|
printf 'creating .venv\n'
|
|
python3 -m venv "$COURSE_ROOT/.venv"
|
|
fi
|
|
"$COURSE_ROOT/.venv/bin/python" -m pip install --quiet --upgrade pip
|
|
|
|
# EVERY requirements file, not just the first: EARIN is six independent
|
|
# labs plus a project, and installing only one of them left the lab it
|
|
# then tried to run missing gymnasium.
|
|
local req
|
|
for req in "${reqs[@]}"; do
|
|
printf 'installing %s\n' "${req#"$COURSE_ROOT"/}"
|
|
"$COURSE_ROOT/.venv/bin/python" -m pip install --quiet -r "$req" || {
|
|
printf 'warning: some requirements in %s failed\n' "${req#"$COURSE_ROOT"/}" >&2
|
|
}
|
|
done
|
|
}
|
|
|
|
# Where a course ships real tests, running them is a stronger check than
|
|
# running the program -- and for an interactive program (ECRYPT_PROJECT prompts
|
|
# on stdin) it is the ONLY check that works without a person at a keyboard.
|
|
_python_run_tests() {
|
|
local suite
|
|
suite="$(find "$COURSE_ROOT" -name 'test_*.py' -not -path '*/.venv/*' -print -quit 2>/dev/null)"
|
|
[[ -n "$suite" ]] || return 0
|
|
local py
|
|
py="$(_python_bin)"
|
|
"$py" -c 'import pytest' 2> /dev/null || {
|
|
printf 'tests present but pytest is not installed; skipping\n'
|
|
return 0
|
|
}
|
|
# Which directory pytest runs FROM is course-specific and cannot be
|
|
# guessed: ECOTE needs program/ so `from translator.main import` resolves,
|
|
# ECRYPT_PROJECT needs sieve/ because its fixtures open P-100000.csv by a
|
|
# relative path. So the stub says, and COURSE_ROOT is only the default.
|
|
local from_dir="${COURSE_TEST_DIR:-$COURSE_ROOT}"
|
|
printf 'running the test suite in %s\n' "${from_dir#"$COURSE_ROOT"/}"
|
|
( cd "$from_dir" && "$py" -m pytest -q )
|
|
}
|
|
|
|
# --- 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")"
|
|
# A document that declares \bibliography but never cites anything
|
|
# makes bibtex exit 2 ("I found no \citation commands"), and
|
|
# latexmk turns that into exit 12 even though the PDF is fine.
|
|
# --no-bibtex says "this one was handed in without a bibliography",
|
|
# which is a fact about the deliverable, not a suppression.
|
|
# An `x && y=1` one-liner would be a set -e landmine here: when x
|
|
# is false the list returns 1 and the whole runner exits.
|
|
local bibflag=()
|
|
if [[ -n "${COURSE_TEX_NO_BIBTEX:-}" ]]; then
|
|
bibflag=(-bibtex-)
|
|
fi
|
|
if [[ $tool == latexmk ]]; then
|
|
( cd "$dir" && latexmk -pdf "${bibflag[@]}" \
|
|
-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:-}"
|
|
# An entry may be given as a directory; resolve it to the module
|
|
# inside. Passing the directory itself makes python look for
|
|
# __main__.py and fail with a message about the wrong thing.
|
|
if [[ -d "$entry" ]]; then
|
|
local cand
|
|
for cand in main.py project.py __main__.py; do
|
|
if [[ -f "$entry/$cand" ]]; then
|
|
entry="$entry/$cand"
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
# Only auto-pick a main.py when there is exactly ONE. A course
|
|
# with six labs has six, and choosing whichever find returns first
|
|
# is not "running the project", it is running an arbitrary lab.
|
|
if [[ ! -f "$entry" ]]; then
|
|
local mains
|
|
mains="$(find "$COURSE_ROOT" -name 'main.py' -not -path '*/.venv/*' | wc -l)"
|
|
[[ "$mains" -eq 1 ]] && entry="$(_first_file 'main.py')"
|
|
fi
|
|
|
|
_python_install_requirements
|
|
|
|
if [[ -f "$entry" ]]; then
|
|
# Word-split on purpose: --entry-args is a fixed string the
|
|
# course stub writes, never user input, and it has to become
|
|
# several argv entries. read -a keeps that explicit.
|
|
local entry_args=()
|
|
if [[ -n "${COURSE_ENTRY_ARGS:-}" ]]; then
|
|
read -r -a entry_args <<< "$COURSE_ENTRY_ARGS"
|
|
fi
|
|
( cd "$(dirname "$entry")" \
|
|
&& "$(_python_bin)" "$(basename "$entry")" "${entry_args[@]}" )
|
|
else
|
|
# A pile of one-shot scripts with no entry contract (NLP is
|
|
# the case here). Syntax-check them all rather than picking one
|
|
# arbitrarily and calling that "running the project".
|
|
printf 'no single entry point; syntax-checking every module instead\n'
|
|
find "$COURSE_ROOT" -name '*.py' -not -path '*/.venv/*' -print0 \
|
|
| xargs -0 -r "$(_python_bin)" -m py_compile
|
|
printf 'all modules compile\n'
|
|
fi
|
|
_python_run_tests
|
|
;;
|
|
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"/}"
|
|
if _is_known_incomplete "${src#"${COURSE_GATES%/gates}"/}"; then
|
|
printf 'skipping %s (see gates/known-incomplete.txt)\n' "$rel"
|
|
continue
|
|
fi
|
|
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)
|
|
_works jupyter || _blocked "needs jupyter"
|
|
# NOT --inplace: all four notebooks in this repo are tracked WITH
|
|
# their outputs, so executing in place rewrites a deliverable.
|
|
# --output-dir sends the executed copy to a temp dir instead.
|
|
# It does NOT redirect what the notebook's own code writes -- the
|
|
# kernel's cwd is still the notebook's directory -- so the
|
|
# before/after tree check in course_main is what actually catches
|
|
# a notebook that saves a figure beside itself.
|
|
local nb nbtmp
|
|
nbtmp="$(mktemp -d)"
|
|
while IFS= read -r nb; do
|
|
jupyter nbconvert --to notebook --execute --output-dir "$nbtmp" "$nb"
|
|
done < <(find "$COURSE_ROOT" -name '*.ipynb' -not -path '*/.ipynb_checkpoints/*' | sort)
|
|
rm -rf "$nbtmp"
|
|
;;
|
|
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
|
|
}
|
|
|