Files
libakgl/CMakeLists.txt
Andrew Kesterson 28fa929f6a Make savegames, optional array elements, empty text and coverage work
Closes Known-and-still-open items 7 and 13, and Defects items 18, 25 and 27.

The savegame name tables carry no length prefix, so writer and reader have to
agree on a field width exactly. The writer used each object's own maximum name
length -- 512 for a spritesheet, a filename -- and the reader used
AKGL_ACTOR_MAX_NAME_LENGTH for all four. Four AKGL_GAME_SAVE_*_NAME_WIDTH
constants drive both sides now.

The failure turned out to be worse than "cannot be read back": a reader
stepping the wrong width does not run off anything, it finds a run of zeros
inside an entry, stops early, and reports success with silently wrong maps. A
test asserting only that the load succeeded passed against the broken reader.
So akgl_game_load checks it is at EOF once the tables are read, which turns a
width disagreement into AKERR_IO instead of a corruption. That is the assertion
the new roundtrip test -- the first with all four registries populated -- hangs
on.

akgl_get_json_with_default gains a third HANDLE_GROUP for AKERR_OUTOFBOUNDS,
which is what the array index accessors report, so "this element is optional"
works for an array element and not only for an object member. The arm goes
above the one holding the memcpy: HANDLE_GROUP emits no break and every arm
falls into that body.

That test needed a second attempt. with_default returns *the context it was
given* when it does not handle the status, so TEST_EXPECT_OK -- which releases
whatever the statement returns -- double-released it against a CLEANUP block
that released it too, and a double-released context corrupts the failure rather
than reporting it. The first draft passed against the unfixed library.

akgl_text_rendertextat returns success without rasterizing for the empty
string, matching akgl_text_measure, which has always accepted it. The check
sits after the font and backend guards, so drawing nothing still refuses what
drawing something refuses. tests/text.c had this case written and unasserted
waiting for the two halves of the header to agree.

character_load_json_state_int_from_strings guards dest rather than testing
states twice. Not asserted: the function is static with one call site that
passes a real pointer, so the guard cannot fire, and reaching it from a test
would mean giving it external linkage purely for that.

Both gcovr invocations take the build tree as an explicit positional search
path. gcovr searches --root when given none, which is the source directory,
where build trees live; --object-directory does not narrow it. Verified by
building two instrumented trees with a source edit between them: the old
invocation fails with "Got function write_exact on multiple lines: 46, 48" and
exits 64, the new one exits 0 and the full coverage run passes with the stale
tree still present.

25/25 pass, reindent --check, check_api_surface and check_error_protocol clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 07:34:51 -04:00

581 lines
23 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.5.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)
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 1.0.0 sizes its own status-name registry; consumers no longer do.
# libakgl claims its status codes at runtime in akgl_heap_init() instead.
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.
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}
)
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
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")
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)
# 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/")