From 8d24ebd56be7e75aad1776a311a8fe4d16d0d1c2 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 14:16:39 +0900 Subject: [PATCH] build: generate libksuid/ksuid_version.h with KSUID_VERSION_* macros Closes #3. Adds compile-time version macros to the public header so downstream consumers can guard new API surface with the conventional preprocessor arithmetic: #if KSUID_VERSION >= ((1 << 16) | (2 << 8) | 0) /* call symbol that landed in 1.2.0 */ #endif The five macros land in libksuid/ksuid_version.h, which is generated via meson's configure_file from libksuid/ksuid_version.h.in. The single source of truth is meson.build's project(version : '0.1.0') clause -- bumping the version there propagates through the binary soversion, the .pc file, and the public header in lockstep. KSUID_VERSION_MAJOR integer KSUID_VERSION_MINOR integer KSUID_VERSION_PATCH integer KSUID_VERSION (M << 16) | (m << 8) | p, single int KSUID_VERSION_STRING meson.project_version() verbatim Implementation notes addressing the architect + critic risk register: R1 (non-semver project_version): meson.build asserts the version string has at least three dot-separated parts and strips a trailing -prerelease / +build suffix from the patch component before .to_int() coerces it. The full string still rides through KSUID_VERSION_STRING so callers can inspect the suffix when present. R2 (reproducibility / meson dist): the .h.in template is the only tracked artifact and substitutes only @VERSION_*@ tokens that come from meson.project_version() -- no vcs_tag, no __DATE__, no build-time variables. The generated header is regenerated deterministically from any tarball. R3 (gst-indent over generated header): the .h.in suffix takes the template out of the *.h glob the lint hook sweeps, and .gitignore picks up libksuid/ksuid_version.h as belt-and-braces against an in-tree generation by mistake. R4 (clang-tidy 22 on generated header): verified locally to produce zero findings -- the body is just five #defines with no executable code. R5 (install_headers + .pc): configure_file's install_dir lays ksuid_version.h next to ksuid.h under ${prefix}/include/libksuid/, and ci-pr.yml's DESTDIR install verification gains a matching test -f assertion. .pc Cflags already cover ${includedir} so no pkg-config edit is needed. R6 (KSUID_VERSION_STRING source): emitted from meson.project_version() directly rather than synthesised via #x stringification, so a pre-release suffix survives in KSUID_VERSION_STRING even though the integer macros only encode MAJOR.MINOR.PATCH. R7 (header hygiene): #ifndef/#define/#endif guard matching the project's existing convention; no #pragma once, no static definitions, no transitive includes. R8 (Windows CRLF): .gitattributes pins *.h.in to LF so configure_file output is byte-identical regardless of which platform's checkout the runner used. A test_version_macros_are_consistent test in test_smoke.c pins the (MAJOR, MINOR, PATCH, KSUID_VERSION layout, KSUID_VERSION_STRING non-empty) invariants; 11/11 tests still pass on Linux GCC and the release-mode install verifies the new header lands at the documented path. --- .gitattributes | 6 ++++++ .github/workflows/ci-pr.yml | 40 +++++++++++++++++++++++++++++++++++++ .gitignore | 5 +++++ libksuid/ksuid.h | 2 ++ libksuid/ksuid_version.h.in | 27 +++++++++++++++++++++++++ libksuid/meson.build | 32 +++++++++++++++++++++++++++++ meson.build | 6 ++++++ tests/test_smoke.c | 29 +++++++++++++++++++++++++++ 8 files changed, 147 insertions(+) create mode 100644 .gitattributes create mode 100644 libksuid/ksuid_version.h.in create mode 100644 libksuid/meson.build diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a6baa64 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Force LF line endings on configure_file templates so substitution +# output is byte-identical regardless of which platform the +# checkout happened on -- otherwise CRLF on Windows runners would +# leak into the generated header and break reproducible-build +# comparisons. Every other file is left to the global git config. +*.h.in text eol=lf diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 70eebad..5cf5255 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -111,6 +111,7 @@ jobs: DESTDIR="$PWD/staging" meson install -C builddir # Architecture-independent paths are stable. test -f staging/usr/local/include/libksuid/ksuid.h + test -f staging/usr/local/include/libksuid/ksuid_version.h test -f staging/usr/local/share/doc/libksuid/LICENSE test -f staging/usr/local/share/doc/libksuid/LICENSE.MIT test -f staging/usr/local/share/doc/libksuid/NOTICE @@ -122,6 +123,45 @@ jobs: find staging -name 'libksuid.a' -print | grep -q . find staging -name 'libksuid.so' -print | grep -q . + # ========================================================================== + # Phase 2b: meson dist round-trip + # Builds a source tarball, extracts it into a clean tree, and rebuilds + # the library + tests from the tarball. Catches reproducibility + # regressions in configure_file templates (R2 from issue #3): if a + # future contributor adds vcs_tag, __DATE__, or anything build-time- + # variable to a generated header, this job fails before the PR can + # land. Runs in parallel to the build matrix. + # ========================================================================== + dist: + name: meson dist round-trip + needs: [lint] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 # meson dist needs full git history + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build + + - name: Build source tarball + run: | + meson setup builddir-dist + meson dist -C builddir-dist --no-tests --formats xztar + + - name: Rebuild from tarball + run tests + run: | + set -eux + tarball=$(ls builddir-dist/meson-dist/libksuid-*.tar.xz) + extract_root=$(mktemp -d) + tar -xJf "$tarball" -C "$extract_root" + src=$(ls -d "$extract_root"/libksuid-*) + meson setup "$src/build" + meson compile -C "$src/build" + meson test -C "$src/build" --print-errorlogs + # ========================================================================== # Phase 3: Sanitizers (ASan + UBSan) # Depends on Phase 2 build matrix passing. diff --git a/.gitignore b/.gitignore index 7598a73..27b14ec 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,8 @@ coverage/ .DS_Store .omc/ *~ + +# Belt-and-braces: configure_file output normally lives under build/ +# (already ignored), but if a stranger generates it in-tree we still +# want git to refuse to track the auto-generated copy. +libksuid/ksuid_version.h diff --git a/libksuid/ksuid.h b/libksuid/ksuid.h index f1f3362..6e1d8d4 100644 --- a/libksuid/ksuid.h +++ b/libksuid/ksuid.h @@ -9,6 +9,8 @@ #ifndef KSUID_H #define KSUID_H +#include + #include #include #include diff --git a/libksuid/ksuid_version.h.in b/libksuid/ksuid_version.h.in new file mode 100644 index 0000000..cb8b60b --- /dev/null +++ b/libksuid/ksuid_version.h.in @@ -0,0 +1,27 @@ +/* SPDX-License-Identifier: LGPL-3.0-or-later + * + * libksuid version macros. THIS FILE IS GENERATED by meson from + * libksuid/ksuid_version.h.in -- the source of truth lives in the + * top-level meson.build's `project(version : ...)` clause. Do not + * edit the generated copy under /libksuid/ksuid_version.h. + */ +#ifndef KSUID_VERSION_H +#define KSUID_VERSION_H + +#define KSUID_VERSION_MAJOR @VERSION_MAJOR@ +#define KSUID_VERSION_MINOR @VERSION_MINOR@ +#define KSUID_VERSION_PATCH @VERSION_PATCH@ + +/* Single integer suitable for `#if KSUID_VERSION >= ...` checks. + * Layout: bits 16-23 major, 8-15 minor, 0-7 patch. */ +#define KSUID_VERSION \ + ((KSUID_VERSION_MAJOR << 16) \ + | (KSUID_VERSION_MINOR << 8) \ + | (KSUID_VERSION_PATCH )) + +/* Full version string verbatim from meson.project_version(); may + * contain a pre-release suffix (e.g. "0.2.0-rc1") that the integer + * macros above do not encode. */ +#define KSUID_VERSION_STRING "@VERSION_STRING@" + +#endif /* KSUID_VERSION_H */ diff --git a/libksuid/meson.build b/libksuid/meson.build new file mode 100644 index 0000000..c9e7229 --- /dev/null +++ b/libksuid/meson.build @@ -0,0 +1,32 @@ +# Generate libksuid/ksuid_version.h from meson.project_version() so +# downstream consumers can guard new API surface with `#if +# KSUID_VERSION >= ...`. Output lands at +# ${MESON_BUILD_ROOT}/libksuid/ksuid_version.h, which the parent +# include_directories('.') puts on the search path so the umbrella +# header can resolve uniformly in-tree +# and post-install. + +ver_parts = meson.project_version().split('.') +assert(ver_parts.length() >= 3, + 'libksuid project version must be MAJOR.MINOR.PATCH (got: ' + + meson.project_version() + ')') + +# Drop a possible "-prerelease" / "+build" suffix from the patch +# component before .to_int() validates that the three numeric macros +# really are integers. The full original string still rides through +# KSUID_VERSION_STRING so callers that care can read the suffix. +ver_patch_clean = ver_parts[2].split('-')[0].split('+')[0] + +ver_conf = configuration_data() +ver_conf.set('VERSION_MAJOR', ver_parts[0].to_int()) +ver_conf.set('VERSION_MINOR', ver_parts[1].to_int()) +ver_conf.set('VERSION_PATCH', ver_patch_clean.to_int()) +ver_conf.set('VERSION_STRING', meson.project_version()) + +ksuid_version_h = configure_file( + input : 'ksuid_version.h.in', + output : 'ksuid_version.h', + configuration : ver_conf, + install : true, + install_dir : get_option('includedir') / 'libksuid', +) diff --git a/meson.build b/meson.build index 13102ab..9d94aaf 100644 --- a/meson.build +++ b/meson.build @@ -70,6 +70,12 @@ endif inc = include_directories('.') public_headers = files('libksuid/ksuid.h') +# Generate libksuid/ksuid_version.h alongside the source tree's +# libksuid/. The configure_file lives in libksuid/meson.build so +# its output lands at ${MESON_BUILD_ROOT}/libksuid/ksuid_version.h +# and resolves through |inc| as . +subdir('libksuid') + core_sources = files( 'libksuid/ksuid.c', 'libksuid/base62.c', diff --git a/tests/test_smoke.c b/tests/test_smoke.c index de7084c..23ebe02 100644 --- a/tests/test_smoke.c +++ b/tests/test_smoke.c @@ -48,6 +48,34 @@ test_compare_first_byte_dominates (void) ASSERT_TRUE (ksuid_compare (&b, &a) < 0); } +static void +test_version_macros_are_consistent (void) +{ + /* DELIBERATE SYNC POINT: these literal values must equal the + * `version :` field in the top-level meson.build. The test exists + * to prove that meson.project_version() flows through the + * configure_file substitution into ksuid_version.h.in -- a + * `>= 0` check would silently accept an empty @VERSION_MAJOR@ + * substitution that the C preprocessor turns into 0. A real + * regression in the substitution chain therefore fails here. + * + * When you bump meson.build's project version you MUST update + * these four asserts in the same commit. */ + ASSERT_EQ_INT (KSUID_VERSION_MAJOR, 0); + ASSERT_EQ_INT (KSUID_VERSION_MINOR, 1); + ASSERT_EQ_INT (KSUID_VERSION_PATCH, 0); + ASSERT_EQ_STR (KSUID_VERSION_STRING, "0.1.0"); + + /* The composite KSUID_VERSION must equal the documented + * (MAJOR << 16) | (MINOR << 8) | PATCH layout for `#if + * KSUID_VERSION >= ...` arithmetic to behave the way callers + * expect. */ + int composed = (KSUID_VERSION_MAJOR << 16) + | (KSUID_VERSION_MINOR << 8) + | (KSUID_VERSION_PATCH); + ASSERT_EQ_INT (KSUID_VERSION, composed); +} + int main (void) { @@ -56,5 +84,6 @@ main (void) RUN_TEST (test_max_is_all_ff); RUN_TEST (test_compare_orders_lex); RUN_TEST (test_compare_first_byte_dominates); + RUN_TEST (test_version_macros_are_consistent); TEST_MAIN_END (); }