firstboot: auto-fill keymap/locale questions with data from firmware, if available (#42177)

Let's make the installer experience a tiny bit nicer, and prefill
everything with the data the firmware gives us.
This commit is contained in:
Yu Watanabe
2026-05-21 03:50:43 +09:00
committed by GitHub
15 changed files with 169 additions and 45 deletions
-15
View File
@@ -136,24 +136,12 @@ SPDX-License-Identifier: LGPL-2.1-or-later
the Varlinkifcation has progressed various non-desktop usescase might not
need D-Bus running at all anymore.
- Reuse the LoaderKeyboardLayout EFI variable/find_vconsole_keymap_for_bcp47()
to pre-select kbd layout in systemd-firstboot. (Right now it's only used in
vconsole-setup as fallback).
- Start using the PlatformLang EFI variable to pre-select language in
systemd-firstboot. (At least HP laptops seem to make good use of/configure
the value)
- format-table: introduce the concept of a "title" for a table, which remains
closely associated with the table. in most cases where want to output
multiple tables from the same tool we want to separate things with a title,
hence we might as well associate the title with the table itself, and
streamline a few things.
- report: add filtering by metric prefix, so that we reports don't have to
include everything, and we have a way to have "small" and "big" reports, that
have different number of fields.
- allow metrics to indicate which values mean
"nothing"/"invalid"/"zero"/"please-suppress". Then use that to reduce noise
in systemd-report output.
@@ -912,7 +900,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
possibly up to 100ms supposedly)
- **EFI:**
- honor language efi variables for default language selection (if there are any?)
- honor timezone efi variables for default timezone selection (if there are any?)
- enable LockMLOCK to take a percentage value relative to physical memory
@@ -3038,8 +3025,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
data in the image, make sure the image filename actually matches this, so
that images cannot be misused.
- when no locale is configured, default to UEFI's PlatformLang variable
- when switching root from initrd to host, set the machine_id env var so that
if the host has no machine ID set yet we continue to use the random one the
initrd had set.
+12 -2
View File
@@ -283,6 +283,7 @@ static void clear_by_backspace(size_t n) {
int ask_string_full(
char **ret,
const char *prefill,
GetCompletionsCallback get_completions,
void *userdata,
const char *text, ...) {
@@ -296,8 +297,17 @@ int ask_string_full(
_cleanup_free_ char *string = NULL;
size_t n = 0;
if (get_completions) {
/* Figure out what string to preselect the query with */
if (prefill) {
/* Prefill query with explicit data if specified */
string = strdup(prefill);
if (!string)
return -ENOMEM;
n = strlen(string);
} else if (get_completions) {
/* Otherwise, figure out what string to preselect the query with */
_cleanup_strv_free_ char **completions = NULL;
r = get_completions("", GET_COMPLETIONS_PRESELECT, &completions, userdata);
if (r < 0)
+2 -2
View File
@@ -95,8 +95,8 @@ typedef enum GetCompletionsFlags {
} GetCompletionsFlags;
typedef int (*GetCompletionsCallback)(const char *key, GetCompletionsFlags flags, char ***ret_list, void *userdata);
int ask_string_full(char **ret, GetCompletionsCallback get_completions, void *userdata, const char *text, ...) _printf_(4, 5);
#define ask_string(ret, text, ...) ask_string_full(ret, NULL, NULL, text, ##__VA_ARGS__)
int ask_string_full(char **ret, const char *prefill, GetCompletionsCallback get_completions, void *userdata, const char *text, ...) _printf_(5, 6);
#define ask_string(ret, text, ...) ask_string_full(ret, NULL, NULL, NULL, text, ##__VA_ARGS__)
bool any_key_to_proceed(void);
int show_menu(char **x, size_t n_columns, size_t column_width, unsigned ellipsize_percentage, const char *grey_prefix, bool with_numbers);
+13
View File
@@ -17,6 +17,7 @@
#include "efivars.h"
#include "errno-util.h"
#include "fd-util.h"
#include "locale-setup.h"
#include "log.h"
#include "pager.h"
#include "pretty-print.h"
@@ -501,6 +502,18 @@ int verb_status(int argc, char *argv[], uintptr_t _data, void *userdata) {
errno = -k;
printf(" Boot into FW: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal());
}
_cleanup_free_ char *lang = NULL;
k = locale_lang_from_efi(&lang, /* flags= */ 0);
if (k > 0)
printf(" Platform Lang: %s\n", lang);
else if (k == 0)
printf(" Platform Lang: n/a\n");
else {
errno = -k;
printf(" Platform Lang: %sfailed%s (%m)\n", ansi_highlight_red(), ansi_normal());
}
printf("\n");
if (loader) {
+18 -12
View File
@@ -8,6 +8,7 @@
#include "sd-varlink.h"
#include "alloc-util.h"
#include "ansi-color.h"
#include "ask-password-api.h"
#include "build.h"
#include "bus-error.h"
@@ -26,6 +27,7 @@
#include "format-table.h"
#include "fs-util.h"
#include "glyph-util.h"
#include "help-util.h"
#include "hostname-util.h"
#include "image-policy.h"
#include "kbd-util.h"
@@ -44,7 +46,6 @@
#include "password-quality-util.h"
#include "path-util.h"
#include "plymouth-util.h"
#include "pretty-print.h"
#include "proc-cmdline.h"
#include "prompt-util.h"
#include "runtime-scope.h"
@@ -293,8 +294,12 @@ static int prompt_locale(int rfd, sd_varlink **mute_console_link) {
} else {
print_welcome(rfd, mute_console_link);
_cleanup_free_ char *prefill = NULL;
(void) locale_lang_from_efi(&prefill, LOCALE_REQUIRE_INSTALLED|LOCALE_SUPPRESS_EN_US);
r = prompt_loop("Please enter the new system locale name or number",
GLYPH_WORLD,
prefill,
locales,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
@@ -312,6 +317,7 @@ static int prompt_locale(int rfd, sd_varlink **mute_console_link) {
r = prompt_loop("Please enter the new system message locale name or number",
GLYPH_WORLD,
/* prefill= */ NULL,
locales,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
@@ -454,9 +460,13 @@ static int prompt_keymap(int rfd, sd_varlink **mute_console_link) {
print_welcome(rfd, mute_console_link);
_cleanup_free_ char *prefill = NULL;
(void) vconsole_keymap_from_efi(&prefill);
return prompt_loop(
"Please enter the new keymap name or number",
GLYPH_KEYBOARD,
prefill,
kmaps,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
@@ -573,6 +583,7 @@ static int prompt_timezone(int rfd, sd_varlink **mute_console_link) {
return prompt_loop(
"Please enter the new timezone name or number",
GLYPH_CLOCK,
/* prefill= */ NULL,
zones,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 30,
@@ -680,6 +691,7 @@ static int prompt_hostname(int rfd, sd_varlink **mute_console_link) {
r = prompt_loop("Please enter the new hostname",
GLYPH_LABEL,
/* prefill= */ NULL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 100,
@@ -888,6 +900,7 @@ static int prompt_root_shell(int rfd, sd_varlink **mute_console_link) {
return prompt_loop(
"Please enter the new root shell",
GLYPH_SHELL,
/* prefill= */ NULL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 0,
@@ -1240,29 +1253,22 @@ static int process_reset(int rfd) {
}
static int help(void) {
_cleanup_free_ char *link = NULL;
_cleanup_(table_unrefp) Table *options = NULL;
int r;
r = terminal_urlify_man("systemd-firstboot", "1", &link);
if (r < 0)
return log_oom();
r = option_parser_get_help_table(&options);
if (r < 0)
return r;
printf("%s [OPTIONS...]\n\n"
"%sConfigures basic settings of the system.%s\n\n",
program_invocation_short_name,
ansi_highlight(),
ansi_normal());
help_cmdline("[OPTIONS...]");
help_abstract("Configures basic settings of the system.");
help_section("Options");
r = table_print_or_warn(options);
if (r < 0)
return r;
printf("\nSee the %s for details.\n", link);
help_man_page_reference("systemd-firstboot", "1");
return 0;
}
+2
View File
@@ -117,6 +117,7 @@ int prompt_groups(const char *username, char ***ret_groups) {
_cleanup_free_ char *s = NULL;
r = ask_string_full(
&s,
/* prefill= */ NULL,
group_completion_callback,
&available,
"%s Please enter an auxiliary group for user %s (empty to continue, \"list\" to list available groups): ",
@@ -231,6 +232,7 @@ int prompt_shell(const char *username, char **ret_shell) {
return prompt_loop(
q,
GLYPH_SHELL,
/* prefill= */ NULL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 0,
+1
View File
@@ -3009,6 +3009,7 @@ static int create_interactively(void) {
r = prompt_loop("Please enter user name to create",
GLYPH_IDCARD,
/* prefill= */ NULL,
/* menu= */ NULL,
/* accepted= */ NULL,
/* ellipsize_percentage= */ 60,
+76
View File
@@ -5,10 +5,12 @@
#include <unistd.h>
#include "alloc-util.h"
#include "efivars.h"
#include "env-file.h"
#include "env-util.h"
#include "errno-util.h"
#include "fd-util.h"
#include "iovec-util.h"
#include "locale-setup.h"
#include "log.h"
#include "proc-cmdline.h"
@@ -318,3 +320,77 @@ const char* etc_vconsole_conf(void) {
return cached;
}
int locale_lang_from_efi(char **ret, LocaleLangFromEfiFlags flags) {
int r;
assert(ret);
if (!is_efi_boot()) {
*ret = NULL;
return 0;
}
/* NB: unlike most other UEFI variables, PlatformLang is actually in 7bit ASCII! Hence we are not
* using efi_get_variable_string() here */
_cleanup_(iovec_done) struct iovec iov = {};
r = efi_get_variable(EFI_GLOBAL_VARIABLE_STR("PlatformLang"), /* ret_attribute= */ NULL, &iov.iov_base, &iov.iov_len);
if (r == -ENOENT) {
*ret = NULL;
return 0;
}
if (r < 0)
return log_debug_errno(r, "Failed to read PlatformLang EFI variable: %m");
_cleanup_free_ char *tag = NULL;
r = make_cstring(iov.iov_base, iov.iov_len, MAKE_CSTRING_ALLOW_TRAILING_NUL, &tag);
if (r < 0)
return log_debug_errno(r, "Failed to convert PlatformLang EFI variable to C string: %m");
/* Convert the UEFI BCP47 language tag into a glibc tag. We'll not bother with the complexity of the
* whole spec, but just convert "xx-XX" into "xx_XX", with some flexibility on the case
* sensitivity. This is a best-effort thing anyway. */
if (strlen(tag) != 5 ||
!strchr(LETTERS, tag[0]) ||
!strchr(LETTERS, tag[1]) ||
tag[2] != '-' ||
!strchr(LETTERS, tag[3]) ||
!strchr(LETTERS, tag[4])) {
log_debug("PlatformLang variable does not have the form 'xx-XX', ignoring: %s", tag);
*ret = NULL;
return 0;
}
tag[0] = ascii_tolower(tag[0]);
tag[1] = ascii_tolower(tag[1]);
tag[2] = '_';
tag[3] = ascii_toupper(tag[3]);
tag[4] = ascii_toupper(tag[4]);
/* Let's optionally suppress en_US locale, since that's almost certainly just the built-in default
* locale of the firmware. Since we typically prefer C.UTF-8 over en_US.UTF-8 as default, let's hence
* suppress it. */
if (FLAGS_SET(flags, LOCALE_SUPPRESS_EN_US) && streq(tag, "en_US")) {
log_debug("Firmware language is en_US, suppressing because likely just the firmware default.");
*ret = NULL;
return 0;
}
if (!strextend(&tag, ".UTF-8"))
return -ENOMEM;
if (FLAGS_SET(flags, LOCALE_REQUIRE_INSTALLED)) {
r = locale_is_installed(tag);
if (r < 0)
return r;
if (r == 0) {
log_debug("Determined locale '%s' from PlatformLang, but it isn't installed, ignoring.", tag);
*ret = NULL;
return 0;
}
}
*ret = TAKE_PTR(tag);
return 1;
}
+7
View File
@@ -31,3 +31,10 @@ int locale_setup(char ***environment);
const char* etc_locale_conf(void);
const char* etc_vconsole_conf(void);
typedef enum LocaleLangFromEfiFlags {
LOCALE_REQUIRE_INSTALLED = 1 << 0,
LOCALE_SUPPRESS_EN_US = 1 << 1,
} LocaleLangFromEfiFlags;
int locale_lang_from_efi(char **ret, LocaleLangFromEfiFlags flags);
+2
View File
@@ -60,6 +60,7 @@ static int get_completions(
int prompt_loop(
const char *text,
Glyph emoji,
const char *prefill, /* if non-NULL: prefill prompt with this string */
char **menu, /* if non-NULL: choices to suggest */
char **accepted, /* if non-NULL: choices to accept (should be a superset of 'menu') */
unsigned ellipsize_percentage,
@@ -116,6 +117,7 @@ int prompt_loop(
_cleanup_free_ char *p = NULL;
r = ask_string_full(
&p,
prefill,
get_completions,
&(CompletionData) { menu, accepted },
"%s%s%s%s: ",
+1
View File
@@ -14,6 +14,7 @@ typedef enum PromptFlags {
int prompt_loop(const char *text,
Glyph emoji,
const char *prefill,
char **menu,
char **accepted,
unsigned ellipsize_percentage,
+27
View File
@@ -4,6 +4,7 @@
#include <unistd.h>
#include "alloc-util.h"
#include "efivars.h"
#include "env-util.h"
#include "extract-word.h"
#include "fd-util.h"
@@ -655,6 +656,32 @@ int find_vconsole_keymap_for_bcp47(const char *tag, char **ret) {
return 1;
}
int vconsole_keymap_from_efi(char **ret) {
int r;
assert(ret);
if (!is_efi_boot()) {
*ret = NULL;
return 0;
}
_cleanup_free_ char *tag = NULL;
r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderKeyboardLayout"), &tag);
if (r == -ENOENT) {
*ret = NULL;
return 0;
}
if (r < 0)
return log_debug_errno(r, "Failed to read LoaderKeyboardLayout EFI variable: %m");
r = find_vconsole_keymap_for_bcp47(tag, ret);
if (r < 0)
return log_debug_errno(r, "Failed to look up vconsole keymap for firmware tag '%s': %m", tag);
return r;
}
int vconsole_serialize(const VCContext *vc, const X11Context *xc, char ***env) {
int r;
+1
View File
@@ -40,5 +40,6 @@ int vconsole_convert_to_x11(const VCContext *vc, X11VerifyCallback verify, X11Co
int x11_convert_to_vconsole(const X11Context *xc, VCContext *ret);
int find_vconsole_keymap_for_bcp47(const char *tag, char **ret);
int vconsole_keymap_from_efi(char **ret);
int vconsole_serialize(const VCContext *vc, const X11Context *xc, char ***env);
+4
View File
@@ -384,6 +384,7 @@ static int prompt_block_device(sd_varlink **repart_link, char **ret_node) {
r = prompt_loop("Please enter target disk device",
GLYPH_COMPUTER_DISK,
/* prefill= */ NULL,
menu,
accepted,
/* ellipsize_percentage= */ 20,
@@ -540,6 +541,7 @@ static int prompt_erase(
"Please type 'keep' to install the OS in addition to what the disk already contains, or 'erase' to erase all data on the disk" :
"Please type 'erase' to confirm that all data on the disk shall be erased",
GLYPH_BROOM,
/* prefill= */ NULL,
/* menu= */ l,
/* accepted= */ l,
/* ellipsize_percentage= */ 20,
@@ -578,6 +580,7 @@ static int prompt_touch_variables(void) {
_cleanup_free_ char *reply = NULL;
r = prompt_loop("Type 'yes' to register OS installation in firmware variables of the local system, 'no' otherwise",
GLYPH_ROCKET,
/* prefill= */ "yes",
/* menu= */ l,
/* accepted= */ l,
/* ellipsize_percentage= */ 20,
@@ -616,6 +619,7 @@ static int prompt_confirm(void) {
r = prompt_loop(arg_summary ? "Please type 'yes' to confirm the choices above and begin the installation" :
"Please type 'yes' to begin the installation",
GLYPH_WARNING_SIGN,
/* prefill= */ NULL,
/* menu= */ l,
/* accepted= */ l,
/* ellipsize_percentage= */ 20,
+3 -14
View File
@@ -15,7 +15,6 @@
#include "alloc-util.h"
#include "creds-util.h"
#include "efivars.h"
#include "env-file.h"
#include "errno-util.h"
#include "fd-util.h"
@@ -76,25 +75,15 @@ static void context_merge_config(
static int context_read_efi(Context *c) {
_cleanup_(context_done) Context v = {};
_cleanup_free_ char *tag = NULL;
int r;
assert(c);
if (!is_efi_boot())
return 0;
r = efi_get_variable_string(EFI_LOADER_VARIABLE_STR("LoaderKeyboardLayout"), &tag);
if (r == -ENOENT)
return 0;
r = vconsole_keymap_from_efi(&v.keymap);
if (r < 0)
return log_debug_errno(r, "Failed to read LoaderKeyboardLayout EFI variable, ignoring: %m");
r = find_vconsole_keymap_for_bcp47(tag, &v.keymap);
if (r < 0)
return log_debug_errno(r, "Failed to look up vconsole keymap for firmware tag '%s', ignoring: %m", tag);
return r;
if (r == 0) {
log_debug("No vconsole keymap matches firmware-provided keyboard layout '%s', ignoring.", tag);
log_debug("No vconsole keymap matches firmware-provided keyboard layout, ignoring.");
return 0;
}