Every libakgl test suite could report success while failing. libakerror's default unhandled-error handler ended in exit(errctx->status), an exit status is one byte wide, and libakgl's band starts at 256 -- so AKGL_ERR_SDL, the most common failure a library built on SDL can have, exited 0 and CTest recorded a pass. tests/character.c aborted at its second of four tests on a bad renderer and was green for months. 0.5.0 worked around that here with TEST_TRAP_UNHANDLED_ERRORS() in tests/testutil.h, and TODO.md ended the entry saying any consumer's suites have the same problem and it was worth raising upstream. It was. 2.0.1 fixes it at the source: akerr_exit() owns the mapping and the default handler calls it, so 0 exits 0, 1 through 255 exit themselves, and anything else exits AKERR_EXIT_STATUS_UNREPRESENTABLE (125). The trap and its 21 call sites are gone. Verified by putting the original failure back rather than by reading the release notes: a FAIL_BREAK(AKGL_ERR_SDL) in tests/character.c's main exits 125 and CTest reports a failure. A standalone consumer raising the same status unhandled exits 125 where it exited 0 before. tests/actor.c installs its own handler and called exit(errctx->status) from it, which is the same defect one layer up. It calls akerr_exit() now. 2.0.0 also makes the error pool and the status registry thread safe, which libakgl needs more than it knew: audio_stream_callback raises error contexts on SDL's audio thread. With an unlocked pool that callback and the main thread could scan AKERR_ARRAY_ERROR at the same time and be handed the same slot. The comment there says so. This is a hard dependency floor, not a preference. 2.0.0 moved __akerr_last_ignored to thread-local storage and made akerr_next_error() return a context that already holds its reference, and both expand at libakgl's call sites -- and at a consumer's, because akerror.h is part of libakgl's public interface. Mixing headers and libraries across that line double-counts every reference and never returns a pool slot. The soname moved to libakerror.so.2; include/akgl/error.h now also feature- tests AKERR_EXIT_STATUS_UNREPRESENTABLE, which is the narrowest probe for 2.0.1 since libakerror publishes no version macro. 0.7.0 for that reason: libakgl's own ABI is unchanged, but the one it re-exports through its headers is not. TODO.md records the pkg-config gap this makes sharper -- akgl.pc names no dependencies at all, so nothing tells a pkg-config consumer which libakerror it needs. Clean build, 26/26 ctest, memcheck clean, warning-clean at -Wall -Werror. libakgl.so.0.7 links libakerror.so.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B8T5FAYXE8HEJqFLCYwNNc
627 lines
26 KiB
CMake
627 lines
26 KiB
CMake
cmake_minimum_required(VERSION 3.10)
|
|
# The single source of truth for the library version. It drives the generated
|
|
# include/akgl/version.h, the shared library's VERSION and SOVERSION, and the
|
|
# Version field in akgl.pc. Bump it here and nowhere else.
|
|
project(akgl VERSION 0.7.0 LANGUAGES C)
|
|
|
|
# Memory checking reuses the suites that already exist -- `ctest -T memcheck`
|
|
# runs every registered test under valgrind -- rather than adding programs of its
|
|
# own. The perf suites carry most of the weight there: they are the only things
|
|
# in the tree that load assets, draw a scene, and run a frame loop in one
|
|
# process, and tests/benchutil.h drops their iteration counts by three orders of
|
|
# magnitude when it finds itself under valgrind, which turns a benchmark into
|
|
# exactly the broad, once-through path coverage a leak check wants.
|
|
#
|
|
# These have to be set before include(CTest): that is what writes them into
|
|
# DartConfiguration.tcl, and a memcheck run reads them from there.
|
|
find_program(MEMORYCHECK_COMMAND valgrind)
|
|
# Set with FORCE, but only when empty. include(CTest) declares both of these as
|
|
# empty cache entries, so a build tree configured before this block existed has
|
|
# them already and a plain set(... CACHE ...) would be ignored -- the values
|
|
# would silently never reach DartConfiguration.tcl. Guarding on emptiness still
|
|
# leaves a deliberate -DMEMORYCHECK_SUPPRESSIONS_FILE=... alone.
|
|
if(NOT MEMORYCHECK_SUPPRESSIONS_FILE)
|
|
set(MEMORYCHECK_SUPPRESSIONS_FILE
|
|
"${CMAKE_CURRENT_SOURCE_DIR}/scripts/valgrind.supp"
|
|
CACHE FILEPATH "Suppressions for third-party findings the memcheck run cannot fix" FORCE)
|
|
endif()
|
|
# Definite losses only. "Still reachable" is every global SDL and FreeType keeps
|
|
# for the process lifetime and says nothing about libakgl; "possibly lost" is
|
|
# dominated by interior pointers into pools and thread stacks. Neither is worth
|
|
# the false positives.
|
|
if(NOT MEMORYCHECK_COMMAND_OPTIONS)
|
|
set(MEMORYCHECK_COMMAND_OPTIONS
|
|
"--leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite --track-origins=yes --num-callers=25"
|
|
CACHE STRING "Options passed to the memory checker" FORCE)
|
|
endif()
|
|
|
|
include(CTest)
|
|
option(AKGL_COVERAGE "Instrument libakgl and generate coverage reports with CTest" OFF)
|
|
|
|
# -Wall is always on for code this project owns. -Werror is not, and the
|
|
# distinction is deliberate.
|
|
#
|
|
# libakgl is consumed with add_subdirectory() -- akbasic does exactly that -- so
|
|
# a target-level -Werror makes every new compiler diagnostic a broken build for
|
|
# somebody else's project. The warning set is not even stable across build types
|
|
# here: an -O2 build reports ten -Wstringop-truncation warnings that -O0 and
|
|
# -fsyntax-only do not, because they need the middle end. A newer GCC or a
|
|
# switch to clang moves the line again.
|
|
#
|
|
# So the errors live in CI, where the compiler is pinned and a new warning is a
|
|
# task for the maintainer rather than an outage for a consumer.
|
|
option(AKGL_WERROR "Treat compiler warnings as errors. For CI; leave OFF when embedding." OFF)
|
|
set(AKGL_WARNING_FLAGS -Wall)
|
|
if(AKGL_WERROR)
|
|
list(APPEND AKGL_WARNING_FLAGS -Werror)
|
|
endif()
|
|
|
|
# -Wextra is deliberately not here. It adds 22 findings, 17 of which are
|
|
# inherent to the design rather than defects: 13 -Wunused-parameter, because a
|
|
# backend vtable entry or an SDL callback has to match a signature whether it
|
|
# reads every argument or not, and 4 -Wimplicit-fallthrough from libakerror's
|
|
# own PROCESS/HANDLE/HANDLE_GROUP, which fall through between case labels by
|
|
# design. Adopting it would mean disabling both permanently to gain 5
|
|
# -Wsign-compare. See deps/libakerror/TODO.md item 8.
|
|
|
|
if(AKGL_COVERAGE)
|
|
if(NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
|
message(FATAL_ERROR "AKGL_COVERAGE requires GCC or Clang")
|
|
endif()
|
|
find_program(GCOVR_EXECUTABLE gcovr REQUIRED)
|
|
endif()
|
|
|
|
# Vendored projects own their test suites. Suppress their CTest registration so
|
|
# the suite that runs contains only the targets built here. The override is
|
|
# lifted again below, before this project registers its own tests.
|
|
#
|
|
# **Only when this project is top-level, and that guard is load-bearing.** CMake
|
|
# exposes an overridden command as `_name` and chains exactly one level deep: a
|
|
# second override rebinds `_add_test` to the *first* override and the builtin
|
|
# becomes unreachable to everybody, forever. So two projects in one tree cannot
|
|
# both shadow add_test() -- whoever goes second breaks, and an embedding
|
|
# consumer that shadows it first sees its own registrations recurse until CMake
|
|
# stops at depth 1000.
|
|
#
|
|
# An embedded libakgl therefore leaves the override alone and lets its consumer
|
|
# suppress what it does not want; the consumer has to have that machinery
|
|
# anyway, for libakerror and libakstdlib. libakstdlib guards its own shadow the
|
|
# same way and for the same reason.
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKGL_SUPPRESS_DEPENDENCY_TESTS TRUE)
|
|
function(add_test)
|
|
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
|
_add_test(${ARGV})
|
|
endif()
|
|
endfunction()
|
|
function(set_tests_properties)
|
|
if(NOT AKGL_SUPPRESS_DEPENDENCY_TESTS)
|
|
_set_tests_properties(${ARGV})
|
|
endif()
|
|
endfunction()
|
|
endif()
|
|
|
|
set(JANSSON_WITHOUT_TESTS ON CACHE BOOL "Do not build vendored Jansson tests" FORCE)
|
|
set(JANSSON_EXAMPLES OFF CACHE BOOL "Do not build vendored Jansson examples" FORCE)
|
|
set(JANSSON_BUILD_DOCS OFF CACHE BOOL "Do not build vendored Jansson docs" FORCE)
|
|
|
|
# Add one vendored dependency, if nobody has already declared it and the
|
|
# submodule is actually checked out.
|
|
#
|
|
# A macro rather than a function on purpose: add_subdirectory() inside a
|
|
# function runs with that function's variable scope, so every cache-ish variable
|
|
# these projects set for their own subdirectories would be discarded on return.
|
|
macro(akgl_add_vendored_dependency target dir)
|
|
if(NOT TARGET ${target})
|
|
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${dir}/CMakeLists.txt")
|
|
add_subdirectory(${dir} EXCLUDE_FROM_ALL)
|
|
set(AKGL_VENDORED_DEPENDENCIES TRUE)
|
|
endif()
|
|
endif()
|
|
endmacro()
|
|
|
|
# Added on both paths, not just when this is the top-level project. Embedded
|
|
# with add_subdirectory(), the submodules under deps/ are sitting right there --
|
|
# the recursive clone the consumer just did put them there -- and refusing to
|
|
# configure until SDL3 is installed system-wide is a failure with its own answer
|
|
# three directories away. if(NOT TARGET ...) means a consumer that has already
|
|
# declared one of these wins; the EXISTS check means a checkout without
|
|
# submodules falls through to find_package below.
|
|
akgl_add_vendored_dependency(jansson::jansson deps/jansson)
|
|
akgl_add_vendored_dependency(akerror::akerror deps/libakerror)
|
|
akgl_add_vendored_dependency(akstdlib::akstdlib deps/libakstdlib)
|
|
akgl_add_vendored_dependency(SDL3::SDL3 deps/SDL)
|
|
akgl_add_vendored_dependency(SDL3_image::SDL3_image deps/SDL_image)
|
|
akgl_add_vendored_dependency(SDL3_mixer::SDL3_mixer deps/SDL_mixer)
|
|
akgl_add_vendored_dependency(SDL3_ttf::SDL3_ttf deps/SDL_ttf)
|
|
|
|
# libakerror sizes its own status-name registry; consumers no longer do. libakgl
|
|
# claims its status codes at runtime in akgl_heap_init() instead.
|
|
#
|
|
# The floor is 2.0.1, and it is a hard one. 2.0.0 moved __akerr_last_ignored to
|
|
# thread-local storage and made akerr_next_error() return a context that already
|
|
# holds its reference -- both of which expand at *libakgl's* call sites through
|
|
# IGNORE and the FAIL macros, and at a *consumer's* too, because akerror.h is
|
|
# part of libakgl's public interface. A tree that mixes headers and libraries
|
|
# across that line double-counts every reference and never returns a pool slot.
|
|
# The soname moved to libakerror.so.2 to stop it happening by accident; the
|
|
# #error in include/akgl/error.h catches a stale header in an install tree.
|
|
|
|
set(AKGL_SUPPRESS_DEPENDENCY_TESTS FALSE)
|
|
|
|
# Anything the vendored block did not supply has to come from the system.
|
|
if(NOT (TARGET SDL3::SDL3 AND TARGET SDL3_image::SDL3_image AND
|
|
TARGET SDL3_mixer::SDL3_mixer AND TARGET SDL3_ttf::SDL3_ttf AND
|
|
TARGET akerror::akerror AND TARGET akstdlib::akstdlib AND
|
|
TARGET jansson::jansson))
|
|
# Only needed to locate installed copies; a fully vendored build does not
|
|
# require pkg-config to be present at all.
|
|
find_package(PkgConfig REQUIRED)
|
|
endif()
|
|
|
|
if(NOT TARGET SDL3::SDL3)
|
|
find_package(SDL3 REQUIRED)
|
|
endif()
|
|
if(NOT TARGET SDL3_image::SDL3_image)
|
|
find_package(SDL3_image REQUIRED)
|
|
endif()
|
|
if(NOT TARGET SDL3_mixer::SDL3_mixer)
|
|
find_package(SDL3_mixer REQUIRED)
|
|
endif()
|
|
if(NOT TARGET SDL3_ttf::SDL3_ttf)
|
|
find_package(SDL3_ttf REQUIRED)
|
|
endif()
|
|
# No version here: libakerror ships no akerrorConfigVersion.cmake, so asking
|
|
# for one makes find_package reject every install. The floor is enforced by
|
|
# the #error in include/akgl/error.h instead, which feature-tests
|
|
# AKERR_FIRST_CONSUMER_STATUS and AKERR_EXIT_STATUS_UNREPRESENTABLE -- the
|
|
# latter arriving in 2.0.1, which is the version libakgl needs.
|
|
if(NOT TARGET akerror::akerror)
|
|
find_package(akerror REQUIRED)
|
|
endif()
|
|
# 0.2 rather than bare: libakstdlib 0.2.0 ships an akstdlibConfigVersion.cmake
|
|
# with SameMinorVersion compatibility, mirroring its soname, so this accepts
|
|
# any 0.2.x and refuses 0.3 and 1.0. Unversioned, this path would silently
|
|
# accept an ABI-incompatible libakstdlib.
|
|
if(NOT TARGET akstdlib::akstdlib)
|
|
find_package(akstdlib 0.2 REQUIRED)
|
|
endif()
|
|
if(NOT TARGET jansson::jansson)
|
|
find_package(jansson)
|
|
endif()
|
|
|
|
# Every header installed into include/akgl/, by base name. This list is the
|
|
# single source of truth for two things that must not drift apart: what gets
|
|
# installed, and what the `headers` suite proves is self-contained. Adding a
|
|
# header here is all a new one needs. SDL_GameControllerDB.h and the generated
|
|
# version.h are installed separately -- neither is an API header.
|
|
set(AKGL_PUBLIC_HEADERS
|
|
actor
|
|
assets
|
|
audio
|
|
character
|
|
controller
|
|
draw
|
|
error
|
|
game
|
|
heap
|
|
iterator
|
|
json_helpers
|
|
physics
|
|
registry
|
|
renderer
|
|
sprite
|
|
staticstring
|
|
text
|
|
tilemap
|
|
types
|
|
util
|
|
)
|
|
|
|
set(GAMECONTROLLERDB_H "include/akgl/SDL_GameControllerDB.h")
|
|
set(prefix ${CMAKE_INSTALL_PREFIX})
|
|
set(exec_prefix "\${prefix}")
|
|
set(libdir "\${exec_prefix}/lib")
|
|
set(includedir "\${prefix}/include")
|
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/akgl.pc.in ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc @ONLY)
|
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/include/akgl/version.h.in
|
|
${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h @ONLY)
|
|
|
|
# Tests use both relative paths and SDL_GetBasePath(), so stage fixtures beside
|
|
# test executables in every out-of-tree build.
|
|
file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/tests/assets"
|
|
DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")
|
|
|
|
# Regenerating the controller database is a deliberate act, not part of a build.
|
|
#
|
|
# This used to be an add_custom_command whose OUTPUT was the *relative* path
|
|
# "include/akgl/SDL_GameControllerDB.h", which CMake resolves against the binary
|
|
# directory while the script writes to the source directory. The declared output
|
|
# therefore never appeared, the command was permanently out of date, and every
|
|
# build re-ran it -- so every build needed network access and left the tracked
|
|
# header dirty with a fresh $(date) stamp.
|
|
#
|
|
# `cmake --build build --target controllerdb` when newer mappings are actually
|
|
# wanted. The result is its own commit, per AGENTS.md.
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKGL_CONTROLLERDB_TARGET controllerdb)
|
|
else()
|
|
set(AKGL_CONTROLLERDB_TARGET akgl_controllerdb)
|
|
endif()
|
|
add_custom_target(${AKGL_CONTROLLERDB_TARGET}
|
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/mkcontrollermappings.sh ${CMAKE_CURRENT_SOURCE_DIR}
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
USES_TERMINAL
|
|
COMMENT "Fetching controller mappings and rewriting ${GAMECONTROLLERDB_H} ..."
|
|
)
|
|
|
|
# Add include directories
|
|
include_directories(${SDL3_INCLUDE_DIRS})
|
|
add_library(akgl SHARED
|
|
deps/semver/semver.c
|
|
src/actor.c
|
|
src/actor_state_string_names.c
|
|
src/audio.c
|
|
src/text.c
|
|
src/assets.c
|
|
src/character.c
|
|
src/draw.c
|
|
src/error.c
|
|
src/game.c
|
|
src/controller.c
|
|
src/heap.c
|
|
src/json_helpers.c
|
|
src/registry.c
|
|
src/renderer.c
|
|
src/physics.c
|
|
src/sprite.c
|
|
src/staticstring.c
|
|
src/tilemap.c
|
|
src/util.c
|
|
src/version.c
|
|
)
|
|
|
|
# While the major version is 0 the ABI is not stable across minor releases, so
|
|
# the soname carries major.minor -- libakgl.so.0.1. A plain SOVERSION 0 would
|
|
# claim 0.1.0 and 0.2.0 are interchangeable, which is exactly the silent
|
|
# mispairing the soname is here to prevent. At 1.0.0 this becomes the major
|
|
# alone, matching libakerror.
|
|
if(PROJECT_VERSION_MAJOR EQUAL 0)
|
|
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
|
|
else()
|
|
set(AKGL_SOVERSION "${PROJECT_VERSION_MAJOR}")
|
|
endif()
|
|
|
|
set_target_properties(akgl PROPERTIES
|
|
VERSION ${PROJECT_VERSION}
|
|
SOVERSION ${AKGL_SOVERSION}
|
|
)
|
|
|
|
target_compile_options(akgl PRIVATE ${AKGL_WARNING_FLAGS})
|
|
# deps/semver/semver.c is vendored and listed directly in add_library(), so the
|
|
# target's PRIVATE options reach it. Exempt it: a future semver update must not
|
|
# be able to fail this build. (PRIVATE options do not reach SDL, jansson,
|
|
# akerror or akstdlib -- those are separate targets.)
|
|
set_source_files_properties(deps/semver/semver.c PROPERTIES COMPILE_OPTIONS "-w")
|
|
|
|
add_library(akgl::akgl ALIAS akgl)
|
|
|
|
add_executable(charviewer util/charviewer.c)
|
|
add_executable(akgl_test_semver_unit deps/semver/semver_unit.c)
|
|
|
|
# Every suite here is a standalone C program named tests/<name>.c, built as
|
|
# akgl_test_<name> and registered with CTest under <name>.
|
|
set(AKGL_TEST_SUITES
|
|
actor
|
|
audio
|
|
bitmasks
|
|
character
|
|
controller
|
|
draw
|
|
error
|
|
game
|
|
headers
|
|
heap
|
|
json_helpers
|
|
physics
|
|
physics_sim
|
|
registry
|
|
renderer
|
|
sprite
|
|
staticstring
|
|
text
|
|
tilemap
|
|
util
|
|
version
|
|
)
|
|
|
|
# The performance suites are built and registered exactly like the unit suites,
|
|
# but they are benchmarks: they drive hot paths for millions of iterations, print
|
|
# a table of nanoseconds per operation, and fail only when a measurement exceeds
|
|
# a budget set at roughly ten times the recorded baseline. They carry the `perf`
|
|
# label, so `ctest -L perf` runs only them and `ctest -LE perf` leaves them out
|
|
# of an ordinary run, and they get a much longer timeout for obvious reasons.
|
|
#
|
|
# Set AKGL_BENCH_SCALE in the environment to change how long they run --
|
|
# `AKGL_BENCH_SCALE=0.1 ctest -L perf` for a tenth of the iterations. Below 1.0
|
|
# the budgets are measured and reported but not enforced, because a short run is
|
|
# a noisy one.
|
|
set(AKGL_PERF_SUITES
|
|
perf
|
|
perf_render
|
|
)
|
|
|
|
# The executables carry an akgl_ prefix but the CTest names do not: a vendored
|
|
# dependency is free to ship its own tests/version.c, and libakstdlib now does.
|
|
# Its target is created by add_subdirectory even though EXCLUDE_FROM_ALL keeps
|
|
# it from being built, so an unprefixed test_version here is a configure error.
|
|
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
|
add_executable(akgl_test_${suite} tests/${suite}.c)
|
|
add_test(NAME ${suite} COMMAND akgl_test_${suite})
|
|
endforeach()
|
|
|
|
add_test(NAME semver_unit COMMAND akgl_test_semver_unit)
|
|
|
|
# Not a C program, so it is not in AKGL_TEST_SUITES: it reads the built library's
|
|
# dynamic symbol table and every public header, and fails when a function with
|
|
# external linkage is declared nowhere. That is an ABI question rather than a
|
|
# behavioural one, and nineteen symbols had drifted out of the headers before
|
|
# anything was checking.
|
|
add_test(
|
|
NAME api_surface
|
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_api_surface.sh
|
|
$<TARGET_FILE:akgl>
|
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
|
${CMAKE_CURRENT_BINARY_DIR}/include
|
|
)
|
|
set_tests_properties(api_surface PROPERTIES SKIP_RETURN_CODE 2)
|
|
|
|
# The two akerror control-flow rules whose failure mode is silent: a *_RETURN
|
|
# inside an ATTEMPT block, and a return out of a HANDLE block. Neither produces
|
|
# a compiler diagnostic and neither fails a test run until the pool it drains is
|
|
# empty, which is why both have already shipped.
|
|
#
|
|
# The mutation target below wants the same interpreter; find_package caches, so
|
|
# asking here as well costs nothing and keeps this block self-contained.
|
|
find_package(Python3 COMPONENTS Interpreter)
|
|
if(Python3_Interpreter_FOUND)
|
|
add_test(
|
|
NAME error_protocol
|
|
COMMAND ${Python3_EXECUTABLE}
|
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/check_error_protocol.py
|
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
|
)
|
|
endif()
|
|
|
|
# One translation unit per public header, each including exactly that header and
|
|
# nothing before it. This is the only shape that proves a header is
|
|
# self-contained: a second #include in the same file proves nothing about the
|
|
# second, because by then the first has dragged its dependencies in. Generated
|
|
# rather than written by hand so the check cannot fall behind
|
|
# AKGL_PUBLIC_HEADERS -- the failure being guarded against is a header nobody
|
|
# remembered to cover.
|
|
set(AKGL_HEADER_SELFTEST_SOURCES "")
|
|
foreach(header IN LISTS AKGL_PUBLIC_HEADERS)
|
|
set(generated "${CMAKE_CURRENT_BINARY_DIR}/tests/header_${header}.c")
|
|
file(WRITE "${generated}"
|
|
"/* Generated by CMakeLists.txt from AKGL_PUBLIC_HEADERS. Do not edit. */\n\
|
|
#include <akgl/${header}.h>\n\
|
|
\n\
|
|
/* ISO C forbids an empty translation unit; the compile is the assertion. */\n\
|
|
int akgl_header_selftest_${header}(void);\n\
|
|
int akgl_header_selftest_${header}(void)\n\
|
|
{\n\
|
|
return 0;\n\
|
|
}\n")
|
|
list(APPEND AKGL_HEADER_SELFTEST_SOURCES "${generated}")
|
|
endforeach()
|
|
target_sources(akgl_test_headers PRIVATE ${AKGL_HEADER_SELFTEST_SOURCES})
|
|
# Hand-written rather than generated: it checks a header's behaviour under a
|
|
# caller-supplied override, which is not something the generated shape covers.
|
|
target_sources(akgl_test_headers PRIVATE tests/header_pool_override.c)
|
|
|
|
# TIMEOUT is generous because CTest applies the same property to `ctest -T
|
|
# memcheck`, where every suite runs under valgrind at something like twenty
|
|
# times its normal cost. The unit suites finish in well under a second each
|
|
# without it; the ceiling is there for the checked run, not the ordinary one.
|
|
set_tests_properties(
|
|
${AKGL_TEST_SUITES} semver_unit
|
|
PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 300
|
|
)
|
|
|
|
set_tests_properties(
|
|
${AKGL_PERF_SUITES}
|
|
PROPERTIES
|
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests"
|
|
TIMEOUT 900
|
|
LABELS perf
|
|
)
|
|
|
|
# Specify include directories for the library's headers (if applicable)
|
|
target_include_directories(akgl PUBLIC
|
|
include/
|
|
deps/semver/
|
|
# akgl/version.h is generated by configure_file, so it lives in the build tree
|
|
# rather than beside the headers it is included from.
|
|
${CMAKE_CURRENT_BINARY_DIR}/include/
|
|
)
|
|
|
|
if(AKGL_COVERAGE)
|
|
target_compile_options(akgl PRIVATE --coverage -O0 -g)
|
|
target_link_options(akgl PRIVATE --coverage)
|
|
|
|
set(AKGL_COVERAGE_DIR "${CMAKE_CURRENT_BINARY_DIR}/coverage")
|
|
file(MAKE_DIRECTORY "${AKGL_COVERAGE_DIR}")
|
|
|
|
# Both gcovr invocations take the build tree as an explicit positional search
|
|
# path, and neither passes --object-directory.
|
|
#
|
|
# gcovr looks for .gcda/.gcno under its search paths, and with none given it
|
|
# searches --root -- the source directory, which is where developers keep
|
|
# their build trees. --object-directory does *not* narrow that: per gcovr's
|
|
# own help it only identifies "the path between gcda files and the directory
|
|
# where the compiler was originally run". So every instrumented tree left
|
|
# inside the source directory was folded into the report alongside the one
|
|
# being measured, and with two trees describing different line numbers for the
|
|
# same function, coverage_reset failed before any test ran:
|
|
#
|
|
# AssertionError: Got function akgl_game_lowfps on multiple lines: 45, 46
|
|
#
|
|
# The `build*/` entry in .gitignore hides those trees from git status, which
|
|
# makes the state easy to get into and hard to notice.
|
|
|
|
add_test(
|
|
NAME coverage_reset
|
|
COMMAND ${GCOVR_EXECUTABLE}
|
|
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
--delete
|
|
"${CMAKE_CURRENT_BINARY_DIR}"
|
|
)
|
|
set_tests_properties(coverage_reset PROPERTIES FIXTURES_SETUP akgl_coverage)
|
|
# Any suite missing from this list runs outside the fixture and has its
|
|
# counters discarded by coverage_reset. The perf suites are deliberately
|
|
# outside it: an instrumented -O0 build measures gcov, not libakgl, and the
|
|
# lines they cover are covered by the unit suites anyway.
|
|
set_tests_properties(
|
|
${AKGL_TEST_SUITES} semver_unit
|
|
PROPERTIES FIXTURES_REQUIRED akgl_coverage
|
|
)
|
|
|
|
add_test(
|
|
NAME coverage_report
|
|
COMMAND ${GCOVR_EXECUTABLE}
|
|
--root "${CMAKE_CURRENT_SOURCE_DIR}"
|
|
--filter "${CMAKE_CURRENT_SOURCE_DIR}/src/"
|
|
--xml-pretty
|
|
--xml "${AKGL_COVERAGE_DIR}/coverage.xml"
|
|
--html-details "${AKGL_COVERAGE_DIR}/index.html"
|
|
"${CMAKE_CURRENT_BINARY_DIR}"
|
|
)
|
|
set_tests_properties(
|
|
coverage_report
|
|
PROPERTIES
|
|
FIXTURES_CLEANUP akgl_coverage
|
|
WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
|
|
)
|
|
endif()
|
|
|
|
target_link_libraries(akgl
|
|
PUBLIC
|
|
SDL3::SDL3
|
|
SDL3_image::SDL3_image
|
|
SDL3_mixer::SDL3_mixer
|
|
SDL3_ttf::SDL3_ttf
|
|
akstdlib::akstdlib
|
|
akerror::akerror
|
|
jansson::jansson
|
|
)
|
|
|
|
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
|
target_link_libraries(akgl_test_${suite} PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
|
target_include_directories(akgl_test_${suite} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
|
target_compile_options(akgl_test_${suite} PRIVATE ${AKGL_WARNING_FLAGS})
|
|
endforeach()
|
|
|
|
target_link_libraries(charviewer PRIVATE akstdlib::akstdlib akerror::akerror akgl SDL3::SDL3 SDL3_ttf::SDL3_ttf SDL3_image::SDL3_image SDL3_mixer::SDL3_mixer jansson::jansson -lm)
|
|
target_compile_options(charviewer PRIVATE ${AKGL_WARNING_FLAGS})
|
|
|
|
# When the vendored SDL satellite libraries are built in-tree they land in per-
|
|
# project subdirectories that are not on the loader's default search path, so a
|
|
# freshly built test aborts before main() with "cannot open shared object file".
|
|
# Bake those directories into the build-tree RPATH. Installed builds resolve the
|
|
# same libraries through find_package and need no help, so this keys on whether
|
|
# anything was actually vendored rather than on being the top-level project.
|
|
if(AKGL_VENDORED_DEPENDENCIES)
|
|
set(AKGL_VENDORED_RPATH
|
|
"$<TARGET_FILE_DIR:SDL3::SDL3>"
|
|
"$<TARGET_FILE_DIR:SDL3_image::SDL3_image>"
|
|
"$<TARGET_FILE_DIR:SDL3_ttf::SDL3_ttf>"
|
|
"$<TARGET_FILE_DIR:SDL3_mixer::SDL3_mixer>"
|
|
"$<TARGET_FILE_DIR:akerror::akerror>"
|
|
"$<TARGET_FILE_DIR:akstdlib::akstdlib>"
|
|
)
|
|
foreach(suite IN LISTS AKGL_TEST_SUITES AKGL_PERF_SUITES)
|
|
set_target_properties(akgl_test_${suite} PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
|
endforeach()
|
|
set_target_properties(charviewer akgl PROPERTIES BUILD_RPATH "${AKGL_VENDORED_RPATH}")
|
|
|
|
# RPATH alone is not enough: LD_LIBRARY_PATH is searched first, so a developer
|
|
# who has previously run rebuild.sh has an installed libakgl.so ahead of the
|
|
# one under test. Prepend the build tree for the CTest run so the suite always
|
|
# exercises what was just compiled.
|
|
set(AKGL_TEST_LIBPATH
|
|
"${CMAKE_CURRENT_BINARY_DIR}"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_image"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_ttf"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/SDL_mixer"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakerror"
|
|
"${CMAKE_CURRENT_BINARY_DIR}/deps/libakstdlib"
|
|
)
|
|
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22")
|
|
set(AKGL_TEST_ENV_MOD "")
|
|
foreach(dir IN LISTS AKGL_TEST_LIBPATH)
|
|
list(APPEND AKGL_TEST_ENV_MOD "LD_LIBRARY_PATH=path_list_prepend:${dir}")
|
|
endforeach()
|
|
set_tests_properties(
|
|
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
|
PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}"
|
|
)
|
|
else()
|
|
string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}")
|
|
set_tests_properties(
|
|
${AKGL_TEST_SUITES} ${AKGL_PERF_SUITES}
|
|
PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${AKGL_TEST_LIBPATH_JOINED}:$ENV{LD_LIBRARY_PATH}"
|
|
)
|
|
endif()
|
|
endif()
|
|
|
|
# Mutation testing copies the repository to scratch space, applies one small
|
|
# source change at a time, and verifies that the passing tests detect it. The
|
|
# intentionally failing character test is excluded by the harness.
|
|
find_package(Python3 COMPONENTS Interpreter)
|
|
if(Python3_FOUND)
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKGL_MUTATION_TARGET mutation)
|
|
else()
|
|
set(AKGL_MUTATION_TARGET akgl_mutation)
|
|
endif()
|
|
add_custom_target(${AKGL_MUTATION_TARGET}
|
|
COMMAND ${Python3_EXECUTABLE}
|
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/mutation_test.py
|
|
--source-root ${CMAKE_CURRENT_SOURCE_DIR}
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
USES_TERMINAL
|
|
COMMENT "Running mutation tests (breaks a scratch copy, expects tests to fail)"
|
|
)
|
|
endif()
|
|
|
|
# Memory checking runs the whole registered suite under valgrind. The wrapper
|
|
# script exists because `ctest -T memcheck` records defects and still exits 0,
|
|
# which cannot gate anything; it also forces the headless drivers, so the vendor
|
|
# GPU stack is never loaded and never has to be suppressed.
|
|
if(MEMORYCHECK_COMMAND)
|
|
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
|
set(AKGL_MEMCHECK_TARGET memcheck)
|
|
else()
|
|
set(AKGL_MEMCHECK_TARGET akgl_memcheck)
|
|
endif()
|
|
add_custom_target(${AKGL_MEMCHECK_TARGET}
|
|
COMMAND ${CMAKE_COMMAND} -E env
|
|
AKGL_BUILD_DIR=${CMAKE_CURRENT_BINARY_DIR}
|
|
${CMAKE_CURRENT_SOURCE_DIR}/scripts/memcheck.sh
|
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
|
|
USES_TERMINAL
|
|
COMMENT "Running every test suite under valgrind (slow; the perf suites scale themselves down)"
|
|
)
|
|
endif()
|
|
|
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akgl.pc DESTINATION "lib/pkgconfig/")
|
|
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/akgl/version.h DESTINATION "include/akgl/")
|
|
install(TARGETS akgl DESTINATION "lib/")
|
|
install(FILES "deps/semver/semver.h" DESTINATION "include/")
|
|
foreach(header IN LISTS AKGL_PUBLIC_HEADERS)
|
|
install(FILES "include/akgl/${header}.h" DESTINATION "include/akgl/")
|
|
endforeach()
|
|
install(FILES ${GAMECONTROLLERDB_H} DESTINATION "include/akgl/")
|