Notcurses-Native.git | scripts/ci/ | build-linux-glibc.sh


#!/usr/bin/env bash # Orchestrator: build + bundle the linux--glibc prebuilt # notcurses archive INSIDE a manylinux_2_28 container. Runs from # `docker run -v $PWD:/work -w /work quay.io/pypa/manylinux_2_28_ bash scripts/ci/build-linux-glibc.sh`. # # manylinux_2_28 = RHEL 8 baseline = glibc 2.28. Successor to # manylinux2014 (CentOS 7, glibc 2.17) which pypa retired March 2025 # and whose mirrors decay after the June 2024 CentOS 7 EOL. Floor # of 2.28 still covers RHEL 8+ / Ubuntu 18.10+ / Debian 10+ — i.e. # every glibc distro under active maintenance in 2026. # # Cache contract: CACHE_DIR (defaulting to /work/_ci-cache/manylinux_2_28) # may already contain a populated lib/ + include/ from a previous # run's actions/cache restore. If so, skip the ~10-min ffmpeg # source build. Otherwise build + populate. # # DEPS_ONLY=1 stops the script right after that codec chain is ready # (chowning $CACHE_DIR back to the host user first) instead of going # on to build notcurses. _build-linux-glibc.yml runs this script # twice on a cache miss — once with DEPS_ONLY=1, so it can save the # actions/cache entry immediately afterwards, before anything that # can still fail; once without, to do the notcurses build against the # now-warm cache. See the DEPS_ONLY branch below for why. set -euxo pipefail CACHE_DIR="${CACHE_DIR:-/work/_ci-cache/manylinux_2_28}" mkdir -p "$CACHE_DIR" # Fetch notcurses source from the pinned NOTCURSES_FORK SHA. # manylinux_2_28 ships git, so no apk/dnf install needed first. # Set NOTCURSES_SRC_CACHE under /work so actions/cache on the host # can persist the checkout across runs (same key bumping rules as # the ffmpeg cache — keyed on the SHA itself). export NOTCURSES_SRC_CACHE="${NOTCURSES_SRC_CACHE:-/work/_ci-cache/notcurses-source}" NOTCURSES_SRC_DIR=$(bash scripts/ci/fetch-notcurses-source.sh) export NOTCURSES_SRC_DIR # System packages via dnf: # * pkgconfig, patchelf, ncurses-devel: base build/runtime needs. # * nasm, yasm: needed by libdav1d / libvpx / ffmpeg for x86 SIMD # (on aarch64 they're no-ops but cheap to install). # RHEL 8 doesn't ship modern ffmpeg / libdeflate / libdav1d / libvpx / # libopus — those get source-built below into $CACHE_DIR with # accelerated paths (--enable-libdav1d in ffmpeg, etc.) so notcurses # gets ~10× faster AV1 decode + matched VP8/9 + Opus accel. # # libunistring-devel is deliberately NOT here any more. It is LGPL # and it ships inside the pack, so we need to be able to point at the # exact source for the binary we handed the user — which a dnf # package version that moves under us cannot do. It is source-built # into $CACHE_DIR below from a pinned, SHA-256-recorded tarball. (As # a side effect the packs move from RHEL 8's libunistring.so.2 to # libunistring.so.5.) ncurses stays package-managed: it is MIT-style # X11 with no source-conveyance duty, and its terminfo-directory # configuration makes a source build genuinely fiddly. dnf install -y --setopt=tsflags=nodocs \ pkgconfig patchelf \ nasm yasm \ ncurses-devel # cmake / meson / ninja via pip — RHEL 8's dnf ships cmake 3.20 and # notcurses needs 3.21+. meson + ninja are required by libdav1d's # build system. manylinux preinstalls Python at /opt/python/cp*/bin/; # the pip wheels for all three are current. PYBIN=$(ls -d /opt/python/cp3*/bin 2>/dev/null | head -1) [[ -n "$PYBIN" ]] || { echo "❌ No /opt/python/cp3*/bin found in manylinux image"; exit 1; } "$PYBIN/pip" install --quiet cmake meson ninja ln -sf "$PYBIN/cmake" /usr/local/bin/cmake ln -sf "$PYBIN/meson" /usr/local/bin/meson ln -sf "$PYBIN/ninja" /usr/local/bin/ninja cmake --version meson --version ninja --version # `| head -1` is informational; tolerate SIGPIPE under `set -o # pipefail` (head closes stdin after line 1, ldd's continuing # version-blob writes then SIGPIPE — bash propagates exit 141 and # kills the script otherwise). ldd --version | head -1 || true export PKG_CONFIG_PATH="$CACHE_DIR/lib/pkgconfig:${PKG_CONFIG_PATH:-}" export LD_LIBRARY_PATH="$CACHE_DIR/lib:${LD_LIBRARY_PATH:-}" # CMake's find_path / find_library / find_package don't read # PKG_CONFIG_PATH — they search CMAKE_PREFIX_PATH plus system dirs. # notcurses' CMakeLists.txt locates libdeflate via a raw find_path # (not pkg-config like it does for ffmpeg), so without this it # bails with "Couldn't find libdeflate.h" even though libdeflate # is sitting in $CACHE_DIR/include. export CMAKE_PREFIX_PATH="$CACHE_DIR:${CMAKE_PREFIX_PATH:-}" # CPATH for raw `cc -I` resolution, LIBRARY_PATH for the linker's # `-l` lookup — defensive in case notcurses' build invokes the # compiler outside of CMake's find_X-managed flag set. export CPATH="$CACHE_DIR/include:${CPATH:-}" export LIBRARY_PATH="$CACHE_DIR/lib:${LIBRARY_PATH:-}" # Cache-hit detection: ffmpeg's pkg-config file is the cheapest # all-or-nothing probe. ffmpeg is the LAST thing built, so if it's # present every prerequisite (libdeflate, libdav1d, libvpx, libopus) # is too. if [[ -f "$CACHE_DIR/lib/pkgconfig/libavcodec.pc" ]]; then echo "✅ Cache hit — skipping libunistring / libdeflate / libdav1d / libvpx / libopus / ffmpeg builds." else # Codec libs MUST land before ffmpeg — ffmpeg's configure probes # them via pkg-config + --enable-libfoo to wire its libfoo-backed # decoder dispatches. libunistring is independent of all of them # (notcurses links it directly, ffmpeg never sees it); it goes # first only so the cache-hit probe below — which keys on ffmpeg, # the last thing built — still implies everything else is present. echo "⏳ Cache miss — building accelerated codec stack from source (~15-20 min first run)." PREFIX="$CACHE_DIR" bash scripts/ci/build-libunistring.sh PREFIX="$CACHE_DIR" bash scripts/ci/build-libdeflate.sh PREFIX="$CACHE_DIR" bash scripts/ci/build-libdav1d.sh PREFIX="$CACHE_DIR" bash scripts/ci/build-libvpx.sh PREFIX="$CACHE_DIR" bash scripts/ci/build-libopus.sh PREFIX="$CACHE_DIR" bash scripts/ci/build-ffmpeg.sh fi # Verify deps resolve before kicking off notcurses build. pkg-config --modversion libdeflate pkg-config --modversion dav1d vpx opus pkg-config --modversion libavcodec libavformat libavutil libswscale libswresample # Escape hatch for the cache-split pattern the workflow uses: # _build-linux-glibc.yml runs this script ONCE with DEPS_ONLY=1 to # populate (or, on a cache hit, merely verify) $CACHE_DIR, saves the # actions/cache entry off the back of THAT docker run, and only then # runs this script a second time — cache warm — for the notcurses # build below. Splitting the docker invocation is what makes the # save actually happen: within a single `docker run`, a later # failure (notcurses build, bundling, the codec probe) would abort # the whole GHA step before a combined actions/cache action's post- # job save step ever gets to run, discarding the ~15-20 min codec # build that had already succeeded — which is exactly what happened # to the Windows lane in the r10 dispatch that motivated this split. # # chown here, not just at the very end of a full run: on the # DEPS_ONLY invocation this IS the end of the run, and $CACHE_DIR is # the one artefact the following host-side `actions/cache/save` step # needs to read — root-owned (this container runs as root against # the /work bind mount) is unreadable-enough to matter there, same # failure class as fix 2's bundle/ permission error. if [[ "${DEPS_ONLY:-}" == "1" ]]; then chown -R "$(stat -c '%u:%g' /work)" "$CACHE_DIR" echo "✅ DEPS_ONLY=1 — codec chain is ready in \$CACHE_DIR; skipping notcurses build." exit 0 fi CMAKE_FLAGS=( -DUSE_MULTIMEDIA=ffmpeg -DBUILD_FFI_LIBRARY=ON -DUSE_CXX=OFF -DBUILD_EXECUTABLES=OFF -DUSE_PANDOC=OFF -DUSE_DOCTEST=OFF -DUSE_POC=OFF -DUSE_STATIC=OFF -DCMAKE_BUILD_TYPE=Release ) ( cd "$NOTCURSES_SRC_DIR" mkdir -p build cmake -B build -S . "${CMAKE_FLAGS[@]}" # Assert notcurses linked OUR libunistring, not a stray system one. # `find_library(unistring unistring REQUIRED)` searches # CMAKE_PREFIX_PATH first and /usr/lib64 after, and this container # can still acquire a libunistring.so as a transitive dnf # dependency of something else — in which case cmake would happily # link the distro's 0.9.9 while bundle-elf.sh copies whichever the # runtime loader picks. Same class of check as the Windows lane's # DEFLATE:FILEPATH assertion. awk-with-exit rather than # `grep | head -1`: head closing the pipe SIGPIPEs its producer and # `set -o pipefail` turns that into a mystery failure. unistring_lib=$(awk -F= '/^unistring:FILEPATH=/{print $2; exit}' build/CMakeCache.txt) case "$unistring_lib" in "$CACHE_DIR"/*) echo "ok: unistring resolved to $unistring_lib" ;; *) echo "❌ find_library(unistring) resolved to '$unistring_lib'," echo " which is outside the source-built prefix '$CACHE_DIR'." echo " The pack would ship a libunistring we never built," echo " pinned, or recorded a source tarball for." exit 1 ;; esac cmake --build build -j"$(nproc)" ) # Bundle .so files + transitive ldd deps into bundle/ with # $ORIGIN rpath. No EXTRA_SKIP_LIBS — default skiplist covers # glibc's system libs. bash scripts/ci/bundle-elf.sh # Compile the perf shim. Linking model mirrors Vips-Native's # shim build — link explicitly against libnotcurses-core from # bundle/ and set DT_RUNPATH to $ORIGIN so the shim's NEEDED entry # resolves at runtime to our patched libnotcurses-core.so at the # same path it was compiled against, not whatever else might be # loaded in the host process. cc -O2 -shared -fPIC \ -I "$NOTCURSES_SRC_DIR/include" \ -Wl,-soname,libnotcurses_native_shim.so \ -o bundle/libnotcurses_native_shim.so \ src/notcurses_native_shim.c \ -Lbundle -lnotcurses-core patchelf --set-rpath '$ORIGIN' bundle/libnotcurses_native_shim.so echo "--- shim symbols ---" nm -g --defined-only bundle/libnotcurses_native_shim.so \ | grep -E 'T (_)?notcurses_native_' || { echo "❌ no notcurses_native_* exports — link silently failed." exit 1 } # Sidecar for Build.rakumod's content-based freshness check: the # SHA-256 of the shim source this shim was compiled from. Without # it, installs fall back to a cross-machine mtime comparison that # always thinks the dist's source is newer than the packed shim # and recompiles (or, toolchain-less, warns and drops to the slow # per-cell path). sha256sum src/notcurses_native_shim.c | awk '{print $1}' \ > bundle/libnotcurses_native_shim.so.srchash # Release gate: confirm libavcodec actually got linked against # libdav1d / libvpx / libopus during ffmpeg's configure. Catches # the regression class where pkg-config silently failed and ffmpeg # fell back to its internal decoders. If the probe fails, the # build job fails, and release.yml's `needs:` chain refuses to # publish this artefact. bash scripts/ci/run-codec-probe.sh # This container runs as root against the /work bind mount, so # everything it created under /work — bundle/ above all — comes out # root-owned on the host. package-and-upload's emit-third-party-kit.sh # step runs AFTER this script, on the host, as the unprivileged # runner user, and needs to create bundle/LICENSES: root-owned, # group/other-writable-less directories made that a permission # denied rather than a licensing-kit write. Restore host ownership # before handing control back. $CACHE_DIR is included too, for the # benefit of a standalone `docker run ... build-linux-glibc.sh` # invocation that never went through the DEPS_ONLY split above (the # split path already chowned it at that point, so this is a no-op # there) — anything else this script writes under /work # (_ci-cache/notcurses-source, populated by fetch-notcurses-source.sh) # is container-read-only from here on and never written to by a # host-side step, so it does not need the same treatment. chown -R "$(stat -c '%u:%g' /work)" /work/bundle "$CACHE_DIR"