TODO.md item 37 said the cast sweep buys nothing until the build turns on the warnings those casts suppress. This is that precondition, and the argument turned out to be right with evidence: -Wall found three genuine signedness mismatches in sprite.c, where uint32_t width, height and speed were passed straight to akgl_get_json_integer_value(..., int *). A cast would have silenced all three. Fixing them properly also turned up that speed is scaled by a million into a 32-bit field, so anything past 4294 ms overflowed rather than being held. The other real finding was ten -Wstringop-truncation warnings, every one a fixed-width strncpy. Those leave a name field unterminated when the input fills it -- and those fields are registry keys, handed to strcmp and SDL property calls that do not stop at the field. Same overread class fixed twice already this release. All ten now use aksl_strncpy from akstdlib, which always terminates and never NUL-pads, bounded to size - 1 so an over-long name still truncates rather than being refused. staticstring.h documented the old strncpy semantics explicitly and has been rewritten to match. Two sites in game.c are deliberately left on strncpy: both stage into a pre-zeroed buffer for the savegame's fixed-width fields, where the NUL padding is part of the format. Neither is flagged. sprite.c also had a memcpy of a full 128-byte field from a NUL-terminated string, which reads past the end of any shorter name. Not flagged by anything; found while converting its neighbours. -Werror is an option, default OFF, on only in the cmake_build and performance CI jobs. libakgl is consumed with add_subdirectory -- akbasic does exactly that -- so a target-level -Werror turns every diagnostic a newer compiler invents into a broken build for somebody else. The warning set is not even stable across build types here: an -O2 build reports ten warnings that -O0 and -fsyntax-only do not, because they need the middle end. The two jobs that set it are one of each. -Wextra is deliberately not adopted. It adds 22 findings, 17 inherent to the design: 13 -Wunused-parameter from backend vtables and SDL callbacks that must match a signature, and 4 -Wimplicit-fallthrough from libakerror's own PROCESS/HANDLE/HANDLE_GROUP, which fall through by design. Filed upstream as deps/libakerror TODO item 8; the submodule bump carries it. deps/semver/semver.c is listed directly in add_library, so the target's PRIVATE options reach it. Exempted with -w so a future semver update cannot fail this build -- verified by forcing -Wextra -Werror and watching our files fail while semver compiled clean. Verified: RelWithDebInfo, Debug and coverage builds all clean under -Werror; -Werror actually fails on a planted unused variable; an embedded consumer with AKGL_WERROR unset still configures and builds. 25/25 pass, memcheck clean, reindent --check, check_api_surface and check_error_protocol clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
616 lines
25 KiB
CMake
616 lines
25 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)
|
|
|
|
# -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 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}
|
|
)
|
|
|
|
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
|
|
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/")
|