Find dlopen'd libraries in Guix profiles; fail loudly without a window
dl-search.c defines dlopen in the executable. GLFW loads every platform library by bare soname, and on Guix nothing on the loader's default search path names a profile's lib directory, so those dlopens fail unless LD_LIBRARY_PATH is set by hand. The interposed dlopen tries the name as given first (unchanged behaviour anywhere the loader already finds it), then retries under SIGIL_LIBRARY_PATH, $GUIX_ENVIRONMENT/lib, ~/.guix-home/profile/lib, ~/.guix-profile/lib and /run/current-system/profile/lib. One definition covers every loader in the process, so GLFW's posix_module.c stays unpatched and a later miniaudio needs nothing extra. Linux only; SIGIL_APP_VERBOSE=1 prints each fallback that resolved.
Measured under guix shell --pure with LD_LIBRARY_PATH holding only the two libraries still linked directly (libGL, libasound): Crash opens as a Wayland client with libwayland-client, libwayland-cursor, libwayland-egl, libxkbcommon and libEGL found under $GUIX_ENVIRONMENT/lib; with GUIX_ENVIRONMENT unset and an empty HOME it fails at glfwInit; with SIGIL_LIBRARY_PATH pointing at ~/.guix-home/profile/lib it runs again.
app-run now raises a runtime error when glfwInit or the window fails, so a game that cannot open a window exits 1 instead of returning 0 with nothing shown. The Wayland app_id defaults to the window title (GLFW's X11 class already does), so compositor rules can name the window on either platform. The verbose startup line prints the framebuffer size again on every resize, which is where a scaled Wayland output shows.
package.sgl | 11 +++++++----
src/c/app-internal.h | 5 +++++
src/c/app.c | 36 +++++++++++++++++++++++++++++-------
src/c/dl-search.c | 127 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 168 insertions(+), 11 deletions(-)package.sglmodified
((darwin) '("-D_GLFW_COCOA")) (else '())))(define app-sources '("src/c/app.c" "src/c/sokol-compat.c"));; dl-search.c interposes dlopen so GLFW's runtime loading finds libraries;; in Guix profile directories; it is Linux-only by design (see the file).(define (app-sources os) (append '("src/c/app.c" "src/c/sokol-compat.c") (if (eq? os 'linux) '("src/c/dl-search.c") '())))(define app-include-dirs '("vendor/glfw/include" "vendor/sokol" "src/c"));; objects on the context; create-static-library archives them all.(define (compile-app ctx) ((compile-c-sources sources: app-sources sources: (app-sources (target-os ctx)) include-dirs: app-include-dirs flags: (with-sigil-c-flags app-flags)) ctx)) name: 'sigil-app ;; The platform-independent files; the build task adds the GLFW ;; backend files for the config's target OS. c-sources: '("src/c/app.c" "src/c/sokol-compat.c") c-sources: '("src/c/app.c" "src/c/sokol-compat.c" "src/c/dl-search.c") c-include-dirs: '("vendor/glfw/include" "vendor/sokol" "src/c") native-init: "sigil__init_sigil_app_module" ;; Platform linker flags.src/c/app-internal.hmodified
int sigil_app_framebuffer_width(void);int sigil_app_framebuffer_height(void);#if defined(__linux__)/* dl-search.c: the directories the interposed dlopen falls back to. */const char *sigil_app_dlopen_search_summary(void);#endif#endif /* SIGIL_APP_INTERNAL_H */src/c/app.cmodified
static void framebuffer_size_callback(GLFWwindow *window, int width, int height){ (void)window; if (!g_app) return; g_app->fb_width = width; g_app->fb_height = height; if (getenv("SIGIL_APP_VERBOSE")) { float sx = 1.0f, sy = 1.0f; glfwGetWindowContentScale(window, &sx, &sy); fprintf(stderr, "sigil-app: framebuffer=%dx%d window=%dx%d content-scale=%.2fx%.2f\n", width, height, g_app->win_width, g_app->win_height, sx, sy); }}static void window_size_callback(GLFWwindow *window, int width, int height) * MAIN LOOP * ============================================================ */static void run_main_loop(const char *title, int width, int height)/* Returns false when no window could be opened; the GLFW error callback * has already said why on stderr. */static bool run_main_loop(const char *title, int width, int height){ glfwSetErrorCallback(glfw_error_callback); apply_platform_hint(); if (!glfwInit()) { fprintf(stderr, "sigil-app: glfwInit failed; no usable display platform\n"); return; return false; } /* GL 4.1 core, the profile sokol_app asked for (and the newest macOS glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_SCALE_FRAMEBUFFER, GLFW_TRUE); /* Wayland app_id and X11 class both default to the title, so a * compositor rule (sway: for_window [app_id="Crash The Stack"]) can * name the window on either platform. GLFW's X11 backend already * falls back to the title; Wayland's default is empty. */ glfwWindowHintString(GLFW_WAYLAND_APP_ID, title); GLFWwindow *window = glfwCreateWindow(width, height, title, NULL, NULL); if (!window) { fprintf(stderr, "sigil-app: window creation failed (platform %s)\n", platform_name(glfwGetPlatform())); glfwTerminate(); return; return false; } g_app->window = window; if (getenv("SIGIL_APP_VERBOSE")) { float sx = 1.0f, sy = 1.0f; glfwGetWindowContentScale(window, &sx, &sy); fprintf(stderr, "sigil-app: platform=%s window=%dx%d framebuffer=%dx%d content-scale=%.2fx%.2f gl=%s\n", fprintf(stderr, "sigil-app: platform=%s window=%dx%d framebuffer=%dx%d content-scale=%.2fx%.2f gl=%d.%d\n", platform_name(glfwGetPlatform()), g_app->win_width, g_app->win_height, g_app->fb_width, g_app->fb_height, sx, sy, glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR) == 4 ? "4.x" : "other"); glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR), glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR));#if defined(__linux__) fprintf(stderr, "sigil-app: dlopen fallback dirs: %s\n", sigil_app_dlopen_search_summary());#endif } glfwSetKeyCallback(window, key_callback); g_app->window = NULL; glfwDestroyWindow(window); glfwTerminate(); return true;}/* ============================================================ height = (int)sigil_as_fixnum(args[5]); } run_main_loop(title, width, height); bool ran = run_main_loop(title, width, height); /* Cleanup */ sigil_app_state_shutdown(); if (!ran) { sigil__vm_error(vm, SIGIL_ERR_RUNTIME, "app-run: could not open a window (see the sigil-app: lines above)"); return SIGIL_UNDEFINED; } return SIGIL_NIL;}src/c/dl-search.cadded
/* * dl-search.c - a dlopen that also looks in the profile library directories * * GLFW loads every platform library at run time by bare soname: * dlopen("libwayland-client.so.0"), dlopen("libX11.so.6"), dlopen("libEGL.so.1"). * The dynamic loader finds such a name through LD_LIBRARY_PATH, the * caller's RUNPATH and the system default directories. On Guix none of * those name a profile's lib directory: a Guix profile does not set * LD_LIBRARY_PATH, and Guix's own packages are patched to dlopen absolute * store paths, which a user's binary cannot be. * * This file defines dlopen in the executable. The executable's symbols come * first in the lookup order, so every dlopen in the process (GLFW's in * posix_module.c, and any other loader compiled into the program) resolves * here. It tries the name as given first, so on a system where the loader * already finds the library nothing changes. When that fails and the name * is a bare soname (no slash), it retries the name joined to each directory * of: * * SIGIL_LIBRARY_PATH colon separated, the user's override * $GUIX_ENVIRONMENT/lib set by `guix shell` * $HOME/.guix-home/profile/lib Guix Home * $HOME/.guix-profile/lib the user profile * /run/current-system/profile/lib Guix System * * The real dlopen is reached with dlsym(RTLD_NEXT, "dlopen"). A library * found this way loads its own dependencies through its RUNPATH, which on * Guix names their store paths, so one absolute path is enough. * * A NEEDED entry (a library linked with -l) is resolved by the loader * before main runs and cannot be helped here; only dlopen is covered. * * SIGIL_APP_VERBOSE=1 prints every fallback that succeeds. * * Linux only: on macOS the two-level namespace binds libraries to * libSystem's dlopen, and on Windows LoadLibrary has its own search order. */#ifndef _GNU_SOURCE#define _GNU_SOURCE#endif#include <dlfcn.h>#include <limits.h>#include <stdio.h>#include <stdlib.h>#include <string.h>typedef void *(*sigil_dlopen_fn)(const char *, int);static sigil_dlopen_fn real_dlopen(void){ static sigil_dlopen_fn fn = NULL; if (!fn) fn = (sigil_dlopen_fn)dlsym(RTLD_NEXT, "dlopen"); return fn;}static void *try_dir(sigil_dlopen_fn real, const char *dir, size_t dirlen, const char *name, int flags){ char path[PATH_MAX]; size_t namelen = strlen(name); if (dirlen == 0 || dirlen + 1 + namelen + 1 > sizeof path) return NULL; memcpy(path, dir, dirlen); path[dirlen] = '/'; memcpy(path + dirlen + 1, name, namelen + 1); void *h = real(path, flags); if (h && getenv("SIGIL_APP_VERBOSE")) { fprintf(stderr, "sigil-app: dlopen %s: found as %s\n", name, path); } return h;}/* dir may be a colon-separated list */static void *try_list(sigil_dlopen_fn real, const char *list, const char *name, int flags){ if (!list) return NULL; while (*list) { const char *colon = strchr(list, ':'); size_t len = colon ? (size_t)(colon - list) : strlen(list); void *h = try_dir(real, list, len, name, flags); if (h) return h; if (!colon) break; list = colon + 1; } return NULL;}static void *try_under(sigil_dlopen_fn real, const char *base, const char *suffix, const char *name, int flags){ char dir[PATH_MAX]; if (!base || !*base) return NULL; int n = snprintf(dir, sizeof dir, "%s%s", base, suffix); if (n <= 0 || (size_t)n >= sizeof dir) return NULL; return try_dir(real, dir, (size_t)n, name, flags);}void *dlopen(const char *name, int flags){ sigil_dlopen_fn real = real_dlopen(); if (!real) return NULL; void *h = real(name, flags); if (h || !name || strchr(name, '/')) return h; /* A bare soname the loader could not find: the profile directories. */ const char *home = getenv("HOME"); h = try_list(real, getenv("SIGIL_LIBRARY_PATH"), name, flags); if (!h) h = try_under(real, getenv("GUIX_ENVIRONMENT"), "/lib", name, flags); if (!h) h = try_under(real, home, "/.guix-home/profile/lib", name, flags); if (!h) h = try_under(real, home, "/.guix-profile/lib", name, flags); if (!h) h = try_dir(real, "/run/current-system/profile/lib", strlen("/run/current-system/profile/lib"), name, flags); if (h) return h; /* Nothing found: repeat the plain call so dlerror() names the soname * the caller asked for, not the last directory tried. */ return real(name, flags);}/* Referenced from app.c so this object is always linked ahead of libc, * whatever order the linker meets the archive members in. */const char *sigil_app_dlopen_search_summary(void){ return "SIGIL_LIBRARY_PATH, $GUIX_ENVIRONMENT/lib, ~/.guix-home/profile/lib, " "~/.guix-profile/lib, /run/current-system/profile/lib";}