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 (); }