localed: Run locale-gen if available to generate missing locale

This change improves integration with distributions using locale-gen to
generate missing locale on-demand, like Debian-based distributions
(Debian/Ubuntu/PureOS/Tanglu/...) and Arch Linux.
We only ever enable new locales for generation, and never disable them.
Furthermore, we only generate UTF-8 locale.

This feature is only used if explicitly enabled at compile-time, and
will also be inert at runtime if the locale-gen binary is missing.
This commit is contained in:
Matthias Klumpp 2021-01-08 23:59:38 +01:00
parent bd47b0dac4
commit 8f20232fcb
6 changed files with 286 additions and 4 deletions

View file

@ -832,6 +832,14 @@ if default_locale == ''
endif
conf.set_quoted('SYSTEMD_DEFAULT_LOCALE', default_locale)
localegen_path = get_option('localegen-path')
have = false
if localegen_path != ''
conf.set_quoted('LOCALEGEN_PATH', localegen_path)
have = true
endif
conf.set10('HAVE_LOCALEGEN', have)
conf.set_quoted('GETTEXT_PACKAGE', meson.project_name())
service_watchdog = get_option('service-watchdog')

View file

@ -240,6 +240,8 @@ option('gshadow', type : 'boolean',
description : 'support for shadow group')
option('default-locale', type : 'string', value : '',
description : 'default locale used when /etc/locale.conf does not exist')
option('localegen-path', type : 'string', value : '',
description : 'absolute path to the locale-gen binary in case the system is using locale-gen')
option('service-watchdog', type : 'string', value : '3min',
description : 'default watchdog setting for systemd services')

View file

@ -6,18 +6,21 @@
#include <unistd.h>
#include "bus-polkit.h"
#include "copy.h"
#include "env-file-label.h"
#include "env-file.h"
#include "env-util.h"
#include "fd-util.h"
#include "fileio-label.h"
#include "fileio.h"
#include "fs-util.h"
#include "kbd-util.h"
#include "keymap-util.h"
#include "locale-util.h"
#include "macro.h"
#include "mkdir.h"
#include "nulstr-util.h"
#include "process-util.h"
#include "string-util.h"
#include "strv.h"
#include "tmpfile-util.h"
@ -780,3 +783,211 @@ int x11_convert_to_vconsole(Context *c) {
return modified;
}
bool locale_gen_check_available(void) {
#if HAVE_LOCALEGEN
if (access(LOCALEGEN_PATH, X_OK) < 0) {
if (errno != ENOENT)
log_warning_errno(errno, "Unable to determine whether " LOCALEGEN_PATH " exists and is executable, assuming it is not: %m");
return false;
}
if (access("/etc/locale.gen", F_OK) < 0) {
if (errno != ENOENT)
log_warning_errno(errno, "Unable to determine whether /etc/locale.gen exists, assuming it does not: %m");
return false;
}
return true;
#else
return false;
#endif
}
#if HAVE_LOCALEGEN
static bool locale_encoding_is_utf8_or_unspecified(const char *locale) {
const char *c = strchr(locale, '.');
return !c || strcaseeq(c, ".UTF-8") || strcasestr(locale, ".UTF-8@");
}
static int locale_gen_locale_supported(const char *locale_entry) {
/* Returns an error valus <= 0 if the locale-gen entry is invalid or unsupported,
* 1 in case the locale entry is valid, and -EOPNOTSUPP specifically in case
* the distributor has not provided us with a SUPPORTED file to check
* locale for validity. */
_cleanup_fclose_ FILE *f = NULL;
int r;
assert(locale_entry);
/* Locale templates without country code are never supported */
if (!strstr(locale_entry, "_"))
return -EINVAL;
f = fopen("/usr/share/i18n/SUPPORTED", "re");
if (!f) {
if (errno == ENOENT)
return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
"Unable to check validity of locale entry %s: /usr/share/i18n/SUPPORTED does not exist",
locale_entry);
return -errno;
}
for (;;) {
_cleanup_free_ char *line = NULL;
r = read_line(f, LONG_LINE_MAX, &line);
if (r < 0)
return log_debug_errno(r, "Failed to read /usr/share/i18n/SUPPORTED: %m");
if (r == 0)
return 0;
line = strstrip(line);
if (strcaseeq_ptr(line, locale_entry))
return 1;
}
}
#endif
int locale_gen_enable_locale(const char *locale) {
#if HAVE_LOCALEGEN
_cleanup_fclose_ FILE *fr = NULL, *fw = NULL;
_cleanup_(unlink_and_freep) char *temp_path = NULL;
_cleanup_free_ char *locale_entry = NULL;
bool locale_enabled = false, first_line = false;
bool write_new = false;
int r;
if (isempty(locale))
return 0;
if (locale_encoding_is_utf8_or_unspecified(locale)) {
locale_entry = strjoin(locale, " UTF-8");
if (!locale_entry)
return -ENOMEM;
} else
return -ENOEXEC; /* We do not process non-UTF-8 locale */
r = locale_gen_locale_supported(locale_entry);
if (r == 0)
return -EINVAL;
if (r < 0 && r != -EOPNOTSUPP)
return r;
fr = fopen("/etc/locale.gen", "re");
if (!fr) {
if (errno != ENOENT)
return -errno;
write_new = true;
}
r = fopen_temporary("/etc/locale.gen", &fw, &temp_path);
if (r < 0)
return r;
if (write_new)
(void) fchmod(fileno(fw), 0644);
else {
/* apply mode & xattrs of the original file to new file */
r = copy_access(fileno(fr), fileno(fw));
if (r < 0)
return r;
r = copy_xattr(fileno(fr), fileno(fw));
if (r < 0)
return r;
}
if (!write_new) {
/* The config file ends with a line break, which we do not want to include before potentially appending a new locale
* instead of uncommenting an existing line. By prepending linebreaks, we can avoid buffering this file but can still write
* a nice config file without empty lines */
first_line = true;
for (;;) {
_cleanup_free_ char *line = NULL;
char *line_locale;
r = read_line(fr, LONG_LINE_MAX, &line);
if (r < 0)
return r;
if (r == 0)
break;
if (locale_enabled) {
/* Just complete writing the file if the new locale was already enabled */
if (!first_line)
fputc('\n', fw);
fputs(line, fw);
first_line = false;
continue;
}
line = strstrip(line);
if (isempty(line)) {
fputc('\n', fw);
first_line = false;
continue;
}
line_locale = line;
if (line_locale[0] == '#')
line_locale = strstrip(line_locale + 1);
else if (strcaseeq_ptr(line_locale, locale_entry))
return 0; /* the file already had our locale activated, so skip updating it */
if (strcaseeq_ptr(line_locale, locale_entry)) {
/* Uncomment existing line for new locale */
if (!first_line)
fputc('\n', fw);
fputs(locale_entry, fw);
locale_enabled = true;
first_line = false;
continue;
}
/* The line was not for the locale we want to enable, just copy it */
if (!first_line)
fputc('\n', fw);
fputs(line, fw);
first_line = false;
}
}
/* Add locale to enable to the end of the file if it was not found as commented line */
if (!locale_enabled) {
if (!write_new)
fputc('\n', fw);
fputs(locale_entry, fw);
}
fputc('\n', fw);
r = fflush_sync_and_check(fw);
if (r < 0)
return r;
if (rename(temp_path, "/etc/locale.gen") < 0)
return -errno;
temp_path = mfree(temp_path);
return 0;
#else
return -EOPNOTSUPP;
#endif
}
int locale_gen_run(void) {
#if HAVE_LOCALEGEN
pid_t pid;
int r;
r = safe_fork("(sd-localegen)", FORK_RESET_SIGNALS|FORK_RLIMIT_NOFILE_SAFE|FORK_CLOSE_ALL_FDS|FORK_LOG|FORK_WAIT, &pid);
if (r < 0)
return r;
if (r == 0) {
execl(LOCALEGEN_PATH, LOCALEGEN_PATH, NULL);
_exit(EXIT_FAILURE);
}
return 0;
#else
return -EOPNOTSUPP;
#endif
}

View file

@ -42,3 +42,7 @@ int x11_convert_to_vconsole(Context *c);
int x11_write_data(Context *c);
void locale_simplify(char *locale[_VARIABLE_LC_MAX]);
int locale_write_data(Context *c, char ***settings);
bool locale_gen_check_available(void);
int locale_gen_enable_locale(const char *locale);
int locale_gen_run(void);

View file

@ -26,6 +26,9 @@
#include "verbs.h"
#include "virt.h"
/* Enough time for locale-gen to finish server-side (in case it is in use) */
#define LOCALE_SLOW_BUS_CALL_TIMEOUT_USEC (2*USEC_PER_MINUTE)
static PagerFlags arg_pager_flags = 0;
static bool arg_ask_password = true;
static BusTransport arg_transport = BUS_TRANSPORT_LOCAL;
@ -176,7 +179,8 @@ static int set_locale(int argc, char **argv, void *userdata) {
if (r < 0)
return bus_log_create_error(r);
r = sd_bus_call(bus, m, 0, &error, NULL);
/* We use a longer timeout for the method call in case localed is running locale-gen */
r = sd_bus_call(bus, m, LOCALE_SLOW_BUS_CALL_TIMEOUT_USEC, &error, NULL);
if (r < 0)
return log_error_errno(r, "Failed to issue method call: %s", bus_error_message(&error, r));

View file

@ -262,6 +262,7 @@ static int property_get_xkb(
static int process_locale_list_item(
const char *assignment,
char *new_locale[static _VARIABLE_LC_MAX],
bool use_localegen,
sd_bus_error *error) {
assert(assignment);
@ -283,7 +284,7 @@ static int process_locale_list_item(
if (!locale_is_valid(e))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale %s is not valid, refusing.", e);
if (locale_is_installed(e) <= 0)
if (!use_localegen && locale_is_installed(e) <= 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale %s not installed, refusing.", e);
if (new_locale[p])
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale variable %s set twice, refusing.", name);
@ -298,6 +299,47 @@ static int process_locale_list_item(
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Locale assignment %s not valid, refusing.", assignment);
}
static int locale_gen_process_locale(char *new_locale[static _VARIABLE_LC_MAX],
sd_bus_error *error) {
int r;
assert(new_locale);
for (LocaleVariable p = 0; p < _VARIABLE_LC_MAX; p++) {
if (p == VARIABLE_LANGUAGE)
continue;
if (isempty(new_locale[p]))
continue;
if (locale_is_installed(new_locale[p]))
continue;
r = locale_gen_enable_locale(new_locale[p]);
if (r == -ENOEXEC) {
log_error_errno(r, "Refused to enable locale for generation: %m");
return sd_bus_error_setf(error,
SD_BUS_ERROR_INVALID_ARGS,
"Specified locale is not installed and non-UTF-8 locale will not be auto-generated: %s",
new_locale[p]);
} else if (r == -EINVAL) {
log_error_errno(r, "Failed to enable invalid locale %s for generation.", new_locale[p]);
return sd_bus_error_setf(error,
SD_BUS_ERROR_INVALID_ARGS,
"Can not enable locale generation for invalid locale: %s",
new_locale[p]);
} else if (r < 0) {
log_error_errno(r, "Failed to enable locale for generation: %m");
return sd_bus_error_set_errnof(error, r, "Failed to enable locale generation: %m");
}
r = locale_gen_run();
if (r < 0) {
log_error_errno(r, "Failed to generate locale: %m");
return sd_bus_error_set_errnof(error, r, "Failed to generate locale: %m");
}
}
return 0;
}
static int method_set_locale(sd_bus_message *m, void *userdata, sd_bus_error *error) {
_cleanup_(locale_variables_freep) char *new_locale[_VARIABLE_LC_MAX] = {};
_cleanup_strv_free_ char **settings = NULL, **l = NULL;
@ -305,6 +347,7 @@ static int method_set_locale(sd_bus_message *m, void *userdata, sd_bus_error *er
bool modified = false;
int interactive, r;
char **i;
bool use_localegen;
assert(m);
assert(c);
@ -317,11 +360,13 @@ static int method_set_locale(sd_bus_message *m, void *userdata, sd_bus_error *er
if (r < 0)
return r;
use_localegen = locale_gen_check_available();
/* If single locale without variable name is provided, then we assume it is LANG=. */
if (strv_length(l) == 1 && !strchr(l[0], '=')) {
if (!locale_is_valid(l[0]))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid locale specification: %s", l[0]);
if (locale_is_installed(l[0]) <= 0)
if (!use_localegen && locale_is_installed(l[0]) <= 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified locale is not installed: %s", l[0]);
new_locale[VARIABLE_LANG] = strdup(l[0]);
@ -333,7 +378,7 @@ static int method_set_locale(sd_bus_message *m, void *userdata, sd_bus_error *er
/* Check whether a variable is valid */
STRV_FOREACH(i, l) {
r = process_locale_list_item(*i, new_locale, error);
r = process_locale_list_item(*i, new_locale, use_localegen, error);
if (r < 0)
return r;
}
@ -392,9 +437,17 @@ static int method_set_locale(sd_bus_message *m, void *userdata, sd_bus_error *er
if (r == 0)
return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
/* Generate locale in case it is missing and the system is using locale-gen */
if (use_localegen) {
r = locale_gen_process_locale(new_locale, error);
if (r < 0)
return r;
}
for (LocaleVariable p = 0; p < _VARIABLE_LC_MAX; p++)
free_and_replace(c->locale[p], new_locale[p]);
/* Write locale configuration */
r = locale_write_data(c, &settings);
if (r < 0) {
log_error_errno(r, "Failed to set locale: %m");