]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
bootctl: unlink and cleanup functions 26103/head
authorLudwig Nussel <ludwig.nussel@suse.de>
Thu, 8 Dec 2022 15:27:31 +0000 (16:27 +0100)
committerLudwig Nussel <ludwig.nussel@suse.de>
Thu, 19 Jan 2023 13:24:43 +0000 (14:24 +0100)
The unlink command removes an entry from the ESP including
referenced files that are not referenced in other entries. That is
useful eg to have multiple entries that use the same kernel with
different options.

The cleanup command removes all files that are not referenced by any
entry.

man/bootctl.xml
meson.build
shell-completion/bash/bootctl
shell-completion/zsh/_bootctl
src/boot/bootctl-status.c
src/boot/bootctl-status.h
src/boot/bootctl.c
src/boot/bootctl.h
src/kernel-install/90-loaderentry.install.in
src/kernel-install/test-kernel-install.sh
src/test/meson.build

index 0d796bedc192eaf318c876a73734d88ef6573276..ada8c03332cacffb3ed83b5dd1ec8a1ee3a19676 100644 (file)
         disables the timeout while always showing the menu. When an empty string ("") is specified the
         bootloader will revert to its default menu timeout.</para></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><option>unlink</option> <replaceable>ID</replaceable></term>
+
+        <listitem><para>Removes a boot loader entry including the files it refers to. Takes a single boot
+        loader entry ID string or a glob pattern as argument. Referenced files such as kernel or initrd are
+        only removed if no other entry refers to them.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>cleanup</option></term>
+
+        <listitem><para>Removes files from the ESP and XBOOTLDR partitions that belong to the entry token but
+        are not referenced in any boot loader entries.</para></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
         the firmware's boot option menu.</para></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><option>--dry-run</option></term>
+        <listitem><para>Dry run for <option>--unlink</option> and <option>--cleanup</option>.</para>
+
+        <para>In dry run mode, the unlink and cleanup operations only print the files that would get deleted
+        without actually deleting them.</para></listitem>
+      </varlistentry>
+
       <xi:include href="standard-options.xml" xpointer="no-pager"/>
       <xi:include href="standard-options.xml" xpointer="json" />
       <xi:include href="standard-options.xml" xpointer="help"/>
index d1f3a00691b212f210f6841fbee6ed14eedab7c1..965b237d23af8f4ffd13b3e3eb9b06314eef9a7a 100644 (file)
@@ -4051,6 +4051,7 @@ public_programs += exe
 if want_tests != 'false' and want_kernel_install
         test('test-kernel-install',
              test_kernel_install_sh,
+             env : test_env,
              args : [exe.full_path(), loaderentry_install])
 endif
 
index 0b7cef7871b02490a44a0f376406a5c8fc11c0ef..8d8b507ea95131b3a651537a9ea702457d3ba664 100644 (file)
@@ -31,7 +31,7 @@ _bootctl() {
     local i verb comps
     local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}
     local -A OPTS=(
-        [STANDALONE]='-h --help -p --print-esp-path -x --print-boot-path --version --no-variables --no-pager --graceful'
+        [STANDALONE]='-h --help -p --print-esp-path -x --print-boot-path --version --no-variables --no-pager --graceful --dry-run'
         [ARG]='--esp-path --boot-path --make-machine-id-directory --root --image --install-source'
     )
 
@@ -67,8 +67,8 @@ _bootctl() {
 
     local -A VERBS=(
         # systemd-efi-options takes an argument, but it is free-form, so we cannot complete it
-        [STANDALONE]='help status install update remove is-installed random-seed systemd-efi-options list set-timeout set-timeout-oneshot'
-        [BOOTENTRY]='set-default set-oneshot'
+        [STANDALONE]='help status install update remove is-installed random-seed systemd-efi-options list set-timeout set-timeout-oneshot cleanup'
+        [BOOTENTRY]='set-default set-oneshot unlink'
         [BOOLEAN]='reboot-to-firmware'
     )
 
index 8634e8b9bc06496d1f5de2176b21b6e43ebb0faf..83d910cda15a6058d36a9bb25f12dfcb597522cb 100644 (file)
@@ -24,6 +24,10 @@ _bootctl_set-oneshot() {
     _bootctl_comp_ids
 }
 
+_bootctl_unlink() {
+    _bootctl_comp_ids
+}
+
 _bootctl_reboot-to-firmware() {
     local -a _completions
     _completions=( yes no )
@@ -48,6 +52,8 @@ _bootctl_reboot-to-firmware() {
         "set-oneshot:Set the default boot loader entry only for the next boot"
         "set-timeout:Set the menu timeout"
         "set-timeout-oneshot:Set the menu timeout for the next boot only"
+        "unlink:Remove boot loader entry"
+        "cleanup:Remove files in ESP not referenced in any boot entry"
     )
     if (( CURRENT == 1 )); then
         _describe -t commands 'bootctl command' _bootctl_cmds || compadd "$@"
@@ -73,6 +79,7 @@ _arguments \
     '--no-variables[Do not touch EFI variables]' \
     '--no-pager[Do not pipe output into a pager]' \
     '--graceful[Do not fail when locating ESP or writing fails]' \
+    '--dry-run[Dry run (unlink and cleanup)]' \
     '--root=[Operate under the specified directory]:PATH' \
     '--image=[Operate on the specified image]:PATH' \
     '--install-source[Where to pick files when using --root=/--image=]:options:(image host auto)' \
index 0adb354bdb830909ea581e97aa54df2a7044e6b2..8077a8a00557ae048a841febf5cdc83bac949243 100644 (file)
@@ -18,6 +18,7 @@
 #include "find-esp.h"
 #include "path-util.h"
 #include "pretty-print.h"
+#include "recurse-dir.h"
 #include "terminal-util.h"
 #include "tpm2-util.h"
 
@@ -503,6 +504,250 @@ int verb_status(int argc, char *argv[], void *userdata) {
         return r;
 }
 
+static int ref_file(Hashmap *known_files, const char *fn, int increment) {
+        char *k = NULL;
+        int n, r;
+
+        assert(known_files);
+
+        /* just gracefully ignore this. This way the caller doesn't
+           have to verify whether the bootloader entry is relevant */
+        if (!fn)
+                return 0;
+
+        n = PTR_TO_INT(hashmap_get2(known_files, fn, (void**)&k));
+        n += increment;
+
+        assert(n >= 0);
+
+        if (n == 0) {
+                (void) hashmap_remove(known_files, fn);
+                free(k);
+        } else if (!k) {
+                _cleanup_free_ char *t = NULL;
+
+                t = strdup(fn);
+                if (!t)
+                        return -ENOMEM;
+                r = hashmap_put(known_files, t, INT_TO_PTR(n));
+                if (r < 0)
+                        return r;
+                TAKE_PTR(t);
+        } else {
+                r = hashmap_update(known_files, fn, INT_TO_PTR(n));
+                if (r < 0)
+                        return r;
+        }
+
+        return n;
+}
+
+static void deref_unlink_file(Hashmap *known_files, const char *fn, const char *root) {
+        _cleanup_free_ char *path = NULL;
+        int r;
+
+        /* just gracefully ignore this. This way the caller doesn't
+           have to verify whether the bootloader entry is relevant */
+        if (!fn || !root)
+                return;
+
+        r = ref_file(known_files, fn, -1);
+        if (r < 0)
+                return (void) log_warning_errno(r, "Failed to deref \"%s\", ignoring: %m", fn);
+        if (r > 0)
+                return;
+
+        if (arg_dry_run) {
+                r = chase_symlinks_and_access(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, F_OK, &path, NULL);
+                if (r < 0)
+                        log_info("Unable to determine whether \"%s\" exists, ignoring: %m", fn);
+                else
+                        log_info("Would remove %s", path);
+                return;
+        }
+
+        r = chase_symlinks_and_unlink(fn, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, 0, &path);
+        if (r >= 0)
+                log_info("Removed %s", path);
+        else if (r != -ENOENT)
+                return (void) log_warning_errno(r, "Failed to remove \"%s\", ignoring: %m", path ?: fn);
+
+        _cleanup_free_ char *d = NULL;
+        if (path_extract_directory(fn, &d) >= 0 && !path_equal(d, "/")) {
+                r = chase_symlinks_and_unlink(d, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS, AT_REMOVEDIR, NULL);
+                if (r < 0 && !IN_SET(r, -ENOTEMPTY, -ENOENT))
+                        log_warning_errno(r, "Failed to remove directoy \"%s\", ignoring: %m", d);
+        }
+}
+
+static int count_known_files(const BootConfig *config, const char* root, Hashmap **ret_known_files) {
+        _cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL;
+        int r = 0;
+
+        assert(config);
+        assert(ret_known_files);
+
+        known_files = hashmap_new(&path_hash_ops);
+        if (!known_files)
+                return -ENOMEM;
+
+        for (size_t i = 0; i < config->n_entries; i++) {
+                const BootEntry *e = config->entries + i;
+
+                if (!path_equal(e->root, root))
+                        continue;
+
+                r = ref_file(known_files, e->kernel, +1);
+                if (r < 0)
+                        return r;
+                r = ref_file(known_files, e->efi, +1);
+                if (r < 0)
+                        return r;
+                STRV_FOREACH(s, e->initrd) {
+                        r = ref_file(known_files, *s, +1);
+                        if (r < 0)
+                                return r;
+                }
+                r = ref_file(known_files, e->device_tree, +1);
+                if (r < 0)
+                        return r;
+                STRV_FOREACH(s, e->device_tree_overlay) {
+                        r = ref_file(known_files, *s, +1);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        *ret_known_files = TAKE_PTR(known_files);
+
+        return 0;
+}
+
+static int boot_config_find_in(const BootConfig *config, const char *root, const char *id) {
+        assert(config);
+
+        if (!root || !id)
+                return -1;
+
+        for (size_t i = 0; i < config->n_entries; i++)
+                if (path_equal(config->entries[i].root, root)
+                        && fnmatch(id, config->entries[i].id, FNM_CASEFOLD) == 0)
+                        return i;
+
+        return -1;
+}
+
+static int unlink_entry(const BootConfig *config, const char *root, const char *id) {
+        _cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL;
+        const BootEntry *e = NULL;
+        int r;
+
+        assert(config);
+
+        r = count_known_files(config, root, &known_files);
+        if (r < 0)
+                return log_error_errno(r, "Failed to count files in %s: %m", root);
+
+        r = boot_config_find_in(config, root, id);
+        if (r < 0)
+                return -ENOENT;
+
+        if (r == config->default_entry)
+                log_warning("%s is the default boot entry", id);
+        if (r == config->selected_entry)
+                log_warning("%s is the selected boot entry", id);
+
+        e = &config->entries[r];
+
+        deref_unlink_file(known_files, e->kernel, e->root);
+        deref_unlink_file(known_files, e->efi, e->root);
+        STRV_FOREACH(s, e->initrd)
+                deref_unlink_file(known_files, *s, e->root);
+        deref_unlink_file(known_files, e->device_tree, e->root);
+        STRV_FOREACH(s, e->device_tree_overlay)
+                deref_unlink_file(known_files, *s, e->root);
+
+        if (arg_dry_run)
+                log_info("Would remove %s", e->path);
+        else {
+                r = chase_symlinks_and_unlink(e->path, root, CHASE_PROHIBIT_SYMLINKS, 0, NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to remove \"%s\": %m", e->path);
+
+                log_info("Removed %s", e->path);
+        }
+
+        return 0;
+}
+
+static int list_remove_orphaned_file(
+                RecurseDirEvent event,
+                const char *path,
+                int dir_fd,
+                int inode_fd,
+                const struct dirent *de,
+                const struct statx *sx,
+                void *userdata) {
+        Hashmap *known_files = userdata;
+
+        assert(path);
+        assert(known_files);
+
+        if (event != RECURSE_DIR_ENTRY)
+                return RECURSE_DIR_CONTINUE;
+
+        if (hashmap_get(known_files, path))
+                return RECURSE_DIR_CONTINUE; /* keep! */
+
+        if (arg_dry_run)
+                log_info("Would remove %s", path);
+        else if (unlinkat(dir_fd, de->d_name, 0) < 0)
+                log_warning_errno(errno, "Failed to remove \"%s\", ignoring: %m", path);
+        else
+                log_info("Removed %s", path);
+
+        return RECURSE_DIR_CONTINUE;
+}
+
+static int cleanup_orphaned_files(
+                const BootConfig *config,
+                const char *root) {
+        _cleanup_(hashmap_free_free_keyp) Hashmap *known_files = NULL;
+        _cleanup_free_ char *full = NULL, *p = NULL;
+        _cleanup_close_ int dir_fd = -1;
+        int r = -1;
+
+        assert(config);
+        assert(root);
+
+        log_info("Cleaning %s", root);
+
+        r = settle_entry_token();
+        if (r < 0)
+                return r;
+
+        r = count_known_files(config, root, &known_files);
+        if (r < 0)
+                return log_error_errno(r, "Failed to count files in %s: %m", root);
+
+        dir_fd = chase_symlinks_and_open(arg_entry_token, root, CHASE_PREFIX_ROOT|CHASE_PROHIBIT_SYMLINKS,
+                        O_DIRECTORY|O_CLOEXEC, &full);
+        if (dir_fd == -ENOENT)
+                return 0;
+        if (dir_fd < 0)
+                return log_error_errno(dir_fd, "Failed to open '%s/%s': %m", root, arg_entry_token);
+
+        p = path_join("/", arg_entry_token);
+        if (!p)
+                return log_oom();
+
+        r = recurse_dir(dir_fd, p, 0, UINT_MAX, RECURSE_DIR_SORT, list_remove_orphaned_file, known_files);
+        if (r < 0)
+                return log_error_errno(r, "Failed to cleanup %s: %m", full);
+
+        return r;
+}
+
 int verb_list(int argc, char *argv[], void *userdata) {
         _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
         dev_t esp_devid = 0, xbootldr_devid = 0;
@@ -534,6 +779,24 @@ int verb_list(int argc, char *argv[], void *userdata) {
                 return 0;
         }
 
-        pager_open(arg_pager_flags);
-        return show_boot_entries(&config, arg_json_format_flags);
+        if (streq(argv[0], "list")) {
+                pager_open(arg_pager_flags);
+                return show_boot_entries(&config, arg_json_format_flags);
+        } else if (streq(argv[0], "cleanup")) {
+                if (arg_xbootldr_path && xbootldr_devid != esp_devid)
+                        cleanup_orphaned_files(&config, arg_xbootldr_path);
+                return cleanup_orphaned_files(&config, arg_esp_path);
+        } else {
+                assert(streq(argv[0], "unlink"));
+                if (arg_xbootldr_path && xbootldr_devid != esp_devid) {
+                        r = unlink_entry(&config, arg_xbootldr_path, argv[1]);
+                        if (r == 0 || r != -ENOENT)
+                                return r;
+                }
+                return unlink_entry(&config, arg_esp_path, argv[1]);
+        }
+}
+
+int verb_unlink(int argc, char *argv[], void *userdata) {
+        return verb_list(argc, argv, userdata);
 }
index 0b57c86f91716eda3079ee6da95efae120a4b31e..f7998a3303eb5cec2980664764d0606e5256f58f 100644 (file)
@@ -2,3 +2,4 @@
 
 int verb_status(int argc, char *argv[], void *userdata);
 int verb_list(int argc, char *argv[], void *userdata);
+int verb_unlink(int argc, char *argv[], void *userdata);
index 7dd1a2f5f29963e21f816496c58d0e41bc9e56b5..e910a72042b71c48fa58a5b43c1117a6b36a680d 100644 (file)
@@ -48,6 +48,7 @@ char *arg_root = NULL;
 char *arg_image = NULL;
 InstallSource arg_install_source = ARG_INSTALL_SOURCE_AUTO;
 char *arg_efi_boot_option_description = NULL;
+bool arg_dry_run = false;
 
 STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep);
 STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep);
@@ -145,6 +146,8 @@ static int help(int argc, char *argv[], void *userdata) {
                "  set-timeout SECONDS Set the menu timeout\n"
                "  set-timeout-oneshot SECONDS\n"
                "                      Set the menu timeout for the next boot only\n"
+               "  unlink ID           Remove boot loader entry\n"
+               "  cleanup             Remove files in ESP not referenced in any boot entry\n"
                "\n%3$ssystemd-boot Commands:%4$s\n"
                "  install             Install systemd-boot to the ESP and EFI variables\n"
                "  update              Update systemd-boot in the ESP and EFI variables\n"
@@ -179,6 +182,7 @@ static int help(int argc, char *argv[], void *userdata) {
                "                       Install all supported EFI architectures\n"
                "     --efi-boot-option-description=DESCRIPTION\n"
                "                       Description of the entry in the boot option list\n"
+               "     --dry-run         Dry run (unlink and cleanup)\n"
                "\nSee the %2$s for details.\n",
                program_invocation_short_name,
                link,
@@ -206,6 +210,7 @@ static int parse_argv(int argc, char *argv[]) {
                 ARG_JSON,
                 ARG_ARCH_ALL,
                 ARG_EFI_BOOT_OPTION_DESCRIPTION,
+                ARG_DRY_RUN,
         };
 
         static const struct option options[] = {
@@ -230,6 +235,7 @@ static int parse_argv(int argc, char *argv[]) {
                 { "json",                        required_argument, NULL, ARG_JSON                        },
                 { "all-architectures",           no_argument,       NULL, ARG_ARCH_ALL                    },
                 { "efi-boot-option-description", required_argument, NULL, ARG_EFI_BOOT_OPTION_DESCRIPTION },
+                { "dry-run",                     no_argument,       NULL, ARG_DRY_RUN                     },
                 {}
         };
 
@@ -379,6 +385,10 @@ static int parse_argv(int argc, char *argv[]) {
                                 return r;
                         break;
 
+                case ARG_DRY_RUN:
+                        arg_dry_run = true;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -387,7 +397,7 @@ static int parse_argv(int argc, char *argv[]) {
                 }
 
         if ((arg_root || arg_image) && argv[optind] && !STR_IN_SET(argv[optind], "status", "list",
-                        "install", "update", "remove", "is-installed", "random-seed"))
+                        "install", "update", "remove", "is-installed", "random-seed", "unlink", "cleanup"))
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                        "Options --root= and --image= are not supported with verb %s.",
                                        argv[optind]);
@@ -398,6 +408,9 @@ static int parse_argv(int argc, char *argv[]) {
         if (arg_install_source != ARG_INSTALL_SOURCE_AUTO && !arg_root && !arg_image)
                 return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--install-from-host is only supported with --root= or --image=.");
 
+        if (arg_dry_run && argv[optind] && !STR_IN_SET(argv[optind], "unlink", "cleanup"))
+                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "--dry is only supported with --unlink or --cleanup");
+
         return 1;
 }
 
@@ -412,6 +425,8 @@ static int bootctl_main(int argc, char *argv[]) {
                 { "kernel-identify",     2,        2,        0,            verb_kernel_identify     },
                 { "kernel-inspect",      2,        2,        0,            verb_kernel_inspect      },
                 { "list",                VERB_ANY, 1,        0,            verb_list                },
+                { "unlink",              2,        2,        0,            verb_unlink              },
+                { "cleanup",             VERB_ANY, 1,        0,            verb_list                },
                 { "set-default",         2,        2,        0,            verb_set_efivar          },
                 { "set-oneshot",         2,        2,        0,            verb_set_efivar          },
                 { "set-timeout",         2,        2,        0,            verb_set_efivar          },
index 5a14faf1a47d3c2ba0ad72af74db624fd8b856a9..311b954c2c8b3fdec90e1589c2e1bf591b03eb16 100644 (file)
@@ -39,6 +39,7 @@ extern char *arg_root;
 extern char *arg_image;
 extern InstallSource arg_install_source;
 extern char *arg_efi_boot_option_description;
+extern bool arg_dry_run;
 
 static inline const char *arg_dollar_boot_path(void) {
         /* $BOOT shall be the XBOOTLDR partition if it exists, and otherwise the ESP */
index 0992c641f0361608a52b3a0c74fd09d9aaca160e..e8e8cf37c36e34c0d70b631bf57fcbe7c5b5c4ae 100755 (executable)
@@ -32,7 +32,7 @@ MACHINE_ID="$KERNEL_INSTALL_MACHINE_ID"
 ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN"
 BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT"
 
-BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
+[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")"
 if [ "$BOOT_MNT" = '/' ]; then
     ENTRY_DIR="$ENTRY_DIR_ABS"
 else
index 2e440636680161319af264380d3e9adf471a3349..f16bb9f50f9d4d328c942b9365bbf54b3c5b54b1 100755 (executable)
@@ -9,6 +9,8 @@ plugin="${2:?}"
 
 D="$(mktemp --tmpdir --directory "test-kernel-install.XXXXXXXXXX")"
 
+export _KERNEL_INSTALL_BOOTCTL="$PROJECT_BUILD_ROOT/bootctl"
+
 # shellcheck disable=SC2064
 trap "rm -rf '$D'" EXIT INT QUIT PIPE
 mkdir -p "$D/boot"
@@ -31,6 +33,7 @@ EOF
 export KERNEL_INSTALL_CONF_ROOT="$D/sources"
 export KERNEL_INSTALL_PLUGINS="$plugin"
 export BOOT_ROOT="$D/boot"
+export BOOT_MNT="$D/boot"
 export MACHINE_ID='3e0484f3634a418b8e6a39e8828b03e3'
 
 "$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd"
@@ -82,3 +85,32 @@ grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry"
 
 grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux"
 grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd"
+
+if test -x "$_KERNEL_INSTALL_BOOTCTL"; then
+    echo "Testing bootctl"
+    e2="${entry%+*}_2.conf"
+    cp "$entry" "$e2"
+    export SYSTEMD_ESP_PATH=/
+
+    # create file that is not referenced. Check if cleanup removes
+    # it but leaves the rest alone
+    :> "$BOOT_ROOT/the-token/1.1.2/initrd"
+    "$_KERNEL_INSTALL_BOOTCTL" --root="$BOOT_ROOT" cleanup
+    test ! -e "$BOOT_ROOT/the-token/1.1.2/initrd"
+    test -e "$BOOT_ROOT/the-token/1.1.2/linux"
+    test -e "$BOOT_ROOT/the-token/1.1.1/linux"
+    test -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+    # now remove duplicated entry and make sure files are left over
+    "$_KERNEL_INSTALL_BOOTCTL" --root="$BOOT_ROOT" unlink "${e2##*/}"
+    test -e "$BOOT_ROOT/the-token/1.1.1/linux"
+    test -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+    test -e "$entry"
+    test ! -e "$e2"
+    # remove last entry referencing those files
+    entry_id="${entry##*/}"
+    entry_id="${entry_id%+*}.conf"
+    "$_KERNEL_INSTALL_BOOTCTL" --root="$BOOT_ROOT" unlink "$entry_id"
+    test ! -e "$entry"
+    test ! -e "$BOOT_ROOT/the-token/1.1.1/linux"
+    test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd"
+fi
index 3aa98e1ffdffa01a4a7ff7d72ba5f25dd745f799..8dfc97c9e8a403ff74afa54a56a89cc219607748 100644 (file)
@@ -15,6 +15,7 @@ path = run_command(sh, '-c', 'echo "$PATH"', check: true).stdout().strip()
 test_env = environment()
 test_env.set('SYSTEMD_LANGUAGE_FALLBACK_MAP', language_fallback_map)
 test_env.set('PATH', project_build_root + ':' + path)
+test_env.set('PROJECT_BUILD_ROOT', project_build_root)
 
 ############################################################