#!/bin/bash # # Regenerate include/akgl/SDL_GameControllerDB.h from the community controller # database. # # That header is tracked on purpose: it is the offline fallback that keeps the # library buildable when upstream is renamed, rate-limited, taken down, or # simply unreachable. This script therefore has one rule above all others -- # **it must never replace a good header with a worse one**. Until 0.5.0 it did # exactly that: no `set -e`, no check on curl's exit status, so a failed fetch # wrote an empty mappings file, overwrote the good tracked header with # `AKGL_SDL_GAMECONTROLLER_DB_LEN 0` and an empty initializer, and exited 0. # Verified by pointing it at an unresolvable host. # # Everything is assembled in a temporary directory and moved into place only # once it has been checked. A failure leaves the existing header untouched and # exits non-zero. # # This is no longer run by an ordinary build. `cmake --build build --target # controllerdb` is the deliberate way to update the mappings, so a build needs # no network access and does not leave the working tree dirty. # # Usage: mkcontrollermappings.sh set -euo pipefail AKGL_CONTROLLERDB_URL="${AKGL_CONTROLLERDB_URL:-https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/refs/heads/master/gamecontrollerdb.txt}" # A floor, not a target. The database has carried well over two thousand entries # for years; a few hundred means something truncated the response, which is a # fetch that "succeeded" and still must not be committed. AKGL_CONTROLLERDB_MIN_LINES="${AKGL_CONTROLLERDB_MIN_LINES:-1000}" function die() { echo "mkcontrollermappings: $*" >&2 exit 1 } if [[ $# -lt 1 ]]; then die "usage: $0 " fi rootdir="$1" target="${rootdir}/include/akgl/SDL_GameControllerDB.h" if [[ ! -d "${rootdir}/include/akgl" ]]; then die "no such directory: ${rootdir}/include/akgl" fi command -v curl >/dev/null 2>&1 || die "curl is not available" workdir="$(mktemp -d)" function cleanup() { rm -rf "${workdir}" } trap cleanup EXIT raw="${workdir}/raw.txt" mappings="${workdir}/mappings.txt" staged="${workdir}/SDL_GameControllerDB.h" # --fail so an HTTP error status is an exit status rather than an error page in # the body. Not a pipeline: the exit status being checked is curl's, and a # pipeline reports the last command's. if ! curl --fail --silent --show-error --location "${AKGL_CONTROLLERDB_URL}" --output "${raw}"; then die "fetch failed from ${AKGL_CONTROLLERDB_URL}; ${target} left as it was" fi grep -v '^#' "${raw}" | grep -v '^$' | sed s/',$'//g > "${mappings}" || true filelen=$(wc -l < "${mappings}" | tr -d ' ') if [[ "${filelen}" -lt "${AKGL_CONTROLLERDB_MIN_LINES}" ]]; then die "fetched only ${filelen} mappings, expected at least ${AKGL_CONTROLLERDB_MIN_LINES}; ${target} left as it was" fi # Every mapping becomes a C string literal, so a quote or a backslash would # produce a header that does not compile -- or, worse, one that does. if grep -q '["\\]' "${mappings}"; then die "fetched mappings contain a quote or a backslash; ${target} left as it was" fi { echo "#ifndef _AKGL_SDL_GAMECONTROLLERDB_H_" echo "#define _AKGL_SDL_GAMECONTROLLERDB_H_" echo "" echo "// Taken from ${AKGL_CONTROLLERDB_URL} on $(date)" echo "" echo "#define AKGL_SDL_GAMECONTROLLER_DB_LEN ${filelen}" echo "" echo "const char *SDL_GAMECONTROLLER_DB[] = {" # One pass: wrap each mapping as a quoted, indented, comma-terminated entry, # then drop the trailing comma from the last. An empty initializer is a # constraint violation in ISO C that compiles only as a GCC extension, and # the length check above is what guarantees there is at least one entry. sed -e 's/.*/ "&",/' -e '$ s/,$//' "${mappings}" echo "};" echo "#endif // _AKGL_SDL_GAMECONTROLLERDB_H_" } > "${staged}" # Checked before it is allowed anywhere near the tracked copy. if ! grep -q "^#define AKGL_SDL_GAMECONTROLLER_DB_LEN *${filelen}\$" "${staged}"; then die "staged header did not come out as expected; ${target} left as it was" fi mv "${staged}" "${target}" echo "mkcontrollermappings: wrote ${filelen} mappings to ${target}"