#!/bin/bash # # Regenerate every figure in docs/ from the listing shown beside it. # # A chapter that says "akgl_draw_rectangle fills a rectangle" and shows a listing # wants a picture of what that listing draws, and the way a picture goes wrong is # that it stops being of the code beside it. Nothing tells you: a screenshot # taken by hand in 2026 still looks like a screenshot in 2027, long after the # call it illustrates has changed. # # So the figure is not an asset, it is *output*. A block tagged # # ```c screenshot=rectangle # # is compiled against tools/docs_screenshot.c, run, and written to # docs/images/rectangle.png, and the chapter shows that file. Regenerate and the # picture follows the code. The generated PNGs are tracked on purpose -- a reader # on the forge has no build tree -- and tests/docs_examples.sh fails a tagged # block with no image, so a new figure cannot be forgotten. # # **This is not run by the build.** `cmake --build build --target docs_screenshots` # is deliberate; see the target's comment in CMakeLists.txt. # # Exit status is the number of figures that failed, or 2 for a usage or setup # error. set -u ROOT="" CFLAGS_FILE="" LDFLAGS_FILE="" CHECK=0 FAILURES=0 usage() { cat >&2 <<'EOF' usage: docs_screenshots.sh --root DIR --cflags-file FILE --ldflags-file FILE [--check] [FILE...] --root DIR repository root; images are written to DIR/docs/images --cflags-file FILE one compiler flag per line --ldflags-file FILE one linker argument per line --check render to a scratch directory and compare, changing nothing FILE... which documents to regenerate (default: docs/*.md) EOF exit 2 } while [ $# -gt 0 ]; do case "$1" in --root) ROOT="$2"; shift 2 ;; --cflags-file) CFLAGS_FILE="$2"; shift 2 ;; --ldflags-file) LDFLAGS_FILE="$2"; shift 2 ;; --check) CHECK=1; shift ;; --help|-h) usage ;; --*) echo "unknown option $1" >&2; usage ;; *) break ;; esac done [ -n "${ROOT}" ] || usage [ -n "${CFLAGS_FILE}" ] || usage [ -n "${LDFLAGS_FILE}" ] || usage # Absolute before anything else, because the loop below cd's into a sandbox to # run each figure -- a listing that loads an asset resolves it relative to its # own directory. for _var in CFLAGS_FILE LDFLAGS_FILE; do _val="${!_var}" case "${_val}" in /*) ;; *) printf -v "${_var}" '%s' "${PWD}/${_val}" ;; esac done unset _var _val [ -r "${CFLAGS_FILE}" ] || { echo "FAIL: no compiler flags at ${CFLAGS_FILE}" >&2; exit 2; } [ -r "${LDFLAGS_FILE}" ] || { echo "FAIL: no linker flags at ${LDFLAGS_FILE}" >&2; exit 2; } cd "${ROOT}" || exit 2 ROOT="${PWD}" HOST="${ROOT}/tools/docs_screenshot.c" [ -r "${HOST}" ] || { echo "FAIL: no figure host at ${HOST}" >&2; exit 2; } DOCS=("$@") if [ ${#DOCS[@]} -eq 0 ]; then DOCS=() for _doc in docs/*.md; do [ -r "${_doc}" ] && DOCS+=("${_doc}") done unset _doc if [ ${#DOCS[@]} -eq 0 ]; then echo "no documents matched docs/*.md; there are no figures to make" exit 0 fi fi for _doc in "${DOCS[@]}"; do [ -r "${_doc}" ] || { echo "FAIL: no document at \"${_doc}\"" >&2; exit 2; } done unset _doc CFLAGS=() while IFS= read -r _flag; do [ -n "${_flag}" ] && CFLAGS+=("${_flag}") done < "${CFLAGS_FILE}" CFLAGS+=("-I${ROOT}/tests/docs_preludes") LDFLAGS=() while IFS= read -r _flag; do [ -n "${_flag}" ] && LDFLAGS+=("${_flag}") done < "${LDFLAGS_FILE}" unset _flag CC="${CC:-cc}" IMAGES="${ROOT}/docs/images" mkdir -p "${IMAGES}" || exit 2 WORK="$(mktemp -d)" trap 'rm -rf "${WORK}"' EXIT # --check renders somewhere else entirely and compares, so a run that finds a # stale figure does not also fix it. A test that repairs what it is measuring # passes the second time for the wrong reason. if [ "${CHECK}" -eq 1 ]; then OUTDIR="${WORK}/rendered" mkdir -p "${OUTDIR}" || exit 2 else OUTDIR="${IMAGES}" fi # The value of attribute $1 in an info string $2, or empty. Same shape as the # `attr` in tests/docs_examples.sh, and deliberately so -- the two read the same # fence tags and a second dialect of them would be a bug waiting to happen. function attr() { local name="$1" info="$2" word for word in ${info}; do case "${word}" in "${name}"=*) echo "${word#*=}"; return 0 ;; esac done echo "" } function figure_failed() { local where="$1"; shift echo "FAIL ${where}: $*" >&2 FAILURES=$((FAILURES + 1)) } # One figure: wrap the listing, compile it against the host, run it, keep the PNG. function render() { local name="$1" body="$2" size="$3" where="$4" setup="$5" local w=320 h=240 out="${OUTDIR}/${name}.png" local unit="${WORK}/frame.c" prog="${WORK}/figure" log="${WORK}/cc.log" local sandbox="${WORK}/sandbox" stdoutlog="" if [ -n "${size}" ]; then w="${size%x*}" h="${size#*x}" fi # The #line makes a compiler diagnostic read `docs/09-drawing.md:112: error:` # rather than pointing into a scratch file. `where` names the opening fence, # so the body starts on the line after it. { cat "${ROOT}/tests/docs_preludes/akglframe.pre" printf '#line %d "%s"\n' "$(( ${where##*:} + 1 ))" "${where%:*}" printf '%s' "${body}" cat "${ROOT}/tests/docs_preludes/akglframe.post" } > "${unit}" rm -f "${prog}" if ! "${CC}" -std=gnu99 -Wall -Werror "${CFLAGS[@]}" \ -o "${prog}" "${unit}" "${HOST}" "${LDFLAGS[@]}" > "${log}" 2>&1; then figure_failed "${where}" "${name} does not build" sed -n '1,25p' "${log}" >&2 return fi rm -rf "${sandbox}" mkdir -p "${sandbox}" # The same setup scripts tests/docs_examples.sh uses, run in the same place # relative to the program. A listing that loads a spritesheet needs one # whether it is being compiled or being photographed. if [ -n "${setup}" ]; then if [ ! -r "${ROOT}/tests/docs_setups/${setup}.sh" ]; then figure_failed "${where}" "no setup script ${setup}.sh" return fi if ! ( cd "${sandbox}" && bash "${ROOT}/tests/docs_setups/${setup}.sh" "${ROOT}" ) \ >/dev/null 2>&1; then figure_failed "${where}" "setup ${setup} failed" return fi fi # **stdout only.** libakgl logs its registry chatter -- "Actor akgl:actor:1 # initialized" -- to stderr, so merging the two would make every sprite # figure look like a failing program. stderr is shown when the tool itself # refuses, which is when it is worth reading. if ! stdoutlog="$(cd "${sandbox}" && \ SDL_VIDEODRIVER=dummy SDL_RENDER_DRIVER=software SDL_AUDIODRIVER=dummy \ "${prog}" "${out}" "${w}" "${h}" 2>"${WORK}/figure.err")"; then figure_failed "${where}" "${name} did not render" cat "${WORK}/figure.err" >&2 return fi # A frame that printed something is a frame that reported a problem, and an # image of a blank screen is worse than no image at all. if [ -n "${stdoutlog}" ]; then figure_failed "${where}" "${name} rendered, but the listing printed:" echo "${stdoutlog}" >&2 return fi if [ "${CHECK}" -eq 1 ]; then if [ ! -r "${IMAGES}/${name}.png" ]; then figure_failed "${where}" "docs/images/${name}.png does not exist" return fi if ! cmp -s "${out}" "${IMAGES}/${name}.png"; then figure_failed "${where}" "docs/images/${name}.png is not what that listing draws" echo " regenerate with: cmake --build build --target docs_screenshots" >&2 return fi echo " ${name}.png matches" return fi echo " ${name}.png (${w}x${h})" } for DOC in "${DOCS[@]}"; do # Walk the file collecting fenced blocks. Only `screenshot=` ones are kept; # everything else is somebody else's problem, and docs_examples.sh is that # somebody. IN_BLOCK=0 INFO="" BODY="" START=0 LINE_NUMBER=0 while IFS= read -r LINE; do LINE_NUMBER=$((LINE_NUMBER + 1)) case "${LINE}" in '```'*) if [ "${IN_BLOCK}" -eq 0 ]; then IN_BLOCK=1 INFO="${LINE#'```'}" BODY="" START="${LINE_NUMBER}" else IN_BLOCK=0 NAME="$(attr screenshot "${INFO}")" if [ -n "${NAME}" ]; then render "${NAME}" "${BODY}" "$(attr size "${INFO}")" \ "${DOC}:${START}" "$(attr setup "${INFO}")" fi fi ;; *) if [ "${IN_BLOCK}" -eq 1 ]; then BODY="${BODY}${LINE}"$'\n' fi ;; esac done < "${DOC}" done if [ "${FAILURES}" -ne 0 ]; then echo "${FAILURES} figure(s) failed" >&2 fi exit "${FAILURES}"