AtlatestRepositorysigil-audio

sigil-audio / tree / src / cdl-search.c

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 and Nix none
8 * of those name a profile's lib directory: the profile sets no
9 * LD_LIBRARY_PATH, and the distribution's own packages are patched to
10 * dlopen absolute store paths, which a user's binary cannot be.
11 *
12 * In sigil-desktop 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; in sigil-audio it exposes a named resolver that miniaudio calls
16 * instead of dlopen. It tries the name as given first, so on a system where the loader
17 * already finds the library nothing changes. When that fails and the name
18 * is a bare soname (no slash), it retries the name under each entry of the
19 * table below, in order. The real dlopen is reached with
20 * dlsym(RTLD_NEXT, "dlopen"). A library found this way loads its own
21 * dependencies through its RUNPATH, which on Guix and Nix names their
22 * store paths, so one absolute path is enough.
23 *
24 * A NEEDED entry (a library linked with -l) is resolved by the loader
25 * before main runs and cannot be helped here; only dlopen is covered.
26 *
27 * The verbose env var (SIGIL_DESKTOP_VERBOSE=1 in sigil-desktop,
28 * SIGIL_AUDIO_VERBOSE=1 in sigil-audio) prints, for every fallback that succeeds, the
29 * soname, the table entry and the path, and whether /gnu/store and
30 * /nix/store exist on this machine.
31 *
32 * Linux only: on macOS the two-level namespace binds libraries to
33 * libSystem's dlopen, and on Windows LoadLibrary has its own search order.
34 */
37/* ---- per-package configuration ---------------------------------------
38 * This file is shared by sigil-desktop and sigil-audio, byte for byte
39 * (each repo's test checks its copy against the other's recorded hash).
40 * The package that compiles it says, with -D flags:
41 * SIGIL_DL_SEARCH_OPEN the resolver's name, e.g. sigil_desktop_dl_open
42 * SIGIL_DL_SEARCH_SUMMARY the summary function's name
43 * SIGIL_DL_SEARCH_TAG the log prefix, e.g. "sigil-desktop"
44 * SIGIL_DL_SEARCH_VERBOSE_ENV the env var that turns the trace on
45 * SIGIL_DL_SEARCH_INTERPOSE 1 to also define dlopen itself (one package
46 * per executable may; sigil-desktop does),
47 * 0 to expose only the named resolver (a
48 * loader compiled in the same package calls
49 * it, e.g. miniaudio through a define) */
50#if !defined(SIGIL_DL_SEARCH_OPEN) || !defined(SIGIL_DL_SEARCH_SUMMARY) || \
51 !defined(SIGIL_DL_SEARCH_TAG) || !defined(SIGIL_DL_SEARCH_VERBOSE_ENV) || \
52 !defined(SIGIL_DL_SEARCH_INTERPOSE)
53#error "dl-search.c: define SIGIL_DL_SEARCH_OPEN, _SUMMARY, _TAG, _VERBOSE_ENV and _INTERPOSE (see the file header)"
54#endif
55#ifndef _GNU_SOURCE
56#define _GNU_SOURCE
57#endif
58#include <dlfcn.h>
59#include <limits.h>
60#include <stdio.h>
61#include <stdlib.h>
62#include <string.h>
63#include <sys/stat.h>
65/* ---- the search table ------------------------------------------------ */
67typedef enum {
68 ENTRY_ENV_COLON_DIRS, /* env var: colon-separated directories */
69 ENTRY_ENV_SPACE_ROOTS, /* env var: space-separated roots, suffix appended */
70 ENTRY_ENV_ROOT, /* env var: one root, suffix appended */
71 ENTRY_HOME, /* $HOME + suffix */
72 ENTRY_USER, /* prefix + $USER + suffix */
73 ENTRY_DIR /* a fixed directory */
74} entry_kind;
76typedef struct {
77 entry_kind kind;
78 const char *name; /* env var, prefix, or the directory itself */
79 const char *suffix; /* appended to each root (may be "") */
80 const char *only_if; /* env var that must be set for the row to apply, or NULL */
81} search_entry;
83/* One row per source of library directories; adding a distribution is a
84 * row. Every row is walked on every machine (Guix and Nix coexist: David
85 * runs Nix inside Guix), so there is no "which distro" branch and a
86 * library may come from a Nix profile while its own dependency, resolved
87 * by RUNPATH, comes from a Guix one. Order: the user's explicit override,
88 * the active environment (guix shell, nix-shell, nix-ld), then the user
89 * profiles, then the system profiles, Guix and Nix interleaved. When two
90 * rows offer the same soname the first hit wins and the trace names it. */
91static const search_entry SEARCH_TABLE[] = {
92 /* explicit override */
93 { ENTRY_ENV_COLON_DIRS, "SIGIL_LIBRARY_PATH", "", NULL },
94 /* the active environment */
95 { ENTRY_ENV_ROOT, "GUIX_ENVIRONMENT", "/lib", NULL }, /* guix shell */
96 { ENTRY_ENV_SPACE_ROOTS, "buildInputs", "/lib", "IN_NIX_SHELL" }, /* nix-shell */
97 { ENTRY_ENV_SPACE_ROOTS, "nativeBuildInputs", "/lib", "IN_NIX_SHELL" },
98 { ENTRY_ENV_COLON_DIRS, "NIX_LD_LIBRARY_PATH", "", NULL }, /* nix-ld */
99 /* user profiles */
100 { ENTRY_HOME, "", "/.guix-home/profile/lib", NULL }, /* Guix Home */
101 { ENTRY_HOME, "", "/.guix-profile/lib", NULL }, /* guix package */
102 { ENTRY_HOME, "", "/.nix-profile/lib", NULL }, /* nix-env, nix profile */
103 { ENTRY_USER, "/etc/profiles/per-user/", "/lib", NULL }, /* NixOS per-user */
104 /* system profiles */
105 { ENTRY_DIR, "/run/current-system/profile/lib", "", NULL }, /* Guix System */
106 { ENTRY_DIR, "/run/current-system/sw/lib", "", NULL }, /* NixOS */
107};
108#define SEARCH_TABLE_LEN (sizeof SEARCH_TABLE / sizeof SEARCH_TABLE[0])
110/* ---- the real dlopen ------------------------------------------------- */
112typedef void *(*sigil_dlopen_fn)(const char *, int);
114static sigil_dlopen_fn real_dlopen(void)
116 static sigil_dlopen_fn fn = NULL;
117 if (!fn) fn = (sigil_dlopen_fn)dlsym(RTLD_NEXT, "dlopen");
118 return fn;
121static int dir_exists(const char *path)
123 struct stat st;
124 return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
127static void trace_found(const search_entry *e, const char *name, const char *path)
129 if (!getenv(SIGIL_DL_SEARCH_VERBOSE_ENV)) return;
130 const char *label = (e->kind == ENTRY_DIR) ? e->name
131 : (e->kind == ENTRY_HOME) ? "HOME"
132 : (e->kind == ENTRY_USER) ? "USER"
133 : e->name;
134 fprintf(stderr, SIGIL_DL_SEARCH_TAG ": dlopen %s: found via %s%s as %s (/gnu/store %s, /nix/store %s)\n",
135 name, label, e->suffix, path,
136 dir_exists("/gnu/store") ? "present" : "absent",
137 dir_exists("/nix/store") ? "present" : "absent");
140/* dir[0..dirlen) + suffix + "/" + name, through the real dlopen */
141static void *try_dir(sigil_dlopen_fn real, const search_entry *e,
142 const char *dir, size_t dirlen, const char *suffix,
143 const char *name, int flags)
145 char path[PATH_MAX];
146 size_t sufflen = strlen(suffix), namelen = strlen(name);
147 if (dirlen == 0 || dirlen + sufflen + 1 + namelen + 1 > sizeof path) return NULL;
148 memcpy(path, dir, dirlen);
149 memcpy(path + dirlen, suffix, sufflen);
150 path[dirlen + sufflen] = '/';
151 memcpy(path + dirlen + sufflen + 1, name, namelen + 1);
152 void *h = real(path, flags);
153 if (h) trace_found(e, name, path);
154 return h;
157/* every item of a separated list */
158static void *try_list(sigil_dlopen_fn real, const search_entry *e, const char *list,
159 char sep, const char *name, int flags)
161 if (!list) return NULL;
162 while (*list) {
163 const char *end = strchr(list, sep);
164 size_t len = end ? (size_t)(end - list) : strlen(list);
165 void *h = try_dir(real, e, list, len, e->suffix, name, flags);
166 if (h) return h;
167 if (!end) break;
168 list = end + 1;
169 }
170 return NULL;
173static void *try_entry(sigil_dlopen_fn real, const search_entry *e, const char *name, int flags)
175 if (e->only_if && !getenv(e->only_if)) return NULL;
176 const char *v;
177 switch (e->kind) {
178 case ENTRY_ENV_COLON_DIRS:
179 return try_list(real, e, getenv(e->name), ':', name, flags);
180 case ENTRY_ENV_SPACE_ROOTS:
181 return try_list(real, e, getenv(e->name), ' ', name, flags);
182 case ENTRY_ENV_ROOT:
183 v = getenv(e->name);
184 return v ? try_dir(real, e, v, strlen(v), e->suffix, name, flags) : NULL;
185 case ENTRY_HOME:
186 v = getenv("HOME");
187 return v ? try_dir(real, e, v, strlen(v), e->suffix, name, flags) : NULL;
188 case ENTRY_USER: {
189 v = getenv("USER");
190 if (!v || !*v) return NULL;
191 char root[PATH_MAX];
192 int n = snprintf(root, sizeof root, "%s%s", e->name, v);
193 if (n <= 0 || (size_t)n >= sizeof root) return NULL;
194 return try_dir(real, e, root, (size_t)n, e->suffix, name, flags);
195 }
196 case ENTRY_DIR:
197 return try_dir(real, e, e->name, strlen(e->name), e->suffix, name, flags);
198 }
199 return NULL;
202/* ---- the resolver ---------------------------------------------------- */
204void *SIGIL_DL_SEARCH_OPEN(const char *name, int flags)
206 sigil_dlopen_fn real = real_dlopen();
207 if (!real) {
208 /* No next dlopen: only a fully static link gets here; say so, since
209 * the caller then reports the library as missing. */
210 fprintf(stderr, SIGIL_DL_SEARCH_TAG ": dlopen(%s): no underlying dlopen (static link?)\n",
211 name ? name : "(null)");
212 return NULL;
213 }
215 void *h = real(name, flags);
216 if (h || !name || strchr(name, '/')) return h;
218 /* A bare soname the loader could not find: the table, in order. */
219 for (size_t i = 0; i < SEARCH_TABLE_LEN; i++) {
220 h = try_entry(real, &SEARCH_TABLE[i], name, flags);
221 if (h) return h;
222 }
224 /* Nothing found: repeat the plain call so dlerror() names the soname
225 * the caller asked for, not the last directory tried. */
226 return real(name, flags);
229#if SIGIL_DL_SEARCH_INTERPOSE
230/* The executable's dlopen: every dlopen in the process resolves here. */
231void *dlopen(const char *name, int flags)
233 return SIGIL_DL_SEARCH_OPEN(name, flags);
235#endif
237/* Referenced from the package's own code so this object is always linked
238 * ahead of libc, whatever order the linker meets the archive members in. */
239const char *SIGIL_DL_SEARCH_SUMMARY(void)
241 return "SIGIL_LIBRARY_PATH; $GUIX_ENVIRONMENT/lib; each $buildInputs and "
242 "$nativeBuildInputs root + /lib under IN_NIX_SHELL; $NIX_LD_LIBRARY_PATH; ~/.guix-home/profile/lib; "
243 "~/.guix-profile/lib; ~/.nix-profile/lib; /etc/profiles/per-user/$USER/lib; "
244 "/run/current-system/profile/lib; /run/current-system/sw/lib";