#!/usr/bin/env bash
#
# Every function with external linkage must be declared in a header.
#
# A non-static function that appears in no header is still in the ABI but is
# unreachable by callers, and it collides with any consumer that happens to pick
# the same name. libakgl shipped nineteen of those. The rule is in AGENTS.md
# under "API Surface"; this is what enforces it.
#
# Usage: check_api_surface.sh <libakgl.so> <header-dir> [<header-dir> ...]
#
# Exits 0 when every exported akgl_* symbol is declared, 1 otherwise, and 2 when
# it cannot do its job -- a missing library or no nm.

set -u

if [ "$#" -lt 2 ]; then
    echo "usage: $0 <libakgl.so> <header-dir> [<header-dir> ...]" >&2
    exit 2
fi

LIBRARY="$1"
shift

if ! command -v nm >/dev/null 2>&1; then
    echo "check_api_surface: nm is not available; skipping" >&2
    exit 2
fi

if [ ! -f "${LIBRARY}" ]; then
    echo "check_api_surface: no such library: ${LIBRARY}" >&2
    exit 2
fi

DECLARATIONS="$(mktemp)"
trap 'rm -f "${DECLARATIONS}"' EXIT

# Comments are stripped first. Several of these names appear in prose -- the
# whole point of the check is that being *mentioned* in a header is not the same
# as being declared in one, and that distinction is exactly what went wrong with
# the gamepad_handle_* handlers.
for dir in "$@"; do
    [ -d "${dir}" ] || continue
    find "${dir}" -name '*.h' -print0 |
        xargs -0 --no-run-if-empty cat |
        sed -e 's://.*::' |
        awk 'BEGIN { incomment = 0 }
             {
                 line = $0
                 while ( 1 ) {
                     if ( incomment ) {
                         idx = index(line, "*/")
                         if ( idx == 0 ) { line = ""; break }
                         line = substr(line, idx + 2); incomment = 0
                     } else {
                         idx = index(line, "/*")
                         if ( idx == 0 ) { break }
                         printf "%s", substr(line, 1, idx - 1)
                         line = substr(line, idx + 2); incomment = 1
                     }
                 }
                 print line
             }' >> "${DECLARATIONS}"
done

UNDECLARED=""
for symbol in $(nm -D --defined-only "${LIBRARY}" | awk '$2 == "T" { print $3 }' | grep '^akgl_' | sort); do
    if ! grep -qE "(^|[^A-Za-z0-9_])${symbol}[^A-Za-z0-9_]" "${DECLARATIONS}"; then
        UNDECLARED="${UNDECLARED} ${symbol}"
    fi
done

if [ -n "${UNDECLARED}" ]; then
    echo "check_api_surface: exported but declared in no header:" >&2
    for symbol in ${UNDECLARED}; do
        echo "    ${symbol}" >&2
    done
    echo "" >&2
    echo "Declare each one in the header for its subsystem, or make it static." >&2
    echo "A function exposed only so tests can reach it goes under the existing" >&2
    echo "\"part of the internal API\" comment block." >&2
    exit 1
fi

echo "check_api_surface: every exported akgl_* symbol is declared"
exit 0
