From 5c9efe272f1cd8c822d3f54e666ed1cf3baaffe6 Mon Sep 17 00:00:00 2001 From: Nathan Howell Date: Wed, 22 Apr 2026 12:44:49 -0700 Subject: [PATCH] release: preserve SONAME/install_name symlinks in liblbug tarballs The liblbug shared-library tarballs (liblbug-linux-*.tar.gz, liblbug-osx-*.tar.gz) extracted to an unversioned filename (liblbug.so, liblbug.dylib) whose embedded SONAME / install_name referred to a version-suffixed name (liblbug.so.0, @rpath/liblbug.0.dylib) that was NOT present in the archive. A consumer linking -llbug would succeed at link time but fail at load time with "Library not loaded: @rpath/liblbug.0.dylib" / "cannot open shared object file: liblbug.so.0". Root cause: the precompiled-bin packaging step used cp -L, which dereferenced the cmake-generated symlink chain liblbug.so -> liblbug.so.0 -> liblbug.so.0.15.3 down to the real file, discarding the intermediate names that satisfy SONAME / install_name. Fix (Option B from the bug report, matching Homebrew/distro convention and what `make install` into /usr/lib produces): copy and archive the full symlink chain, so the unversioned link-time name, the versioned SONAME / install_name, and the real file are all present in the tarball. Also adds a CI verification step that reads the SONAME (via readelf) / install_name (via otool -D) from the built library and asserts a tarball entry matches, so any regression is caught at build time rather than on a user's machine. The Windows shared build is unaffected (PE uses the DLL name directly, no SONAME indirection) and the static libraries are unaffected (no SONAME / install_name). --- .../workflows/precompiled-bin-workflow.yml | 56 +++++++++++++++++-- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/.github/workflows/precompiled-bin-workflow.yml b/.github/workflows/precompiled-bin-workflow.yml index b7efb0035e..bf8d205274 100644 --- a/.github/workflows/precompiled-bin-workflow.yml +++ b/.github/workflows/precompiled-bin-workflow.yml @@ -183,7 +183,10 @@ jobs: run: | cp install/include/lbug.h . cp install/include/lbug.hpp . - cp -L install/lib*/liblbug.so . + # Preserve the versioned SONAME symlink chain so that consumers + # linking against liblbug.so can resolve the SONAME (liblbug.so.0) + # at load time. See precompiled-bin-workflow.yml SONAME check below. + cp -P install/lib*/liblbug.so install/lib*/liblbug.so.* . cp -L install/lib*/liblbug.a . cp install/bin/lbug . @@ -199,7 +202,10 @@ jobs: run: | cp install/include/lbug.h . cp install/include/lbug.hpp . - cp -L install/lib/liblbug.dylib . + # Preserve the versioned install_name symlink chain so that consumers + # linking against liblbug.dylib can resolve @rpath/liblbug.0.dylib + # at load time. See precompiled-bin-workflow.yml install_name check below. + cp -P install/lib/liblbug.dylib install/lib/liblbug.*.dylib . cp -L install/lib/liblbug.a . cp install/bin/lbug . @@ -227,7 +233,10 @@ jobs: - name: Create tarballs (Linux compat) if: runner.os == 'Linux' && matrix.variant == 'compat' run: | - tar -czvf liblbug-linux-${{ matrix.arch }}.tar.gz lbug.h lbug.hpp liblbug.so + # liblbug.so, liblbug.so.0, liblbug.so.0.15.3: unversioned link-time + # name, SONAME, and real file. The latter two are symlinks preserved + # by tar so that DT_NEEDED resolution works at load time. + tar -czvf liblbug-linux-${{ matrix.arch }}.tar.gz lbug.h lbug.hpp liblbug.so liblbug.so.* tar -czvf liblbug-static-linux-${{ matrix.arch }}-compat.tar.gz lbug.h lbug.hpp liblbug.a tar -czvf lbug_cli-linux-${{ matrix.arch }}.tar.gz lbug @@ -239,7 +248,11 @@ jobs: - name: Create tarballs (macOS) if: runner.os == 'macOS' run: | - tar -czvf liblbug-osx-${{ matrix.arch }}.tar.gz lbug.h lbug.hpp liblbug.dylib + # liblbug.dylib, liblbug.0.dylib, liblbug.0.15.3.dylib: unversioned + # link-time name, install_name, and real file. The latter two are + # symlinks preserved by tar so that LC_LOAD_DYLIB resolution works + # at load time. + tar -czvf liblbug-osx-${{ matrix.arch }}.tar.gz lbug.h lbug.hpp liblbug.dylib liblbug.*.dylib tar -czvf liblbug-static-osx-${{ matrix.arch }}.tar.gz lbug.h lbug.hpp liblbug.a tar -czvf lbug_cli-osx-${{ matrix.arch }}.tar.gz lbug @@ -251,6 +264,41 @@ jobs: Compress-Archive -Path lbug.h, lbug.hpp, lbug.lib -DestinationPath liblbug-static-windows-x86_64.zip Compress-Archive -Path lbug.exe -DestinationPath lbug_cli-windows-x86_64.zip + # ---- Verify shared-library tarball self-consistency ---- + # Guards against the class of bug where the tarball's on-disk filenames + # don't match the SONAME / install_name embedded in the shared library. + # A consumer linking -llbug resolves the link-time name (liblbug.so / + # liblbug.dylib) and stamps the SONAME / install_name into its own + # binary; that stamped name is what the dynamic loader then looks up at + # runtime, so it has to be present in the tarball too. + + - name: Verify tarball SONAME consistency (Linux compat) + if: runner.os == 'Linux' && matrix.variant == 'compat' + run: | + set -euo pipefail + tarball=liblbug-linux-${{ matrix.arch }}.tar.gz + soname=$(readelf -d liblbug.so | awk '/\(SONAME\)/ {gsub(/[][]/,"",$NF); print $NF}') + entries=$(tar -tzf "$tarball" | sort) + printf 'SONAME: %s\n%s entries:\n%s\n' "$soname" "$tarball" "$entries" + if ! grep -Fxq "$soname" <<<"$entries"; then + echo "::error::SONAME '$soname' is not present as an entry in $tarball" + exit 1 + fi + + - name: Verify tarball install_name consistency (macOS) + if: runner.os == 'macOS' + run: | + set -euo pipefail + tarball=liblbug-osx-${{ matrix.arch }}.tar.gz + install_name=$(otool -D liblbug.dylib | awk 'NR==2') + base=$(basename "$install_name") + entries=$(tar -tzf "$tarball" | sort) + printf 'install_name: %s\nbasename: %s\n%s entries:\n%s\n' "$install_name" "$base" "$tarball" "$entries" + if ! grep -Fxq "$base" <<<"$entries"; then + echo "::error::install_name '$install_name' (basename '$base') is not present as an entry in $tarball" + exit 1 + fi + # ---- Upload artifacts ---- - uses: actions/upload-artifact@v4