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.2.0 LANGUAGES C) 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() if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) # Vendored projects own their test suites. Suppress their CTest registration # while embedded so the top-level suite contains only targets built here. 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() 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_subdirectory(deps/jansson EXCLUDE_FROM_ALL) add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL) add_subdirectory(deps/libakstdlib EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_image EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_mixer EXCLUDE_FROM_ALL) add_subdirectory(deps/SDL_ttf EXCLUDE_FROM_ALL) # 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) else() find_package(PkgConfig REQUIRED) 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() endif() 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}") add_custom_command( OUTPUT ${GAMECONTROLLERDB_H} COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/mkcontrollermappings.sh ${CMAKE_CURRENT_SOURCE_DIR} COMMENT "Generating controller mappings ..." ) # 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 ${GAMECONTROLLERDB_H} ) # 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/.c, built as # akgl_test_ and registered with CTest under . set(AKGL_TEST_SUITES actor audio bitmasks character controller draw error game heap json_helpers physics registry sprite staticstring text tilemap util version ) # 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) 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) set_tests_properties( ${AKGL_TEST_SUITES} semver_unit PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/tests" TIMEOUT 30 ) # 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}") add_test( NAME coverage_reset COMMAND ${GCOVR_EXECUTABLE} --root "${CMAKE_CURRENT_SOURCE_DIR}" --object-directory "${CMAKE_CURRENT_BINARY_DIR}" --delete ) 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. 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}" --object-directory "${CMAKE_CURRENT_BINARY_DIR}" --filter "${CMAKE_CURRENT_SOURCE_DIR}/src/" --xml-pretty --xml "${AKGL_COVERAGE_DIR}/coverage.xml" --html-details "${AKGL_COVERAGE_DIR}/index.html" ) 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) 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. if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) set(AKGL_VENDORED_RPATH "$" "$" "$" "$" "$" "$" ) foreach(suite IN LISTS AKGL_TEST_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} PROPERTIES ENVIRONMENT_MODIFICATION "${AKGL_TEST_ENV_MOD}" ) else() string(REPLACE ";" ":" AKGL_TEST_LIBPATH_JOINED "${AKGL_TEST_LIBPATH}") set_tests_properties( ${AKGL_TEST_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() 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/") install(FILES "include/akgl/actor.h" DESTINATION "include/akgl/") install(FILES "include/akgl/types.h" DESTINATION "include/akgl/") install(FILES "include/akgl/text.h" DESTINATION "include/akgl/") install(FILES "include/akgl/assets.h" DESTINATION "include/akgl/") install(FILES "include/akgl/audio.h" DESTINATION "include/akgl/") install(FILES "include/akgl/character.h" DESTINATION "include/akgl/") install(FILES "include/akgl/error.h" DESTINATION "include/akgl/") install(FILES "include/akgl/draw.h" DESTINATION "include/akgl/") install(FILES "include/akgl/game.h" DESTINATION "include/akgl/") install(FILES "include/akgl/controller.h" DESTINATION "include/akgl/") install(FILES "include/akgl/heap.h" DESTINATION "include/akgl/") install(FILES "include/akgl/iterator.h" DESTINATION "include/akgl/") install(FILES "include/akgl/json_helpers.h" DESTINATION "include/akgl/") install(FILES "include/akgl/renderer.h" DESTINATION "include/akgl/") install(FILES "include/akgl/physics.h" DESTINATION "include/akgl/") install(FILES "include/akgl/registry.h" DESTINATION "include/akgl/") install(FILES "include/akgl/sprite.h" DESTINATION "include/akgl/") install(FILES "include/akgl/staticstring.h" DESTINATION "include/akgl/") install(FILES "include/akgl/tilemap.h" DESTINATION "include/akgl/") install(FILES "include/akgl/util.h" DESTINATION "include/akgl/") install(FILES ${GAMECONTROLLERDB_H} DESTINATION "include/akgl/")