Fix the AKERR_USE_STDLIB=OFF build (issue #12)
All checks were successful
libakerror CI Build / cmake_build_freestanding (push) Successful in 2m51s
libakerror CI Build / coverage (push) Successful in 2m51s
libakerror CI Build / cmake_build (push) Successful in 2m55s
libakerror CI Build / mutation_test (push) Successful in 47m2s

The freestanding build (-DAKERR_USE_STDLIB=OFF) did not compile at all:
bool, PATH_MAX and NULL were used unconditionally in the public header
but only included under the stdlib branch, and the CMake option was
pasted straight into a preprocessor definition, so a non-numeric cache
spelling (-DAKERR_USE_STDLIB=ON) silently evaluated to 0.

- Normalize AKERR_USE_STDLIB to a plain 1/0 in CMake before stamping it,
  and use a consistent '#if AKERR_USE_STDLIB' everywhere it is tested.
- Include <stdbool.h>/<stddef.h> unconditionally (freestanding-safe);
  keep <stdlib.h>/<string.h>/<stdio.h> behind AKERR_USE_STDLIB; move
  <limits.h> out of the public header into src/error.c, its only user.
- Define the freestanding runtime contract: AKERR_RUNTIME_HEADER must
  name a header providing exit, memset, snprintf, strcmp, strlen and
  strncpy when AKERR_USE_STDLIB is OFF, or the header #errors naming
  them. Add cmake/akerr_default_runtime.h, a libc-backed convenience
  default so this repo's own OFF build and tests work out of the box.
- Route ENSURE_ERROR_READY's pool-exhaustion path through akerr_exit()
  instead of a direct exit(1). Deliberate behavior change: that exit
  code moves from 1 to AKERR_EXIT_STATUS_UNREPRESENTABLE (125), the same
  sentinel every other unrepresentable status already uses. Documented
  in docs/building.md and UPGRADING.md. Not an ABI break.
- Retire PATH_MAX: AKERR_MAX_ERROR_FNAME_LENGTH is now stamped by
  scripts/generrno.sh from a new AKERR_MAX_ERROR_FNAME_LENGTH cache
  variable, defaulting to 4096 (PATH_MAX on Linux/glibc) so
  sizeof(akerr_ErrorContext) and the soname are unchanged. Rewrite the
  now-stale PATH_MAX justification in src/lock.h's feature-test-macro
  comment.
- Fail the configure with a FATAL_ERROR, not a warning, when
  AKERR_USE_STDLIB=OFF and AKERR_THREADS would resolve to pthread,
  naming -DAKERR_THREADS=none as the fix.
- Keep the generated errno table (errno.c) out of the OFF build; skip
  the 'errno --list' shellout in scripts/generrno.sh under OFF and
  stamp AKERR_LAST_ERRNO_VALUE from a new fallback cache variable
  (default 133, Linux's EHWPOISON) instead.
- Update docs/building.md: drop the known-defect paragraph, fix
  sprintf -> snprintf, add size_t, drop PATH_MAX, and document the new
  options and the exit-code change.
- Guard tests/err_errno.c's registered-name assertion behind
  AKERR_USE_STDLIB: akerr_init_errno() is not called when it is OFF.
- Add a CI job that configures/builds/tests AKERR_USE_STDLIB=OFF with
  AKERR_THREADS=none, plus a compile-only -nostdinc -ffreestanding
  check of tests/freestanding_fixture.c against the generated header.

Bump the project version to 2.0.2 (no ABI break: soname and struct
layout are unchanged).

Verified locally: OFF+none configures and builds clean with all tests
passing; ON and the plain default build both build and pass their full
test suites (37/37); OFF with the default/auto thread backend fails
configure with a message naming -DAKERR_THREADS=none; sizeof(akerr_ErrorContext)
is unchanged (37296 bytes, fname/function still 4096 each) versus the
pre-change tree.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 10:29:39 -04:00
parent 11b6308eff
commit 983ecf31c9
12 changed files with 352 additions and 45 deletions

View File

@@ -9,13 +9,35 @@ cmake_minimum_required(VERSION 3.10)
# 2.0.1 fixes the unhandled-error exit code, which reported success for any
# status whose low byte was zero. It adds akerr_exit() but breaks nothing: the
# soname is unchanged and no existing entry point changed shape.
project(akerror VERSION 2.0.1 LANGUAGES C)
# 2.0.2 fixes the AKERR_USE_STDLIB=OFF build, which did not compile at all
# (issue #12): untangles the header's includes, defines the AKERR_RUNTIME_HEADER
# freestanding contract, retires PATH_MAX in favor of the AKERR_MAX_ERROR_FNAME_LENGTH
# build option (default unchanged, so this is not an ABI break), and fails the
# configure instead of the build when AKERR_THREADS would resolve to pthread
# under AKERR_USE_STDLIB=OFF. ENSURE_ERROR_READY's pool-exhaustion path now
# calls akerr_exit() instead of exit(1) directly, which changes that exit code
# from 1 to AKERR_EXIT_STATUS_UNREPRESENTABLE (125) -- a deliberate behavior
# change, not an ABI break: no soname move, no entry point changed shape.
project(akerror VERSION 2.0.2 LANGUAGES C)
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
include(CTest)
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
set(AKERR_MAX_ERROR_FNAME_LENGTH 4096 CACHE STRING
"Bytes reserved for the fname/function fields of akerr_ErrorContext. Defaults to PATH_MAX on Linux/glibc, which keeps sizeof(akerr_ErrorContext) and the soname unchanged; changing it is an ABI break.")
set(AKERR_LAST_ERRNO_VALUE_FALLBACK 133 CACHE STRING
"AKERR_LAST_ERRNO_VALUE to stamp when AKERR_USE_STDLIB is OFF, since the freestanding build cannot shell out to 'errno --list'. Defaults to 133 (Linux's EHWPOISON).")
# Mandatory under AKERR_USE_STDLIB=OFF: the generated header #errors at
# compile time if it is unset when included (see include/akerror.tmpl.h). Left
# empty here so an explicit -DAKERR_RUNTIME_HEADER=... is honored; if still
# empty once AKERR_USE_STDLIB=OFF is known (below), it defaults to a
# convenience header that is merely a thin, libc-backed stand-in, so this
# repository's own OFF build and tests work without a real freestanding
# runtime. A genuinely freestanding consumer should override this.
set(AKERR_RUNTIME_HEADER "" CACHE STRING
"Header providing exit, memset, snprintf, strcmp, strlen and strncpy, required when AKERR_USE_STDLIB is OFF")
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
set(AKERR_SANITIZE "" CACHE STRING
"Sanitizers to build the library and tests with, e.g. thread or address,undefined")
@@ -51,6 +73,34 @@ else()
"AKERR_THREADS must be auto, pthread or none, not '${AKERR_THREADS}'")
endif()
# Normalize the stdlib option. CMake cache booleans may be spelled ON/OFF,
# TRUE/FALSE, 1/0, YES/NO and more; pasting AKERR_USE_STDLIB straight into a
# preprocessor definition (as this used to) left non-numeric spellings such as
# -DAKERR_USE_STDLIB=ON expanding to "#if ON == 1", where ON reads as an
# undefined identifier -- silently 0. Reduce it to a plain 1 or 0 once, here.
if(AKERR_USE_STDLIB)
set(AKERR_USE_STDLIB_DEFINE 1)
else()
set(AKERR_USE_STDLIB_DEFINE 0)
if(AKERR_RUNTIME_HEADER STREQUAL "")
set(AKERR_RUNTIME_HEADER "${CMAKE_CURRENT_SOURCE_DIR}/cmake/akerr_default_runtime.h")
endif()
endif()
# A freestanding build cannot be thread safe through pthreads: src/lock.h's
# pthread backend calls into libc (pthread_mutex_init, abort) unconditionally,
# and the freestanding runtime contract (AKERR_RUNTIME_HEADER) does not cover
# it. Fail the configure rather than produce a library that silently links
# libc anyway.
if(NOT AKERR_USE_STDLIB AND AKERR_THREAD_SAFE)
message(FATAL_ERROR
"AKERR_USE_STDLIB=OFF is incompatible with a pthread threading "
"backend: libakerror serializes its global state with a recursive "
"pthread mutex, and pthreads pull in the C standard library. "
"Configure with -DAKERR_THREADS=none to build a freestanding "
"library instead.")
endif()
# Size of the private status-name hash table. Must be a power of two; usable
# capacity is 75% of it (src/error.c asserts both). The host's errno list
# consumes part of that at akerr_init() time, so the remainder is what all
@@ -154,6 +204,9 @@ add_custom_command(
${CMAKE_CURRENT_SOURCE_DIR}
${GENERATED_DIR}
${AKERR_THREAD_SAFE}
${AKERR_USE_STDLIB_DEFINE}
${AKERR_LAST_ERRNO_VALUE_FALLBACK}
${AKERR_MAX_ERROR_FNAME_LENGTH}
DEPENDS ${SCRIPT} ${INFILE} ${GENERATED_THREAD_STAMP}
VERBATIM
)
@@ -165,9 +218,20 @@ add_custom_target(akerror_generated
DEPENDS ${GENERATED_ERRNO_C} ${GENERATED_AKERROR_H}
)
# The generated errno table is produced by shelling out to `errno --list`
# (moreutils) and #include <errno.h>, neither of which is freestanding-safe.
# It is also useless there: akerr_init_errno() is only ever called under
# AKERR_USE_STDLIB (see src/error.c), so a freestanding build does not compile
# or link it into the library at all.
if(AKERR_USE_STDLIB)
set(AKERR_ERRNO_SOURCES ${GENERATED_ERRNO_C})
else()
set(AKERR_ERRNO_SOURCES)
endif()
add_library(akerror SHARED
src/error.c
${GENERATED_ERRNO_C}
${AKERR_ERRNO_SOURCES}
)
add_dependencies(akerror akerror_generated)
@@ -189,12 +253,25 @@ else()
set(AKERR_THREADS_DEFINE AKERR_THREADS_NONE=1)
endif()
# PUBLIC and unconditional: the generated header #errors at compile time if
# AKERR_USE_STDLIB is OFF and this is not defined, and that check runs for any
# translation unit that includes the header -- the library's own sources as
# much as a consumer's. Harmless (and unused) when AKERR_USE_STDLIB is ON.
if(NOT AKERR_USE_STDLIB)
set(AKERR_RUNTIME_HEADER_DEFINE "AKERR_RUNTIME_HEADER=\"${AKERR_RUNTIME_HEADER}\"")
else()
set(AKERR_RUNTIME_HEADER_DEFINE "")
endif()
target_compile_definitions(akerror
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB_DEFINE}
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
PRIVATE ${AKERR_THREADS_DEFINE}
)
if(AKERR_RUNTIME_HEADER_DEFINE)
target_compile_definitions(akerror PUBLIC ${AKERR_RUNTIME_HEADER_DEFINE})
endif()
if(AKERR_THREAD_SAFE)
target_link_libraries(akerror PRIVATE Threads::Threads)
@@ -216,18 +293,21 @@ akerr_instrument_for_sanitizers(akerror)
# weakens the production library.
add_library(akerror_init_failure SHARED
src/error.c
${GENERATED_ERRNO_C}
${AKERR_ERRNO_SOURCES}
)
add_dependencies(akerror_init_failure akerror_generated)
target_include_directories(akerror_init_failure PUBLIC
${GENERATED_DIR}/include
)
target_compile_definitions(akerror_init_failure
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB_DEFINE}
PRIVATE AKERR_STATUS_NAME_SLOTS=8
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=0
PRIVATE ${AKERR_THREADS_DEFINE}
)
if(AKERR_RUNTIME_HEADER_DEFINE)
target_compile_definitions(akerror_init_failure PUBLIC ${AKERR_RUNTIME_HEADER_DEFINE})
endif()
if(AKERR_THREAD_SAFE)
target_link_libraries(akerror_init_failure PRIVATE Threads::Threads)
endif()