The BootLoaderSpec (BLS) defines a scheme where different bootloaders can share a format for boot items and a configuration directory that accepts these common configurations as drop-in files.
Signed-off-by: Peter Jones <pjo...@redhat.com> Signed-off-by: Javier Martinez Canillas <javi...@redhat.com> Signed-off-by: Will Thompson <w...@endlessm.com> Signed-off-by: Alec Brown <alec.r.br...@oracle.com> --- Makefile.util.def | 16 + docs/grub.texi | 27 + grub-core/Makefile.core.def | 10 + grub-core/commands/blsuki.c | 1028 ++++++++++++++++++++++++++++++++ grub-core/commands/legacycfg.c | 4 +- grub-core/commands/menuentry.c | 8 +- grub-core/lib/vercmp.c | 317 ++++++++++ grub-core/normal/main.c | 6 + include/grub/lib/vercmp.h | 35 ++ include/grub/menu.h | 15 + include/grub/normal.h | 2 +- tests/vercmp_unit_test.c | 65 ++ 12 files changed, 1527 insertions(+), 6 deletions(-) create mode 100644 grub-core/commands/blsuki.c create mode 100644 grub-core/lib/vercmp.c create mode 100644 include/grub/lib/vercmp.h create mode 100644 tests/vercmp_unit_test.c diff --git a/Makefile.util.def b/Makefile.util.def index 038253b37..a911f2e0a 100644 --- a/Makefile.util.def +++ b/Makefile.util.def @@ -1373,6 +1373,22 @@ program = { ldadd = '$(LIBDEVMAPPER) $(LIBZFS) $(LIBNVPAIR) $(LIBGEOM)'; }; +program = { + testcase = native; + name = vercmp_unit_test; + common = tests/vercmp_unit_test.c; + common = tests/lib/unit_test.c; + common = grub-core/kern/list.c; + common = grub-core/kern/misc.c; + common = grub-core/tests/lib/test.c; + common = grub-core/lib/vercmp.c; + ldadd = libgrubmods.a; + ldadd = libgrubgcry.a; + ldadd = libgrubkern.a; + ldadd = grub-core/lib/gnulib/libgnu.a; + ldadd = '$(LIBDEVMAPPER) $(LIBZFS) $(LIBNVPAIR) $(LIBGEOM)'; +}; + program = { name = grub-menulst2cfg; mansection = 1; diff --git a/docs/grub.texi b/docs/grub.texi index d9b26fa36..19b0cc024 100644 --- a/docs/grub.texi +++ b/docs/grub.texi @@ -6417,6 +6417,7 @@ you forget a command, you can run the command @command{help} * background_image:: Load background image for active terminal * badram:: Filter out bad regions of RAM * blocklist:: Print a block list +* blscfg:: Load Boot Loader Specification menu entries * boot:: Start up your operating system * cat:: Show the contents of a file * clear:: Clear the screen @@ -6603,6 +6604,32 @@ Print a block list (@pxref{Block list syntax}) for @var{file}. @end deffn +@node blscfg +@subsection blscfg + +@deffn Command blscfg [@option{--path} dir] [@option{--show-default}] [@option{--show-non-default}] [@option{--entry} file] +Load Boot Loader Specification entries into the GRUB menu. + +The @option{--path} option overrides the default path to the directory containing +the BLS entries. If this option isn't used, the default location is +/loader/entries in @code{$BOOT}. + +The @option{--show-default} option allows the default boot entry to be added to the +GRUB menu from the BLS entries. + +The @option{--show-non-default} option allows non-default boot entries to be added to +the GRUB menu from the BLS entries. + +The @option{--entry} option allows specific boot entries to be added to the GRUB menu +from the BLS entries. + +The @option{--entry}, @option{--show-default}, and @option{--show-non-default} options +are used to filter which BLS entries are added to the GRUB menu. If none are +used, all entries in the default location or the location specified by @option{--path} +will be added to the GRUB menu. +@end deffn + + @node boot @subsection boot diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def index f70e02e69..f3b776c0a 100644 --- a/grub-core/Makefile.core.def +++ b/grub-core/Makefile.core.def @@ -845,6 +845,16 @@ module = { common = commands/blocklist.c; }; +module = { + name = blsuki; + common = commands/blsuki.c; + common = lib/vercmp.c; + enable = powerpc_ieee1275; + enable = efi; + enable = i386_pc; + enable = emu; +}; + module = { name = boot; common = commands/boot.c; diff --git a/grub-core/commands/blsuki.c b/grub-core/commands/blsuki.c new file mode 100644 index 000000000..0f77fb568 --- /dev/null +++ b/grub-core/commands/blsuki.c @@ -0,0 +1,1028 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <grub/list.h> +#include <grub/types.h> +#include <grub/misc.h> +#include <grub/mm.h> +#include <grub/err.h> +#include <grub/dl.h> +#include <grub/extcmd.h> +#include <grub/i18n.h> +#include <grub/fs.h> +#include <grub/env.h> +#include <grub/file.h> +#include <grub/normal.h> +#include <grub/safemath.h> +#include <grub/lib/envblk.h> +#include <grub/lib/vercmp.h> + +GRUB_MOD_LICENSE ("GPLv3+"); + +#define GRUB_BLS_CONFIG_PATH "/loader/entries/" + +static const struct grub_arg_option bls_opt[] = + { + {"path", 'p', 0, N_("Specify path to find BLS entries."), N_("DIR"), ARG_TYPE_PATHNAME}, + {"show-default", 'd', 0, N_("Allow the default BLS entry to be added to the GRUB menu."), 0, ARG_TYPE_NONE}, + {"show-non-default", 'n', 0, N_("Allow the non-default BLS entries to be added to the GRUB menu."), 0, ARG_TYPE_NONE}, + {"entry", 'e', 0, N_("Allow specific BLS entries to be added to the GRUB menu."), N_("FILE"), ARG_TYPE_FILE}, + {0, 0, 0, 0, 0, 0} + }; + +struct keyval +{ + const char *key; + char *val; +}; + +static grub_blsuki_entry_t *entries = NULL; + +#define FOR_BLSUKI_ENTRIES(var) FOR_LIST_ELEMENTS (var, entries) + +static grub_err_t +blsuki_add_keyval (grub_blsuki_entry_t *entry, char *key, char *val) +{ + char *k, *v; + struct keyval **kvs, *kv; + grub_size_t size; + int new_n = entry->nkeyvals + 1; + + if (entry->keyvals_size == 0) + { + size = sizeof (struct keyval *); + kvs = grub_malloc (size); + if (kvs == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't allocate space for BLS key values"); + + entry->keyvals = kvs; + entry->keyvals_size = size; + } + else if (entry->keyvals_size < new_n * sizeof (struct keyval *)) + { + size = entry->keyvals_size * 2; + kvs = grub_realloc (entry->keyvals, size); + if (kvs == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't reallocate space for BLS key values"); + + entry->keyvals = kvs; + entry->keyvals_size = size; + } + + kv = grub_malloc (sizeof (struct keyval)); + if (kv == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't find space for new BLS key value"); + + k = grub_strdup (key); + if (k == NULL) + { + grub_free (kv); + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't find space for new BLS key value"); + } + + v = grub_strdup (val); + if (v == NULL) + { + grub_free (k); + grub_free (kv); + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't find space for new BLS key value"); + } + + kv->key = k; + kv->val = v; + + entry->keyvals[entry->nkeyvals] = kv; + entry->nkeyvals = new_n; + + return GRUB_ERR_NONE; +} + +/* + * Find the value of the key named by keyname. If there are allowed to be + * more than one, pass a pointer to an int set to -1 the first time, and pass + * the same pointer through each time after, and it'll return them in sorted + * order as defined in the BLS fragment file. + */ +static char * +blsuki_get_val (grub_blsuki_entry_t *entry, const char *keyname, int *last) +{ + int idx, start = 0; + struct keyval *kv = NULL; + char *ret = NULL; + + if (last != NULL) + start = *last + 1; + + for (idx = start; idx < entry->nkeyvals; idx++) + { + kv = entry->keyvals[idx]; + + if (grub_strcmp (keyname, kv->key) == 0) + { + ret = kv->val; + break; + } + } + + if (last != NULL) + { + if (idx == entry->nkeyvals) + *last = -1; + else + *last = idx; + } + + return ret; +} + +static grub_err_t +blsuki_add_entry (grub_blsuki_entry_t *entry) +{ + grub_blsuki_entry_t *e, *last = NULL; + grub_err_t err; + int rc; + + if (entries == NULL) + { + grub_dprintf ("blsuki", "Add entry with id \"%s\"\n", entry->filename); + entries = entry; + return GRUB_ERR_NONE; + } + + FOR_BLSUKI_ENTRIES (e) + { + err = grub_split_vercmp (entry->filename, e->filename, true, &rc); + if (err != GRUB_ERR_NONE) + return err; + + if (rc == GRUB_VERCMP_SAME) + return grub_error (GRUB_ERR_BAD_ARGUMENT, "duplicate file"); + + if (rc == GRUB_VERCMP_NEWER) + { + grub_dprintf ("blsuki", "Add entry with id \"%s\"\n", entry->filename); + grub_list_push (GRUB_AS_LIST_P (&e), GRUB_AS_LIST (entry)); + if (entry->next == entries) + { + entries = entry; + entry->prev = NULL; + } + else + last->next = entry; + return GRUB_ERR_NONE; + } + last = e; + } + + if (last != NULL) + { + grub_dprintf ("blsuki", "Add entry with id \"%s\"\n", entry->filename); + last->next = entry; + entry->prev = &last; + } + + return GRUB_ERR_NONE; +} + +static grub_err_t +bls_read_entry (grub_file_t f, grub_blsuki_entry_t *entry) +{ + grub_err_t err = GRUB_ERR_NONE; + + for (;;) + { + char *buf; + char *separator; + + buf = grub_file_getline (f); + if (buf == NULL) + break; + + while (buf != NULL && buf[0] != '\0' && (buf[0] == ' ' || buf[0] == '\t')) + buf++; + if (buf[0] == '#') + { + grub_free (buf); + continue; + } + + separator = grub_strchr (buf, ' '); + + if (separator == NULL) + separator = grub_strchr (buf, '\t'); + + if (separator == NULL || separator[1] == '\0') + { + grub_free (buf); + break; + } + + separator[0] = '\0'; + + do + { + separator++; + } + while (*separator == ' ' || *separator == '\t'); + + err = blsuki_add_keyval (entry, buf, separator); + grub_free (buf); + if (err != GRUB_ERR_NONE) + break; + } + + return err; +} + +struct read_entry_info +{ + const char *devid; + const char *dirname; + grub_file_t file; +}; + +static int +blsuki_read_entry (const char *filename, + const struct grub_dirhook_info *dirhook_info __attribute__ ((__unused__)), + void *data) +{ + grub_size_t m = 0, n, ext_len = 5; + grub_err_t err; + char *p = NULL; + grub_file_t f = NULL; + grub_blsuki_entry_t *entry; + struct read_entry_info *info = (struct read_entry_info *) data; + + grub_dprintf ("blsuki", "filename: \"%s\"\n", filename); + + n = grub_strlen (filename); + + if (info->file != NULL) + { + f = info->file; + } + else + { + if (filename[0] == '.') + return 0; + + if (n <= 5) + return 0; + + if (grub_strcmp (filename + n - ext_len, ".conf") != 0) + return 0; + + p = grub_xasprintf ("(%s)%s/%s", info->devid, info->dirname, filename); + + f = grub_file_open (p, GRUB_FILE_TYPE_CONFIG); + grub_free (p); + if (f == NULL) + goto finish; + } + + entry = grub_zalloc (sizeof (*entry)); + if (entry == NULL) + goto finish; + + if (info->file != NULL) + { + char *slash; + + slash = grub_strrchr (filename, '/'); + if (slash == NULL) + slash = grub_strrchr (filename, '\\'); + + if (slash != NULL) + { + while (*slash == '/' || *slash == '\\') + slash++; + + m = slash - filename; + } + else + m = 0; + } + n -= m; + + entry->filename = grub_strndup (filename + m, n); + if (entry->filename == NULL) + { + grub_free (entry); + goto finish; + } + + err = bls_read_entry (f, entry); + + if (err == GRUB_ERR_NONE) + blsuki_add_entry (entry); + else + grub_free (entry); + + finish: + if (f != NULL) + grub_file_close (f); + + return 0; +} + +static char ** +blsuki_make_list (grub_blsuki_entry_t *entry, const char *key, int *num) +{ + int last = -1; + char *val; + int nlist = 0; + char **list; + + list = grub_malloc (sizeof (char *)); + if (list == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, "failed to allocate BLS list"); + return NULL; + } + list[0] = NULL; + + while (1) + { + char **new; + + val = blsuki_get_val (entry, key, &last); + if (val == NULL) + break; + + new = grub_realloc (list, (nlist + 2) * sizeof (char *)); + if (new == NULL) + break; + + list = new; + list[nlist++] = val; + list[nlist] = NULL; + } + + if (nlist == 0) + { + grub_free (list); + return NULL; + } + + if (num != NULL) + *num = nlist; + + return list; +} + +static char * +blsuki_field_append (bool is_env_var, char *buffer, const char *start, const char *end) +{ + char *tmp; + const char *field; + int term = (is_env_var == true) ? 2 : 1; + grub_size_t size = 0; + + tmp = grub_strndup (start, end - start + 1); + if (tmp == NULL) + return NULL; + + field = tmp; + + if (is_env_var == true) + { + field = grub_env_get (tmp); + if (field == NULL) + return buffer; + } + + if (grub_add (grub_strlen (field), term, &size)) + return NULL; + + if (buffer == NULL) + buffer = grub_zalloc (size); + else + { + if (grub_add (size, grub_strlen (buffer), &size)) + return NULL; + buffer = grub_realloc (buffer, size); + } + + if (buffer == NULL) + return NULL; + + tmp = buffer + grub_strlen (buffer); + tmp = grub_stpcpy (tmp, field); + + if (is_env_var == true) + tmp = grub_stpcpy (tmp, " "); + + return buffer; +} + +static char * +blsuki_expand_val (const char *value) +{ + char *buffer = NULL; + const char *start = value; + const char *end = value; + bool is_env_var = false; + + if (value == NULL) + return NULL; + + while (*value != '\0') + { + if (*value == '$') + { + if (start != end) + { + buffer = blsuki_field_append (is_env_var, buffer, start, end); + if (buffer == NULL) + return NULL; + } + + is_env_var = true; + start = value + 1; + } + else if (is_env_var == true) + { + if (grub_isalnum (*value) == 0 && *value != '_') + { + buffer = blsuki_field_append (is_env_var, buffer, start, end); + is_env_var = false; + start = value; + if (*start == ' ') + start++; + } + } + + end = value; + value++; + } + + if (start != end) + { + buffer = blsuki_field_append (is_env_var, buffer, start, end); + if (buffer == NULL) + return NULL; + } + + return buffer; +} + +static char ** +blsuki_early_initrd_list (const char *initrd) +{ + int nlist = 0; + char **list = NULL; + char *separator; + char *tmp; + + while ((separator = grub_strchr (initrd, ' '))) + { + list = grub_realloc (list, (nlist + 2) * sizeof (char *)); + if (list == NULL) + return NULL; + + tmp = grub_strndup (initrd, separator - initrd); + if (tmp == NULL) + { + grub_free (list); + return NULL; + } + list[nlist++] = tmp; + list[nlist] = NULL; + initrd = separator + 1; + } + + list = grub_realloc (list, (nlist + 2) * sizeof (char *)); + if (list == NULL) + return NULL; + + tmp = grub_strndup (initrd, grub_strlen (initrd)); + if (tmp == NULL) + { + grub_free (list); + return NULL; + } + list[nlist++] = tmp; + list[nlist] = NULL; + + return list; +} + +static void +bls_create_entry (grub_blsuki_entry_t *entry) +{ + int argc = 0; + const char **argv = NULL; + char *title = NULL; + char *clinux = NULL; + char *options = NULL; + char **initrds = NULL; + char *initrd = NULL; + int initrd_size; + const char *early_initrd = NULL; + char **early_initrds = NULL; + char *initrd_prefix = NULL; + char *prefix = NULL; + char *devicetree = NULL; + char *dt = NULL; + int dt_size; + char *id = entry->filename; + char *dotconf = id; + char *hotkey = NULL; + char *users = NULL; + char **classes = NULL; + char **args = NULL; + char *src = NULL; + char *tmp; + const char *sdval = NULL; + int i; + grub_size_t size = 0; + bool add_dt_prefix = false; + bool savedefault; + + clinux = blsuki_get_val (entry, "linux", NULL); + if (clinux == NULL) + { + grub_dprintf ("blsuki", "Skipping file %s with no 'linux' key.\n", entry->filename); + goto finish; + } + + /* Strip the ".conf" off the end before we make it our "id" field. */ + do + { + dotconf = grub_strstr (dotconf, ".conf"); + } + while (dotconf != NULL && dotconf[5] != '\0'); + if (dotconf != NULL) + dotconf[0] = '\0'; + + title = blsuki_get_val (entry, "title", NULL); + options = blsuki_expand_val (blsuki_get_val (entry, "options", NULL)); + + if (options == NULL) + options = blsuki_expand_val (grub_env_get ("default_kernelopts")); + + initrds = blsuki_make_list (entry, "initrd", NULL); + + devicetree = blsuki_expand_val (blsuki_get_val (entry, "devicetree", NULL)); + + if (devicetree == NULL) + { + devicetree = blsuki_expand_val (grub_env_get ("devicetree")); + add_dt_prefix = true; + } + + hotkey = blsuki_get_val (entry, "grub_hotkey", NULL); + users = blsuki_expand_val (blsuki_get_val (entry, "grub_users", NULL)); + classes = blsuki_make_list (entry, "grub_class", NULL); + args = blsuki_make_list (entry, "grub_arg", &argc); + + argc += 1; + if (grub_mul (argc + 1, sizeof (char *), &size)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, N_("overflow detected creating argv list")); + goto finish; + } + argv = grub_malloc (size); + if (argv == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, "failed to allocate argv list"); + goto finish; + } + argv[0] = title ? title : clinux; + for (i = 1; i < argc; i++) + argv[i] = args[i-1]; + argv[argc] = NULL; + + early_initrd = grub_env_get ("early_initrd"); + + grub_dprintf ("blsuki", "adding menu entry for \"%s\" with id \"%s\"\n", + title, id); + if (early_initrd != NULL) + { + early_initrds = blsuki_early_initrd_list (early_initrd); + if (early_initrds == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to create early initrd list")); + goto finish; + } + + if (initrds != NULL && initrds[0] != NULL) + { + initrd_prefix = grub_strrchr (initrds[0], '/'); + initrd_prefix = grub_strndup (initrds[0], initrd_prefix - initrds[0] + 1); + } + else + { + initrd_prefix = grub_strrchr (clinux, '/'); + initrd_prefix = grub_strndup (clinux, initrd_prefix - clinux + 1); + } + + if (initrd_prefix == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to allocate initrd prefix buffer")); + goto finish; + } + } + + if (early_initrds != NULL || initrds != NULL) + { + initrd_size = sizeof ("initrd"); + + for (i = 0; early_initrds != NULL && early_initrds[i] != NULL; i++) + { + if (grub_add (initrd_size, sizeof (" "), &initrd_size) || + grub_add (initrd_size, grub_strlen (initrd_prefix), &initrd_size) || + grub_add (initrd_size, grub_strlen (early_initrds[i]), &initrd_size) || + grub_add (initrd_size, 1, &initrd_size)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating initrd buffer size"); + goto finish; + } + } + + for (i = 0; initrds != NULL && initrds[i] != NULL; i++) + { + if (grub_add (initrd_size, sizeof (" "), &initrd_size) || + grub_add (initrd_size, grub_strlen (initrds[i]), &initrd_size) || + grub_add (initrd_size, 1, &initrd_size)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating initrd buffer size"); + goto finish; + } + } + + if (grub_add (initrd_size, 1, &initrd_size)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating initrd buffer size"); + goto finish; + } + + initrd = grub_malloc (initrd_size); + if (initrd == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to allocate initrd buffer")); + goto finish; + } + + tmp = grub_stpcpy (initrd, "initrd"); + for (i = 0; early_initrds != NULL && early_initrds[i] != NULL; i++) + { + grub_dprintf ("blsuki", "adding early initrd %s\n", early_initrds[i]); + tmp = grub_stpcpy (tmp, " "); + tmp = grub_stpcpy (tmp, initrd_prefix); + tmp = grub_stpcpy (tmp, early_initrds[i]); + grub_free (early_initrds[i]); + } + + for (i = 0; initrds != NULL && initrds[i] != NULL; i++) + { + grub_dprintf ("blsuki", "adding initrd %s\n", initrds[i]); + tmp = grub_stpcpy (tmp, " "); + tmp = grub_stpcpy (tmp, initrds[i]); + } + tmp = grub_stpcpy (tmp, "\n"); + } + + if (devicetree != NULL) + { + if (add_dt_prefix == true) + { + prefix = grub_strrchr (clinux, '/'); + prefix = grub_strndup (clinux, prefix - clinux + 1); + if (prefix == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to allocate prefix buffer")); + goto finish; + } + } + + if (grub_add (sizeof ("devicetree "), grub_strlen (devicetree), &dt_size) || + grub_add (dt_size, 1, &dt_size)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating device tree buffer size"); + goto finish; + } + + if (add_dt_prefix == true) + { + if (grub_add (dt_size, grub_strlen (prefix), &dt_size)) + { + grub_error (GRUB_ERR_OUT_OF_RANGE, "overflow detected calculating device tree buffer size"); + goto finish; + } + } + + dt = grub_malloc (dt_size); + if (dt == NULL) + { + grub_error (GRUB_ERR_OUT_OF_MEMORY, N_("failed to allocate device tree buffer")); + goto finish; + } + tmp = dt; + tmp = grub_stpcpy (dt, "devicetree"); + tmp = grub_stpcpy (tmp, " "); + if (add_dt_prefix == true) + tmp = grub_stpcpy (tmp, prefix); + tmp = grub_stpcpy (tmp, devicetree); + tmp = grub_stpcpy (tmp, "\n"); + + grub_free (prefix); + } + + grub_dprintf ("blsuki", "devicetree %s for id:\"%s\"\n", dt, id); + + sdval = grub_env_get ("save_default"); + savedefault = ((NULL != sdval) && (grub_strcmp (sdval, "true") == 0)); + src = grub_xasprintf ("%slinux %s%s%s\n" + "%s%s", + savedefault ? "savedefault\n" : "", + clinux, options ? " " : "", options ? options : "", + initrd ? initrd : "", dt ? dt : ""); + + grub_normal_add_menu_entry (argc, argv, classes, id, users, hotkey, NULL, src, 0, entry); + + finish: + grub_free (dt); + grub_free (initrd); + grub_free (initrd_prefix); + grub_free (early_initrds); + grub_free (devicetree); + grub_free (initrds); + grub_free (options); + grub_free (classes); + grub_free (args); + grub_free (argv); + grub_free (src); +} + +struct find_entry_info +{ + const char *dirname; + const char *devid; + grub_device_t dev; + grub_fs_t fs; +}; + +/* info: the filesystem object the file is on. */ +static void +blsuki_find_entry (struct find_entry_info *info) +{ + struct read_entry_info read_entry_info; + grub_fs_t dir_fs = NULL; + grub_device_t dir_dev = NULL; + const char *dir = info->dirname; + int fallback = 0; + int r = 0; + + if (dir == NULL) + dir = GRUB_BLS_CONFIG_PATH; + + read_entry_info.file = NULL; + read_entry_info.dirname = dir; + + grub_dprintf ("blsuki", "scanning dir: %s\n", dir); + + dir_dev = info->dev; + dir_fs = info->fs; + read_entry_info.devid = info->devid; + + read_fallback: + r = dir_fs->fs_dir (dir_dev, read_entry_info.dirname, blsuki_read_entry, + &read_entry_info); + if (r != 0) + { + grub_dprintf ("blsuki", "blsuki_read_entry returned error\n"); + grub_errno = GRUB_ERR_NONE; + } + + /* + * If we aren't able to find BLS entries in the directory given by info->dirname, + * we can fallback to the default location "/boot/loader/entries/" and see if we + * can find the files there. + */ + if (r != 0 && info->dirname == NULL && fallback == 0) + { + read_entry_info.dirname = "/boot" GRUB_BLS_CONFIG_PATH; + grub_dprintf ("blsuki", "Entries weren't found in %s, fallback to %s\n", + dir, read_entry_info.dirname); + fallback = 1; + goto read_fallback; + } +} + +static grub_err_t +blsuki_load_entries (char *path) +{ + grub_size_t len; + grub_fs_t fs; + grub_device_t dev; + static grub_err_t r; + const char *devid = NULL; + char *dir = NULL; + struct find_entry_info info = { + .dev = NULL, + .fs = NULL, + .dirname = NULL, + }; + struct read_entry_info rei = { + .devid = NULL, + .dirname = NULL, + }; + + if (path != NULL) + { + len = grub_strlen (path); + if (grub_strcmp (path + len - 5, ".conf") == 0) + { + rei.file = grub_file_open (path, GRUB_FILE_TYPE_CONFIG); + if (rei.file == NULL) + return grub_errno; + + /* blsuki_read_entry() closes the file. */ + return blsuki_read_entry (path, NULL, &rei); + } + else if (path[0] == '(') + { + devid = path + 1; + + dir = grub_strchr (path, ')'); + if (dir == NULL) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("invalid file name `%s'"), path); + + *dir = '\0'; + + /* Check if there is more than the devid in the path. */ + if (dir + 1 < path + len) + dir = dir + 1; + } + else if (path[0] == '/') + dir = path; + } + + if (devid == NULL) + { +#ifdef GRUB_MACHINE_EMU + devid = "host"; +#else + devid = grub_env_get ("root"); +#endif + if (devid == NULL) + return grub_error (GRUB_ERR_FILE_NOT_FOUND, N_("variable `%s' isn't set"), "root"); + } + + grub_dprintf ("blsuki", "opening %s\n", devid); + dev = grub_device_open (devid); + if (dev == NULL) + return grub_errno; + + grub_dprintf ("blsuki", "probing fs\n"); + fs = grub_fs_probe (dev); + if (fs == NULL) + { + r = grub_errno; + goto finish; + } + + info.dirname = dir; + info.devid = devid; + info.dev = dev; + info.fs = fs; + blsuki_find_entry (&info); + + finish: + if (dev != NULL) + grub_device_close (dev); + + return r; +} + +static bool +blsuki_is_default_entry (const char *def_entry, grub_blsuki_entry_t *entry, int idx) +{ + const char *title; + int def_idx; + + if (def_entry == NULL) + return false; + + if (grub_strcmp (def_entry, entry->filename) == 0) + return true; + + title = blsuki_get_val (entry, "title", NULL); + + if (title != NULL && grub_strcmp (def_entry, title) == 0) + return true; + + def_idx = (int) grub_strtol (def_entry, NULL, 0); + if (grub_errno != GRUB_ERR_NONE) + { + grub_errno = GRUB_ERR_NONE; + return false; + } + + if (def_idx == idx) + return true; + + return false; +} + +static grub_err_t +blsuki_create_entries (bool show_default, bool show_non_default, char *entry_id) +{ + const char *def_entry = NULL; + grub_blsuki_entry_t *entry = NULL; + int idx = 0; + + def_entry = grub_env_get ("default"); + + FOR_BLSUKI_ENTRIES(entry) + { + if (entry->visible == 1) + { + idx++; + continue; + } + if ((show_default == true && blsuki_is_default_entry (def_entry, entry, idx) == true) || + (show_non_default == true && blsuki_is_default_entry (def_entry, entry, idx) == false) || + (entry_id != NULL && grub_strcmp (entry_id, entry->filename) == 0)) + { + bls_create_entry (entry); + entry->visible = 1; + } + idx++; + } + + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_cmd_blscfg (grub_extcmd_context_t ctxt, int argc __attribute__ ((unused)), + char **args __attribute__ ((unused))) +{ + grub_err_t err; + struct grub_arg_list *state = ctxt->state; + char *path = NULL; + char *entry_id = NULL; + bool show_default = false; + bool show_non_default = false; + bool all = true; + + if (state[0].set) + path = state[0].arg; + if (state[1].set) + { + show_default = true; + all = false; + } + if (state[2].set) + { + show_non_default = true; + all = false; + } + if (state[3].set) + { + entry_id = state[3].arg; + all = false; + } + if (all == true) + { + show_default = true; + show_non_default = true; + } + + err = blsuki_load_entries (path); + if (err != GRUB_ERR_NONE) + return err; + + return blsuki_create_entries (show_default, show_non_default, entry_id); +} + +static grub_extcmd_t bls_cmd; + +GRUB_MOD_INIT(blsuki) +{ + bls_cmd = grub_register_extcmd ("blscfg", grub_cmd_blscfg, 0, + N_("[-p|--path] DIR [-d|--show-default] [-n|--show-non-default] [-e|--entry] FILE"), + N_("Import Boot Loader Specification snippets."), + bls_opt); +} + +GRUB_MOD_FINI(blsuki) +{ + grub_unregister_extcmd (bls_cmd); +} diff --git a/grub-core/commands/legacycfg.c b/grub-core/commands/legacycfg.c index 3bf9fe2e4..f3c86dc7f 100644 --- a/grub-core/commands/legacycfg.c +++ b/grub-core/commands/legacycfg.c @@ -143,7 +143,7 @@ legacy_file (const char *filename) args[0] = oldname; grub_normal_add_menu_entry (1, args, NULL, NULL, "legacy", NULL, NULL, - entrysrc, 0); + entrysrc, 0, NULL); grub_free (args); entrysrc[0] = 0; grub_free (oldname); @@ -204,7 +204,7 @@ legacy_file (const char *filename) } args[0] = entryname; grub_normal_add_menu_entry (1, args, NULL, NULL, NULL, - NULL, NULL, entrysrc, 0); + NULL, NULL, entrysrc, 0, NULL); grub_free (args); } diff --git a/grub-core/commands/menuentry.c b/grub-core/commands/menuentry.c index 720e6d8ea..09749c415 100644 --- a/grub-core/commands/menuentry.c +++ b/grub-core/commands/menuentry.c @@ -78,7 +78,7 @@ grub_normal_add_menu_entry (int argc, const char **args, char **classes, const char *id, const char *users, const char *hotkey, const char *prefix, const char *sourcecode, - int submenu) + int submenu, grub_blsuki_entry_t *blsuki) { int menu_hotkey = 0; char **menu_args = NULL; @@ -188,6 +188,7 @@ grub_normal_add_menu_entry (int argc, const char **args, (*last)->args = menu_args; (*last)->sourcecode = menu_sourcecode; (*last)->submenu = submenu; + (*last)->blsuki = blsuki; menu->size++; return GRUB_ERR_NONE; @@ -286,7 +287,8 @@ grub_cmd_menuentry (grub_extcmd_context_t ctxt, int argc, char **args) users, ctxt->state[2].arg, 0, ctxt->state[3].arg, - ctxt->extcmd->cmd->name[0] == 's'); + ctxt->extcmd->cmd->name[0] == 's', + NULL); src = args[argc - 1]; args[argc - 1] = NULL; @@ -303,7 +305,7 @@ grub_cmd_menuentry (grub_extcmd_context_t ctxt, int argc, char **args) ctxt->state[0].args, ctxt->state[4].arg, users, ctxt->state[2].arg, prefix, src + 1, - ctxt->extcmd->cmd->name[0] == 's'); + ctxt->extcmd->cmd->name[0] == 's', NULL); src[len - 1] = ch; args[argc - 1] = src; diff --git a/grub-core/lib/vercmp.c b/grub-core/lib/vercmp.c new file mode 100644 index 000000000..726e2321b --- /dev/null +++ b/grub-core/lib/vercmp.c @@ -0,0 +1,317 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see <http://www.gnu.org/licenses/>. + */ +#include <grub/misc.h> +#include <grub/mm.h> +#include <grub/err.h> +#include <grub/lib/vercmp.h> + +#define GOTO_RETURN(x) ({ *ret = (x); goto finish; }) + +/* + * compare alpha and numeric segments of two versions + * return 1: a is newer than b + * 0: a and b are the same version + * -1: a is older than b + */ +grub_err_t +grub_vercmp (const char *a, const char *b, int *ret) +{ + char oldch1, oldch2; + char *abuf, *bbuf; + char *str1, *str2; + char *one, *two; + int rc; + bool isnum; + + if (ret == NULL) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("return parameter is not set")); + *ret = 0; + + grub_dprintf ("blscfg", "%s comparing %s and %s\n", __func__, a, b); + if (grub_strcmp (a, b) == 0) + return GRUB_ERR_NONE; + + abuf = grub_strdup (a); + if (abuf == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate string to compare versions"); + + bbuf = grub_strdup (b); + if (bbuf == NULL) + { + grub_free (abuf); + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate string to compare versions"); + } + + str1 = abuf; + str2 = bbuf; + + one = str1; + two = str2; + + /* Loop through each version segment of str1 and str2 and compare them. */ + while (*one != '\0' || *two != '\0') + { + while (*one != '\0' && grub_isalnum (*one) == 0 && *one != '~' && *one != '+') + one++; + while (*two != '\0' && grub_isalnum (*two) == 0 && *two != '~' && *two != '+') + two++; + + /* Handle the tilde separator, it sorts before everything else. */ + if (*one == '~' || *two == '~') + { + if (*one != '~') + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (*two != '~') + GOTO_RETURN (GRUB_VERCMP_OLDER); + one++; + two++; + continue; + } + + /* + * Handle the plus separator. Concept is the same as tilde, except that if + * one of the strings ends (base version), the other is considered as the + * higher version. + */ + if (*one == '+' || *two == '+') + { + if (*one == '\0') + GOTO_RETURN (GRUB_VERCMP_OLDER); + if (*two == '\0') + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (*one != '+') + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (*two != '+') + GOTO_RETURN (GRUB_VERCMP_OLDER); + one++; + two++; + continue; + } + + /* If we ran to the end of either, we are finished with the loop. */ + if (*one == '\0' || *two == '\0') + break; + + str1 = one; + str2 = two; + + /* + * Grab the first completely alpha or completely numeric segment. + * Leave one and two pointing to the start of the alpha or numeric + * segment and walk str1 and str2 to end of segment. + */ + if (grub_isdigit (*str1) == 1) + { + while (*str1 != '\0' && grub_isdigit (*str1) == 1) + str1++; + while (*str2 != '\0' && grub_isdigit (*str2) == 1) + str2++; + isnum = true; + } + else + { + while (*str1 != '\0' && grub_isalpha (*str1) == 1) + str1++; + while (*str2 != '\0' && grub_isalpha (*str2) == 1) + str2++; + isnum = false; + } + + /* + * Save the character at the end of the alpha or numeric segment so that + * they can be restored after the comparison. + */ + oldch1 = *str1; + *str1 = '\0'; + oldch2 = *str2; + *str2 = '\0'; + + /* + * This cannot happen, as we previously tested to make sure that + * the first string has a non-null segment. + */ + if (one == str1) + GOTO_RETURN (GRUB_VERCMP_OLDER); /* arbitrary */ + + /* + * Take care of the case where the two version segments are different + * types: one numeric, the other alpha (i.e. empty). Numeric segments are + * always newer than alpha segments. + */ + if (two == str2) + GOTO_RETURN (isnum ? GRUB_VERCMP_NEWER : GRUB_VERCMP_OLDER); + + if (isnum == true) + { + grub_size_t onelen, twolen; + /* + * This used to be done by converting the digit segments to ints using + * atoi() - it's changed because long digit segments can overflow an int - + * this should fix that. + */ + + /* Throw away any leading zeros - it's a number, right? */ + while (*one == '0') + one++; + while (*two == '0') + two++; + + /* Whichever number that has more digits wins. */ + onelen = grub_strlen (one); + twolen = grub_strlen (two); + if (onelen > twolen) + GOTO_RETURN (GRUB_VERCMP_NEWER); + if (twolen > onelen) + GOTO_RETURN (GRUB_VERCMP_OLDER); + } + + /* + * grub_strcmp will return which one is greater - even if the two segments + * are alpha or if they are numeric. Don't return if they are equal + * because there might be more segments to compare. + */ + rc = grub_strcmp (one, two); + if (rc != 0) + GOTO_RETURN (rc < 1 ? GRUB_VERCMP_OLDER : GRUB_VERCMP_NEWER); + + /* Restore the character that was replaced by null above. */ + *str1 = oldch1; + one = str1; + *str2 = oldch2; + two = str2; + } + + /* + * This catches the case where all alpha and numeric segments have compared + * identically but the segment separating characters were different. + */ + if (*one == '\0' && *two == '\0') + GOTO_RETURN (GRUB_VERCMP_SAME); + + /* Whichever version that still has characters left over wins. */ + if (*one == '\0') + GOTO_RETURN (GRUB_VERCMP_OLDER); + else + GOTO_RETURN (GRUB_VERCMP_NEWER); + + finish: + grub_free (abuf); + grub_free (bbuf); + return GRUB_ERR_NONE; +} + +/* + * returns name/version/release + * NULL string pointer returned if nothing is found + */ +void +grub_split_package_string (char *package_string, char **name, + char **version, char **release) +{ + char *package_version, *package_release; + + /* Release */ + package_release = grub_strrchr (package_string, '-'); + + if (package_release != NULL) + *package_release++ = '\0'; + + *release = package_release; + + if (name == NULL) + *version = package_string; + else + { + /* Version */ + package_version = grub_strrchr (package_string, '-'); + + if (package_version != NULL) + *package_version++ = '\0'; + + *version = package_version; + /* Name */ + *name = package_string; + } + + /* Bubble up non-null values from release to name */ + if (name != NULL && *name == NULL) + { + *name = (*version == NULL ? *release : *version); + *version = *release; + *release = NULL; + } + if (*version == NULL) + { + *version = *release; + *release = NULL; + } +} + +/* + * return 1: nvr0 is newer than nvr1 + * 0: nvr0 and nvr1 are the same version + * -1: nvr1 is newer than nvr0 + */ +grub_err_t +grub_split_vercmp (const char *nvr0, const char *nvr1, bool has_name, int *ret) +{ + grub_err_t err = GRUB_ERR_NONE; + char *str0, *name0, *version0, *release0; + char *str1, *name1, *version1, *release1; + + if (ret == NULL) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("return parameter is not set")); + + str0 = grub_strdup (nvr0); + if (str0 == NULL) + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate BLS filename to compare"); + str1 = grub_strdup (nvr1); + if (str1 == NULL) + { + grub_free (str0); + return grub_error (GRUB_ERR_OUT_OF_MEMORY, "couldn't duplicate BLS filename to compare"); + } + + grub_split_package_string (str0, has_name ? &name0 : NULL, &version0, &release0); + grub_split_package_string (str1, has_name ? &name1 : NULL, &version1, &release1); + + if (has_name == true) + { + err = grub_vercmp (name0 == NULL ? "" : name0, name1 == NULL ? "" : name1, ret); + if (*ret != 0 || err != GRUB_ERR_NONE) + { + grub_free (str0); + grub_free (str1); + return err; + } + } + + err = grub_vercmp (version0 == NULL ? "" : version0, version1 == NULL ? "" : version1, ret); + if (*ret != 0 || err != GRUB_ERR_NONE) + { + grub_free (str0); + grub_free (str1); + return err; + } + + err = grub_vercmp (release0 == NULL ? "" : release0, release1 == NULL ? "" : release1, ret); + + grub_free (str0); + grub_free (str1); + return err; +} diff --git a/grub-core/normal/main.c b/grub-core/normal/main.c index 04d058f55..2d493f21e 100644 --- a/grub-core/normal/main.c +++ b/grub-core/normal/main.c @@ -21,6 +21,7 @@ #include <grub/net.h> #include <grub/normal.h> #include <grub/dl.h> +#include <grub/menu.h> #include <grub/misc.h> #include <grub/file.h> #include <grub/mm.h> @@ -67,6 +68,11 @@ grub_normal_free_menu (grub_menu_t menu) grub_free (entry->args); } + if (entry->blsuki) + { + entry->blsuki->visible = 0; + } + grub_free ((void *) entry->id); grub_free ((void *) entry->users); grub_free ((void *) entry->title); diff --git a/include/grub/lib/vercmp.h b/include/grub/lib/vercmp.h new file mode 100644 index 000000000..e588ef530 --- /dev/null +++ b/include/grub/lib/vercmp.h @@ -0,0 +1,35 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see <http://www.gnu.org/licenses/>. + */ +#ifndef GRUB_VERCMP_H +#define GRUB_VERCMP_H 1 + +enum + { + GRUB_VERCMP_OLDER = -1, + GRUB_VERCMP_SAME = 0, + GRUB_VERCMP_NEWER = 1, + }; + +grub_err_t grub_vercmp (const char *a, const char *b, int *ret); + +void grub_split_package_string (char *package_string, char **name, + char **version, char **release); + +grub_err_t grub_split_vercmp (const char *nvr0, const char *nvr1, bool has_name, int *ret); + +#endif /* ! GRUB_VERCMP_H */ diff --git a/include/grub/menu.h b/include/grub/menu.h index ee2b5e910..c25a0d16d 100644 --- a/include/grub/menu.h +++ b/include/grub/menu.h @@ -20,6 +20,18 @@ #ifndef GRUB_MENU_HEADER #define GRUB_MENU_HEADER 1 +struct grub_blsuki_entry +{ + struct grub_blsuki_entry *next; + struct grub_blsuki_entry **prev; + struct keyval **keyvals; + grub_size_t keyvals_size; + int nkeyvals; + char *filename; + int visible; +}; +typedef struct grub_blsuki_entry grub_blsuki_entry_t; + struct grub_menu_entry_class { char *name; @@ -60,6 +72,9 @@ struct grub_menu_entry /* The next element. */ struct grub_menu_entry *next; + + /* BLS used to populate the entry */ + grub_blsuki_entry_t *blsuki; }; typedef struct grub_menu_entry *grub_menu_entry_t; diff --git a/include/grub/normal.h b/include/grub/normal.h index 218cbabcc..d0150e3c2 100644 --- a/include/grub/normal.h +++ b/include/grub/normal.h @@ -145,7 +145,7 @@ grub_normal_add_menu_entry (int argc, const char **args, char **classes, const char *id, const char *users, const char *hotkey, const char *prefix, const char *sourcecode, - int submenu); + int submenu, grub_blsuki_entry_t *blsuki); grub_err_t grub_normal_set_password (const char *user, const char *password); diff --git a/tests/vercmp_unit_test.c b/tests/vercmp_unit_test.c new file mode 100644 index 000000000..d0dfb1e8b --- /dev/null +++ b/tests/vercmp_unit_test.c @@ -0,0 +1,65 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2025 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <string.h> +#include <stdlib.h> +#include <grub/test.h> +#include <grub/lib/vercmp.h> + +#define MSG "vercmp test failed" + +static void +vercmp_test (void) +{ + const char *s1 = "name0-1.0.0-1.0.0.el8uek.x86_64"; + const char *s2 = "name0-1.0.0-1.1.0.el8uek.x86_64"; + const char *s3 = "name0-1.1.0-1.0.0.el8uek.x86_64"; + const char *s4 = "name1-1.0.0-1.0.0.el8uek.x86_64"; + const char *s5 = "version0"; + const char *s6 = "version1"; + char *nvr, *name, *version, *release; + int ret; + + nvr = strdup (s1); + + grub_split_package_string (nvr, &name, &version, &release); + grub_test_assert (strcmp (name, "name0") == 0, MSG); + grub_test_assert (strcmp (version, "1.0.0") == 0, MSG); + grub_test_assert (strcmp (release, "1.0.0.el8uek.x86_64") == 0, MSG); + free (nvr); + + grub_split_vercmp (s1, s1, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_SAME, MSG); + grub_split_vercmp (s1, s2, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_split_vercmp (s1, s3, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_split_vercmp (s1, s4, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_split_vercmp (s2, s1, true, &ret); + grub_test_assert (ret == GRUB_VERCMP_NEWER, MSG); + + grub_vercmp (s5, s5, &ret); + grub_test_assert (ret == GRUB_VERCMP_SAME, MSG); + grub_vercmp (s5, s6, &ret); + grub_test_assert (ret == GRUB_VERCMP_OLDER, MSG); + grub_vercmp (s6, s5, &ret); + grub_test_assert (ret == GRUB_VERCMP_NEWER, MSG); +} + +GRUB_UNIT_TEST ("vercmp_unit_test", vercmp_test); -- 2.27.0 _______________________________________________ Grub-devel mailing list Grub-devel@gnu.org https://lists.gnu.org/mailman/listinfo/grub-devel