Notcurses-Native.git | Changes


Revision history for Notcurses::Native {{$NEXT}} 0.5.0 2026-08-12T18:42:31+01:00 - Both Windows lanes source-build GNU libiconv 1.19 instead of taking MSYS2's. It is the last copyleft library in the packs that we could not point a user at the exact source for: LGPL-2.1, shipped inside the .zip, and sourced from a pacman package whose version moves under us and is garbage-collected off the mirrors — the identical argument that moved libunistring onto the self-built chain. A PE import scan of both published r10 packs settled who actually needs it: avcodec-62.dll (our ffmpeg's configure autodetects iconv) and libunistring-5.dll (its AM_ICONV takes an external libiconv on any platform whose libc has none), and nothing else — ncursesw included. Both importers are libraries we build, so this is a self-contained change rather than an attempt to out-run the toolchain's package closure. The pinned tarball and its SHA-256 are in resources/third-party.json and are now attached to every binary release like the other six. - MSYS2's libiconv cannot be uninstalled — the toolchain group depends on it — and its DLL has the identical basename, so "ours, not theirs" is enforced three times rather than assumed. build-libiconv.sh runs before libunistring and ffmpeg so the prefix is populated when they configure; libunistring gets --with-libiconv-prefix and ffmpeg gets --extra-cflags/-ldflags pointing at that prefix (iconv ships no .pc file, so PKG_CONFIG_PATH cannot steer either of them); and a new step re-hashes every bundled DLL against the one the lane staged, failing the release if they differ. Both configure changes are conditional on an iconv.h actually existing in the prefix, so the Linux and macOS lanes — which take iconv from libc — build exactly as before. - The Windows packs stop shipping four DLLs that nothing in them loads. A PE import-reachability closure from libnotcurses{,-core,-ffi}.dll over the published r10 packs found libintl-8.dll (both lanes) imported by nothing at all; libstdc++-6.dll and libgomp-1.dll (UCRT64) likewise — notcurses builds -DUSE_CXX=OFF and a decoder-only ffmpeg uses no OpenMP — and libgcc_s_seh-1.dll imported only by libstdc++, i.e. reachable only from another orphan. Every one of them arrived because bundle-dll's belt-and-braces sweep list named it, which is a list that copies things whether or not anything needs them. libintl was the expensive one: LGPL, package-manager sourced, no producible corresponding source, and no caller — the whole cost of a compliance obligation for none of the benefit. It is also why build-libiconv.sh configures --disable-nls, since libiconv would otherwise link libintl straight back in. - New release gate behind that: bundle-dll now fails the lane on any bundled DLL that nothing in the pack imports. The sweep list exists to survive an ldd that lies, so it will always over-collect at the margin; this is the counterweight, and it is general rather than a deny-list of four names. gcc-runtime keeps its manifest entry (the licences are the reason the decision went the way it did) but with empty pattern lists, so a reappearance now fails the third-party audit as an unlisted file too. What is left of GCC's runtime in the packs is the part gcc links statically into the UCRT64 DLLs, which is exactly what the GCC Runtime Library Exception exists to permit. - The macOS lanes gained the same "is this really ours?" check, by LC_UUID rather than by hash: bundle-macos rewrites install names, strips and re-signs every dylib it stages, so the bytes cannot match by design, while LC_UUID survives all three. It matters most for libunistring, which the macos-14 runner also has under brew as a transitive dependency of several preinstalled formulae — brew's bottle is 1.4.2 today, the same version we pin, so shipping it would even produce a true THIRD-PARTY.md, which is precisely the kind of coincidence that stops being true without anyone noticing. - opus builds with --disable-rtcd on Windows ARM64. opus's ARM runtime CPU detection has backends for Linux, Apple and MSVC — and none for mingw/clang on aarch64, where celt/arm/armcpu.c stops the build with an #error naming this exact flag. Safe rather than a compromise: NEON is architecturally baseline on aarch64, so the compile-time paths are always valid; the loss is runtime dispatch to optional dotprod/i8mm extensions, which matters to nobody decoding audio in a terminal. Proven on real aarch64 (alpine container): the flag removes every armcpu compile unit and the library builds clean. - build-ffmpeg.sh's architecture assertion reads config.h's exact ARCH_* macros instead of the configure summary's ARCH line. The summary normalises to the architecture family — an x86_64 build prints "ARCH x86", identical to i686 — so the assertion could never pass on UCRT64 and failed the first r10 dispatch against a correctly-configured build; aarch64 survives normalisation unchanged, so the arm lanes would have passed by luck rather than by checking anything. config.h defines ARCH_X86_64 / ARCH_AARCH64 exactly (verified against a real 8.1.2 configure), which is the question the assertion was always trying to ask. - The macos-arm64 release lane source-builds ffmpeg instead of installing brew's. brew's ffmpeg formula is a GPLv3 build (--enable-gpl, plus x264, x265, SvtAv1Enc and mp3lame), and dylibbundler dutifully pulled that whole encoder chain into the arm64 archive — a GPL-encumbered and considerably fatter bundle for a library that never encodes anything. That lane now runs the same build-{libdav1d,libvpx,libopus,ffmpeg}.sh chain macos-x86_64 and the Linux lanes already used: LGPL-2.1, decoder-only, and a bundle whose load commands name nothing but our own codec libs and Apple system frameworks. ncurses is the one library still taken from brew there — arm64 bottles target 11.0+, which already matches that lane's floor, and X11-style licensing asks nothing of us beyond the notice we ship. libunistring and libdeflate left that list later in this same release; see their entries below. - build-ffmpeg.sh now asserts what it built. configure's output is tee'd, and the script fails if ffmpeg reports anything other than "License: LGPL version 2.1 or later", or if any --enable-decoder / -demuxer / -parser / -protocol name "did not match anything". ffmpeg only *warns* on an unmatched component, so a codec renamed out from under us would otherwise ship as a silently missing decoder — a build that succeeds and an archive that can't open the file. - Which immediately caught one: the Truevision TGA decoder is named `targa`; `tga` is only ever the file extension. TGA has been listed in the decoder allowlist — and absent from every bundle we ever shipped — for as long as the allowlist has existed. Now spelled correctly, and now actually present. - Pinned versions: ffmpeg 6.1.2 → 8.1.2 (newest 8.1 point release; staying a series behind 9.0 deliberately), dav1d 1.4.3 → 1.5.4, libvpx 1.14.1 → 1.16.0, opus 1.5.2 → 1.6.1, libdeflate 1.20 → 1.25. opus now comes from downloads.xiph.org: xiph stopped attaching dist tarballs to their GitHub releases after 1.5.2, and the auto-generated tag tarball has no `configure` in it. - build-ffmpeg.sh also pins off the [autodetect] platform libraries — xlib, libxcb, sdl2, vulkan, libdrm. Every one of them serves a component we'd already switched off, but autodetection still put them on the link line: on a host with brew's libxcb installed, libavcodec came out with load commands on /opt/homebrew/opt/libx11 and friends, which dylibbundler would have dragged into the archive. What we ship should be a function of these scripts, not of the runner's package list. - nasm is only demanded of x86 hosts now. It assembles x86 SIMD and nothing else, so the arm64 lanes were being asked for a package they have no use for. - Both musl release lanes (linux-x86_64-musl, linux-aarch64-musl) source-build that same chain instead of `apk add ffmpeg-dev`. Alpine's ffmpeg is a GPLv3 build too: its libavcodec carries DT_NEEDEDs on libx264, libx265, libSvtAv1Enc, libmp3lame, libxvidcore, libaom and librav1e, `avutil_license()` answers "GPL version 3 or later", and bundle-elf.sh's ldd walk copied that entire 20 MB encoder chain into the archive we publish. Both lanes now run build-{libdeflate,libdav1d,libvpx,libopus, ffmpeg}.sh into a per-arch cache prefix with the same PKG_CONFIG_PATH / CMAKE_PREFIX_PATH wiring the glibc lanes use. The bundle's whole DT_NEEDED closure is now our own codec libs plus ncurses, libunistring, zlib and musl libc — 19 files, 15.7 MB — and `avutil_license()` reports LGPL 2.1. - Windows (windows-x86_64 UCRT64 + windows-arm64 CLANGARM64) now source-builds the same LGPL-2.1, decoder-only ffmpeg chain (libdeflate, libdav1d, libvpx, libopus, ffmpeg) the other platforms use, instead of installing MSYS2's mingw-w64-*-ffmpeg. That package is a `--enable-gpl --enable-version3` build and bundle-dll swept its whole encoder tree — x264, x265, SvtAv1Enc, lame, rubberband — into the published archive, making the Windows prebuilts GPL-encumbered for a library that only ever decodes. The chain is cached per-msystem, verified for prefix-containment on every run (including cache hits), and a new release gate re-checks the shipped binaries for GPL imports and reads avcodec's embedded licence string. libvpx is linked statically on Windows — its configure refuses to emit a DLL off ELF/OS-2/Darwin — so it lives inside avcodec there; the libvpx_vp8 / libvpx_vp9 decoders the codec probe gates on are unaffected. Also fixes a latent arch bug: build-ffmpeg.sh's nasm gate read `uname -m`, which reports x86_64 under MSYS2 even on Windows-on-ARM. With this, no lane installs a package-manager ffmpeg: all four platforms ship the identical decoder-only, LGPL-2.1 codec surface. - A workflow_dispatch of release.yml no longer publishes. It used to — useful when dispatch was the release path — but it meant "validate the recipe on a branch" and "overwrite the published archives every installed dist checksum-verifies against" were one click apart. Dispatch now builds and smokes all eight lanes and stops; only pushing the binaries-* tag itself publishes, the same contract as the app release workflows downstream. - musl apk list follows: `ffmpeg-dev` and `libdeflate-dev` out (libdeflate is source-built for version parity, not licensing — Alpine froze 1.20, we pin 1.25); curl / xz / bzip2 in, since the base image has neither curl nor GNU tar and the dep tarballs are .tar.xz and .tar.bz2; meson + ninja for dav1d; perl and diffutils for libvpx, whose configure hard-fails with "diff missing: Try installing diffutils" on busybox's applet; zlib-dev, because ffmpeg's png decoder has a hard zlib dependency that used to arrive transitively with ffmpeg-dev. nasm/yasm are added on the x86_64 lane only. - `_build-linux-musl.yml` gained the source-built-deps cache the lane never previously needed — actions/cache over `_ci-cache/alpine-3.20-`, keyed on the image tag, the arch, and a hash of all five build-lib*.sh / build-ffmpeg.sh scripts, with no restore-keys. Same discipline as the glibc lane's, for the same reason: a partially-restored prefix would corrupt the install tree mid-pkgconfig. - GNU libunistring joins the self-built chain on all four platforms, pinned at 1.4.2. It was the last package-manager library in the packs with a source-conveyance duty attached to it: it is LGPL-3.0-or-later OR GPL-2.0-or-later, we ship it as a binary, and conveying it under the LGPL means being able to hand a recipient the corresponding source for the exact `libunistring.so.5` they were given. dnf, apk, brew and pacman versions all move under us and get garbage-collected off the mirrors, so that was unanswerable a few months after any given release; a pinned tarball with its SHA-256 recorded in `resources/third-party.json` answers it indefinitely. Out of dnf's list on manylinux (which was still on 0.9.9, so the glibc packs shipped `libunistring.so.2` — they now ship `.so.5` like everyone else), out of apk's on alpine, out of `brew install` on macos-arm64, out of pacman's on both Windows lanes. macOS x86_64 already built it from source for deployment-target reasons and simply moved its invocation into the shared step. - `scripts/ci/build-libunistring.sh` stopped being the macOS x86_64 lane's private helper and became a peer of the codec scripts — same `$PREFIX` contract, same fetch-with-fallback, and a real post-install check (`unigbrk.h` plus a linkable `libunistring.*`) instead of an `ls | head`. The cache keys in all four `_build-*.yml` now hash it along with the rest. - Every lane now asserts that notcurses actually linked OUR libunistring, by reading `unistring:FILEPATH` out of `CMakeCache.txt` and requiring it to sit inside the source-built prefix — the same shape as the Windows lane's existing `DEFLATE:FILEPATH` check. This is not theoretical: locally, with the prefix deliberately missing libunistring, notcurses' `find_library(unistring unistring REQUIRED)` silently resolved `/opt/homebrew/lib/libunistring.dylib` and configured happily. A brew or dnf copy arriving transitively as some other package's dependency would have shipped a binary we never built, pinned, or published source for, which is precisely the failure this whole change exists to make impossible. - New `resources/third-party.json`: one entry per component that ships inside a pack, with SPDX licence, copyright notice, upstream URL, the exact pinned source (tarball URL + SHA-256, git URL + commit, or an honest "package manager"), and per-platform binary basename patterns. Fifteen components, including the ones nobody had written down before: the MSYS2 toolchain runtimes the Windows packs carry (`libwinpthread-1`, the `libgcc_s_*`/`libstdc++`/`libgomp` set under the GCC Runtime Library Exception, or `libc++`/`libomp` under Apache-2.0-with-LLVM-exception on CLANGARM64), and `libiconv` / `libintl`. Those last two are a KNOWN GAP recorded in the file: they are LGPL and package-manager-sourced, i.e. exactly the problem libunistring just stopped having, but they arrive transitively rather than by our choosing and moving them is a separate piece of work. `zlib` and `ncursesw` stay package-managed with no gap — Zlib and X11-style licences ask for a notice, which we now ship, and nothing else. - Generated licensing kit in every archive: `THIRD-PARTY.md` (component / version / licence / copyright / source URL + hash table, plus the per-component notes) and a `LICENSES/` directory with the full text of every licence that pack's contents are under. Written by `scripts/ci/emit-third-party-kit.sh` from the manifest. It ships inside the archive rather than only in the repository because the archive is what a user actually receives, and a notice they have to go and find discharges nothing. - New release gate, `scripts/ci/audit-third-party.sh`, run in every lane: every file in `bundle/` must match some component's patterns for that platform (or the platform's system-library allow-list), and every component the manifest says ships there must actually be present. The first direction catches the accident that started this whole thread — the x264 / x265 / SvtAv1Enc encoder tree arrived in the packs because nobody chose it, it just came in on ffmpeg's dependency closure, and it took a licence audit rather than a build failure to notice. The second catches a library silently dropped by a bundling walk, which is a pack that breaks at `dlopen` on a user's machine. Verified against real locally-built artefacts: the macos-arm64 bundle passes, the published r9 macos-arm64 pack fails naming all eight of its GPL/OpenSSL strays, a planted `libx264.165.dylib` fails, a removed `libunistring.5.dylib` fails, and a `libunistring.2.dylib` renamed in from a distro build fails on both counts at once. - Both licensing steps live in the `package-and-upload` composite action rather than in the four build workflows. That action is the single choke point every lane goes through on the way to producing an archive, so a lane cannot be added that ships an unaudited pack, and cannot be edited to skip the gate without also losing its upload. - `_release-publish.yml` attaches the six source tarballs (ffmpeg, dav1d, libvpx, opus, libdeflate, libunistring) to every binary release, downloading them at publish time from the same pinned URLs the build scripts use and hard-failing if any does not match the manifest's SHA-256 — attaching source that does not correspond to the shipped binaries would be worse than attaching none. `checksums.txt` now covers the tarballs as well as the eight archives; `Build.rakumod` looks entries up by artefact name, so the extra lines are inert to it. - The release body was also two years stale: it advertised `quay.io/pypa/manylinux2014_*` and a glibc 2.17 floor for lanes that have been manylinux_2_28 / glibc 2.28 for a while, and did not mention the Windows lanes at all. Fixed, and given a Licensing section that inventories what is copyleft (ffmpeg, libunistring — with their source attached) versus permissive, and states plainly that platform C runtimes are dynamically linked and therefore not redistributed. - `t/17-third-party-manifest.rakutest` validates the manifest hermetically on every `prove6`: SPDX strings come from a closed known-good set (adding a component under an unread licence has to be a decision, not a typo that sails through), URLs are https, tarball hashes are 64 hex digits, git refs are full commit SHAs, patterns are non-empty and whitespace-free and not claimed by two components at once, `required` is never true with an empty pattern list, and `resources/licenses/` and the manifest reference each other exactly — no orphan texts, no dangling references. It also cross-checks the notcurses ref against `NOTCURSES_FORK` and every component version against the `VERSION=` line in its `scripts/ci/build-*.sh`, so a version bump applied to one and not the other cannot ship source for 8.1.2 alongside binaries built from 8.1.3. - Fixed in passing, found by tripping over it while proving this chunk out locally: the `notcurses-source-` actions/cache entry included the `build/` tree cmake creates inside the checkout. That key is shared by all four Linux lanes, so whichever finished first saved its CMakeCache — absolute paths, compiler paths, object files and all — and the other three restored it on the next run. Both container lanes bind-mount to `/work`, so cmake could not even notice the source directory had moved; it would have reused a manylinux-x86_64 configure in an alpine-aarch64 build. The cache path now excludes `*/build`. (Locally the same collision surfaced as cmake refusing to configure at all, which is how it got noticed — in CI it would have been a mystifying cross-lane failure appearing long after the commit that enabled it.) - `.gitattributes` pins `*.txt` and `*.json` to LF. The licence texts are copied verbatim into every pack, so without this the same file would hash differently in a Windows .zip than in a Linux .tar.gz depending on the runner's checkout settings. - ncursesw stays package-managed, deliberately. Its licence is MIT-style X11: a notice duty, which `LICENSES/X11.txt` and the copyright line in `THIRD-PARTY.md` now discharge, and no source duty at all. Source-building it for uniformity is a reasonable future pass, but its `--with-default-terminfo-dir` / `ticdir` configuration is genuinely delicate per platform (the macOS x86_64 lane, which does build it, has to skip `install.data` because `tic` would write to SIP-protected `/usr/share/terminfo`), and that is a poor trade against a licence that asks nothing of us. - The first live CI dispatch of the above found four more bugs the local proofs hadn't: - ffmpeg.org is flaky from GitHub-hosted runners — every lane's ffmpeg download saw connection timeouts/resets against it, reproduced locally too, so it wasn't a one-off. `build-ffmpeg.sh` now fetches ffmpeg's own GitHub mirror tag archive (`FFmpeg/FFmpeg` @ `refs/tags/n8.1.2.tar.gz`) instead of `ffmpeg.org/releases`. That only works because ffmpeg checks `configure` into git rather than generating it via `make dist` — most projects' tag archives have no buildable configure, this one does. VERSION stays the bare `8.1.2` (still what t/17 cross-checks the manifest against); a new TAG variable carries the `n`-prefixed release-tag spelling. `resources/third-party.json` picked up the new URL, sha256 and an explicit `filename` (`ffmpeg-n8.1.2.tar.gz` — the bare `n8.1.2.tar.gz` says nothing about what it is and would collide with the next project pinned at the same tag, same reasoning as libvpx's entry already documents), and t/17 gained an ffmpeg-specific check tying the manifest's URL/filename to `build-ffmpeg.sh`'s `n${VERSION}` tag convention explicitly, rather than trusting the generic substring-contains-version check to have caught a dropped `n` or a reversion to the old ffmpeg.org layout. - The two container lanes (`build-linux-glibc.sh`, `build-linux-musl.sh`) now `chown` `bundle/` and `$CACHE_DIR` back to the host user before exiting. Docker on a Linux runner runs these containers as root against a bind-mounted `/work`, so everything they create there — the bundle directory above all — comes out root-owned on the host. The new `package-and-upload` composite action's `emit-third-party-kit.sh` step runs afterwards, on the host, as the unprivileged runner user, and its `mkdir bundle/LICENSES` hit exactly that: "Permission denied" on musl (both arches) and glibc-aarch64. Ownership is restored by reading `/work`'s own uid:gid off the bind mount itself (`stat -c '%u:%g' /work`) rather than assuming a fixed runner uid, so the fix holds regardless of which uid GitHub happens to run the job as. `_ci-cache/notcurses-source` didn't need the same treatment — nothing host-side ever writes into it, only containers read and write it across runs. - `_build-macos.yml`'s arm64 lane stopped taking libdeflate from brew and now source-builds it into the same workspace cache prefix as ffmpeg and friends, alongside the ncurses it still keeps from brew. The lane's `PKG_CONFIG_PATH` / `CMAKE_PREFIX_PATH` / `CPATH` env block only ever pointed at that cache prefix, never at brew's include path, so notcurses' `find_path(libdeflate.h)` came up empty despite the brew keg being installed — "Couldn't find libdeflate.h" at configure time. The earlier local proof of this lane missed it because it built libdeflate into the prefix on both arches by hand; the live workflow, as written, hadn't. This also makes `resources/third-party.json`'s libdeflate entry — which already claims "ours everywhere" — actually true on arm64 instead of silently shipping brew's copy under that claim. The cache key's `hashFiles` list already covered `build-libdeflate.sh`, so no cache-invalidation change was needed. - All four `_build-*.yml` dependency-chain caches moved from the combined `actions/cache` to a restore/save split. The combined action only saves in a post step gated on job success, and this dispatch showed exactly what that costs: the Windows lane built the entire libdeflate→dav1d→vpx→opus chain, died at the ffmpeg download (see above), and saved nothing — the next push would have re-paid the whole ~15-20 min build for a failure that had nothing to do with the codec chain. `_build-windows.yml` and `_build-macos.yml` now save immediately after their already-separate source-build step, before the steps that can still fail (notcurses configure/build). The two container lanes needed more than a step reorder: their entire build previously ran as one `docker run` per architecture, chain and notcurses build together, so there was no step boundary to save at. Both `build-linux-glibc.sh` and `build-linux-musl.sh` gained a `DEPS_ONLY=1` escape hatch that stops (and chowns `$CACHE_DIR`, per the fix above) right after the codec chain is ready, letting the workflow split the single docker invocation into two: one that builds (or, cache-hit, skips) just the chain, cached and saved immediately after; a second, unconditional one that builds notcurses against the now-warm cache. Precedent for the restore/save shape is App-Moneymoor's vcpkg cache block. 0.4.4 2026-08-12T10:43:56+01:00 - Fix `Build.rakumod`'s `!sha256` unconditionally shelling out to `shasum -a 256` on every POSIX platform. manylinux/EL-minimal containers ship `sha256sum` (coreutils) but no `shasum` (a Perl tool) — the run failed, the digest came back undefined, and the caller reported "Checksum mismatch ... got unknown", rejecting a perfectly valid prebuilt pack and falling back to a source compile that then dies for want of ncursesw/libunistring. POSIX now tries `sha256sum` first (universal on Linux), then `shasum -a 256` (macOS/BSD); either tool missing, unspawnable, non-zero-exit, or producing no 64-char lowercase-hex digest falls through to the next. The mismatch note now also names which tool(s) were tried when the digest came back unknown. 0.4.3 2026-08-11T15:36:14+01:00 - Bump BINARY_TAG to binaries-notcurses-3.0.17-r9 — first pack that ships the shim .srchash sidecars. - Shim freshness is now content-based, not mtime-based. A libnotcurses_native_shim..srchash sidecar (SHA-256 of the shim source) ships in the prebuilt packs and is written by every local compile; mtimes are meaningless across machines, and the old comparison made every fresh ecosystem install think the dist's shim source was newer than the pack's prebuilt shim — silently recompiling (or, without a C toolchain, warning and falling back to the slow per-cell path despite a good shim in the pack; observed on a toolchain-less Alpine CI container). - Successful shim compiles are parked in a content-addressed build cache (/shims/.), so a machine that compiled once never recompiles for the same shim source + binary tag, even though extraction wipes the staged dir on every install. 0.4.2 2026-08-11T12:17:30+01:00 - Clarify docs 0.4.1 2026-08-03T17:00:29+01:00 - Use notcurses with patch to support tmux 3.4 0.4.0 2026-05-20T09:19:16+01:00 - CI fix: codec-probe PNG fixture moved from chunli44.png → chunli01.png. The notcurses repo has ~11 chunli* PNGs that are symlinks to earlier frames (chunli32-37, 39, 41-44 → others). On Unix git checks them out as real symlinks; on Windows git's default core.symlinks=false materializes each as a 12-byte text file containing the target's name, so the codec probe opened "chunli40.png" as content, libavcodec read 8 bytes, saw "chunli40" instead of the PNG magic 89 50 4E 47 0D 0A 1A 0A, and rejected the file with "Invalid PNG signature 0x6368756E6C693430". chunli01.png is a regular file and works identically on every lane. Header comment in codec-probe.c now warns future authors to pick regular files only. - CI fix: fetch-notcurses-source.sh now strips trailing \r from each parsed line of NOTCURSES_FORK before applying the 40-char lowercase-hex SHA check. Windows checkouts with core.autocrlf=true (default for Git for Windows) were leaving a CR on the SHA, pushing it to 41 chars and failing the regex with "sha=… must be a 40-char lowercase hex string." `read -r` strips \n but not \r, so the fix is one `${line%$'\r'}` trim per iteration. Linux glibc/musl workflows' `awk` SHA extraction gains a `tr -d '\r'` for the same belt-and-braces reason. Primary fix: new top-level .gitattributes pins NOTCURSES_FORK / BINARY_TAG / *.sh / *.yml / *.yaml to `text eol=lf` so fresh checkouts no longer get CRLF in the first place. Existing clones renormalize via `git add --renormalize .` (or the bash/awk hardening covers them transparently). - vendor/notcurses/ deleted from the repo. Build.rakumod's shim- compile path now resolves include headers (and the Windows import lib) by calling !ensure-notcurses-source — the same SHA- keyed git fetch the source-build fallback uses, wrapped in a try{} so prebuilt-only installs that hit the shim short-circuit don't trigger an unnecessary fetch. Selkie's `examples/viewported-card-list.raku` previously reached into the sibling Notcurses-Native vendor/notcurses/data/ dir for sample images; Selkie 0.8.0+ vendors the 10 referenced images under its own examples/data/ tree. .gitignore drops the now- meaningless `vendor/notcurses/build/` entry. - CI/CD: all four prebuilt-binary lanes (macOS arm64/x86_64, Linux glibc x86_64/aarch64, Linux musl x86_64/aarch64, Windows x86_64/arm64) and the bundle / codec-probe helpers now fetch notcurses from the SHA pinned in NOTCURSES_FORK instead of reading from vendor/notcurses. Single source of truth — both install-time (Build.rakumod) and release-time (CI) build from the exact same commit, so no more "I bumped NOTCURSES_FORK and forgot to sync vendor/notcurses" drift. New shared script scripts/ci/fetch-notcurses-source.sh mirrors Build.rakumod's !ensure-notcurses-source: git init + fetch-by-SHA into $NOTCURSES_SRC_CACHE//, idempotent reuse on cache-hit, writes NOTCURSES_SRC_DIR= to $GITHUB_ENV when running under GHA. Workflows cache the per-SHA checkout via actions/cache keyed on the pin so bumping NOTCURSES_FORK auto- invalidates. Migrations: * scripts/ci/build-linux-glibc.sh + build-linux-musl.sh + bundle-elf.sh now consume $NOTCURSES_SRC_DIR. musl lane adds `git` to its `apk add` list. * .github/workflows/_build-{macos,windows}.yml gain a "Fetch notcurses source" step; subsequent steps use $NOTCURSES_SRC_DIR. Windows lane adds `git` to its msys2 install set. * .github/actions/bundle-{dll,macos}/action.yml read $NOTCURSES_SRC_DIR/build instead of vendor/notcurses/build. bundle-dll converts the Windows-style env value through cygpath for MSYS2 bash; bundle-elf captures $workspace before cd'ing into the build dir so absolute-path copies don't depend on `cd ../../..` relative math. * scripts/ci/codec-probe.c switches from $WORKSPACE_DIR + a hardcoded vendor/notcurses/data relpath to a single $NOTCURSES_DATA_DIR env var (set by run-codec-probe.sh to $NOTCURSES_SRC_DIR/data). FIXTURES paths now bare basenames. * NOTCURSES_FORK header updated — drops the now-obsolete "CI prebuilt lanes still read from vendor; collapse onto the fetch once CI is migrated" caveat. * .gitignore: ignore _ci-cache/ and bundle/. Escape hatch unchanged: NOTCURSES_NATIVE_VENDOR_DIR= short-circuits the fetch for fork iteration / airgapped builds. - new module lib/Notcurses/Native/Str.rakumod with three helpers: libc-name() (musl/glibc/macOS/Windows libc resolver shared with Native.rakumod's setenv path), strdup-copy-and-free() (decodes a malloc'd C char* and frees via libc free), and borrowed-str-from-pointer() (decodes a pointer into caller- provided storage without freeing). Used to fix the memory-leak class below. - memory leak fix: every binding that returned a heap-allocated char* and was declared `--> Str` silently leaked the original malloc'd pointer at every call. MoarVM's NativeCall copies the bytes into a Raku Str and does NOT free the source. Affected functions in Context.rakumod (ncwcsrtombs, notcurses_at_yx, notcurses_detected_terminal, notcurses_accountname, notcurses_hostname, notcurses_osversion), Cell.rakumod (nccell_extract, nccell_strdup), Plane.rakumod (ncplane_at_cursor, ncplane_at_yx, ncplane_contents), Direct.rakumod (ncdirect_readline, ncdirect_detected_terminal), and Widgets.rakumod (ncreader_contents). Each now rebinds to `--> Pointer` under a `__raw` symbol and exposes the public name as a Raku wrapper that calls strdup-copy-and-free. - pointer-into-buf returns (Context.rakumod's ncnmetric, ncqprefix, nciprefix, ncbprefix family) rebind similarly but route through borrowed-str-from-pointer so the caller's CArray[uint8] $buf isn't double-freed. - library-owned static pointer returns (notcurses_str_blitter, notcurses_str_scalemode, notcurses_version, ncplane_name, ncselector_selected/previtem/nextitem, ncmenu_selected/ mouse_selected, nctab_name, nctabbed_separator, nccell_extended_gcluster) keep their `--> Str` binding and gain an `OWNED-BY-LIBRARY` Pod comment so a future audit doesn't flag them as leaks. - new API: ncpile-render-to-string(NcplaneHandle --> Str) in Context.rakumod. Owns the malloc'd buffer lifecycle for ncpile_render_to_buffer — no more "caller forgets to free the output pointer" leaks for snapshot/rendering consumers. The raw ncpile_render_to_buffer binding stays exported for callers who want to manage the lifecycle themselves. - behavior fix: setenv(3) failures in lib/Notcurses/Native.rakumod now `note` to stderr instead of being silently ignored. Pre-fix, a libc setenv failure (ENOMEM/EINVAL) silently desynced %*ENV and the C environment — ncurses then couldn't find terminfo and notcurses_core_init failed with a vague error. The libc resolver replaces the previous hardcoded 'libc.so.6' so musl Alpine / distroless containers no longer dlopen-fail at module load. The per-call redeclaration of mac_setenv / linux_setenv is gone too — one module-level binding now serves every call. 0.3.4 2026-05-16T04:01:59+01:00 - Build.rakumod: drop $MIN-GLIBC from v2.35 to v2.28. The Linux glibc lanes are rebased onto manylinux_2_28 containers (quay.io/pypa/manylinux_2_28_{x86_64,aarch64}, RHEL 8 baseline) so the prebuilts now load on every glibc Linux distro under active maintenance in 2026 — RHEL 8+, Ubuntu 18.10+, Debian 10+. Previously the 2.35 floor meant Ubuntu 20.04 / Debian 11 users fell back to a 5-15 min CMake source build. (manylinux2014 / RHEL 7 / glibc 2.17 was the first target but pypa retired it in March 2025 and its CentOS 7 yum mirrors are decaying after the June 2024 EOL — manylinux_2_28 is the maintained successor.) - Build.rakumod: add libc axis to detect-platform. Linux keys now carry a libc suffix (`linux-x86_64-glibc`, `linux-x86_64-musl`, etc.); non-Linux keys unchanged. New `detect-libc` method probes `/lib/ld-musl-*.so.1` then falls back to `!detect-glibc-version`. Implicitly fixes a silent bug where musl users (Alpine / Postmarket OS / Void) were downloading the glibc artefact and segfaulting at first dlopen because `ldd --version` returns non-zero on musl, which short-circuited the glibc-too-old guard. - %PLATFORM-SLUGS: add linux-x86_64-musl, linux-aarch64-musl, linux-x86_64-glibc, linux-aarch64-glibc keys (the glibc entries are renamed from the previous `linux-` keys). The musl lanes are built in alpine:3.20 containers (musl 1.2.5 headers, 1.20+ runtime floor per notcurses' declared support level). - Unknown-platform diagnostic now reports the detected libc on Linux, so a user on an unsupported libc-arch combination can see exactly which axis didn't match. - %PLATFORM-SLUGS: add 'darwin-x86_64' => 'macos-x86_64' so Intel Macs (Mac Pro 2013, iMac Pro, the 2016-2020 Intel MacBook Pro line, Mac Pro 2019) and Hackintoshes get a prebuilt download instead of a 5-15 min CMake source build. The CI repo (m-doughty/Notcurses-Native) produces the artefact on an arm64 GHA runner under Rosetta 2 — clang under Rosetta emits ordinary x86_64 Mach-O that native Intel Macs run identically. Pinned at MACOSX_DEPLOYMENT_TARGET=10.15 (Catalina) so the artefact loads on every Intel Mac Apple supports back to ~2012 hardware. The build path was chosen because GitHub's macos-13 native-x86_64 runner is on its way out; arm64-with-Rosetta is the long-lived option. Side effect: x86_64 Rakudo running under Rosetta on Apple Silicon now gets a working prebuilt too instead of falling through to source build — not the audience this lane targets, but a free win. - NOTE: this version (0.3.4) still ships BINARY_TAG=r5. The darwin-x86_64 slug-map entry only becomes useful once the m-doughty/Notcurses-Native CI repo publishes a binaries- notcurses-3.0.17-r6 release that includes the new notcurses-macos-x86_64.tar.gz artefact; until then, Intel Mac installs will still take the source-build fallback (just like pre-0.3.4). When r6 ships, BINARY_TAG → r6 + resources/ checksums.txt update happen as a single follow-up commit (resources/checksums.txt has the procedure documented at the top of the file). That commit cuts 0.3.5. - Build.rakumod unknown-platform diagnostic: dropped the Apple-Silicon-Rosetta hint introduced in 0.3.3. With darwin-x86_64 now mapped, that case never reaches the `without $plat` branch — the hint was unreachable and would have misled anyone hitting the diagnostic for an actually-novel platform. - t/16-build-detect-platform.rakutest: updated darwin/x86_64 assertion (now expects 'macos-x86_64') and added darwin-x86_64 to known-platform-keys expected set. Unknown-platform Str:U coverage stays via the freebsd/riscv64 case. - Build.rakumod: fix `zef install` aborting with "Type check failed for return value; expected Str but got Any" on platforms not mapped in %PLATFORM-SLUGS. The headline case is x86_64 Rakudo running under Rosetta on Apple Silicon, which reports $*KERNEL.hardware = 'x86_64' → key 'darwin-x86_64' → unmapped. Root cause: the slug-map hash was untyped, so missing-key lookups returned `Any` (not `Str`) and tripped detect-platform's `--> Str` constraint before the intended source-build fallback at line 102 could fire. Typed the hash `my Str %PLATFORM-SLUGS`. While here, promoted `!detect-platform` to a public `detect-platform(:$os, :$hardware --> Str)` so tests can inject kernel pairs without having to override `$*KERNEL`, and added `detect-platform-key` / `known-platform-keys` helpers used by the unknown-platform diagnostic. - Build.rakumod: the unknown-platform warning now lists the exact key that was looked up (e.g. 'darwin-x86_64'), the values of $*KERNEL.name / $*KERNEL.hardware, the full set of platforms that DO have prebuilts, and an Apple-Silicon-under-Rosetta hint. Saves the next person an hour of staring at the previous one-line message. - t/16-build-detect-platform.rakutest: new regression test covering every entry in %PLATFORM-SLUGS plus the darwin-x86_64 (Rosetta) and freebsd-riscv64 (fully unknown) fall-throughs. Would have failed on 0.3.2. 0.3.2 2026-05-12T16:55:21+01:00 - lib/Notcurses/Native.rakumod: convert the `$nc-lib`, `$ffi-lib`, `$core-lib`, `$shim-lib` library-path bindings from `constant` to state-cached subs (`sub nc-lib { state $r = _resolve-lib(...); $r }`). `constant X = _resolve-lib(...)` ran at compile time and baked the resolved path into the precompiled bytecode — and Rakudo doesn't track `resources/BINARY_TAG` as a precomp dependency. A BINARY_TAG bump (which moves staged libs to a new versioned directory and may GC the previous one) would leave the precomp pointing at the old path, producing "Cannot locate native library" errors on freshly installed packages until the user manually ran `rm -rf ~/.raku/precomp/`. Deferring resolution to first sub-call means each process picks up the current tag, regardless of when the precomp was built. NativeCall accepts a Callable for `is native()` and invokes it lazily on first use of each bound sub. All 600+ bindings in `lib/Notcurses/ Native/*.rakumod` updated from `is native($X-lib)` to `is native(&X-lib)` to match. - t/15-shim-presence.rakutest: updated to call `shim-lib()` instead of treating `$shim-lib` as a Str — the constant is now a state-cached sub. - BINARY_TAG bumped to binaries-notcurses-3.0.17-r5. The r4 tag was never published — the workflow that produces its archives failed before the release step on Linux (the --unresolved-symbols=ignore-in-shared-libs flag was misnamed and left object-file unresolved refs intact) and on Windows (the objdump-based export check matched the wrong format). The shim binary's runtime contract changed as part of fixing those builds — Linux now links directly against libnotcurses-core with DT_NEEDED + DT_RUNPATH=$ORIGIN instead of deferring resolution to the host process's flat symbol namespace; macOS now reserves -Wl,-headerpad_max_- install_names so any future install_name_tool relocation doesn't overflow the Mach-O header — so the new artifact isn't bit-identical to the r4 spec even though the API is the same. Bumping the tag avoids any ambiguity about which shim build downstream consumers are running against. - .github/workflows/build-binaries.yml: Linux shim step now links the shim explicitly against bundle's libnotcurses-core (-Lbundle -lnotcurses-core) and sets DT_RUNPATH=$ORIGIN via patchelf. Mirrors Vips-Native's working pattern. Drops the strip --strip-unneeded step that was clearing the regular symtab and breaking the post-strip nm -g check. - .github/workflows/build-binaries.yml: Windows shim step replaces the objdump -p export-table regex with nm -g --defined-only (matches Vips-Native; stable across MinGW/UCRT/CLANGARM64 binutils versions where the objdump output format differs). Drops the strip --strip-unneeded step too — same regular-symtab issue manifests on PE. - .github/workflows/build-binaries.yml: macOS shim step adds -Wl,-headerpad_max_install_names so any future install_name_tool relocation has the load-command padding it needs (defensive — Build.rakumod's !rewrite-macos-install-names explicitly skips the shim today, but this is belt-and-braces against future relocators). - Build.rakumod: !try-compile-shim mirrors the workflow changes — Linux links explicitly, macOS adds the headerpad option. Source-build path now matches the prebuilt path in every meaningful way. - Build.rakumod: !rewrite-macos-install-names glob now excludes `libnotcurses_native_shim.dylib` via `!~~ /'_shim'/`. The shim has no @rpath/libnotcurses*.dylib dependencies to rewrite (it's compiled -undefined dynamic_- lookup) and its short @loader_path install-name can't be replaced with the absolute staged path without headerpad space — on a force-install the previous run's shim was already on disk and the rewrite pass loudly refused it. 0.3.1 2026-05-12T16:01:23+01:00 - CI/CD republish binaries - src/notcurses_native_shim.c: new C-side perf-shim module with batched primitives that are unaffordable to express call-per- cell over Raku's NativeCall boundary. Initial export `notcurses_native_copy_cells`, a direct port of Selkie::Widget::ViewportedCardList's per-cell read+write loop (copies a rows × cols slice from one ncplane to another with base-cell substitution for empty source cells, matching ncplane_at_yx semantics). Selkie's VCL!copy-cells used to spend ~75,000 NativeCall trips per render on a chat with five visible cards × five widget planes × ~3000 cells; the shim collapses that to one. Linked with -undefined dynamic_lookup (macOS) or -Wl,--unresolved-symbols=ignore-in-shared-libs (Linux) so it has no link-time dependency on libnotcurses — symbols resolve at runtime against the host process's already-loaded libnotcurses. - Build.rakumod: !try-compile-shim stages a compiled libnotcurses_native_shim alongside the existing notcurses libs on every install path (prebuilt, source build, fallback). Non-fatal when no C toolchain is available — Selkie's binding flips an internal latch and falls back to the per-cell Raku merge with a one-shot user-visible warning so the perf cost is attributable. - lib/Notcurses/Native.rakumod: $shim-lib constant exported, resolved by the same _resolve-lib lookup as the core libs. - lib/Notcurses/Native/Plane.rakumod: notcurses_native_copy_cells Raku binding bound to $shim-lib. - .github/workflows/build-binaries.yml: CI builds + ships the shim binary in the prebuilt archives so users on supported platforms don't need a C toolchain at install time. - BINARY_TAG bumped to binaries-notcurses-3.0.17-r4 so prebuilt caches invalidate and consumers pick up archives that include the shim. - t/15-shim-presence.rakutest: assert libnotcurses_native_shim is staged. Gated on NOTCURSES_NATIVE_REQUIRE_SHIM=1 — set in both test.yml and glibc-fallback.yml workflows so CI fails loudly if either the prebuilt archive drops the shim or Build.rakumod's !try-compile-shim silently failed on the source-build path. End-user installs without the env var skip the test cleanly so a missing toolchain doesn't break the install. - lib/Notcurses/Native.rakumod: $NOTCURSES_NATIVE_LIB_DIR override doc tightened to spell out that the patched libnotcurses we ship (0.3.0's ncvisual_blit_internal begy/begx fix) is ABI-compatible at the C symbol level but BEHAVIOURALLY incompatible — pointing the override at vanilla system notcurses 3.0.17 silently misrenders any clipped sprixel (chat avatars at the top of the scroll, for example). 0.3.0 2026-05-12T00:11:57+01:00 - vendor/notcurses (src/lib/visual.c): patch ncvisual_blit_internal to honor ncvisual_options.begy/begx/leny/lenx for all blit paths (generic resize, FFmpeg, OIIO, all sprixel and cell blitters). Upstream notcurses 3.0.17 silently drops these fields whenever the source needs resizing — sprixel blitters take only (data, leny, lenx) with no begy/begx parameter and consume from data[0], so a cropped blit shows the top of the source instead of the requested sub-region. Cell blitters reference begy/begx but index the resized buffer with the input-space offset, which is a latent out-of-bounds read for begy > 0. The fix introduces a static ncvisual_subregion_internal helper that materializes the requested source region once at the entry of ncvisual_blit_internal (single alloc + single row-loop memcpy, honoring source rowstride padding and re-padding the destination via pad_for_image). Downstream backends then see a "full source" and consume normally, with begy/begx/leny/lenx zeroed in a local blitterargs copy. Fixes Selkie's ViewportedCardList rendering the wrong rows when an image is partially clipped at the top of the viewport, and incidentally addresses the FIXMEs at upstream src/lib/internal.h (blitterargs comment) and src/media/oiio.cpp. - Build.rakumod: rewrite macOS install-names to absolute staged paths via install_name_tool after staging the dylibs. Without this, dyld resolves `@rpath/libnotcurses-core.3.dylib` (and siblings) through the LOADER CHAIN'S rpaths, which on a typical Homebrew-Raku setup means `raku`'s `@executable_path/../lib` (= `/opt/homebrew/lib`) is searched first — and if Homebrew's notcurses is also installed there, dyld silently loads Homebrew's unpatched library instead of ours. Tests and the module load succeed (path resolution at the Raku level still reports our staged path) but the actual symbol resolution runs the wrong code. Baking the absolute path into the install-name eliminates dyld's discretion. Also expanded find-lib to stage every version variant (`libfoo.dylib`, `libfoo.3.dylib`, `libfoo.3.0.17.dylib`) because the rewritten install-names point to the `.3.dylib` symlinks, which must exist at the staged path. - BINARY_TAG bumped to binaries-notcurses-3.0.17-r3 to invalidate prebuilt binary caches and force consumers to pick up the patched library. 0.2.6 2026-04-29T23:53:41+01:00 - Bump Github actions to use node 24+ - `Build.rakumod` now garbage-collects sibling staged dirs for older BINARY_TAGs after each successful install. Without this, every release accumulated another `binaries-notcurses-*` dir under `~/.local/share/Notcurses-Native/`, where stale Raku precomp could load the older libs alongside the new ones — cf. the Vips::Native r7→r8 incident that revealed this class of bug. Set `NOTCURSES_NATIVE_KEEP_OLD_STAGES=1` to opt out (e.g. when intentionally pinning multiple versions for testing). 0.2.5 2026-04-16T03:16:51+01:00 - Native.rakumod: set TERMINFO_DIRS at module load via libc setenv(3) so ncurses finds terminal definitions on systems without Homebrew ncurses installed. Our bundled libncursesw was compiled against Homebrew's ncurses, which bakes the terminfo search path to the Homebrew cellar — on a fresh Mac without `brew install ncurses`, that path doesn't exist and notcurses_core_init fails with "No terminal available" even though the libraries loaded fine. The fix points TERMINFO_DIRS at macOS's system /usr/share/terminfo/ (always present) plus common Linux paths. Uses the same _setenv-c pattern as Vips-Native (Raku's %*ENV doesn't propagate to C getenv on macOS). Respects user-set TERMINFO_DIRS. - test.yml: run prebuilt-path t/ tests before installing system deps (brew/apt) so the self-contained bundle is exercised with no system notcurses present. xt/ tests (terminal-dependent) run after system deps install since they need terminfo data on macOS. Saves several minutes per failed run. 0.2.4 2026-04-16T02:58:01+01:00 - Build.rakumod: detect system glibc via `ldd --version` and fall back to CMake source compile when it's older than the prebuilt target (currently v2.35, matching the ubuntu-22.04 CI runner). Previously, users on Ubuntu 20.04 / Debian 11 / RHEL 8 downloaded prebuilt libnotcurses + ffmpeg libs that loaded but failed at first symbol use with "GLIBC_2.xx not found". The guard fires before the download so affected users just see a one-line note and a ~3–5 min source compile (core-only if their ffmpeg dev packages are missing) instead of a broken install. NOTCURSES_NATIVE_BINARY_ONLY=1 now hard-fails with a clear message on old-glibc systems rather than producing a broken install. - New CI workflow .github/workflows/glibc-fallback.yml: runs `zef install .` inside an ubuntu:20.04 container (glibc 2.31) with apt-installed cmake + ncurses/unistring/deflate dev packages and asserts both that the fallback message appears in the build log and that the source-compiled libs load. 0.2.3 2026-04-15T02:52:48+01:00 - CI: rework test.yml to install via zef (which runs Build.rakumod → downloads prebuilt → SHA-verifies → stages libs to the XDG data dir) instead of building notcurses by hand and dropping libs into resources/lib/. The hand-build step was a vestige of the pre-XDG-staging layout and stopped working when META6.json dropped the lib resource entries in 0.2.2 — tests started failing with "cannot open shared object file" because nothing was actually staging libs to where Native.rakumod now looks. Workflow now also re-installs with NOTCURSES_NATIVE_BUILD_FROM_ SOURCE=1 as a second pass to keep the CMake fallback path covered on every CI run. 0.2.2 2026-04-15T02:46:26+01:00 - Stage native libs to an XDG-style data dir ($XDG_DATA_HOME/Notcurses-Native//lib/, or $LOCALAPPDATA on Windows, ~/.local/share fallback) instead of the dist's resources/. Reason: zef hashes every staged resource filename to a SHA-keyed name, which silently breaks the inter- dylib references baked into notcurses (libnotcurses.dylib loads libnotcurses-core.3.0.17.dylib via @loader_path; same on Linux with $ORIGIN, same on Windows with sibling-DLL search). Hashed filenames meant the loader couldn't find any sibling lib by its real name, and `Notcurses::Native` died at first dlopen with a cryptic "Library not loaded" error post-install. Tests passed at install time (pre-staging) so the bug only surfaced when consumers like Selkie tried to actually use the module. - META6.json no longer lists the 9 dylib/.so/.dll entries; only checksums.txt and a new BINARY_TAG resource (a tiny text file immune to the hash-renaming problem, used by Native.rakumod to locate the staged-libs directory at runtime). - Native.rakumod resolver: env override (NOTCURSES_NATIVE_LIB_DIR) → XDG-staged dir → fail with the staged path in the error message for actionable debug. - New env knob: NOTCURSES_NATIVE_DATA_DIR to override the XDG base directory (e.g. for system-wide installs or sandboxed envs). 0.2.1 2026-04-15T02:23:12+01:00 - Build: extract Windows .zip prebuilts via PowerShell's Expand-Archive instead of `tar`. GNU tar (which is first on PATH inside MSYS2 install environments) parses `D:\...` as a remote `host:path` and bombs with "Cannot connect to D: resolve failed". Expand-Archive ships on every supported Windows and has no such quirk. macOS / Linux still use `tar`. 0.2.0 2026-04-15T02:17:17+01:00 - Prebuilt-binary-first install path. Build.rakumod now attempts to download a per-platform archive from the repo's GitHub Releases containing all three notcurses libs (libnotcurses, libnotcurses-core, libnotcurses-ffi) plus the ffmpeg sibling dylibs notcurses dyn-links, before falling back to the existing CMake source compilation. Saves the 5–15 minute CMake build + sidesteps the "install these 10 -dev packages first" pain that the HN thread surfaced. - ffmpeg sibling bundling with rpath relocation: @loader_path/ on macOS via dylibbundler, $ORIGIN on Linux via patchelf, sibling-DLL layout on Windows. Archive self-contained — no system ffmpeg/ncurses/libunistring/libdeflate needed at runtime. - SHA256 verification against bundled resources/checksums.txt; refuses any prebuilt whose hash isn't recorded (hard security boundary). - Cache downloaded archives in $XDG_CACHE_HOME/Notcurses-Native- binaries/ (or $HOME/.cache/ fallback). - New env knobs: NOTCURSES_NATIVE_BUILD_FROM_SOURCE=1 to skip prebuilts; NOTCURSES_NATIVE_BINARY_ONLY=1 to refuse fallback; NOTCURSES_NATIVE_BINARY_URL to override release base URL; NOTCURSES_NATIVE_CACHE_DIR to override cache dir; NOTCURSES_NATIVE_LIB_DIR for runtime lib-dir override. - New BINARY_TAG file at repo root as single source of truth for the pinned binary release tag (binaries-notcurses-- r), read by both Build.rakumod and the CI workflow. - New .github/workflows/build-binaries.yml: builds + publishes prebuilt archives for five platforms (macOS arm64, Linux x86_64/aarch64 glibc, Windows x86_64/arm64) on manual dispatch or binaries-* tag push. macOS uses dylibbundler, Linux uses recursive ldd walk + patchelf, Windows uses recursive ldd on MSYS2 + sibling DLL layout. - macOS is arm64-only for v1. Intel Macs fall through to the compile fallback; universal builds need cross-arch brew setup that isn't worth the CI complexity for initial ship. Can revisit if Intel Mac users complain. - FFI lookup in Notcurses::Native now respects the NOTCURSES_NATIVE_LIB_DIR env override before falling back to %?RESOURCES. Escape hatch for custom notcurses builds. - Fixed $os.contains('win') bug in FFI lookup: "darwin" matches "/win/", causing file-extension detection to pick 'dll' on macOS. Now uses $*DISTRO.is-win. 0.1.5 2026-04-12T21:17:32+01:00 - Build: tighten library-matching regex in Build.rakumod so that staging `libnotcurses` doesn't accidentally pick up `libnotcurses-ffi` (only `.` is a valid separator after the library name, never `-`). The 3.0.16 → 3.0.17 bump surfaced this: filesystem ordering began handing us the FFI shim first, which lacks `notcurses_init` and quietly broke every terminal-dependent test. - Bump vendored notcurses 3.0.16 → 3.0.17, which upstream describes as "Fix build problems on Windows and Mac OSX." The public API is unchanged between these releases (single-character attribute-macro typo fix in notcurses.h, no ABI impact), so our bindings need no changes. Drops the Windows termios workaround that would otherwise have been needed for 3.0.16. 0.1.4 2026-04-12T20:19:12+01:00 - CI: add Windows (MSYS2 UCRT64) to the GitHub Actions test matrix. Uses OpenImageIO as the multimedia backend (per upstream notcurses Windows recommendation). Build-only since notcurses-tester is Unix-only. - README: document system dependencies with per-OS install commands for Linux (Debian/Fedora), macOS (Homebrew), and Windows (MSYS2 UCRT64). Added a core-only (no multimedia) note. 0.1.3 2026-04-09T23:56:25+01:00 - Switch library resolution from $?FILE to %?RESOURCES for portable loading when used as a dependency by other modules 0.1.2 2026-04-09T18:06:28+01:00 - Move terminal-dependent tests to xt/ to avoid prove6 TAP harness bug during zef install (t/ has pure-Raku tests only) - CI runs prove6 on t/ and Perl 5 prove on xt/ - Fix NcBlitter enum values (NCBLIT_PIXEL was 6, should be 7) - Fix visual functions: use $nc-lib (full) not $core-lib for FFmpeg - Add 130 NCKEY_* key code constants, NcPixelImpl enum, NCBOX_*, NCMICE_*, NCALPHA_*, NC_BG_* channel bitmasks - Add NcvisualOptions.set-plane for correct plane compositing - Add 8 example programs (hello, colors, boxes, input, clock, image viewer with kitty pixel support, direct mode, progress bars) - Build.rakumod: search /opt/homebrew/bin for cmake when PATH is stripped by mi6/zef subprocess - Windows CI disabled pending upstream notcurses termios fix 0.1.1 2026-04-09T17:23:50+01:00 - Fix TAP harness corruption: redirect stdout/stderr to /dev/null before notcurses init, reroute $*OUT via /dev/fd/N for TAP output - Fix Build.rakumod: search /opt/homebrew/bin for cmake when PATH is stripped by mi6/zef subprocess - Fix NcBlitter enum values (NCBLIT_PIXEL was 6, should be 7) - Fix visual functions: use $nc-lib (full) not $core-lib for FFmpeg - Skip Unicode cell tests on non-UTF-8 environments - Set LANG/LC_ALL=en_US.UTF-8 in CI for proper UTF-8 detection - CI uses prove (Perl 5) instead of prove6 to avoid TAP parser bug - Windows CI disabled pending upstream notcurses termios fix - Add installation troubleshooting to README 0.1.0 2026-04-09T16:41:32+01:00 - Complete NativeCall wrapper for notcurses 3.0.16 TUI library - 606 functions bound across 9 modules (100% of bindable symbols) - Vendored notcurses 3.0.16 built with FFmpeg multimedia + FFI lib - Build.rakumod: CMake build for macOS, Linux, Windows (MSYS2) - Modules: Native (core), Types, Plane, Cell, Channel, Context, Direct, Input, Visual, Widgets - 25 CStruct types: all notcurses options structs, nccell, ncinput, ncstats, nccapabilities, ncvgeom, ncvisual_options, timespec, widget options (selector, menu, tree, tabbed, plot, reader, etc.) - 19 opaque CPointer handle types for type-safe FFI - 226 constants: NCKEY_* (130 key codes), NCSTYLE_*, NCOPTION_*, NCALPHA_*, NCVISUAL_OPTION_*, NCMICE_*, NCBOX_*, NC_BG_* bitmasks - 7 enums: NcLogLevel, NcAlign, NcBlitter, NcScale, NcInputType, NcPixelImpl, NcBlitter (with corrected values matching C header) - CStruct Str field workaround: set-cstruct-str helper + multi method new constructors for all structs with string fields - NcvisualOptions.set-plane method for correct plane compositing - Visual functions use libnotcurses (full) for FFmpeg backend - Variadic printf bindings (Rakudo 2026.03+) - 16 test files, 161 subtests, 748+ assertions - Tests cover: channel math, cell operations, plane lifecycle, widget lifecycle, input handling, context/capabilities, direct mode, visual/image loading, rendered output verification - Render verification tests: exact text, color, style, z-order, box drawing, erase, merge, and gradient checks via notcurses_at_yx - 4x4 PNG test fixture for visual pipeline testing - 8 example programs: hello, colors, boxes, input, clock, image viewer (with kitty pixel protocol), direct mode, progress bars - GitHub Actions CI for Linux, macOS, Windows - Only unbound: 4 vprintf variants (va_list is not FFI-bridgeable)