mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 19:47:32 +01:00
The archive reached zero ruff findings this morning, so the standing rule in
.pre-commit-config.yaml -- a hook lands only once the thing it gates is clean
-- is finally satisfied for ruff. `ruff check` and `ruff format --check` are
hooks now, pinned at v0.16.5 per rules/dependency-freshness, plus
`no-suppressions`, which rejects `# noqa` and `# type: ignore`. CLAUDE.md has
always claimed a hook did that; until now the only thing stopping them was
whoever was reading ruff's output, and I typed `# noqa-free` three times in
this session by accident. mypy stays out: six of its errors are third-party
typing limits rather than our code.
The first attempt used `language: system`, which passed `pre-commit run
--all-files` and then failed the actual commit with "Executable `ruff` not
found" -- git hooks do not get ~/.local/bin on PATH. Taking the hooks from
astral-sh/ruff-pre-commit means pre-commit installs ruff itself, which is also
what makes the pin mean anything.
`ruff format --check` failed the moment it was wired up, on four TRAK files I
wrote today and never formatted. Formatted, and the ray_tracing0 render is
still 02484bdb6b6c1ddeff317d7e5566580f.
Then the run guard from f780d023 needed proving on more than TRAK, since it
now applies to every course kind. make, cpp and python courses pass it; node
and target and obj are all properly gitignored, so npm, maven and dotnet
builds cannot trip it either. But NotProgramming/MOM failed, and not because
of the guard:
`_first_file '*.tex'` used `find -print -quit`, which returns whatever readdir
hands back. After the 19 reports were split into sections, that was
`report_three/sections/02-model-dwukryterialny.tex`, and latexmk faithfully
tried to typeset a bare `\section` as a document. DOCS-runnability.md has
recorded MOM as runnable throughout. The tex branch now picks a file that
actually contains `\documentclass`.
Deliberately not sorted, and that is the interesting part. Sorting is the
obvious fix and it made things worse: it swapped ELAC's pick to projectA and
AIS's to report/final/ver1, both of which fail with latexmk exit 12. Those two
documents are broken independently, and sorting merely surfaced them; turning
two green courses red to fix a third is a bad trade. Recorded in
TODO-refactor-remaining.md instead. MOM, ELAC, AIS, SPD, ZPOB, SDM, DPZ and
ESOEN all exit 0 now.
`_works` also grew a `timeout 5`, because `unityhub --version` never returns
on this machine and a probe without one would hang the status sweep.
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_tex_document)"
|
|
[[ -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
|
|
}
|
|
|