diff --git a/man/kernel-install.xml b/man/kernel-install.xml
index 0bf57b4d53..26ae87f1d4 100644
--- a/man/kernel-install.xml
+++ b/man/kernel-install.xml
@@ -562,6 +562,13 @@
on the file name and extension: Files named initrd* will be installed as INITRD-FILEs,
and files named microcode* will be prepended before INITRD-FILEs.
+ $KERNEL_INSTALL_ENTRY_NAME is set for the plugins to the resolved entry name,
+ derived by expanding specifiers in the entry name format string. This is the base name (without file
+ extension) used for UKI files, loader entry configuration files, and similar. By default this is
+ ENTRY-TOKEN-KERNEL-VERSION,
+ matching the traditional naming. See $KERNEL_INSTALL_ENTRY_NAME_FORMAT below for
+ details on the available specifiers.
+
@@ -589,6 +596,73 @@
The last two variables may also be set in install.conf. Variables set in the
environment take precedence over the values specified in the config file.
+
+ $KERNEL_INSTALL_ENTRY_NAME_FORMAT can be set to configure the format string
+ used to derive the entry name for boot loader entries and UKI files. The format string is expanded
+ using specifier expansion. May also be configured as entry_name_format= in
+ install.conf. The following specifiers are understood:
+
+
+ Specifiers available in $KERNEL_INSTALL_ENTRY_NAME_FORMAT
+
+
+
+
+
+
+ Specifier
+ Meaning
+ Details
+
+
+
+
+
+
+
+ %e
+ Entry token
+ The entry token, same as $KERNEL_INSTALL_ENTRY_TOKEN.
+
+
+
+
+
+
+
+
+ %v
+ Kernel version
+ The kernel version being installed. Note that unlike the common specifier %v which expands to the currently running kernel version (i.e. uname -r), here it refers to the target kernel version passed to kernel-install.
+
+
+
+
+
+
+
+
+ The resolved entry name must be a valid filename (no path separators). Defaults to
+ %e-%v, which produces the traditional
+ ENTRY-TOKEN-KERNEL-VERSION
+ naming.
+
+ For example, setting entry_name_format=%M_%A_%a on a system with
+ IMAGE_ID=myos, IMAGE_VERSION=20250830, and architecture
+ x86-64 would produce entry names like
+ myos_20250830_x86-64.
+
+ Note that the entry name is resolved at the time of each
+ kernel-install invocation. If the format string uses specifiers that
+ depend on mutable system state (e.g. %M, %A,
+ %H), and that state changes between installing and removing a kernel,
+ the remove operation may compute a different entry name than
+ add did, leaving stale files on the boot partition. The
+ entry_name_format= setting should therefore remain stable across a
+ kernel's install/remove lifecycle. Additionally, the format should produce a unique name
+ for each installed kernel, e.g. by including %v (kernel version) or
+ %A (IMAGE_VERSION); otherwise installing multiple
+ kernels will overwrite each other's boot entries.
@@ -726,7 +800,8 @@
BOOT_ROOT=,
layout=,
initrd_generator=,
- uki_generator=.
+ uki_generator=,
+ entry_name_format=.
See the Environment variables section above for details.
diff --git a/src/bootctl/bootctl-install.c b/src/bootctl/bootctl-install.c
index 1610d11e92..080900c1f5 100644
--- a/src/bootctl/bootctl-install.c
+++ b/src/bootctl/bootctl-install.c
@@ -338,7 +338,8 @@ static int load_kernel_install_layout(InstallContext *c) {
/* ret_boot_root= */ NULL,
&layout,
/* ret_initrd_generator= */ NULL,
- /* ret_uki_generator= */ NULL);
+ /* ret_uki_generator= */ NULL,
+ /* ret_entry_name_format= */ NULL);
if (r <= 0)
return r;
diff --git a/src/kernel-install/90-loaderentry.install.in b/src/kernel-install/90-loaderentry.install.in
index 66cfec8886..f791cab3c2 100755
--- a/src/kernel-install/90-loaderentry.install.in
+++ b/src/kernel-install/90-loaderentry.install.in
@@ -35,6 +35,7 @@ fi
MACHINE_ID="${KERNEL_INSTALL_MACHINE_ID:?}"
ENTRY_TOKEN="${KERNEL_INSTALL_ENTRY_TOKEN:?}"
BOOT_ROOT="${KERNEL_INSTALL_BOOT_ROOT:?}"
+ENTRY_NAME="${KERNEL_INSTALL_ENTRY_NAME:-$ENTRY_TOKEN-$KERNEL_VERSION}"
[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
if [ "$BOOT_MNT" = '/' ]; then
@@ -45,7 +46,7 @@ fi
KERNEL_DEST="$ENTRY_DIR_ABS/linux"
KERNEL_ENTRY="$ENTRY_DIR/linux"
-LOADER_ENTRY="$BOOT_ROOT/loader/entries/$ENTRY_TOKEN-$KERNEL_VERSION.conf"
+LOADER_ENTRY="$BOOT_ROOT/loader/entries/$ENTRY_NAME.conf"
case "$COMMAND" in
remove)
diff --git a/src/kernel-install/90-uki-copy.install b/src/kernel-install/90-uki-copy.install
index a55c7b8e84..48ed86ccb5 100755
--- a/src/kernel-install/90-uki-copy.install
+++ b/src/kernel-install/90-uki-copy.install
@@ -28,6 +28,7 @@ KERNEL_IMAGE="$4"
ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN"
BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT"
+ENTRY_NAME="${KERNEL_INSTALL_ENTRY_NAME:-$ENTRY_TOKEN-$KERNEL_VERSION}"
UKI_DIR="$BOOT_ROOT/EFI/Linux"
@@ -39,11 +40,11 @@ case "$COMMAND" in
exit 0
fi
[ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \
- echo "Removing $UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION*.efi and extras"
+ echo "Removing $UKI_DIR/$ENTRY_NAME*.efi and extras"
exec rm -rf \
- "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi" \
- "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi.extra.d/" \
- "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+"*".efi"
+ "$UKI_DIR/$ENTRY_NAME.efi" \
+ "$UKI_DIR/$ENTRY_NAME.efi.extra.d/" \
+ "$UKI_DIR/$ENTRY_NAME+"*".efi"
;;
add)
;;
@@ -67,24 +68,24 @@ if [ -f "$TRIES_FILE" ]; then
echo "$TRIES_FILE does not contain an integer." >&2
exit 1
fi
- if [ -f "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi" ]; then
+ if [ -f "$UKI_DIR/$ENTRY_NAME.efi" ]; then
[ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \
- echo "Removing previous UKI '$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi' without boot counting." >&2
- rm -f "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi"
+ echo "Removing previous UKI '$UKI_DIR/$ENTRY_NAME.efi' without boot counting." >&2
+ rm -f "$UKI_DIR/$ENTRY_NAME.efi"
fi
- for uki in "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+"*.efi; do
- [ "$uki" = "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+*.efi" ] && break
+ for uki in "$UKI_DIR/$ENTRY_NAME+"*.efi; do
+ [ "$uki" = "$UKI_DIR/$ENTRY_NAME+*.efi" ] && break
[ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \
echo "Removing previous UKI '$uki' that has not yet booted successfully." >&2
rm -f "$uki"
done
- UKI_FILE="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+$TRIES.efi"
+ UKI_FILE="$UKI_DIR/$ENTRY_NAME+$TRIES.efi"
else
- UKI_FILE="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi"
+ UKI_FILE="$UKI_DIR/$ENTRY_NAME.efi"
fi
-EXTRA_DIR="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi.extra.d"
+EXTRA_DIR="$UKI_DIR/$ENTRY_NAME.efi.extra.d"
# If there is a UKI named uki.efi on the staging area use that, if not use what
# was passed in as $KERNEL_IMAGE but insist it has a .efi extension
diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c
index 69fdbcfc34..be097cd5cd 100644
--- a/src/kernel-install/kernel-install.c
+++ b/src/kernel-install/kernel-install.c
@@ -35,6 +35,7 @@
#include "path-util.h"
#include "recurse-dir.h"
#include "rm-rf.h"
+#include "specifier.h"
#include "stat-util.h"
#include "string-table.h"
#include "string-util.h"
@@ -108,12 +109,19 @@ typedef struct Context {
char **initrds;
char *initrd_generator;
char *uki_generator;
+ char *entry_name_format;
+ char *entry_name;
char *staging_area;
char **plugins;
char **argv;
char **envp;
} Context;
+/* Longest filename suffix plugins append to the entry name (from 90-uki-copy.install) */
+#define ENTRY_NAME_SUFFIX_MAX STRLEN(".efi.extra.d")
+
+#define DEFAULT_ENTRY_NAME_FORMAT "%e-%v"
+
#define CONTEXT_NULL \
(Context) { \
.rfd = XAT_FDROOT, \
@@ -137,6 +145,8 @@ static void context_done(Context *c) {
strv_free(c->initrds);
free(c->initrd_generator);
free(c->uki_generator);
+ free(c->entry_name_format);
+ free(c->entry_name);
if (c->action == ACTION_INSPECT)
free(c->staging_area);
else
@@ -199,6 +209,9 @@ static int context_copy(const Context *source, Context *ret) {
if (r < 0)
return r;
r = strdup_to(©.uki_generator, source->uki_generator);
+ if (r < 0)
+ return r;
+ r = strdup_to(©.entry_name_format, source->entry_name_format);
if (r < 0)
return r;
r = strdup_to(©.staging_area, source->staging_area);
@@ -327,6 +340,63 @@ static int context_set_uki_generator(Context *c, const char *s, const char *sour
return context_set_string(s, source, "UKI_GENERATOR", &c->uki_generator);
}
+static int context_set_entry_name_format(Context *c, const char *s, const char *source) {
+ assert(c);
+ return context_set_string(s, source, "ENTRY_NAME_FORMAT", &c->entry_name_format);
+}
+
+static int context_resolve_entry_name(Context *c) {
+ assert(c);
+
+ if (c->entry_name)
+ return 0;
+
+ const Specifier table[] = {
+ { 'e', specifier_string, c->entry_token },
+ { 'm', specifier_id128, &c->machine_id },
+ { 'v', specifier_string, c->version ?: "KERNEL_VERSION" },
+ { 'a', specifier_architecture, NULL },
+ { 'A', specifier_os_image_version, NULL },
+ { 'B', specifier_os_build_id, NULL },
+ { 'H', specifier_hostname, NULL },
+ { 'l', specifier_short_hostname, NULL },
+ { 'q', specifier_pretty_hostname, NULL },
+ { 'M', specifier_os_image_id, NULL },
+ { 'o', specifier_os_id, NULL },
+ { 'w', specifier_os_version_id, NULL },
+ { 'W', specifier_os_variant_id, NULL },
+ {}
+ };
+
+ _cleanup_free_ char *resolved = NULL;
+ int r;
+
+ r = specifier_printf(
+ c->entry_name_format ?: DEFAULT_ENTRY_NAME_FORMAT,
+ NAME_MAX - ENTRY_NAME_SUFFIX_MAX,
+ table,
+ arg_root,
+ /* userdata= */ c,
+ &resolved);
+ if (r < 0)
+ return log_error_errno(r, "Failed to expand entry name format '%s': %m",
+ c->entry_name_format ?: DEFAULT_ENTRY_NAME_FORMAT);
+
+ if (isempty(resolved))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Entry name format '%s' resolved to empty string.",
+ c->entry_name_format ?: DEFAULT_ENTRY_NAME_FORMAT);
+
+ if (!filename_is_valid(resolved))
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Entry name format '%s' resolved to invalid filename: %s",
+ c->entry_name_format ?: DEFAULT_ENTRY_NAME_FORMAT, resolved);
+
+ c->entry_name = TAKE_PTR(resolved);
+ log_debug("Using entry name: %s", c->entry_name);
+ return 0;
+}
+
static int context_set_version(Context *c, const char *s) {
assert(c);
@@ -449,12 +519,14 @@ static int context_load_environment(Context *c) {
(void) context_set_boot_root(c, getenv("BOOT_ROOT"), "environment");
(void) context_set_conf_root(c, getenv("KERNEL_INSTALL_CONF_ROOT"), "environment");
(void) context_set_plugins(c, getenv("KERNEL_INSTALL_PLUGINS"), "environment");
+ (void) context_set_entry_name_format(c, getenv("KERNEL_INSTALL_ENTRY_NAME_FORMAT"), "environment");
return 0;
}
static int context_load_install_conf(Context *c) {
_cleanup_free_ char *machine_id = NULL, *boot_root = NULL, *layout = NULL,
- *initrd_generator = NULL, *uki_generator = NULL;
+ *initrd_generator = NULL, *uki_generator = NULL,
+ *entry_name_format = NULL;
int r;
assert(c);
@@ -467,7 +539,8 @@ static int context_load_install_conf(Context *c) {
&boot_root,
&layout,
&initrd_generator,
- &uki_generator);
+ &uki_generator,
+ &entry_name_format);
if (r <= 0)
return r;
@@ -476,6 +549,7 @@ static int context_load_install_conf(Context *c) {
(void) context_set_layout(c, layout, "config");
(void) context_set_initrd_generator(c, initrd_generator, "config");
(void) context_set_uki_generator(c, uki_generator, "config");
+ (void) context_set_entry_name_format(c, entry_name_format, "config");
log_debug("Loaded config.");
return 0;
@@ -1032,6 +1106,10 @@ static int context_build_environment(Context *c) {
if (c->envp)
return 0;
+ r = context_resolve_entry_name(c);
+ if (r < 0)
+ return r;
+
r = strv_env_assign_many(&e,
"LC_COLLATE", SYSTEMD_DEFAULT_LOCALE,
"KERNEL_INSTALL_VERBOSE", one_zero(arg_verbose),
@@ -1043,7 +1121,8 @@ static int context_build_environment(Context *c) {
"KERNEL_INSTALL_INITRD_GENERATOR", strempty(c->initrd_generator),
"KERNEL_INSTALL_UKI_GENERATOR", strempty(c->uki_generator),
"KERNEL_INSTALL_BOOT_ENTRY_TYPE", boot_entry_type_to_string(c->entry_type),
- "KERNEL_INSTALL_STAGING_AREA", c->staging_area);
+ "KERNEL_INSTALL_STAGING_AREA", c->staging_area,
+ "KERNEL_INSTALL_ENTRY_NAME", c->entry_name);
if (r < 0)
return log_error_errno(r, "Failed to build environment variables for plugins: %m");
@@ -1413,6 +1492,10 @@ static int verb_inspect(int argc, char *argv[], uintptr_t _data, void *userdata)
TABLE_STRING, boot_entry_token_type_to_string(c.entry_token_type),
TABLE_FIELD, "Entry Token",
TABLE_STRING, c.entry_token,
+ TABLE_FIELD, "Entry Name Format",
+ TABLE_STRING, c.entry_name_format ?: DEFAULT_ENTRY_NAME_FORMAT,
+ TABLE_FIELD, "Entry Name",
+ TABLE_STRING, c.entry_name,
TABLE_FIELD, "Entry Directory",
TABLE_STRING, c.entry_dir,
TABLE_FIELD, "Kernel Version",
diff --git a/src/kernel-install/test-kernel-install.sh b/src/kernel-install/test-kernel-install.sh
index ee29890edd..32278f8a61 100755
--- a/src/kernel-install/test-kernel-install.sh
+++ b/src/kernel-install/test-kernel-install.sh
@@ -294,6 +294,99 @@ test ! -e "$BOOT_ROOT/hoge/1.1.1"
test -d "$BOOT_ROOT/hoge"
rmdir "$BOOT_ROOT/hoge"
+###########################################
+# tests for entry_name_format=
+###########################################
+
+# Test inspect with custom entry_name_format
+cat >>"$D/sources/install.conf" </dev/null
+echo "$output" | grep -E '"EntryName" : "the-token_1.1.1"' >/dev/null
+
+# Test type#1 BLS entry with custom entry_name_format
+rm -f "$D/sources/tries"
+cat >"$D/sources/install.conf" <"$D/sources/install.conf" <"$D/sources/tries"
+ KERNEL_INSTALL_PLUGINS="'${ukify_install}' '${loaderentry_install}' '${uki_copy_install}'" \
+ "$kernel_install" -v add 1.1.5 "$D/sources/linux" "$D/sources/initrd"
+ uki="${BOOT_ROOT}/EFI/Linux/the-token_1.1.5+56.efi"
+ test -f "$uki"
+ KERNEL_INSTALL_PLUGINS="'${ukify_install}' '${loaderentry_install}' '${uki_copy_install}'" \
+ "$kernel_install" -v remove 1.1.5
+ test ! -e "$uki"
+
+ rm -f "$D/sources/tries"
+fi
+
+# Test add-all with custom entry_name_format (exercises context_copy)
+KVER="$(uname -r)"
+if test -f "/usr/lib/modules/$KVER/vmlinuz"; then
+ rm -rf "$BOOT_ROOT"
+ mkdir -p "$BOOT_ROOT"
+ cat >"$D/sources/install.conf" <"$D/sources/install.conf" <&2 <&2 <= 0; /* Return 0 if we got -ENOENT above, 1 otherwise. */
}
diff --git a/src/shared/kernel-config.h b/src/shared/kernel-config.h
index d46aba8278..34c71387b9 100644
--- a/src/shared/kernel-config.h
+++ b/src/shared/kernel-config.h
@@ -12,7 +12,8 @@ int load_kernel_install_conf_at(
char **ret_boot_root,
char **ret_layout,
char **ret_initrd_generator,
- char **ret_uki_generator);
+ char **ret_uki_generator,
+ char **ret_entry_name_format);
static inline int load_kernel_install_conf(
const char *root,
@@ -21,7 +22,8 @@ static inline int load_kernel_install_conf(
char **ret_boot_root,
char **ret_layout,
char **ret_initrd_generator,
- char **ret_uki_generator) {
+ char **ret_uki_generator,
+ char **ret_entry_name_format) {
- return load_kernel_install_conf_at(root, XAT_FDROOT, conf_root, ret_machine_id, ret_boot_root, ret_layout, ret_initrd_generator, ret_uki_generator);
+ return load_kernel_install_conf_at(root, XAT_FDROOT, conf_root, ret_machine_id, ret_boot_root, ret_layout, ret_initrd_generator, ret_uki_generator, ret_entry_name_format);
}