Commit714d3edaRecorded20 Sep 2026Repositorysigil-app

Find dlopen'd libraries in Guix profiles; fail loudly without a window

Message

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.

Changed
 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(-)
Diff
package.sglmodified
@@ -67,8 +67,11 @@
67
((darwin) '("-D_GLFW_COCOA"))
68
(else '())))
69
−70
(define app-sources
−71
'("src/c/app.c" "src/c/sokol-compat.c"))
+70
;; dl-search.c interposes dlopen so GLFW's runtime loading finds libraries
+71
;; in Guix profile directories; it is Linux-only by design (see the file).
+72
(define (app-sources os)
+73
(append '("src/c/app.c" "src/c/sokol-compat.c")
+74
(if (eq? os 'linux) '("src/c/dl-search.c") '())))
75
76
(define app-include-dirs
77
'("vendor/glfw/include" "vendor/sokol" "src/c"))
@@ -94,7 +97,7 @@
97
;; objects on the context; create-static-library archives them all.
98
(define (compile-app ctx)
99
((compile-c-sources
−97
sources: app-sources
+100
sources: (app-sources (target-os ctx))
101
include-dirs: app-include-dirs
102
flags: (with-sigil-c-flags app-flags))
103
ctx))
@@ -149,7 +152,7 @@
152
name: 'sigil-app
153
;; The platform-independent files; the build task adds the GLFW
154
;; backend files for the config's target OS.
−152
c-sources: '("src/c/app.c" "src/c/sokol-compat.c")
+155
c-sources: '("src/c/app.c" "src/c/sokol-compat.c" "src/c/dl-search.c")
156
c-include-dirs: '("vendor/glfw/include" "vendor/sokol" "src/c")
157
native-init: "sigil__init_sigil_app_module"
158
;; Platform linker flags.
src/c/app-internal.hmodified
@@ -81,4 +81,9 @@ int sigil_app_key_code_from_symbol(SigilVM *vm, Value sym);
81
int sigil_app_framebuffer_width(void);
82
int sigil_app_framebuffer_height(void);
83
+84
#if defined(__linux__)
+85
/* dl-search.c: the directories the interposed dlopen falls back to. */
+86
const char *sigil_app_dlopen_search_summary(void);
+87
#endif
+88
89
#endif /* SIGIL_APP_INTERNAL_H */
src/c/app.cmodified
@@ -213,10 +213,15 @@ static void window_close_callback(GLFWwindow *window)
213
214
static void framebuffer_size_callback(GLFWwindow *window, int width, int height)
215
{
−216
(void)window;
216
if (!g_app) return;
217
g_app->fb_width = width;
218
g_app->fb_height = height;
+219
if (getenv("SIGIL_APP_VERBOSE")) {
+220
float sx = 1.0f, sy = 1.0f;
+221
glfwGetWindowContentScale(window, &sx, &sy);
+222
fprintf(stderr, "sigil-app: framebuffer=%dx%d window=%dx%d content-scale=%.2fx%.2f\n",
+223
width, height, g_app->win_width, g_app->win_height, sx, sy);
+224
}
225
}
226
227
static void window_size_callback(GLFWwindow *window, int width, int height)
@@ -286,14 +291,16 @@ static const char *platform_name(int platform)
291
* MAIN LOOP
292
* ============================================================ */
293
−289
static void run_main_loop(const char *title, int width, int height)
+294
/* Returns false when no window could be opened; the GLFW error callback
+295
* has already said why on stderr. */
+296
static bool run_main_loop(const char *title, int width, int height)
297
{
298
glfwSetErrorCallback(glfw_error_callback);
299
apply_platform_hint();
300
301
if (!glfwInit()) {
302
fprintf(stderr, "sigil-app: glfwInit failed; no usable display platform\n");
−296
return;
+303
return false;
304
}
305
306
/* GL 4.1 core, the profile sokol_app asked for (and the newest macOS
@@ -305,13 +312,18 @@ static void run_main_loop(const char *title, int width, int height)
312
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
313
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
314
glfwWindowHint(GLFW_SCALE_FRAMEBUFFER, GLFW_TRUE);
+315
/* Wayland app_id and X11 class both default to the title, so a
+316
* compositor rule (sway: for_window [app_id="Crash The Stack"]) can
+317
* name the window on either platform. GLFW's X11 backend already
+318
* falls back to the title; Wayland's default is empty. */
+319
glfwWindowHintString(GLFW_WAYLAND_APP_ID, title);
320
321
GLFWwindow *window = glfwCreateWindow(width, height, title, NULL, NULL);
322
if (!window) {
323
fprintf(stderr, "sigil-app: window creation failed (platform %s)\n",
324
platform_name(glfwGetPlatform()));
325
glfwTerminate();
−314
return;
+326
return false;
327
}
328
g_app->window = window;
329
@@ -325,11 +337,15 @@ static void run_main_loop(const char *title, int width, int height)
337
if (getenv("SIGIL_APP_VERBOSE")) {
338
float sx = 1.0f, sy = 1.0f;
339
glfwGetWindowContentScale(window, &sx, &sy);
−328
fprintf(stderr, "sigil-app: platform=%s window=%dx%d framebuffer=%dx%d content-scale=%.2fx%.2f gl=%s\n",
+340
fprintf(stderr, "sigil-app: platform=%s window=%dx%d framebuffer=%dx%d content-scale=%.2fx%.2f gl=%d.%d\n",
341
platform_name(glfwGetPlatform()),
342
g_app->win_width, g_app->win_height,
343
g_app->fb_width, g_app->fb_height, sx, sy,
−332
glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR) == 4 ? "4.x" : "other");
+344
glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR),
+345
glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR));
+346
#if defined(__linux__)
+347
fprintf(stderr, "sigil-app: dlopen fallback dirs: %s\n", sigil_app_dlopen_search_summary());
+348
#endif
349
}
350
351
glfwSetKeyCallback(window, key_callback);
@@ -377,6 +393,7 @@ static void run_main_loop(const char *title, int width, int height)
393
g_app->window = NULL;
394
glfwDestroyWindow(window);
395
glfwTerminate();
+396
return true;
397
}
398
399
/* ============================================================
@@ -422,11 +439,16 @@ static Value native_app_run(SigilVM *vm, int argc, Value *args)
439
height = (int)sigil_as_fixnum(args[5]);
440
}
441
−425
run_main_loop(title, width, height);
+442
bool ran = run_main_loop(title, width, height);
443
444
/* Cleanup */
445
sigil_app_state_shutdown();
446
+447
if (!ran) {
+448
sigil__vm_error(vm, SIGIL_ERR_RUNTIME,
+449
"app-run: could not open a window (see the sigil-app: lines above)");
+450
return SIGIL_UNDEFINED;
+451
}
452
return SIGIL_NIL;
453
}
454
src/c/dl-search.cadded
@@ -0,0 +1,127 @@
+1
/*
+2
* dl-search.c - a dlopen that also looks in the profile library directories
+3
*
+4
* GLFW loads every platform library at run time by bare soname:
+5
* dlopen("libwayland-client.so.0"), dlopen("libX11.so.6"), dlopen("libEGL.so.1").
+6
* The dynamic loader finds such a name through LD_LIBRARY_PATH, the
+7
* caller's RUNPATH and the system default directories. On Guix none of
+8
* those name a profile's lib directory: a Guix profile does not set
+9
* LD_LIBRARY_PATH, and Guix's own packages are patched to dlopen absolute
+10
* store paths, which a user's binary cannot be.
+11
*
+12
* This file defines dlopen in the executable. The executable's symbols come
+13
* first in the lookup order, so every dlopen in the process (GLFW's in
+14
* posix_module.c, and any other loader compiled into the program) resolves
+15
* here. It tries the name as given first, so on a system where the loader
+16
* already finds the library nothing changes. When that fails and the name
+17
* is a bare soname (no slash), it retries the name joined to each directory
+18
* of:
+19
*
+20
* SIGIL_LIBRARY_PATH colon separated, the user's override
+21
* $GUIX_ENVIRONMENT/lib set by `guix shell`
+22
* $HOME/.guix-home/profile/lib Guix Home
+23
* $HOME/.guix-profile/lib the user profile
+24
* /run/current-system/profile/lib Guix System
+25
*
+26
* The real dlopen is reached with dlsym(RTLD_NEXT, "dlopen"). A library
+27
* found this way loads its own dependencies through its RUNPATH, which on
+28
* Guix names their store paths, so one absolute path is enough.
+29
*
+30
* A NEEDED entry (a library linked with -l) is resolved by the loader
+31
* before main runs and cannot be helped here; only dlopen is covered.
+32
*
+33
* SIGIL_APP_VERBOSE=1 prints every fallback that succeeds.
+34
*
+35
* Linux only: on macOS the two-level namespace binds libraries to
+36
* libSystem's dlopen, and on Windows LoadLibrary has its own search order.
+37
*/
+38
+39
#ifndef _GNU_SOURCE
+40
#define _GNU_SOURCE
+41
#endif
+42
#include <dlfcn.h>
+43
#include <limits.h>
+44
#include <stdio.h>
+45
#include <stdlib.h>
+46
#include <string.h>
+47
+48
typedef void *(*sigil_dlopen_fn)(const char *, int);
+49
+50
static sigil_dlopen_fn real_dlopen(void)
+51
{
+52
static sigil_dlopen_fn fn = NULL;
+53
if (!fn) fn = (sigil_dlopen_fn)dlsym(RTLD_NEXT, "dlopen");
+54
return fn;
+55
}
+56
+57
static void *try_dir(sigil_dlopen_fn real, const char *dir, size_t dirlen,
+58
const char *name, int flags)
+59
{
+60
char path[PATH_MAX];
+61
size_t namelen = strlen(name);
+62
if (dirlen == 0 || dirlen + 1 + namelen + 1 > sizeof path) return NULL;
+63
memcpy(path, dir, dirlen);
+64
path[dirlen] = '/';
+65
memcpy(path + dirlen + 1, name, namelen + 1);
+66
void *h = real(path, flags);
+67
if (h && getenv("SIGIL_APP_VERBOSE")) {
+68
fprintf(stderr, "sigil-app: dlopen %s: found as %s\n", name, path);
+69
}
+70
return h;
+71
}
+72
+73
/* dir may be a colon-separated list */
+74
static void *try_list(sigil_dlopen_fn real, const char *list, const char *name, int flags)
+75
{
+76
if (!list) return NULL;
+77
while (*list) {
+78
const char *colon = strchr(list, ':');
+79
size_t len = colon ? (size_t)(colon - list) : strlen(list);
+80
void *h = try_dir(real, list, len, name, flags);
+81
if (h) return h;
+82
if (!colon) break;
+83
list = colon + 1;
+84
}
+85
return NULL;
+86
}
+87
+88
static void *try_under(sigil_dlopen_fn real, const char *base, const char *suffix,
+89
const char *name, int flags)
+90
{
+91
char dir[PATH_MAX];
+92
if (!base || !*base) return NULL;
+93
int n = snprintf(dir, sizeof dir, "%s%s", base, suffix);
+94
if (n <= 0 || (size_t)n >= sizeof dir) return NULL;
+95
return try_dir(real, dir, (size_t)n, name, flags);
+96
}
+97
+98
void *dlopen(const char *name, int flags)
+99
{
+100
sigil_dlopen_fn real = real_dlopen();
+101
if (!real) return NULL;
+102
+103
void *h = real(name, flags);
+104
if (h || !name || strchr(name, '/')) return h;
+105
+106
/* A bare soname the loader could not find: the profile directories. */
+107
const char *home = getenv("HOME");
+108
h = try_list(real, getenv("SIGIL_LIBRARY_PATH"), name, flags);
+109
if (!h) h = try_under(real, getenv("GUIX_ENVIRONMENT"), "/lib", name, flags);
+110
if (!h) h = try_under(real, home, "/.guix-home/profile/lib", name, flags);
+111
if (!h) h = try_under(real, home, "/.guix-profile/lib", name, flags);
+112
if (!h) h = try_dir(real, "/run/current-system/profile/lib",
+113
strlen("/run/current-system/profile/lib"), name, flags);
+114
if (h) return h;
+115
+116
/* Nothing found: repeat the plain call so dlerror() names the soname
+117
* the caller asked for, not the last directory tried. */
+118
return real(name, flags);
+119
}
+120
+121
/* Referenced from app.c so this object is always linked ahead of libc,
+122
* whatever order the linker meets the archive members in. */
+123
const char *sigil_app_dlopen_search_summary(void)
+124
{
+125
return "SIGIL_LIBRARY_PATH, $GUIX_ENVIRONMENT/lib, ~/.guix-home/profile/lib, "
+126
"~/.guix-profile/lib, /run/current-system/profile/lib";
+127
}