Files
libakgl/.gitea/workflows/ci.yaml
Andrew Kesterson 4d80664cd9 Build clean under -Wall, with -Werror in CI only
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>
2026-08-01 09:18:52 -04:00

213 lines
8.5 KiB
YAML

name: libakgl CI Build
run-name: ${{ gitea.actor }} libakgl test
on: [push]
jobs:
cmake_build:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake doxygen gcc gcovr pkg-config \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
- name: Configure and build
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Debug \
-DAKGL_COVERAGE=ON \
-DAKGL_WERROR=ON
cmake --build build --parallel
- name: Build API documentation
run: doxygen Doxyfile
# The perf suites run here too -- they are ordinary CTest tests -- but this
# is a Debug build with coverage instrumentation, where a timing is a
# measurement of gcov. AKGL_BENCH_SCALE cuts their iteration counts so
# they contribute path coverage without spending ten minutes proving
# nothing; benchutil.h already refuses to enforce budgets in an
# unoptimized build. The performance job below is where the timings are
# taken and the budgets are checked.
- name: Test (JUnit)
env:
AKGL_BENCH_SCALE: '0.02'
run: |
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
ctest \
--test-dir build \
--output-on-failure \
--output-junit "$(pwd)/ctest-junit.xml"
- name: Publish test results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'ctest-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'true'
- name: Upload coverage reports
if: always()
uses: actions/upload-artifact@v4
with:
name: code-coverage
path: build/coverage/
if-no-files-found: warn
# The benchmarks, in the only build where their numbers mean anything.
# tests/benchutil.h enforces a budget per measurement only when it is compiled
# optimized and running at full scale, which is exactly this job and no other:
# the coverage job above is Debug, and the memory job below is under valgrind.
# A budget is roughly ten times the baseline recorded in PERFORMANCE.md, which
# is what makes a shared, slower runner viable -- it catches an algorithmic
# regression and ignores a busy machine. If a budget still proves flaky here,
# raise that one budget with a measurement behind it and re-record the
# baseline; do not drop the gate.
performance:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install build dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake gcc pkg-config \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
- name: Configure and build
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DAKGL_WERROR=ON
cmake --build build --parallel
# --verbose rather than --output-on-failure: a passing benchmark's output
# is the entire point of running it, and --output-on-failure prints
# nothing at all for a green run. pipefail because the step ends in a
# pipe, whose status would otherwise be tee's -- always 0 -- and a blown
# budget would be reported as a passing step.
- name: Benchmarks (JUnit)
run: |
set -o pipefail
export LD_LIBRARY_PATH="$PWD/build:$PWD/build/deps/SDL:$PWD/build/deps/SDL_image:$PWD/build/deps/SDL_mixer:$PWD/build/deps/SDL_ttf"
ctest \
--test-dir build \
-L perf \
--verbose \
--output-junit "$(pwd)/perf-junit.xml" \
2>&1 | tee "$(pwd)/perf-baseline.txt"
- name: Publish benchmark results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'perf-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'true'
# Kept whether the job passed or failed: the tables are the point. A run
# that went red says which measurement moved, and a run that went green is
# the next baseline if somebody re-records PERFORMANCE.md.
- name: Upload benchmark tables
if: always()
uses: actions/upload-artifact@v4
with:
name: performance-baseline
path: perf-baseline.txt
if-no-files-found: warn
# Every suite under valgrind. No memory-check programs of their own: the perf
# suites scale themselves down when they detect valgrind, which turns them
# into the broadest path coverage in the tree at a cost this job can afford.
#
# This job gates. A definite leak, a read past the end of an allocation, or a
# branch on uninitialised memory fails the build on the push that introduced
# it -- not on the day somebody gets around to caring. The six findings this
# job had on its first run were fixed to make that possible rather than
# excused into a warning.
memory_check:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install memory-check dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake gcc pkg-config valgrind \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
# RelWithDebInfo rather than Debug: valgrind needs the symbols, and -O0
# would leave every inlined frame in the stacks it prints.
- name: Configure and build
run: |
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build --parallel
# Every suite, character included. It was excluded from both this job
# and the coverage job on the grounds that it failed deliberately -- it
# did not. It was reporting success while running one of its four tests;
# see TODO.md, "Test suites that could not fail".
- name: Memory check
run: scripts/memcheck.sh
- name: Upload valgrind logs
if: always()
uses: actions/upload-artifact@v4
with:
name: valgrind-logs
path: build/Testing/Temporary/MemoryChecker.*.log
if-no-files-found: warn
mutation_test:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Install mutation-test dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y \
cmake gcc pkg-config python3 \
libasound2-dev libfreetype-dev libharfbuzz-dev \
libpng-dev libtiff-dev libwebp-dev \
libudev-dev libx11-dev libxcursor-dev libxext-dev \
libxfixes-dev libxi-dev libxrandr-dev libxrender-dev \
libxss-dev libxtst-dev
# Keep CI bounded to a focused source file. Run the default target locally
# for the complete libakgl-owned src/ tree.
- name: Mutation testing
run: |
python3 scripts/mutation_test.py \
--target src/staticstring.c \
--junit mutation-junit.xml \
--threshold 50
- name: Publish mutation results
if: always()
uses: mikepenz/action-junit-report@v4
with:
report_paths: 'mutation-junit.xml'
annotate_only: true
detailed_summary: true
include_passed: true
fail_on_failure: 'false'