Commit 331dd57d for libheif
commit 331dd57d95515459db3d970fc2c7f364bfd903ae
Author: Dirk Farin <dirk.farin@gmail.com>
Date: Mon May 25 21:09:58 2026 +0200
CI: add check that all public API functions actually exist (#1822)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index afed0b2c..05f59be0 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -40,6 +40,20 @@ jobs:
run: |
./scripts/check-c-headers.sh
+ api-symbols:
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install build tools
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y cmake g++
+
+ - name: Check that all declared API functions are defined in the library
+ run: |
+ ./scripts/check-api-symbols.sh
+
licenses:
env:
CHECK_LICENSES: 1
diff --git a/scripts/check-api-symbols.sh b/scripts/check-api-symbols.sh
new file mode 100755
index 00000000..d9bf9331
--- /dev/null
+++ b/scripts/check-api-symbols.sh
@@ -0,0 +1,128 @@
+#!/bin/bash
+set -e
+#
+# HEIF codec.
+# Copyright (c) 2026 Dirk Farin <dirk.farin@gmail.com>
+#
+# This file is part of libheif.
+#
+# libheif is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# libheif is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with libheif. If not, see <http://www.gnu.org/licenses/>.
+#
+# Verify that every function declared in the public C API headers is actually
+# defined (and exported) in the built libheif shared library.
+#
+# A public declaration whose definition has a mismatched name -- or no
+# definition at all -- compiles cleanly and even links into libheif itself,
+# because libheif never calls its own public API. Neither the C++ build nor the
+# C-header check (scripts/check-c-headers.sh, which only compiles) notices.
+# The breakage surfaces only downstream, as an "undefined symbol" link error in
+# any program that calls the declared function.
+#
+# This is exactly what happened in https://github.com/strukturag/libheif/issues/1822:
+# heif_properties.h declared heif_image_get_bayer_pattern_size() while the
+# definition in heif_properties.cc was named heif_image_has_bayer_pattern().
+# The library built and shipped fine; Rust consumers got a link error.
+#
+# This script extracts every LIBHEIF_API-marked declaration from the public
+# headers and checks each name against the defined, exported symbols of a built
+# libheif.so (read with nm). Any declared-but-undefined symbol is reported and
+# the script exits non-zero.
+#
+# Usage:
+# check-api-symbols.sh [path/to/libheif.so]
+# If no library is given, a minimal libheif (all codecs off, no plugins, no
+# examples) is built into a temporary directory. That build needs only cmake
+# and a C++ compiler -- no external codec libraries.
+#
+# heif_experimental.h is excluded: its declarations are compiled only when
+# ENABLE_EXPERIMENTAL_FEATURES is on, so they are legitimately absent from a
+# default build. heif_cxx.h / heif_emscripten.h are C++-only and declare no
+# plain-C ABI symbols; heif_version.h is generated and declares none either.
+
+ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." >/dev/null 2>&1 && pwd)"
+API_DIR="$ROOT/libheif/api/libheif"
+
+# Headers whose LIBHEIF_API declarations are not expected in a default build,
+# or that declare no plain-C ABI symbols. See the note above.
+EXCLUDE="heif_experimental.h heif_cxx.h heif_emscripten.h heif_version.h"
+
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+
+# --- Locate (or build) the libheif shared library ---------------------------
+
+LIB="${1:-}"
+if [ -z "$LIB" ]; then
+ echo "No library given; building a minimal libheif (all codecs off) ..."
+ cmake -S "$ROOT" -B "$TMP/build" \
+ -DBUILD_SHARED_LIBS=ON \
+ -DWITH_LIBDE265=OFF -DWITH_X265=OFF \
+ -DWITH_AOM_DECODER=OFF -DWITH_AOM_ENCODER=OFF \
+ -DWITH_X264=OFF -DWITH_OpenH264_DECODER=OFF -DWITH_DAV1D=OFF \
+ -DWITH_EXAMPLES=OFF -DWITH_GDK_PIXBUF=OFF \
+ -DBUILD_TESTING=OFF -DENABLE_PLUGIN_LOADING=OFF \
+ >"$TMP/cmake.log" 2>&1 \
+ && cmake --build "$TMP/build" -j"$(nproc)" >>"$TMP/cmake.log" 2>&1 \
+ || { echo "ERROR: minimal libheif build failed:" >&2; cat "$TMP/cmake.log" >&2; exit 1; }
+ LIB="$TMP/build/libheif/libheif.so"
+fi
+
+if [ ! -e "$LIB" ]; then
+ echo "ERROR: libheif shared library not found: $LIB" >&2
+ exit 1
+fi
+
+# --- Collect declared API symbols and defined library symbols ---------------
+
+# Declared symbols: every identifier introduced by a LIBHEIF_API declaration.
+# For a function this is the identifier just before the argument list '(';
+# for an exported data object (e.g. heif_error_success) it is the last
+# identifier before the terminating ';'. Comments and preprocessor lines are
+# stripped first so that commented-out declarations and the macro definition
+# itself are ignored.
+find_args=()
+for h in $EXCLUDE; do find_args+=( ! -name "$h" ); done
+find "$API_DIR" -maxdepth 1 -name '*.h' "${find_args[@]}" -print0 \
+ | xargs -0 cat \
+ | perl -0777 -ne '
+ s{//[^\n]*|/\*.*?\*/}{}gs; # strip line and block comments
+ s/^\s*#.*$//mg; # strip preprocessor directives
+ while (/\bLIBHEIF_API\b\s+(.*?);/gs) {
+ my $d = $1;
+ if ($d =~ /(\w+)\s*\(/) { print "$1\n"; } # function
+ elsif ($d =~ /(\w+)\s*$/) { print "$1\n"; } # data object
+ }' \
+ | sort -u > "$TMP/declared.txt"
+
+nm -D --defined-only "$LIB" | awk '{print $NF}' | sort -u > "$TMP/defined.txt"
+
+declared_count=$(wc -l < "$TMP/declared.txt")
+missing=$(comm -23 "$TMP/declared.txt" "$TMP/defined.txt")
+
+# --- Report ------------------------------------------------------------------
+
+echo ""
+if [ -n "$missing" ]; then
+ missing_count=$(echo "$missing" | wc -l)
+ echo "API symbol check FAILED: $missing_count of $declared_count declared API symbols"
+ echo "are NOT defined in the built library ($(basename "$LIB")):"
+ echo "$missing" | sed 's/^/ /'
+ echo ""
+ echo "Each name above is declared (with LIBHEIF_API) in a public header but has"
+ echo "no matching definition. The usual cause is a definition whose name does"
+ echo "not match the declaration (see issue #1822)."
+ exit 1
+fi
+
+echo "API symbol check passed ($declared_count declared API symbols, all defined)."