summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Klumpp <matthias@tenstral.net>2021-01-08 23:59:38 +0100
committerMatthias Klumpp <matthias@tenstral.net>2021-01-12 23:15:12 +0100
commit8f20232fcb52dbe6255f3df6101fc057af90bcfa (patch)
treeea0fc53090f6b2bc14e1a72a93ef3e24a2bd37d9
parentbd47b0dac4a1ff6e686c99b9958693e86d44007b (diff)
downloadsystemd-8f20232fcb52dbe6255f3df6101fc057af90bcfa.tar.gz
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.
-rw-r--r--meson.build8
-rw-r--r--meson_options.txt2
-rw-r--r--src/locale/keymap-util.c211
-rw-r--r--src/locale/keymap-util.h4
-rw-r--r--src/locale/localectl.c6
-rw-r--r--src/locale/localed.c59
6 files changed, 286 insertions, 4 deletions
diff --git a/meson.build b/meson.build
index 841dd97003..4a56ee3ee4 100644
--- a/meson.build
+++ b/meson.build
@@ -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')
diff --git a/meson_options.txt b/meson_options.txt
index d1096655dc..2704f65baa 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -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')
diff --git a/src/locale/keymap-util.c b/src/locale/keymap-util.c
index cb8153f4fe..697133ad84 100644
--- a/src/locale/keymap-util.c
+++ b/src/locale/keymap-util.c
@@ -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
+}
diff --git a/src/locale/keymap-util.h b/src/locale/keymap-util.h
index 49976472ef..c087dbcbbe 100644
--- a/src/locale/keymap-util.h
+++ b/src/locale/keymap-util.h
@@ -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);
diff --git a/src/locale/localectl.c b/src/locale/localectl.c
index 7d2e887660..7d3d3f8c18 100644
--- a/src/locale/localectl.c
+++ b/src/locale/localectl.c
@@ -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));
diff --git a/src/locale/localed.c b/src/locale/localed.c
index 736dacdee9..12073bd6f3 100644
--- a/src/locale/localed.c
+++ b/src/locale/localed.c
@@ -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");