Notcurses-Native.git | scripts/ci/ | codec-probe.c


/* * codec-probe.c — release gate. Loads the bundle's libavcodec + * libavutil at runtime via dlopen / LoadLibrary, then performs two * categories of check: * * 1. Accelerated decoder registration. avcodec_find_decoder_by_name * must return non-NULL for "libdav1d", "libvpx", "libvpx-vp9", * "libopus". Catches the regression class where ffmpeg's * configure silently dropped --enable-libfoo (pkg-config probe * failed at build time and ffmpeg fell back to internal-only * decoders) — the bundle's size + linkage look correct but * AV1 video would silently use the ~10× slower internal * decoder. * * 2. Actual decode of PNG, JPEG, BMP fixtures. Reads * $NOTCURSES_DATA_DIR/{chunli01.png, * tetris-background.jpg, warmech.bmp} into memory, feeds each * to libavcodec's internal decoder for that format, requires * a frame back. Catches: * * ABI mismatch between bundled libavcodec / libavutil * (decoders load but crash / error on actual decode). * * Missing transitive deps that the registration check * can't spot (e.g. zlib for PNG): bundle-audit catches * this earlier but here is a second line of defense. * * The general "bundle compiles + bundles + loads but * can't actually decode anything" failure mode. * GIF + WebP are not tested separately — if PNG / JPEG / BMP * decode works, ffmpeg's internal decoders for similar simple * image formats can be assumed working. * * Cross-platform: Linux (.so), macOS (.dylib), Windows (.dll). * Enumerates the CWD (bundle/) to find libavcodec + libavutil * regardless of versioning — the bundled files may be * libavcodec.62.28.100.dylib (no SONAME symlinks) or * libavcodec.so.60 (Linux SONAME-named) or avcodec-62.dll * (Windows). We dlopen whatever matches the prefix. * * Built standalone (no libavcodec headers required) — opaque * pointer types + dlsym for every function we need. AVPacket * data/size fields are set via av_packet_from_data() (an API * call) rather than direct struct access. */ #include #include #include #include #ifdef _WIN32 # include typedef HMODULE dlh_t; # define DL_OPEN(p) LoadLibraryA(p) # define DL_SYM(h, s) ((void *)GetProcAddress(h, s)) # define DL_CLOSE(h) (void)FreeLibrary(h) #else # include # include typedef void *dlh_t; # define DL_OPEN(p) dlopen(p, RTLD_NOW | RTLD_GLOBAL) # define DL_SYM(h, s) dlsym(h, s) # define DL_CLOSE(h) (void)dlclose(h) #endif /* ---------- forward-declared opaque types ---------- */ typedef struct AVCodec AVCodec; typedef struct AVCodecContext AVCodecContext; typedef struct AVPacket AVPacket; typedef struct AVFrame AVFrame; typedef struct AVDictionary AVDictionary; /* ---------- function pointer types ---------- */ typedef const AVCodec *(*fn_find_decoder_by_name_t)(const char *); typedef AVCodecContext *(*fn_alloc_context3_t)(const AVCodec *); typedef int (*fn_open2_t)(AVCodecContext *, const AVCodec *, AVDictionary **); typedef void (*fn_free_context_t)(AVCodecContext **); typedef int (*fn_send_packet_t)(AVCodecContext *, const AVPacket *); typedef int (*fn_receive_frame_t)(AVCodecContext *, AVFrame *); typedef AVPacket *(*fn_packet_alloc_t)(void); typedef void (*fn_packet_free_t)(AVPacket **); typedef int (*fn_packet_from_data_t)(AVPacket *, uint8_t *, int); typedef AVFrame *(*fn_frame_alloc_t)(void); typedef void (*fn_frame_free_t)(AVFrame **); typedef void *(*fn_av_malloc_t)(size_t); /* AV_INPUT_BUFFER_PADDING_SIZE: documented constant, ffmpeg * requires this much zero-padding past the end of any packet data * buffer to allow optimized bitstream readers to read past the * end without segfaulting. ffmpeg 6.x: 64 bytes. */ #define AV_INPUT_BUFFER_PADDING_SIZE 64 /* AVERROR(EAGAIN) — decoder needs more input before producing a * frame. Single-image formats (PNG/JPEG/BMP) shouldn't return * EAGAIN since the entire image is one packet, but treat it as * neither success nor terminal failure. */ /* ---------- function pointer table ---------- */ struct ff_api { fn_find_decoder_by_name_t find_decoder_by_name; fn_alloc_context3_t alloc_context3; fn_open2_t open2; fn_free_context_t free_context; fn_send_packet_t send_packet; fn_receive_frame_t receive_frame; fn_packet_alloc_t packet_alloc; fn_packet_free_t packet_free; fn_packet_from_data_t packet_from_data; fn_frame_alloc_t frame_alloc; fn_frame_free_t frame_free; fn_av_malloc_t av_malloc; }; /* ---------- enumerate CWD for libavcodec / libavutil ---------- * * The bundle's filenames vary by platform / build system: * - macOS source-built ffmpeg: libavcodec.62.28.100.dylib (no * SONAME symlink — full versioned name only). * - macOS brew bottle: libavcodec.61.dylib (SONAME-named real file). * - Linux: libavcodec.so.60 (SONAME-named real file). * - Windows: avcodec-62.dll. * Enumerate CWD to find whichever name matches. */ static int starts_with(const char *s, const char *prefix) { return strncmp(s, prefix, strlen(prefix)) == 0; } static char enum_buf[512]; #ifdef _WIN32 static const char *find_lib(const char *prefix) { WIN32_FIND_DATAA fd; char pattern[256]; snprintf(pattern, sizeof(pattern), "%s*.dll", prefix); HANDLE h = FindFirstFileA(pattern, &fd); if (h == INVALID_HANDLE_VALUE) return NULL; snprintf(enum_buf, sizeof(enum_buf), "%s", fd.cFileName); FindClose(h); return enum_buf; } #else static const char *find_lib(const char *prefix) { DIR *d = opendir("."); if (!d) return NULL; struct dirent *e; const char *match = NULL; while ((e = readdir(d))) { if (starts_with(e->d_name, prefix)) { snprintf(enum_buf, sizeof(enum_buf), "%s", e->d_name); match = enum_buf; break; } } closedir(d); return match; } #endif /* Platform-specific prefixes that uniquely identify each library * (with disambiguation against `libavutil` for libav, `libavcodec` * for libavcodec, etc. — `libav` alone would match both libavcodec * and libavutil ambiguously). */ #ifdef _WIN32 # define AVCODEC_PREFIX "avcodec-" # define AVUTIL_PREFIX "avutil-" #elif defined(__APPLE__) # define AVCODEC_PREFIX "libavcodec." # define AVUTIL_PREFIX "libavutil." #else # define AVCODEC_PREFIX "libavcodec.so" # define AVUTIL_PREFIX "libavutil.so" #endif static const char *dl_error_str(void) { #ifdef _WIN32 static char buf[256]; DWORD code = GetLastError(); FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, code, 0, buf, sizeof(buf), NULL); return buf; #else const char *e = dlerror(); return e ? e : "(no error)"; #endif } /* Path prefix character. On Unix dlopen("foo") searches LD_LIBRARY_PATH * etc. but dlopen("./foo") strictly uses the relative path — what we * want to ensure we load from CWD. On Windows LoadLibrary("./foo") * also works but cwd-relative load needs CWD to be on the search * path; the wrapper script handles CWD setup. */ static dlh_t dlopen_cwd(const char *name) { char path[640]; #ifdef _WIN32 /* Windows LoadLibrary searches via DLL search order; CWD is * included (Safe DLL Search mode notwithstanding, our wrapper * cd's into bundle/ before invoking the probe, and there are * no conflicting system avcodec/avutil DLLs to mask the * bundled ones). Pass the bare name. */ snprintf(path, sizeof(path), "%s", name); #else snprintf(path, sizeof(path), "./%s", name); #endif return DL_OPEN(path); } /* ---------- decode test ---------- */ struct fixture { const char *display_name; const char *workspace_relpath; const char *decoder_name; /* libavcodec internal decoder .name */ }; static int decode_fixture(struct ff_api *api, const struct fixture *fx, const char *data_dir) { char full_path[1024]; snprintf(full_path, sizeof(full_path), "%s/%s", data_dir, fx->workspace_relpath); FILE *f = fopen(full_path, "rb"); if (!f) { fprintf(stderr, " cannot open %s\n", full_path); return -1; } if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return -1; } long sz = ftell(f); if (sz <= 0) { fclose(f); return -1; } rewind(f); /* av_malloc so av_packet_from_data can take ownership and * av_packet_free can av_free it back. Heap allocated with * AV_INPUT_BUFFER_PADDING_SIZE bytes of zero padding past * the end — required by libavcodec's bitstream readers. */ uint8_t *buf = (uint8_t *)api->av_malloc((size_t)sz + AV_INPUT_BUFFER_PADDING_SIZE); if (!buf) { fclose(f); return -1; } size_t read_n = fread(buf, 1, (size_t)sz, f); fclose(f); if ((long)read_n != sz) return -1; memset(buf + sz, 0, AV_INPUT_BUFFER_PADDING_SIZE); const AVCodec *codec = api->find_decoder_by_name(fx->decoder_name); if (!codec) { fprintf(stderr, " decoder '%s' not found\n", fx->decoder_name); /* buf is now owned by us; we never handed it to a packet. */ /* No av_free dlsym'd; the leak is intentional, the probe * is a short-lived process. */ return -1; } AVCodecContext *ctx = api->alloc_context3(codec); if (!ctx) return -1; if (api->open2(ctx, codec, NULL) < 0) { api->free_context(&ctx); return -1; } AVPacket *pkt = api->packet_alloc(); AVFrame *frame = api->frame_alloc(); if (!pkt || !frame) { if (pkt) api->packet_free(&pkt); if (frame) api->frame_free(&frame); api->free_context(&ctx); return -1; } /* av_packet_from_data wraps buf into pkt. After this, pkt owns * buf; av_packet_free will av_free it. */ if (api->packet_from_data(pkt, buf, (int)sz) < 0) { api->packet_free(&pkt); api->frame_free(&frame); api->free_context(&ctx); return -1; } int rc = api->send_packet(ctx, pkt); int got_frame = 0; if (rc >= 0) { rc = api->receive_frame(ctx, frame); if (rc >= 0) got_frame = 1; } api->packet_free(&pkt); api->frame_free(&frame); api->free_context(&ctx); return got_frame ? 0 : -1; } /* ---------- registration check ---------- */ static const char *REQUIRED_DECODERS[] = { "libdav1d", "libvpx", "libvpx-vp9", "libopus", NULL, }; /* ---------- decode fixtures (paths relative to NOTCURSES_DATA_DIR) ---------- * * IMPORTANT: only reference REGULAR FILES here, not symlinks. The * notcurses data/ dir has ~11 chunli* PNGs that are symlinks back to * earlier frames (chunli32-37, chunli39, chunli41-44 → chunli30/40 * etc.). On Unix git checks those out as real symlinks; on Windows * git's default `core.symlinks=false` materializes them as 12-byte * text files containing the target's name, so opening one gets you * "chunli40.png" as the file's first bytes and the PNG header check * fails. chunli01.png is the canonical "definitely a regular file" * pick. */ static const struct fixture FIXTURES[] = { { "PNG", "chunli01.png", "png" }, { "JPEG", "tetris-background.jpg", "mjpeg" }, { "BMP", "warmech.bmp", "bmp" }, { NULL, NULL, NULL } }; int main(int argc, char **argv) { (void)argc; const char *data_dir = getenv("NOTCURSES_DATA_DIR"); if (argc > 1) data_dir = argv[1]; if (!data_dir || !*data_dir) { fprintf(stderr, "FAIL: NOTCURSES_DATA_DIR not set (and no argv[1] either).\n"); fprintf(stderr, " run-codec-probe.sh is supposed to set it to\n"); fprintf(stderr, " $NOTCURSES_SRC_DIR/data before invoking the probe.\n"); return 2; } /* ----- load libavcodec ----- */ const char *avcodec_name = find_lib(AVCODEC_PREFIX); if (!avcodec_name) { fprintf(stderr, "FAIL: no libavcodec.* in CWD (\"%s\" prefix).\n", AVCODEC_PREFIX); fprintf(stderr, " Wrapper script should have cd'd into bundle/.\n"); return 2; } dlh_t avcodec_h = dlopen_cwd(avcodec_name); if (!avcodec_h) { fprintf(stderr, "FAIL: dlopen %s failed: %s\n", avcodec_name, dl_error_str()); return 2; } printf("Loaded libavcodec via: %s\n", avcodec_name); /* ----- load libavutil (for av_malloc, av_frame_*) ----- */ const char *avutil_name = find_lib(AVUTIL_PREFIX); if (!avutil_name) { fprintf(stderr, "FAIL: no libavutil.* in CWD (\"%s\" prefix).\n", AVUTIL_PREFIX); return 2; } dlh_t avutil_h = dlopen_cwd(avutil_name); if (!avutil_h) { fprintf(stderr, "FAIL: dlopen %s failed: %s\n", avutil_name, dl_error_str()); return 2; } printf("Loaded libavutil via: %s\n", avutil_name); /* ----- resolve symbols. On Linux/macOS RTLD_GLOBAL puts * everything in the default namespace so either handle works, * but on Windows GetProcAddress is strictly per-DLL — use the * right handle for each symbol. ----- */ struct ff_api api = {0}; api.find_decoder_by_name = (fn_find_decoder_by_name_t) DL_SYM(avcodec_h, "avcodec_find_decoder_by_name"); api.alloc_context3 = (fn_alloc_context3_t) DL_SYM(avcodec_h, "avcodec_alloc_context3"); api.open2 = (fn_open2_t) DL_SYM(avcodec_h, "avcodec_open2"); api.free_context = (fn_free_context_t) DL_SYM(avcodec_h, "avcodec_free_context"); api.send_packet = (fn_send_packet_t) DL_SYM(avcodec_h, "avcodec_send_packet"); api.receive_frame = (fn_receive_frame_t) DL_SYM(avcodec_h, "avcodec_receive_frame"); api.packet_alloc = (fn_packet_alloc_t) DL_SYM(avcodec_h, "av_packet_alloc"); api.packet_free = (fn_packet_free_t) DL_SYM(avcodec_h, "av_packet_free"); api.packet_from_data = (fn_packet_from_data_t) DL_SYM(avcodec_h, "av_packet_from_data"); api.frame_alloc = (fn_frame_alloc_t) DL_SYM(avutil_h, "av_frame_alloc"); api.frame_free = (fn_frame_free_t) DL_SYM(avutil_h, "av_frame_free"); api.av_malloc = (fn_av_malloc_t) DL_SYM(avutil_h, "av_malloc"); #define CHECK_SYM(field, name) \ do { if (!api.field) { \ fprintf(stderr, "FAIL: dlsym(%s) returned NULL\n", name); return 3; } } while (0) CHECK_SYM(find_decoder_by_name, "avcodec_find_decoder_by_name"); CHECK_SYM(alloc_context3, "avcodec_alloc_context3"); CHECK_SYM(open2, "avcodec_open2"); CHECK_SYM(free_context, "avcodec_free_context"); CHECK_SYM(send_packet, "avcodec_send_packet"); CHECK_SYM(receive_frame, "avcodec_receive_frame"); CHECK_SYM(packet_alloc, "av_packet_alloc"); CHECK_SYM(packet_free, "av_packet_free"); CHECK_SYM(packet_from_data, "av_packet_from_data"); CHECK_SYM(frame_alloc, "av_frame_alloc"); CHECK_SYM(frame_free, "av_frame_free"); CHECK_SYM(av_malloc, "av_malloc"); #undef CHECK_SYM int fail = 0; /* ----- check 1: accelerated decoder registration ----- */ printf("\n── Check 1: accelerated decoder registration ──\n"); for (int i = 0; REQUIRED_DECODERS[i]; i++) { const AVCodec *c = api.find_decoder_by_name(REQUIRED_DECODERS[i]); if (!c) { fprintf(stderr, "❌ MISSING decoder: %s\n", REQUIRED_DECODERS[i]); fail = 1; } else { printf("✅ registered: %s\n", REQUIRED_DECODERS[i]); } } /* ----- check 2: actual decode of common image formats ----- */ printf("\n── Check 2: decode common image formats ──\n"); for (int i = 0; FIXTURES[i].display_name; i++) { printf(" decoding %s (%s)...\n", FIXTURES[i].display_name, FIXTURES[i].workspace_relpath); if (decode_fixture(&api, &FIXTURES[i], data_dir) == 0) { printf(" ✅ %s decoded successfully\n", FIXTURES[i].display_name); } else { fprintf(stderr, " ❌ %s FAILED to decode\n", FIXTURES[i].display_name); fail = 1; } } DL_CLOSE(avcodec_h); DL_CLOSE(avutil_h); if (fail) { fprintf(stderr, "\n────────────────────────────────────────────────────────\n"); fprintf(stderr, "Bundle is NOT releasable. Investigate failures above.\n"); fprintf(stderr, "\nCommon causes:\n"); fprintf(stderr, " * Decoder missing → ffmpeg's configure dropped\n"); fprintf(stderr, " --enable-libfoo. Check scripts/ci/build-ffmpeg.sh\n"); fprintf(stderr, " flags and that build-{libdav1d,libvpx,libopus}.sh\n"); fprintf(stderr, " ran successfully before it.\n"); fprintf(stderr, " * Decode failed → ABI mismatch between bundled\n"); fprintf(stderr, " libavcodec / libavutil (mixed source-build +\n"); fprintf(stderr, " cached versions). Wipe _ci-cache/ and rebuild.\n"); return 1; } printf("\n✅ All checks passed — bundle is releasable.\n"); return 0; }