mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 17:47:14 +01:00
run.sh runs the program now, and says which one it is running
Reported by kuhy: "I run run.sh and I have no idea where the actual program is
and how to run it". In EPFU that was one `gcc -Wall -Wextra -pedantic main.c`
line and nothing else -- the makefile has no -o, so the binary is an a.out that
nothing mentions, and it was never executed.
Two separate failures, both fixed.
Building is not running. make, cxx, maven and dotnet built and stopped; make,
maven and dotnet did not even print where the output went, while cxx and tex
did. They all execute what they produced now. `make` is the awkward one,
because a makefile need not say what it writes: the runner snapshots the
executables either side of the build and runs whatever is new.
A course is not always one program. Twelve of them are not -- EPFU a game and
a lab, EOOP six binaries, EARIN eight labs, MOM three reports. With no target
named, an interactive run prints a numbered menu and asks; `run <dir> 2`, a
path substring, or `all` skips the prompt. Enumeration deliberately scans the
course root rather than --entry, because EPFU pinning --entry to penguins/src
is precisely why labs/krudnic3_lab1.c had never been compiled. --entry is the
default now, not the only.
Interactive by default: EPFU's penguins and ECOAR's puzzle both read stdin,
and you typed `run`, so you are at the keyboard. --batch (and any
non-terminal stdin) means no prompt, stdin from /dev/null and a 60s cap per
program, so an unattended run cannot wedge. --probe never reaches any of this
and does not even enumerate targets: all 44 probes produce a byte-identical
gates/runnability-local.tsv, and `status` is still seconds long.
Running things immediately found two bugs that could not have surfaced before,
both recorded in TODO-refactor-remaining.md rather than fixed here, since both
mean touching a submitted .c:
- EPFU/labs/krudnic3_lab1.c dies with SIGFPE, integer division by zero.
- ECOAR's puzzle.out writes dest.bmp beside its source, so the tree guard
fails the run. That guard is doing exactly its job.
Verified: EPFU both targets, EOOP under a pty answering the prompt, EADS,
ECOAR, MOM. Full gate green, shellcheck clean on all 44 runners.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
This commit is contained in:
parent
2a59de6363
commit
d5b94cd8a7
25
CLAUDE.md
25
CLAUDE.md
@ -33,9 +33,22 @@ Work directly on `main`; commit and push straight there.
|
||||
./install.sh # deps + pre-commit hooks, then verifies and fails loudly
|
||||
./run.sh check # the full local gate
|
||||
./run.sh status # probe every course dir, regenerate DOCS-runnability.md
|
||||
./run.sh run <dir> # build/run one course, capped at 2G RAM and 2 cores
|
||||
./run.sh run <dir> # build AND run one course, capped at 2G RAM and 2 cores
|
||||
./run.sh run <dir> --list # what targets does this course have?
|
||||
./run.sh run <dir> 2 # run target 2 (an index, path substring, or "all")
|
||||
./run.sh run <dir> --batch # never prompt; stdin /dev/null, 60s per program
|
||||
```
|
||||
|
||||
`run` runs the program, it does not just build it. Twelve courses have more
|
||||
than one target (EPFU a game and a lab, EOOP six binaries, MOM three reports);
|
||||
with no target named, an interactive `run` prints a numbered menu and asks.
|
||||
Not a terminal, or `--batch`, means all targets and no prompt -- a prompt
|
||||
nobody can answer is how an unattended run hangs. `--probe`/`status` never
|
||||
execute anything and never enumerate targets, so the sweep stays seconds long.
|
||||
|
||||
It is interactive by default on purpose: EPFU's penguins and ECOAR's puzzle
|
||||
both read stdin, and you typed `run`, so you are at the keyboard.
|
||||
|
||||
`run` executes inside a transient systemd scope (`MemoryMax=2G`,
|
||||
`MemorySwapMax=0`, `CPUQuota=200%`, `nice -n 19`, `ionice -c 3`), so a course
|
||||
build cannot make an interactive machine stutter. This is not caution, it is a
|
||||
@ -96,6 +109,16 @@ is mirrored into `~/utils/file_length/_tables.py`, `[tool.ruff] exclude` and
|
||||
|
||||
- `ruff` runs with `select = ["ALL"]` and mypy with `strict = true`.
|
||||
- No `# noqa`, no `# type: ignore`. A pre-commit hook rejects both outright.
|
||||
- mypy runs **per directory**, never repo-wide: many courses ship a `main.py`
|
||||
and mypy stops at `Duplicate module named "main"`. Point it at the course's
|
||||
own venv or every import is unresolvable:
|
||||
`mypy --strict --python-executable Programming/<course>/.venv/bin/python <dir>`.
|
||||
- **One deliberate mypy override exists**, in `pyproject.toml`:
|
||||
`temperature_anomaly_detector` sets `disallow_subclassing_any = false`
|
||||
because it must subclass pyflink's `MapFunction` and `KeyedProcessFunction`,
|
||||
which ship no types and are not installed in `Programming/PSD/.venv`. It is
|
||||
scoped to that one module. Do not widen it, and do not add a second override
|
||||
for our own code without the same kind of reason written beside it.
|
||||
- **No per-file-ignores either, including for tests.** `pyproject.toml` has no
|
||||
`[tool.ruff.lint.per-file-ignores]` table at all, which is stricter than the
|
||||
rest of the fleet. So a test asserts by raising:
|
||||
|
||||
@ -39,7 +39,7 @@ own docs and the rest of this fleet carry. A test asserts by raising.
|
||||
What that cost, and what it bought, is in the commit messages; the standing
|
||||
rules for the next pass are below.
|
||||
|
||||
## mypy: run per directory, six deliberate errors left
|
||||
## mypy: run per directory; the six deliberate errors are gone
|
||||
|
||||
`pyproject.toml` sets `strict = true` and `CLAUDE.md` says the repo runs mypy.
|
||||
mypy IS installed now (`~/.local/bin/mypy`) -- the earlier note here saying it
|
||||
@ -55,7 +55,8 @@ RUFF findings unless it says otherwise**; only `TRAK` has been type-checked.
|
||||
| `TRAK` | **0** -- cleared 2026-08-29 |
|
||||
| `twm_4` | **0** -- cleared 2026-08-29 |
|
||||
| `PBAD` | not run (no venv; notebooks only) |
|
||||
| `PSD` | **2**, both unfixable -- see below |
|
||||
| `PSD` | **0** -- cleared 2026-08-29 (scoped override; see below) |
|
||||
| `ERSMS-project` | **0** -- cleared 2026-08-29 (typed DeclarativeBase) |
|
||||
|
||||
Two structural notes for whoever runs it next:
|
||||
|
||||
@ -68,16 +69,25 @@ Two structural notes for whoever runs it next:
|
||||
`mypy --strict --python-executable Programming/<course>/.venv/bin/python <dir>`.
|
||||
Without that, TRAK reported 4 phantom `matplotlib` import errors on top of
|
||||
the real ones.
|
||||
- **`ERSMS-project/backend/app.py` keeps four errors on purpose.**
|
||||
flask-sqlalchemy builds `db.Model` dynamically, so mypy sees `Any` and
|
||||
`--strict` refuses to subclass it. Migrating to a typed `DeclarativeBase`
|
||||
would rewrite both models in an app that cannot be run here.
|
||||
- **PSD keeps two mypy errors on purpose.** `--strict` implies
|
||||
`disallow_subclassing_any`, and `temperature_anomaly_detector.py` has to
|
||||
subclass pyflink's `MapFunction` and `KeyedProcessFunction`, which ship no
|
||||
types. The only fixes are to weaken strict globally or to add an override
|
||||
for our own module; neither is worth it, so the two are reported rather
|
||||
than hidden. Everything else in PSD is clean under `--strict`.
|
||||
- **`ERSMS-project` is at 0** (was 4), fixed 2026-08-29. The old note here
|
||||
claimed a fix "would rewrite both models"; that was wrong. flask-sqlalchemy
|
||||
3.1.1 *does* ship `py.typed`. The actual blocker was that `class
|
||||
User(db.Model)` is attribute access, which mypy cannot resolve in a
|
||||
base-class position. `backend/models.py` now declares a named
|
||||
`DeclarativeBase` with SQLAlchemy 2.0 `Mapped[]` columns, and the six
|
||||
legacy `Model.query` call sites became `db.session` selects, because the
|
||||
`Model` mixin goes away with the base. Verified by driving the real routes
|
||||
through Flask's test client with Firebase stubbed: 16 scenarios and both
|
||||
`CREATE TABLE` statements byte-identical to the submitted version.
|
||||
Two traps worth keeping: `mapped_column(ForeignKey("user.uid"))` silently
|
||||
infers VARCHAR from the referenced column, which changed the submitted
|
||||
`user_id INTEGER` until an explicit `Integer` was put back; and
|
||||
`backend/Dockerfile` copied only `app.py`, so the split needed it too.
|
||||
- **PSD is at 0** (was 2), by a scoped `[[tool.mypy.overrides]]` for
|
||||
`temperature_anomaly_detector` with `disallow_subclassing_any = false`.
|
||||
pyflink ships no types AND is not installed in that venv, so a hand-written
|
||||
`.pyi` would assert an API nothing here can check. Recorded in `CLAUDE.md`
|
||||
as well as here, because this file gets deleted and that one does not.
|
||||
- Point `MYPYPATH` at `Programming/PSD/zin3/python/code` for that tree: the
|
||||
modules import `model.*` via a runtime `sys.path.append`, which mypy
|
||||
cannot follow.
|
||||
@ -99,8 +109,11 @@ exit 0.
|
||||
|
||||
Two things surfaced on the way and are still true:
|
||||
|
||||
- **A multi-report course builds one arbitrary report.** MOM has three, ELAC
|
||||
two, AIS several drafts, and which one gets built depends on readdir order.
|
||||
- **A multi-report course builds one arbitrary report.** PARTLY FIXED
|
||||
2026-08-29: `run.sh run <dir>` now enumerates every document and either asks
|
||||
which, or builds all of them when not interactive, so nothing depends on
|
||||
readdir order any more. What is still open is that some of those documents
|
||||
are broken -- see below. MOM has three, ELAC two, AIS several drafts.
|
||||
Sorting looked like the fix until it swapped ELAC to `projectA` and AIS to
|
||||
`report/final/ver1`, **both of which fail with latexmk exit 12** -- so those
|
||||
documents are broken and sorting merely surfaced it. Either build every
|
||||
@ -110,6 +123,26 @@ Two things surfaced on the way and are still true:
|
||||
carries a `timeout 5`. Use `_works` only for tools that answer `--version`;
|
||||
`_have` is still right for the other ten probe call sites.
|
||||
|
||||
## Found by actually running the programs, not yet fixed
|
||||
|
||||
`run.sh run` executes what it builds as of 2026-08-29, and the first thing that
|
||||
fell out is a course that had never been run by the harness at all:
|
||||
|
||||
- **`EPFU/labs/krudnic3_lab1.c` dies with SIGFPE** (exit 136) on
|
||||
`printf("\n%f", i_one / i_two)` -- an integer division by zero. It was
|
||||
invisible before because EPFU's stub pins `--entry` to `penguins/src`, so
|
||||
the lab was never compiled, let alone run. gcc also warns twice about
|
||||
`%d`/`%f` against the wrong argument types on lines 12 and 14. Left alone
|
||||
for now: it is a submitted deliverable and fixing it is a separate decision
|
||||
from making the runner work.
|
||||
- **`ECOAR` overwrites a tracked deliverable when it runs.** `C/puzzle.out`
|
||||
writes `C/dest.bmp` beside its source, so the before/after tree guard fails
|
||||
the run -- correctly. It was invisible before for the same reason as EPFU's
|
||||
lab: the runner built `puzzle.out` and never executed it. The fix is to make
|
||||
the program write into `build/`, which means touching a submitted `.c`, so
|
||||
it is recorded rather than done. `git checkout --` restores the file
|
||||
meanwhile; the guard is what makes that recoverable at all.
|
||||
|
||||
## Still open
|
||||
|
||||
### 1. The 250-line cap: 50 files
|
||||
|
||||
17
run.sh
17
run.sh
@ -7,12 +7,23 @@
|
||||
# ./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 run <dir> --list list that course's targets, run nothing
|
||||
# ./run.sh run <dir> <target> run one target: an index, a path substring,
|
||||
# or "all". Without one, a course with several
|
||||
# targets asks which; a course with one just runs.
|
||||
# ./run.sh run <dir> --batch never prompt, stdin is /dev/null, timeout each
|
||||
# run. This is the unattended/CI form.
|
||||
# ./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.
|
||||
#
|
||||
# `run` BUILDS AND THEN RUNS. Building is not running: this used to print one
|
||||
# gcc line and stop, leaving you to find the binary yourself. It is interactive
|
||||
# by default because several of these programs read stdin -- use --batch for
|
||||
# anything unattended.
|
||||
# ============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
@ -150,6 +161,7 @@ MD
|
||||
cmd_run() {
|
||||
local target="${1:-}"
|
||||
[[ -n "$target" ]] || { err "run needs a directory"; usage 1; }
|
||||
shift
|
||||
local runner="${SCRIPT_DIR}/${target}/run.sh"
|
||||
[[ -x "$runner" ]] || { err "no executable run.sh in $target"; return 1; }
|
||||
log "running $target (capped at ${RUN_MEMORY_MAX} RAM, ${RUN_CPU_QUOTA} CPU)"
|
||||
@ -160,7 +172,10 @@ cmd_run() {
|
||||
# export, not an assignment prefix: _run_capped is a shell function, and a
|
||||
# prefixed assignment on one is not put into the child's environment.
|
||||
export MPLBACKEND="${MPLBACKEND:-Agg}"
|
||||
_run_capped "$runner"
|
||||
# Remaining arguments are the course runner's: a target to pick, --batch,
|
||||
# --list. They are forwarded rather than parsed here, because which
|
||||
# targets exist is a question only the course runner can answer.
|
||||
_run_capped "$runner" "$@"
|
||||
}
|
||||
|
||||
# Run a command under a memory/CPU cap, niced so it yields to anything
|
||||
|
||||
120
scripts/course_exec.sh
Normal file
120
scripts/course_exec.sh
Normal file
@ -0,0 +1,120 @@
|
||||
# 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"))
|
||||
|
||||
if [[ ${#new_paths[@]} -eq 0 ]]; then
|
||||
printf 'nothing new to run after %s (already built, or it produces no binary)\n' \
|
||||
"$what"
|
||||
return 0
|
||||
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"
|
||||
}
|
||||
@ -144,6 +144,12 @@ _COURSE_LIB_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=course_run.sh
|
||||
. "${_COURSE_LIB_DIR}/course_run.sh"
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=course_targets.sh
|
||||
. "${_COURSE_LIB_DIR}/course_targets.sh"
|
||||
# shellcheck source-path=SCRIPTDIR
|
||||
# shellcheck source=course_exec.sh
|
||||
. "${_COURSE_LIB_DIR}/course_exec.sh"
|
||||
|
||||
course_main() {
|
||||
COURSE_KIND=""
|
||||
@ -165,23 +171,59 @@ course_main() {
|
||||
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'
|
||||
# Everything after `--` is the user's, not the stub's.
|
||||
COURSE_BATCH=""
|
||||
COURSE_SELECT=""
|
||||
local mode="run"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--probe) mode="probe"; shift ;;
|
||||
--list) mode="list"; shift ;;
|
||||
--batch) COURSE_BATCH=1; shift ;;
|
||||
-h | --help) mode="help"; shift ;;
|
||||
-*) printf 'unknown option: %s\n' "$1" >&2; return 1 ;;
|
||||
# A bare word picks a target: an index, a path substring, or "all".
|
||||
*) COURSE_SELECT="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
# --probe must stay cheap and must never execute anything, so it does
|
||||
# not even enumerate targets.
|
||||
probe) _probe ;;
|
||||
help)
|
||||
printf 'usage: %s [--probe|--list] [--batch] [<target>]\n' "$(basename "$0")"
|
||||
printf ' --probe report runnable/blocked/no-code without running\n'
|
||||
printf ' --list list this course'"'"'s targets and stop\n'
|
||||
printf ' --batch no prompts, stdin is /dev/null, %ss timeout per run\n' \
|
||||
"$(_batch_timeout)"
|
||||
printf ' <target> an index, a path substring, or "all"\n'
|
||||
;;
|
||||
"")
|
||||
local before after
|
||||
before="$(_tree_state)"
|
||||
_run
|
||||
after="$(_tree_state)"
|
||||
if [[ "$before" != "$after" ]]; then
|
||||
printf 'FAILED: the run changed files under %s\n' "$COURSE_ROOT" >&2
|
||||
diff <(printf '%s\n' "$before") <(printf '%s\n' "$after") >&2 || true
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
*) printf 'unknown option: %s\n' "$1" >&2; return 1 ;;
|
||||
list) _collect_targets; _print_menu ;;
|
||||
run) _run_selected ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Run every chosen target, with the tree guard around the whole thing.
|
||||
_run_selected() {
|
||||
_collect_targets
|
||||
_choose_target || return 1
|
||||
|
||||
local before after target
|
||||
before="$(_tree_state)"
|
||||
for target in "${COURSE_CHOSEN[@]}"; do
|
||||
COURSE_ENTRY="$target"
|
||||
_run
|
||||
done
|
||||
after="$(_tree_state)"
|
||||
if [[ "$before" != "$after" ]]; then
|
||||
printf 'FAILED: the run changed files under %s\n' "$COURSE_ROOT" >&2
|
||||
diff <(printf '%s\n' "$before") <(printf '%s\n' "$after") >&2 || true
|
||||
return 1
|
||||
fi
|
||||
if [[ "$COURSE_EXEC_FAILURES" -gt 0 ]]; then
|
||||
printf '\n%s of the programs did not exit cleanly (see above)\n' \
|
||||
"$COURSE_EXEC_FAILURES" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@ -146,39 +146,66 @@ _run() {
|
||||
_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 ) ;;
|
||||
maven)
|
||||
( cd "${COURSE_ENTRY:-$COURSE_ROOT}" && mvn -q package )
|
||||
_run_jar "${COURSE_ENTRY:-$COURSE_ROOT}"
|
||||
;;
|
||||
make)
|
||||
# A target is either a directory with a makefile, or a standalone
|
||||
# .c program that no makefile covers.
|
||||
local mtarget="${COURSE_ENTRY:-$COURSE_ROOT}"
|
||||
if [[ -f "$mtarget" ]]; then
|
||||
_compile_and_run_c "$mtarget"
|
||||
else
|
||||
_make_and_run "$mtarget"
|
||||
fi
|
||||
;;
|
||||
dotnet)
|
||||
# `dotnet run` builds too, so this is one step, not two.
|
||||
# EGUI is a web app: running it starts Kestrel and stays
|
||||
# up, which is what running it means. --batch caps that.
|
||||
local dir rc=0
|
||||
dir="${COURSE_ENTRY:-$COURSE_ROOT}"
|
||||
if [[ -n "${COURSE_BATCH:-}" ]]; then
|
||||
( cd "$dir" && timeout "$(_batch_timeout)" \
|
||||
dotnet run < /dev/null ) || rc=$?
|
||||
else
|
||||
( cd "$dir" && dotnet run ) || rc=$?
|
||||
fi
|
||||
_exec_note "$rc" "$(_target_label "$dir")"
|
||||
;;
|
||||
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.
|
||||
# One .cpp per invocation: each is a self-contained assignment with
|
||||
# its own main(), and the target list already enumerated them, so
|
||||
# picking one is the caller's job rather than this loop's.
|
||||
local cxx src rel out
|
||||
cxx="$(_require_any 'a C++ compiler' g++ clang++)"
|
||||
src="${COURSE_ENTRY:-}"
|
||||
[[ -f "$src" ]] || _no_code "no .cpp entry to compile"
|
||||
rel="${src#"$COURSE_ROOT"/}"
|
||||
# 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"
|
||||
if _is_known_incomplete "${src#"${COURSE_GATES%/gates}"/}"; then
|
||||
printf 'skipping %s (see gates/known-incomplete.txt)\n' "$rel"
|
||||
return 0
|
||||
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, but there is then
|
||||
# nothing to run.
|
||||
if grep -qE '\bint[[:space:]]+main[[:space:]]*\(' "$src"; then
|
||||
printf 'compiling %s\n' "$rel"
|
||||
"$cxx" -O2 -o "$out" "$src"
|
||||
_exec_built "$out"
|
||||
else
|
||||
printf 'checking %s (no main; -c only)\n' "$rel"
|
||||
"$cxx" -O2 -c -o "${out}.o" "$src"
|
||||
fi
|
||||
;;
|
||||
notebook)
|
||||
_require_working jupyter
|
||||
|
||||
157
scripts/course_targets.sh
Normal file
157
scripts/course_targets.sh
Normal file
@ -0,0 +1,157 @@
|
||||
# shellcheck shell=bash
|
||||
# Enumerating and choosing what to run. Sourced by course_lib.sh, never run.
|
||||
#
|
||||
# A course directory is not always one program. EPFU ships a game AND a lab
|
||||
# exercise, EOOP six standalone .cpp files, MOM three reports -- twelve courses
|
||||
# have more than one target. The runner used to build whichever one `find`
|
||||
# handed back first and say nothing about it, which is how you end up staring
|
||||
# at a single gcc line with no idea what it just did.
|
||||
#
|
||||
# Enumeration scans COURSE_ROOT rather than only COURSE_ENTRY on purpose: EPFU
|
||||
# pins --entry to penguins/src, and that pin is exactly why its labs/ was
|
||||
# invisible. --entry still decides the DEFAULT, it no longer decides the only.
|
||||
|
||||
COURSE_TARGETS=()
|
||||
COURSE_CHOSEN=()
|
||||
|
||||
# Fill COURSE_TARGETS for this kind. One entry means there is no choice to make.
|
||||
_collect_targets() {
|
||||
COURSE_TARGETS=()
|
||||
local f
|
||||
case "$COURSE_KIND" in
|
||||
tex)
|
||||
while IFS= read -r f; do
|
||||
if grep -q '\\documentclass' "$f"; then COURSE_TARGETS+=("$f"); fi
|
||||
done < <(find "$COURSE_ROOT" -name '*.tex' -not -path '*/build/*' | sort)
|
||||
;;
|
||||
python)
|
||||
while IFS= read -r f; do
|
||||
COURSE_TARGETS+=("$f")
|
||||
done < <(find "$COURSE_ROOT" -name 'main.py' -not -path '*/.venv/*' | sort)
|
||||
;;
|
||||
make)
|
||||
local mkdirs=()
|
||||
while IFS= read -r f; do
|
||||
mkdirs+=("$(dirname "$f")")
|
||||
COURSE_TARGETS+=("$(dirname "$f")")
|
||||
done < <(find "$COURSE_ROOT" \( -name Makefile -o -name makefile \) \
|
||||
-not -path '*/build/*' -not -path '*/node_modules/*' | sort)
|
||||
# A loose .c with its own main() and no makefile above it is still
|
||||
# a program. EPFU's labs/krudnic3_lab1.c is the case that started
|
||||
# this: --entry pinned the runner to penguins/, so the lab was
|
||||
# invisible and nothing ever compiled it.
|
||||
while IFS= read -r f; do
|
||||
if grep -qE '\bint[[:space:]]+main[[:space:]]*\(' "$f" \
|
||||
&& ! _under_any "$f" ${mkdirs[@]+"${mkdirs[@]}"}; then
|
||||
COURSE_TARGETS+=("$f")
|
||||
fi
|
||||
done < <(find "$COURSE_ROOT" -name '*.c' -not -path '*/build/*' \
|
||||
-not -path '*/node_modules/*' | sort)
|
||||
;;
|
||||
cxx)
|
||||
while IFS= read -r f; do
|
||||
if grep -qE '\bint[[:space:]]+main[[:space:]]*\(' "$f"; then
|
||||
COURSE_TARGETS+=("$f")
|
||||
fi
|
||||
done < <(find "$COURSE_ROOT" -name '*.cpp' -not -path '*/build/*' \
|
||||
-not -path '*/node_modules/*' | sort)
|
||||
;;
|
||||
esac
|
||||
# Kinds with no natural list -- and any course whose scan found nothing --
|
||||
# keep the single implicit target the stub already declares.
|
||||
if [[ ${#COURSE_TARGETS[@]} -eq 0 ]]; then
|
||||
COURSE_TARGETS=("${COURSE_ENTRY:-$COURSE_ROOT}")
|
||||
fi
|
||||
}
|
||||
|
||||
_target_label() { printf '%s' "${1#"$COURSE_ROOT"/}"; }
|
||||
|
||||
# Is this path inside any of the given directories? Used to keep a source file
|
||||
# that some makefile already builds out of the target list.
|
||||
_under_any() {
|
||||
local path="$1"; shift
|
||||
local dir
|
||||
for dir in "$@"; do
|
||||
if [[ "$path" == "$dir"/* ]]; then return 0; fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Resolve an explicit selection: an index, or a substring of a target's path.
|
||||
# Prints the chosen indices, one per line; empty output means "no match".
|
||||
_match_selection() {
|
||||
local want="$1" i=1 t
|
||||
if [[ "$want" == "all" ]]; then
|
||||
for ((i = 1; i <= ${#COURSE_TARGETS[@]}; i++)); do printf '%s\n' "$i"; done
|
||||
return 0
|
||||
fi
|
||||
if [[ "$want" =~ ^[0-9]+$ ]]; then
|
||||
if [[ "$want" -ge 1 && "$want" -le ${#COURSE_TARGETS[@]} ]]; then
|
||||
printf '%s\n' "$want"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
for t in "${COURSE_TARGETS[@]}"; do
|
||||
if [[ "$(_target_label "$t")" == *"$want"* ]]; then printf '%s\n' "$i"; fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
_print_menu() {
|
||||
printf '\n%s has %d targets:\n\n' "${COURSE_ROOT##*/}" "${#COURSE_TARGETS[@]}"
|
||||
local i=1 t marker
|
||||
for t in "${COURSE_TARGETS[@]}"; do
|
||||
marker=""
|
||||
if [[ -n "${COURSE_ENTRY:-}" && "$t" == "$COURSE_ENTRY"* ]]; then
|
||||
marker=" <- the stub's default"
|
||||
fi
|
||||
printf ' %2d) %s%s\n' "$i" "$(_target_label "$t")" "$marker"
|
||||
i=$((i + 1))
|
||||
done
|
||||
printf ' a) all of them\n\n'
|
||||
}
|
||||
|
||||
# Decide what to run, into COURSE_CHOSEN.
|
||||
_choose_target() {
|
||||
COURSE_CHOSEN=()
|
||||
local idx
|
||||
|
||||
if [[ ${#COURSE_TARGETS[@]} -eq 1 ]]; then
|
||||
COURSE_CHOSEN=("${COURSE_TARGETS[@]}")
|
||||
return 0
|
||||
fi
|
||||
|
||||
# An explicit choice on the command line skips the prompt entirely, which
|
||||
# is what makes the menu scriptable as well as answerable.
|
||||
if [[ -n "${COURSE_SELECT:-}" ]]; then
|
||||
while IFS= read -r idx; do
|
||||
COURSE_CHOSEN+=("${COURSE_TARGETS[$((idx - 1))]}")
|
||||
done < <(_match_selection "$COURSE_SELECT")
|
||||
if [[ ${#COURSE_CHOSEN[@]} -eq 0 ]]; then
|
||||
printf 'no target matches "%s"\n' "$COURSE_SELECT" >&2
|
||||
_print_menu >&2
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# A prompt nobody can answer is how an unattended run hangs forever, so
|
||||
# --batch and a non-terminal stdin both mean "all of them" instead.
|
||||
if [[ -n "${COURSE_BATCH:-}" || ! -t 0 ]]; then
|
||||
COURSE_CHOSEN=("${COURSE_TARGETS[@]}")
|
||||
printf 'not interactive: running all %d targets\n' "${#COURSE_TARGETS[@]}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
_print_menu
|
||||
local reply
|
||||
read -r -p "which? [1-${#COURSE_TARGETS[@]} or a, default 1] " reply || reply=""
|
||||
reply="${reply:-1}"
|
||||
while IFS= read -r idx; do
|
||||
COURSE_CHOSEN+=("${COURSE_TARGETS[$((idx - 1))]}")
|
||||
done < <(_match_selection "$reply")
|
||||
if [[ ${#COURSE_CHOSEN[@]} -eq 0 ]]; then
|
||||
printf 'not a valid choice: %s\n' "$reply" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user