AtlatestRepositorysigil-audio
sigil-audio / tree / src / cdl-search.c
1
/*2
* dl-search.c - a dlopen that also looks in the profile library directories3
*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, the7
* caller's RUNPATH and the system default directories. On Guix and Nix none8
* of those name a profile's lib directory: the profile sets no9
* LD_LIBRARY_PATH, and the distribution's own packages are patched to10
* 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 come13
* first in the lookup order, so every dlopen in the process (GLFW's in14
* posix_module.c, and any other loader compiled into the program) resolves15
* here; in sigil-audio it exposes a named resolver that miniaudio calls16
* instead of dlopen. It tries the name as given first, so on a system where the loader17
* already finds the library nothing changes. When that fails and the name18
* is a bare soname (no slash), it retries the name under each entry of the19
* table below, in order. The real dlopen is reached with20
* dlsym(RTLD_NEXT, "dlopen"). A library found this way loads its own21
* dependencies through its RUNPATH, which on Guix and Nix names their22
* store paths, so one absolute path is enough.23
*24
* A NEEDED entry (a library linked with -l) is resolved by the loader25
* 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, the29
* soname, the table entry and the path, and whether /gnu/store and30
* /nix/store exist on this machine.31
*32
* Linux only: on macOS the two-level namespace binds libraries to33
* 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 byte39
* (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_open42
* SIGIL_DL_SEARCH_SUMMARY the summary function's name43
* SIGIL_DL_SEARCH_TAG the log prefix, e.g. "sigil-desktop"44
* SIGIL_DL_SEARCH_VERBOSE_ENV the env var that turns the trace on45
* SIGIL_DL_SEARCH_INTERPOSE 1 to also define dlopen itself (one package46
* per executable may; sigil-desktop does),47
* 0 to expose only the named resolver (a48
* loader compiled in the same package calls49
* 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
#endif55
#ifndef _GNU_SOURCE56
#define _GNU_SOURCE57
#endif58
#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 ------------------------------------------------ */67
typedef 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;76
typedef 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 a84
* row. Every row is walked on every machine (Guix and Nix coexist: David85
* runs Nix inside Guix), so there is no "which distro" branch and a86
* library may come from a Nix profile while its own dependency, resolved87
* 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 user89
* profiles, then the system profiles, Guix and Nix interleaved. When two90
* rows offer the same soname the first hit wins and the trace names it. */91
static 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 ------------------------------------------------- */112
typedef void *(*sigil_dlopen_fn)(const char *, int);114
static sigil_dlopen_fn real_dlopen(void)115
{116
static sigil_dlopen_fn fn = NULL;117
if (!fn) fn = (sigil_dlopen_fn)dlsym(RTLD_NEXT, "dlopen");118
return fn;119
}121
static int dir_exists(const char *path)122
{123
struct stat st;124
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);125
}127
static void trace_found(const search_entry *e, const char *name, const char *path)128
{129
if (!getenv(SIGIL_DL_SEARCH_VERBOSE_ENV)) return;130
const char *label = (e->kind == ENTRY_DIR) ? e->name131
: (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");138
}140
/* dir[0..dirlen) + suffix + "/" + name, through the real dlopen */141
static 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)144
{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;155
}157
/* every item of a separated list */158
static void *try_list(sigil_dlopen_fn real, const search_entry *e, const char *list,159
char sep, const char *name, int flags)160
{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;171
}173
static void *try_entry(sigil_dlopen_fn real, const search_entry *e, const char *name, int flags)174
{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;200
}202
/* ---- the resolver ---------------------------------------------------- */204
void *SIGIL_DL_SEARCH_OPEN(const char *name, int flags)205
{206
sigil_dlopen_fn real = real_dlopen();207
if (!real) {208
/* No next dlopen: only a fully static link gets here; say so, since209
* 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 soname225
* the caller asked for, not the last directory tried. */226
return real(name, flags);227
}229
#if SIGIL_DL_SEARCH_INTERPOSE230
/* The executable's dlopen: every dlopen in the process resolves here. */231
void *dlopen(const char *name, int flags)232
{233
return SIGIL_DL_SEARCH_OPEN(name, flags);234
}235
#endif237
/* Referenced from the package's own code so this object is always linked238
* ahead of libc, whatever order the linker meets the archive members in. */239
const char *SIGIL_DL_SEARCH_SUMMARY(void)240
{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";245
}