Version the library at 0.1.0
All checks were successful
libakstdlib CI Build / cmake_build (push) Successful in 2m49s
libakstdlib CI Build / coverage (push) Successful in 2m37s
libakstdlib CI Build / mutation_test (push) Successful in 10m13s

project() now carries VERSION 0.1.0, and is the single place a version number
is spelled. It flows into generated version macros, the shared library's
VERSION/SOVERSION, the Version: field in akstdlib.pc, and a new
akstdlibConfigVersion.cmake. Before this @PROJECT_VERSION@ expanded to
nothing, so akstdlib.pc shipped an empty Version: and libakstdlib.so carried
no soname at all.

0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
fixes change documented behaviour, so the API is not being promised yet. While
the major version is 0 the soname carries MAJOR.MINOR -- 0.1 and 0.2 are
different ABIs -- and becomes MAJOR alone at 1.0. The if() in CMakeLists.txt
and the #if in tests/test_version.c encode that rule and are tested against
each other.

include/akstdlib_version.h.in is configured into the build tree as
akstdlib_version.h and installed beside akstdlib.h. It defines
AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
AKSL_VERSION_NUMBER is computed rather than written as a literal, because a
literal 000100 is octal in C and would make 0.1.0 compare as 64;
test_version.c asserts it against the runtime components, so a rewrite to a
literal fails.

Those macros record what a caller was compiled against. aksl_version(),
aksl_version_string() and aksl_version_soname() report what actually loaded,
and AKSL_VERSION_CHECK() compares the two, raising AKERR_VALUE naming both.
It is a macro so that it expands at the caller's site and captures the
caller's numbers; the function compares them against the ones baked into the
library. Compatibility is "same soname", so patch is ignored -- a caller built
against 0.1.0 keeps working against 0.1.7.

Normally the soname catches a mismatch at load time and the check never fires.
It earns its keep when the soname is bypassed: a 0.2.0 build dropped in under
the 0.1 filename loads happily, and only the check notices.

write_basic_package_version_file() uses SameMinorVersion to mirror the soname,
falling back to ExactVersion below CMake 3.11 where that mode does not exist.
The fallback is stricter than the soname rule -- it pins the patch level too --
but never laxer, and wrongly refusing a good pairing beats wrongly accepting a
bad one.

Coverage of src/stdlib.c rose to 99.1% of lines (217/219), 45.1% of branches
and 25/25 functions. That puts branch coverage back over the old 45 gate, but
the gate stays at 40: 0.1 points of headroom is not a ratchet.

ctest 14/14, ASan+UBSan 14/14, coverage 16/16 at 90/40. Also verified out of
tree: SONAME libakstdlib.so.0.1 recorded in consumers, pkg-config
--modversion reporting 0.1.0, find_package(akstdlib 0.1) accepted with 0.2 and
1.0 refused, a patch-bumped 0.1.1 loading and passing the check, a 0.2.0
dropped in under the 0.1 filename caught by it, and an embedded
add_subdirectory build keeping its own version rather than the parent's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:40:49 -04:00
parent 07c448508b
commit 95e5002512
9 changed files with 484 additions and 23 deletions

View File

@@ -45,12 +45,17 @@ jobs:
# scripts/coverage.py needs nothing but python3 and gcc's own gcov, so
# there is no lcov/gcovr to install here.
#
# The gate is a ratchet, not a target: src/stdlib.c is at 99.0% of lines
# and 44.3% of branches, so 90/40 fails on a real regression (a test
# The gate is a ratchet, not a target: src/stdlib.c is at 99.1% of lines
# and 45.1% of branches, so 90/40 fails on a real regression (a test
# deleted, or new untested code added) without tripping over rounding.
# The report is printed either way -- the uncovered lines it lists are the
# missing tests.
#
# Branch coverage is back over 45 (the aksl_version_* functions are fully
# covered, which lifted it from 44.3%), but the gate stays at 40: 0.1
# points of headroom is not a ratchet, it is a coin flip on the next
# rounding change.
#
# The branch gate was 45 against 51.0% until the libakerror 1.0.0 bump.
# Nothing about this library's tests changed: line coverage held at
# 99.0% (200/202) and function coverage at 100% (21/21), but the branch

View File

@@ -59,7 +59,7 @@ right list in `CMakeLists.txt`. `AKSL_TESTS` must exit zero.
`AKSL_KNOWN_FAILING_TESTS` assert documented defects from `TODO.md`; when one
starts unexpectedly passing, move it into `AKSL_TESTS` with the fix.
`src/stdlib.c` is at 99.0% line coverage and CI gates it at 90 (line) / 40
`src/stdlib.c` is at 99.1% line coverage and CI gates it at 90 (line) / 40
(branch), so new code needs tests in the same commit. Run
`cmake --build build-coverage --target coverage` and check the uncovered-line
listing before proposing a change. Tests for behaviour that `TODO.md` records as

View File

@@ -1,5 +1,32 @@
cmake_minimum_required(VERSION 3.10)
project(akstdlib LANGUAGES C)
# The single source of truth for the version. It flows from here into
# include/akstdlib_version.h (generated from the .in template next to it), the
# shared library's VERSION/SOVERSION, the Version: field in akstdlib.pc, and
# akstdlibConfigVersion.cmake. Nothing else should spell a version number.
#
# 0.x on purpose: TODO.md section 2.1 still records four confirmed defects whose
# fixes change documented behaviour (aksl_atoi's contract, aksl_realpath's
# signature), so this library is not promising a stable API yet.
project(akstdlib VERSION 0.1.0 LANGUAGES C)
# The granularity at which the ABI is allowed to break, and therefore the
# soname: libakstdlib.so.0.1. Pre-1.0 that is MAJOR.MINOR, because a 0.x library
# makes no compatibility promise across a minor bump. At 1.0 this becomes
# ${PROJECT_VERSION_MAJOR} alone -- change it here, and AKSL_VERSION_SONAME in
# the header template follows automatically because it is configured from this.
if(PROJECT_VERSION_MAJOR EQUAL 0)
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}")
else()
set(AKSL_SOVERSION "${PROJECT_VERSION_MAJOR}")
endif()
set(AKSL_GENERATED_INCLUDE_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated/include")
set(AKSL_VERSION_HEADER "${AKSL_GENERATED_INCLUDE_DIR}/akstdlib_version.h")
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/include/akstdlib_version.h.in"
"${AKSL_VERSION_HEADER}"
@ONLY
)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -ggdb -pg")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -g -ggdb -pg")
@@ -135,12 +162,25 @@ add_library(akstdlib SHARED
add_library(akstdlib::akstdlib ALIAS akstdlib)
# Specify include directories for the library's headers (if applicable)
# Specify include directories for the library's headers (if applicable).
# The generated directory carries akstdlib_version.h, which akstdlib.h includes;
# it is PUBLIC because consumers building against the build tree need it too. On
# install both headers land side by side in ${CMAKE_INSTALL_INCLUDEDIR}, so the
# install interface needs no second entry.
target_include_directories(akstdlib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${AKSL_GENERATED_INCLUDE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/>
)
# VERSION gives the real file (libakstdlib.so.0.1.0); SOVERSION gives the symlink
# and the ELF soname recorded in every consumer (libakstdlib.so.0.1), so a
# consumer built against 0.1 will not silently load an ABI-incompatible 0.2.
set_target_properties(akstdlib PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${AKSL_SOVERSION}
)
target_link_libraries(akstdlib PUBLIC akerror::akerror)
aksl_target_coverage(akstdlib)
@@ -155,6 +195,7 @@ install(TARGETS akstdlib
)
install(FILES "include/akstdlib.h" DESTINATION "include/")
install(FILES "${AKSL_VERSION_HEADER}" DESTINATION "include/")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/akstdlib.pc DESTINATION "lib/pkgconfig/")
@@ -170,8 +211,31 @@ configure_package_config_file(
INSTALL_DESTINATION ${akstdlib_install_cmakedir}
)
# Without this, find_package(akstdlib 0.1 REQUIRED) is refused for want of a
# version file no matter what is installed -- which is exactly the gap that
# stops cmake/akstdlib.cmake.in from requesting a version of akerror.
#
# SameMinorVersion mirrors the soname: pre-1.0, 0.1 and 0.2 are different ABIs.
# It arrived in CMake 3.11 and this project declares 3.10, so fall back to
# ExactVersion on older CMake. That is stricter than the soname rule -- it pins
# the patch level too, so a 0.1.0 request refuses a compatible 0.1.1 -- but it is
# never laxer, and wrongly refusing a good pairing beats wrongly accepting a bad
# one. Drop the branch when the minimum moves past 3.11.
if(CMAKE_VERSION VERSION_LESS 3.11)
set(AKSL_VERSION_COMPATIBILITY ExactVersion)
else()
set(AKSL_VERSION_COMPATIBILITY SameMinorVersion)
endif()
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY ${AKSL_VERSION_COMPATIBILITY}
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/akstdlibConfigVersion.cmake"
DESTINATION ${akstdlib_install_cmakedir}
)
@@ -200,6 +264,7 @@ set(AKSL_TESTS
stream
strhash
tree
version
)
set(AKSL_WILL_FAIL_TESTS

View File

@@ -25,6 +25,70 @@ A top-level build compiles the vendored `deps/libakerror`. When `libakstdlib` is
consumed as a subproject, it uses whatever `akerror::akerror` target or installed
package the parent provides instead.
### This library's own version
`libakstdlib` is at **0.1.0**. The version lives in exactly one place — the
`project()` call in `CMakeLists.txt` — and flows from there into everything
else, so a bump is a one-line edit:
| Artifact | Value at 0.1.0 | From |
| --- | --- | --- |
| `AKSL_VERSION_MAJOR` / `_MINOR` / `_PATCH` | `0` / `1` / `0` | `include/akstdlib_version.h.in` |
| `AKSL_VERSION_STRING` | `"0.1.0"` | same |
| `AKSL_VERSION_NUMBER` | `100` | same |
| `AKSL_VERSION_SONAME` | `"0.1"` | same |
| shared library | `libakstdlib.so.0.1.0`, soname `libakstdlib.so.0.1` | `VERSION` / `SOVERSION` |
| `pkg-config --modversion akstdlib` | `0.1.0` | `akstdlib.pc` |
| `find_package(akstdlib 0.1)` | accepted; `0.2` and `1.0` refused | `akstdlibConfigVersion.cmake` |
`akstdlib_version.h` is **generated** — that is why there is no such file in the
source tree, only the `.in` template beside `akstdlib.h`. Don't hand-edit the
copy in your build directory; change the template or `project()`.
It is `0.x` deliberately. `TODO.md` §2.1 still records four confirmed defects
whose fixes change documented behaviour, so the API is not being promised yet.
While the major version is `0`, **the soname carries `MAJOR.MINOR`**: 0.1 and 0.2
are different ABIs and the loader will not substitute one for the other. At 1.0
the soname becomes `MAJOR` alone — the `if(PROJECT_VERSION_MAJOR EQUAL 0)` in
`CMakeLists.txt` and the matching `#if` in `tests/test_version.c` are the two
places that encode this, and they are tested against each other.
`AKSL_VERSION_NUMBER` is computed rather than written as a literal, because a
literal `000100` is *octal* in C and would make 0.1.0 compare as 64.
#### Compiled-against vs. loaded
The macros above record what a caller was **compiled** against. What it actually
**loaded** is a different question, and the two can disagree:
```c
int major, minor, patch;
akerr_ErrorContext *e = aksl_version(&major, &minor, &patch); /* the loaded .so */
const char *v = aksl_version_string(); /* likewise */
e = AKSL_VERSION_CHECK(); /* compares the two; AKERR_VALUE on a mismatch */
```
`AKSL_VERSION_CHECK()` is a macro on purpose: it expands at *your* call site, so
it captures the `AKSL_VERSION_*` you were built with and passes them to a
function that compares against the values baked into the library. Calling
`aksl_version_check()` with hand-written numbers defeats the whole mechanism.
Compatibility is defined as "same soname", so pre-1.0 both major and minor must
match and the patch level is ignored — a caller built against 0.1.0 keeps working
against 0.1.7, which is exactly the promise the shared soname makes.
In normal use the soname catches the mismatch first, at load time, and the check
never fires. It earns its keep when the soname is bypassed: a hand-install that
drops a 0.2.0 build in under the 0.1 filename, or a package that strips
versioning. Then the loader is happy and only the check notices:
```
compiled against : 0.1.0 (soname 0.1)
loaded : 0.2.0 (0.2.0)
MISMATCH DETECTED: compiled against libakstdlib 0.1.0, loaded 0.2.0 (soname 0.2)
```
### The libakerror version floor
**libakerror 1.0.0 or newer is required.** That release made the status-name
@@ -182,8 +246,8 @@ cmake -S . -B build-coverage -DAKSL_COVERAGE=ON \
-DAKSL_COVERAGE_THRESHOLD=90 -DAKSL_COVERAGE_BRANCH_THRESHOLD=40
```
**Where it stands.** `src/stdlib.c` is at **99.0% of lines (200/202)**, **44.3% of
branches (481/1087)** and **21/21 functions**, so 90/40 above is a ratchet with
**Where it stands.** `src/stdlib.c` is at **99.1% of lines (217/219)**, **45.1% of
branches (519/1151)** and **25/25 functions**, so 90/40 above is a ratchet with
headroom rather than a target. The two uncovered lines are both
`} HANDLE(e, AKERR_ITERATOR_BREAK) {` — in libakerror that macro begins with the
`break;` belonging to `PROCESS`'s `case 0:` arm, which is only reachable when a
@@ -197,8 +261,10 @@ stack-trace buffer limits, `akerr_valid_error_address` failures — and belong t
libakerror's own suite rather than to this one. The libakerror 1.0.0 bump made
that gap wider without changing a line of this library: the branch denominator
went from 661 to 1087 as those macros grew, so the same tests that scored 51.0%
(337/661) now score 44.3% (481/1087). The gate moved 45 → 40 to match; line and
function coverage did not move at all.
(337/661) dropped to 44.3% (481/1087). The gate moved 45 → 40 to match; line and
function coverage did not move at all. Adding the `aksl_version_*` family and its
tests brought the figure back to 45.1% (519/1151), but the gate stays at 40 —
0.1 points of headroom is not a ratchet.
Two caveats. Coverage is measured at `-O0`, because the optimizer reorders lines
until per-line counts stop matching the source — so a coverage build is not the

44
TODO.md
View File

@@ -406,14 +406,19 @@ cue to move it into `AKSL_TESTS`.
15. **`aksl_TreeNode.parent` is declared but never set or read** by any library function.
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; no version macros
(`AKSL_VERSION_MAJOR`…); `akstdlib.h` pulls in `stdio.h`/`stdlib.h`/`string.h`/`stdint.h`
into every consumer's namespace.
16. **Header hygiene**: no `extern "C" { }` guard for C++ consumers; `akstdlib.h` pulls in
`stdio.h`/`stdlib.h`/`string.h`/`stdint.h` into every consumer's namespace. The version
macros this item also asked for are **done**: `AKSL_VERSION_MAJOR`/`_MINOR`/`_PATCH`/
`_STRING`/`_NUMBER`/`_SONAME` are generated into `akstdlib_version.h` from
`project(akstdlib VERSION …)`, with `aksl_version()`/`aksl_version_string()`/
`aksl_version_soname()` and `AKSL_VERSION_CHECK()` reporting the *loaded* library so a
header/`.so` mismatch is nameable. See `tests/test_version.c` and `README.md`.
17. **Doxygen coverage is 2 functions out of 20.** A `Doxyfile` exists but only
17. **Doxygen coverage is 2 functions out of 24.** A `Doxyfile` exists but only
`aksl_tree_iterate` and `aksl_list_iterate` have doc comments. Every public function
needs `@param`/`@throws`/`@return`, especially the ones whose contract deviates from libc
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict).
(`aksl_free(NULL)` is an error; `aksl_atoi` will become strict). The four
`aksl_version_*` entry points have prose comments in the header but no Doxygen tags.
### 2.3 Build, CI and repository
@@ -456,15 +461,26 @@ cue to move it into `AKSL_TESTS`.
would mean testing libakerror's macros rather than this library; that belongs to
libakerror's mutation suite, since macros expand at the call site and coverage cannot
see them properly from either side. Revisit if the gap ever hides a real regression.
- [ ] **`project()` declares no `VERSION`**, so `@PROJECT_VERSION@` expands to nothing and
the installed `akstdlib.pc` ships an empty `Version:` field — a consumer cannot pin
this library the way `akstdlib.pc` now pins `akerror >= 1.0.0`. libakstdlib also has
no `SOVERSION`, so `libakstdlib.so` carries no soname even though its public header
re-exports libakerror's ABI and therefore breaks whenever libakerror's does. Give the
project a version and a `SOVERSION`, and install an `akstdlibConfigVersion.cmake` via
`write_basic_package_version_file()` so `find_package(akstdlib 1.2 REQUIRED)` can work.
(libakerror has the same `ConfigVersion` gap, which is why
`cmake/akstdlib.cmake.in` calls `find_dependency(akerror)` with no version.)
- [x] **`project()` declared no `VERSION`**, so `@PROJECT_VERSION@` expanded to nothing, the
installed `akstdlib.pc` shipped an empty `Version:` field, and `libakstdlib.so` carried
no soname. Now `project(akstdlib VERSION 0.1.0)`, with `SOVERSION` `0.1`
(`MAJOR.MINOR` while major is 0, `MAJOR` from 1.0 — the `if()` in `CMakeLists.txt` and
the matching `#if` in `tests/test_version.c` encode the rule and are tested against
each other), an `akstdlibConfigVersion.cmake` at `SameMinorVersion`, and version macros
generated into `akstdlib_version.h`. Verified: `find_package(akstdlib 0.1)` is accepted
while `0.2` and `1.0` are refused, and a 0.2.0 build dropped in under the 0.1 filename
is caught by `AKSL_VERSION_CHECK()`.
- [ ] **libakerror still installs no `akerrorConfigVersion.cmake`**, so
`cmake/akstdlib.cmake.in` has to call `find_dependency(akerror)` with no version — a
request for one would be refused for want of a version file regardless of what is
installed. libakstdlib now does this correctly and libakerror does not; fix it there
with the same `write_basic_package_version_file()` call, then add the `1.0.0` floor to
the `find_dependency` here. Until then the floor rests on `akstdlib.pc`'s `Requires:`
and the `#error` guard in `akstdlib.h`.
- [ ] **`aksl_version_check()` ignores its `patch` argument** (`src/stdlib.c`, the `(void)patch`),
which is correct for the current "same soname" rule but means the parameter exists only
so the error message can name the caller's full version. If a future rule needs patch
to participate, that is the line to change.
- [ ] **CI does not build against the submodule it pins.** `.gitea/workflows/ci.yaml` clones
`libakerror@main` and installs it, while the build it then runs is top-level and so
compiles `deps/libakerror` at the pinned commit — two different libakerror versions

View File

@@ -21,6 +21,14 @@
#error "libakstdlib requires libakerror >= 1.0.0: the akerror.h on the include path predates the status registry. Rebuild and reinstall libakerror."
#endif
/*
* AKSL_VERSION_MAJOR/MINOR/PATCH/STRING/NUMBER and AKSL_VERSION_SONAME.
* Generated by CMake from include/akstdlib_version.h.in, which is why there is
* no such file in the source tree -- it is configured into the build tree and
* installed alongside this header.
*/
#include <akstdlib_version.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -52,6 +60,42 @@ typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_TreeNodeIterator)(aksl_TreeNod
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_AllocFunc)(size_t size, void **dest);
typedef akerr_ErrorContext AKERR_NOIGNORE *(*aksl_FreeFunc)(void *ptr);
/*
* Version of the shared library actually loaded, as opposed to the
* AKSL_VERSION_* macros above, which record what the caller was *compiled*
* against. The two differ exactly when a stale libakstdlib.so is on the loader
* path, which is the failure these entry points exist to name.
*
* All three pointers are required; this library treats a NULL out-param as a
* caller error rather than as "don't care" (so does aksl_free).
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch);
/*
* The loaded library's version as "MAJOR.MINOR.PATCH", and its soname. Neither
* can fail -- they return pointers to string literals in the library's own
* .rodata -- so they return the string directly rather than an error context,
* the same exception akerr_name_for_status() makes for the same reason.
*/
const char *aksl_version_string(void);
const char *aksl_version_soname(void);
/*
* Compare the caller's compiled-in version against the loaded library's.
* Compatibility is defined as "same soname", so pre-1.0 both major and minor
* must match and patch is ignored; from 1.0 only major will matter. Raises
* AKERR_VALUE naming both versions on a mismatch.
*
* Call it through AKSL_VERSION_CHECK() below rather than directly. The macro
* expands at *your* call site, so it captures the AKSL_VERSION_* the caller was
* built with; the function compares them against the values baked into the
* library. Passing the numbers by hand defeats the entire mechanism.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch);
#define AKSL_VERSION_CHECK() \
aksl_version_check(AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH)
akerr_ErrorContext AKERR_NOIGNORE *aksl_fopen(char *pathname, char *mode, FILE **fp);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
akerr_ErrorContext AKERR_NOIGNORE *aksl_fwrite(void *ptr, size_t size, size_t nmemb, FILE *fp);

View File

@@ -0,0 +1,37 @@
#ifndef _AKSTDLIB_VERSION_H_
#define _AKSTDLIB_VERSION_H_
/*
* GENERATED FILE -- do not edit.
*
* Configured from include/akstdlib_version.h.in by CMake. The version itself
* lives in exactly one place, the project() call in CMakeLists.txt, and flows
* from there into these macros, the shared library's SOVERSION, the Version:
* field in akstdlib.pc, and akstdlibConfigVersion.cmake. To bump the version,
* edit project(); to change what the macros look like, edit this template.
*/
#define AKSL_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define AKSL_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define AKSL_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define AKSL_VERSION_STRING "@PROJECT_VERSION@"
/*
* A single ordered integer, for `#if AKSL_VERSION_NUMBER >= ...` compares.
* Computed rather than written out as a literal on purpose: a literal like
* 000100 is octal in C, which would silently make version 0.1.0 compare as 64.
* Each component gets two decimal digits, so this is correct up to x.99.99.
*/
#define AKSL_VERSION_NUMBER \
((AKSL_VERSION_MAJOR * 10000) + (AKSL_VERSION_MINOR * 100) + AKSL_VERSION_PATCH)
/*
* The soname of the library these headers describe -- "0.1" for libakstdlib.so.0.1.
* This is the granularity at which the ABI is allowed to break, and it is what
* aksl_version_check() compares: pre-1.0 a minor bump is a break, so the soname
* carries MAJOR.MINOR. From 1.0 it becomes MAJOR alone, and the SOVERSION
* expression in CMakeLists.txt and this macro change together.
*/
#define AKSL_VERSION_SONAME "@AKSL_SOVERSION@"
#endif // _AKSTDLIB_VERSION_H_

View File

@@ -6,6 +6,57 @@
#include <stdarg.h>
#include <stdint.h>
/*
* Version of the library itself. These read the AKSL_VERSION_* macros as they
* were when *this translation unit* was compiled, which is what makes them the
* loaded library's version rather than the caller's: the caller's copy of the
* same macros comes from whatever akstdlib_version.h it compiled against.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_version(int *major, int *minor, int *patch)
{
PREPARE_ERROR(e);
FAIL_ZERO_RETURN(e, major, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
(void *)major, (void *)minor, (void *)patch);
FAIL_ZERO_RETURN(e, minor, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
(void *)major, (void *)minor, (void *)patch);
FAIL_ZERO_RETURN(e, patch, AKERR_NULLPOINTER, "major=%p, minor=%p, patch=%p",
(void *)major, (void *)minor, (void *)patch);
*major = AKSL_VERSION_MAJOR;
*minor = AKSL_VERSION_MINOR;
*patch = AKSL_VERSION_PATCH;
SUCCEED_RETURN(e);
}
const char *aksl_version_string(void)
{
return AKSL_VERSION_STRING;
}
const char *aksl_version_soname(void)
{
return AKSL_VERSION_SONAME;
}
/*
* Compatibility is "same soname". Pre-1.0 that is MAJOR.MINOR, so both are
* compared and patch is deliberately ignored; from 1.0 the minor comparison
* comes out, in step with the SOVERSION expression in CMakeLists.txt. The
* caller's numbers arrive as arguments because AKSL_VERSION_CHECK() expanded
* them at the caller's site -- see the macro in akstdlib.h.
*/
akerr_ErrorContext AKERR_NOIGNORE *aksl_version_check(int major, int minor, int patch)
{
PREPARE_ERROR(e);
(void)patch;
FAIL_NONZERO_RETURN(e,
(major != AKSL_VERSION_MAJOR || minor != AKSL_VERSION_MINOR),
AKERR_VALUE,
"compiled against libakstdlib %d.%d.%d, loaded %s (soname %s)",
major, minor, patch,
AKSL_VERSION_STRING, AKSL_VERSION_SONAME);
SUCCEED_RETURN(e);
}
akerr_ErrorContext AKERR_NOIGNORE *aksl_malloc(size_t size, void **dst)
{
PREPARE_ERROR(e);

177
tests/test_version.c Normal file
View File

@@ -0,0 +1,177 @@
/*
* Version reporting -- TODO.md section 2.2.16.
*
* There are two versions in play and the whole point of this API is that they
* are allowed to differ:
*
* the AKSL_VERSION_* macros what the caller was COMPILED against, baked
* into the caller's object file from whatever
* akstdlib_version.h was on its include path
* aksl_version() and friends what is actually LOADED, baked into
* libakstdlib.so when the library was built
*
* In this test they necessarily agree, because the test binary and the library
* are built from one tree in one configure. So the assertions below split into
* two kinds: the ones that check the two sides agree (which would catch a build
* that compiled the library and its tests against different generated headers),
* and the ones that drive aksl_version_check() with deliberately wrong numbers
* to prove it actually refuses a mismatch rather than always returning success.
* The second kind is what makes the first kind worth anything.
*/
#include "aksl_capture.h"
static int test_version_string_matches_its_components(void)
{
char expected[64];
snprintf(expected, sizeof(expected), "%d.%d.%d",
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
AKSL_CHECK(strcmp(AKSL_VERSION_STRING, expected) == 0);
return 0;
}
/*
* AKSL_VERSION_NUMBER must be ordinary decimal arithmetic. Written out as a
* literal it would be tempting to spell 0.1.0 as 000100, which C reads as
* octal 64 -- so this compares the compile-time macro against the same figure
* computed at runtime from the loaded library's own components. A literal that
* went octal fails here; the current computed form cannot.
*/
static int test_version_number_is_decimal_and_ordered(void)
{
int major = -1;
int minor = -1;
int patch = -1;
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
AKSL_CHECK(AKSL_VERSION_NUMBER == (major * 10000) + (minor * 100) + patch);
/* Usable in the preprocessor, which is the reason it exists at all. */
#if AKSL_VERSION_NUMBER < 0
AKSL_CHECK(0 && "AKSL_VERSION_NUMBER is negative");
#endif
AKSL_CHECK(AKSL_VERSION_NUMBER >= 0);
return 0;
}
/* Header and shared library came out of the same configure. */
static int test_loaded_version_matches_the_header(void)
{
int major = -1;
int minor = -1;
int patch = -1;
AKSL_CHECK_OK(aksl_version(&major, &minor, &patch));
AKSL_CHECK(major == AKSL_VERSION_MAJOR);
AKSL_CHECK(minor == AKSL_VERSION_MINOR);
AKSL_CHECK(patch == AKSL_VERSION_PATCH);
AKSL_CHECK(strcmp(aksl_version_string(), AKSL_VERSION_STRING) == 0);
AKSL_CHECK(strcmp(aksl_version_soname(), AKSL_VERSION_SONAME) == 0);
return 0;
}
/*
* The soname is the ABI-break granularity, and it mirrors the SOVERSION
* expression in CMakeLists.txt: MAJOR.MINOR while major is 0, MAJOR alone after
* that. If one of the two ever changes without the other, this fails.
*/
static int test_soname_matches_the_documented_rule(void)
{
char expected[64];
#if AKSL_VERSION_MAJOR == 0
snprintf(expected, sizeof(expected), "%d.%d",
AKSL_VERSION_MAJOR, AKSL_VERSION_MINOR);
#else
snprintf(expected, sizeof(expected), "%d", AKSL_VERSION_MAJOR);
#endif
AKSL_CHECK(strcmp(aksl_version_soname(), expected) == 0);
return 0;
}
static int test_version_check_accepts_a_matching_build(void)
{
AKSL_CHECK_OK(AKSL_VERSION_CHECK());
return 0;
}
/*
* The refusals. Without these the success above proves nothing -- a
* aksl_version_check() that returned NULL unconditionally would pass it.
*/
static int test_version_check_refuses_a_mismatch(void)
{
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH),
AKERR_VALUE);
AKSL_CHECK_STATUS(aksl_version_check(AKSL_VERSION_MAJOR,
AKSL_VERSION_MINOR + 1,
AKSL_VERSION_PATCH),
AKERR_VALUE);
return 0;
}
/*
* Patch is deliberately not part of the comparison: a patch bump is an ABI
* promise, so a caller built against 0.1.0 must keep working against 0.1.7.
*/
static int test_version_check_ignores_the_patch_level(void)
{
AKSL_CHECK_OK(aksl_version_check(AKSL_VERSION_MAJOR,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH + 9));
return 0;
}
/* The error has to say which two versions disagree, or it is not actionable. */
static int test_mismatch_message_names_both_versions(void)
{
char compiled[64];
snprintf(compiled, sizeof(compiled), "%d.%d.%d",
AKSL_VERSION_MAJOR + 1, AKSL_VERSION_MINOR, AKSL_VERSION_PATCH);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH),
AKERR_VALUE, compiled);
AKSL_CHECK_STATUS_MSG_CONTAINS(aksl_version_check(AKSL_VERSION_MAJOR + 1,
AKSL_VERSION_MINOR,
AKSL_VERSION_PATCH),
AKERR_VALUE, AKSL_VERSION_STRING);
return 0;
}
static int test_rejects_null_arguments(void)
{
int major = 0;
int minor = 0;
int patch = 0;
AKSL_CHECK_STATUS(aksl_version(NULL, &minor, &patch), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_version(&major, NULL, &patch), AKERR_NULLPOINTER);
AKSL_CHECK_STATUS(aksl_version(&major, &minor, NULL), AKERR_NULLPOINTER);
return 0;
}
int main(void)
{
int failures = 0;
akerr_init();
AKSL_RUN(failures, test_version_string_matches_its_components);
AKSL_RUN(failures, test_version_number_is_decimal_and_ordered);
AKSL_RUN(failures, test_loaded_version_matches_the_header);
AKSL_RUN(failures, test_soname_matches_the_documented_rule);
AKSL_RUN(failures, test_version_check_accepts_a_matching_build);
AKSL_RUN(failures, test_version_check_refuses_a_mismatch);
AKSL_RUN(failures, test_version_check_ignores_the_patch_level);
AKSL_RUN(failures, test_mismatch_message_names_both_versions);
AKSL_RUN(failures, test_rejects_null_arguments);
AKSL_REPORT(failures);
}