locale-util: dlopen() libintl instead of linking against it

dgettext() lives in libc on glibc and in libintl.so.8 on musl with
gettext. Resolve it via dlsym() so neither configuration produces a
hard link-time dependency on libintl: try libintl.so.8 first and fall
back to RTLD_DEFAULT (which finds dgettext in libc on glibc).

The _() macro now expands to a runtime check that returns the
untranslated string if dlopen_libintl() has not run successfully, so
callers don't have to gate every translatable message on a runtime
check. pam_systemd_home — currently the only consumer of _() — calls
dlopen_libintl() best-effort from each PAM entry point.

The meson find_library('intl') dance is replaced with a has_header()
check; the only thing we need at build time is the prototype.
This commit is contained in:
Daan De Meyer
2026-05-18 21:17:38 +00:00
parent b2bfe5b474
commit e6e65dc261
6 changed files with 60 additions and 18 deletions
+31
View File
@@ -7,7 +7,12 @@
#include <sys/stat.h>
#include <unistd.h>
#ifndef __GLIBC__
#include "sd-dlopen.h"
#endif
#include "dirent-util.h"
#include "dlfcn-util.h"
#include "env-util.h"
#include "fd-util.h"
#include "fileio.h"
@@ -22,6 +27,32 @@
#include "strv.h"
#include "utf8.h"
#ifdef __GLIBC__
DLSYM_PROTOTYPE(dgettext) = dgettext;
#else
DLSYM_PROTOTYPE(dgettext) = NULL;
#endif
int dlopen_libintl(int log_level) {
#ifdef __GLIBC__
return 1;
#else
static void *libintl_dl = NULL;
SD_ELF_NOTE_DLOPEN(
"intl",
"Support for message translation via gettext",
SD_ELF_NOTE_DLOPEN_PRIORITY_SUGGESTED,
"libintl.so.8");
return dlopen_many_sym_or_warn(
&libintl_dl,
"libintl.so.8",
log_level,
DLSYM_ARG(dgettext));
#endif
}
static char* normalize_locale(const char *name) {
const char *e;
+12 -1
View File
@@ -1,9 +1,18 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include <libintl.h> /* IWYU pragma: export */
#include <locale.h> /* IWYU pragma: export */
#include "basic-forward.h"
#include "dlfcn-util.h"
/* format_arg(2) propagates the format-string nature of the second argument to the return value, so that
* printf(_("Hello %s"), name) still gets checked. It survives both DLSYM_PROTOTYPE's typeof() and the
* ternary in _() below — verified on gcc and clang. */
extern DLSYM_PROTOTYPE(dgettext) __attribute__((format_arg(2)));
int dlopen_libintl(int log_level);
typedef enum LocaleVariable {
/* We don't list LC_ALL here on purpose. People should be
@@ -31,7 +40,9 @@ int get_locales(char ***ret);
bool locale_is_valid(const char *name);
int locale_is_installed(const char *name);
#define _(String) dgettext(GETTEXT_PACKAGE, String)
/* Falls back to the untranslated string if dlopen_libintl() hasn't run or has failed, so callers don't have
* to gate every translatable message on a runtime check. */
#define _(String) (sym_dgettext ? sym_dgettext(GETTEXT_PACKAGE, (String)) : (String))
#define N_(String) String
bool is_locale_utf8(void);