Version the library at 0.1.0
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
74
README.md
74
README.md
@@ -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
44
TODO.md
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
37
include/akstdlib_version.h.in
Normal file
37
include/akstdlib_version.h.in
Normal 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_
|
||||
51
src/stdlib.c
51
src/stdlib.c
@@ -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
177
tests/test_version.c
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user