279 lines
8.3 KiB
Bash
279 lines
8.3 KiB
Bash
|
|
#!/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}"
|