/* * limine-sync - reconcile /boot/efi against /boot, regenerate boot configs * https://rawnix.org * * Copyright (c) 2026 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Pure function of three directory listings: * * /boot payload staged by packages * /lib/modules module trees, deciding which kernels are usable * /boot/efi current ESP state, reconciled to match * * Takes no arguments that change what it does. Fired by the mkpkg * "boot/" trigger on both add and remove, and safe to run by hand at * any time. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #ifndef BOOT_DIR #define BOOT_DIR "/boot" #endif #ifndef ESP_DIR #define ESP_DIR "/boot/efi" #endif #ifndef MOD_DIR #define MOD_DIR "/lib/modules" #endif #ifndef TEMPLATE #define TEMPLATE "/etc/limine.conf.in" #endif #ifndef CONF_FILE #define CONF_FILE "/etc/limine-sync.conf" #endif #ifndef LIMINE_EFI #define LIMINE_EFI "/usr/share/limine/BOOTX64.EFI" #endif #define ESP_EFI "EFI/BOOT/BOOTX64.EFI" #define MARKER "@KERNELS@" #define MAXLINE 1024 #define COPY_BUF (256 * 1024) /* ── config ──────────────────────────────────────────────────────────── */ /* * No default. Every other setting here has a right answer that is the * same on every machine; the root device does not. A compiled-in * guess writes a bootable-looking menu that points at the wrong disk * and reports success, and the evidence is a file the machine does not * have. Refusing costs one line in limine-sync.conf. */ static char cf_cmdline[MAXLINE] = ""; static char cf_title[MAXLINE] = "rawnix"; static char cf_microcode[MAXLINE] = "amd-ucode.img"; /* * The initramfs is version-independent -- tinyrd carries no modules and * takes the LUKS UUID from the cmdline -- so one filename serves every * kernel and there is nothing to key on a version. Set INITRD= empty * to boot without one. */ static char cf_initrd[MAXLINE] = "tinyrd.img"; static char cf_xen_kernel[MAXLINE] = ""; /* series pin, e.g. "6.12" */ static char cf_xen_cmdline[MAXLINE] = ""; static char cf_xen_options[MAXLINE] = ""; static char cf_xen_ucode[MAXLINE] = "no"; static int opt_dry; static int opt_quiet; /* ── plumbing ────────────────────────────────────────────────────────── */ static void die(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2))); static void warn_(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static void info(const char *fmt, ...) __attribute__((format(printf, 1, 2))); static void die(const char *fmt, ...) { va_list ap; fputs("limine-sync: ", stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); exit(1); } static void warn_(const char *fmt, ...) { va_list ap; fputs("limine-sync: warning: ", stderr); va_start(ap, fmt); vfprintf(stderr, fmt, ap); va_end(ap); fputc('\n', stderr); } static void info(const char *fmt, ...) { va_list ap; /* * Verbose by default. This tool only speaks when it changes * something, so a steady-state run is silent anyway — and a run * that rewrites the ESP bootloader must never be. Fired from a * trigger, its output is the only evidence of what happened. */ if (opt_quiet) return; fputs(opt_dry ? "would: " : "", stdout); va_start(ap, fmt); vprintf(fmt, ap); va_end(ap); putchar('\n'); } static void *xmalloc(size_t n) { void *p = malloc(n); if (!p) die("out of memory"); return p; } static char *xstrdup(const char *s) { char *p = strdup(s); if (!p) die("out of memory"); return p; } /* ── string list ─────────────────────────────────────────────────────── */ typedef struct { char **item; size_t count; size_t cap; } list_t; static void list_add(list_t *l, const char *s) { if (l->count == l->cap) { l->cap = l->cap ? l->cap * 2 : 8; l->item = realloc(l->item, l->cap * sizeof(char *)); if (!l->item) die("out of memory"); } l->item[l->count++] = xstrdup(s); } static int list_has(const list_t *l, const char *s) { size_t i; for (i = 0; i < l->count; i++) if (strcmp(l->item[i], s) == 0) return 1; return 0; } static void list_free(list_t *l) { size_t i; for (i = 0; i < l->count; i++) free(l->item[i]); free(l->item); l->item = NULL; l->count = l->cap = 0; } /* ── version comparison ──────────────────────────────────────────────── */ /* * Compare dotted numeric versions field by field, numerically. * 6.12.107 > 6.12.74 and 6.9 < 6.12, both of which lexicographic * sorting gets wrong. Non-digit separators are skipped, so this * also handles 6.18.49-rc1 style suffixes tolerably. */ static int ver_cmp(const char *a, const char *b) { for (;;) { long na = 0, nb = 0; while (*a && !isdigit((unsigned char)*a)) a++; while (*b && !isdigit((unsigned char)*b)) b++; while (isdigit((unsigned char)*a)) na = na * 10 + (*a++ - '0'); while (isdigit((unsigned char)*b)) nb = nb * 10 + (*b++ - '0'); if (na != nb) return na < nb ? -1 : 1; if (!*a && !*b) return 0; } } static int ver_cmp_desc(const void *a, const void *b) { return -ver_cmp(*(char * const *)a, *(char * const *)b); } /* Does version `v` belong to series `s`? "6.12" matches "6.12.107". */ static int in_series(const char *v, const char *s) { size_t n = strlen(s); if (strncmp(v, s, n) != 0) return 0; return v[n] == '\0' || v[n] == '.'; } /* ── path helpers ────────────────────────────────────────────────────── */ static int has_prefix(const char *s, const char *p) { return strncmp(s, p, strlen(p)) == 0; } static int has_suffix(const char *s, const char *p) { size_t ls = strlen(s), lp = strlen(p); return ls >= lp && strcmp(s + ls - lp, p) == 0; } static char *joinpath(const char *dir, const char *base) { size_t n = strlen(dir) + strlen(base) + 2; char *p = xmalloc(n); snprintf(p, n, "%s/%s", dir, base); return p; } /* * A directory is a mountpoint if its st_dev differs from its parent's. * Without this check the tool happily writes kernels into an empty * directory on the root filesystem, reports success, and leaves you * booting the old kernel with no indication why. */ static int is_mountpoint(const char *path) { struct stat a, b; char *parent = joinpath(path, ".."); int r; if (stat(path, &a) != 0 || stat(parent, &b) != 0) r = -1; else r = a.st_dev != b.st_dev; free(parent); return r; } /* ── file classes ────────────────────────────────────────────────────── */ /* * Files the reconciler owns on the ESP. Two properties per class: * * required — an empty class in /boot aborts the whole run * prune — delete ESP members with no /boot counterpart * * A class that is empty in /boot is never pruned, whatever `prune` * says. That is what lets amd-ucode.img survive on the ESP until the * amd-firmware port is changed to stage into /boot: nothing in /boot * matches *.img yet, so the class is dormant rather than destructive. */ struct class { const char *name; const char *prefix; const char *suffix; int prune; }; static const struct class classes[] = { { "vmlinuz-*", "vmlinuz-", "", 1 }, { "initramfs-*", "initramfs-", "", 1 }, { "xen-*.efi", "xen-", ".efi", 1 }, { "*.img", "", ".img", 1 }, { NULL, NULL, NULL, 0 } }; static const struct class *classify(const char *name) { const struct class *c; for (c = classes; c->name; c++) { if (!has_prefix(name, c->prefix)) continue; if (*c->suffix && !has_suffix(name, c->suffix)) continue; /* "" prefix + ".img" must not swallow initramfs-*.img */ if (!*c->prefix) { const struct class *o; int taken = 0; for (o = classes; o->name; o++) if (o != c && *o->prefix && has_prefix(name, o->prefix)) taken = 1; if (taken) continue; } return c; } return NULL; } /* ── directory scanning ──────────────────────────────────────────────── */ static void scan_managed(const char *dir, list_t *out) { DIR *d = opendir(dir); struct dirent *e; if (!d) die("cannot read %s: %s", dir, strerror(errno)); while ((e = readdir(d)) != NULL) { char *full; struct stat st; if (e->d_name[0] == '.') continue; if (!classify(e->d_name)) continue; /* * lstat, not stat: the mirror set must contain real files only. * Ports ship version-stripped symlinks next to their payload * (xen-4.22.efi -> xen-4.22.0.efi); following them would copy * the same binary to the ESP twice under two names, and the * ESP is FAT, which has no symlinks to mirror them with. */ full = joinpath(dir, e->d_name); if (lstat(full, &st) == 0 && S_ISREG(st.st_mode)) list_add(out, e->d_name); free(full); } closedir(d); } static void scan_modules(list_t *out) { DIR *d = opendir(MOD_DIR); struct dirent *e; if (!d) return; /* no module trees at all is legal, just unusual */ while ((e = readdir(d)) != NULL) { char *full; struct stat st; if (e->d_name[0] == '.') continue; full = joinpath(MOD_DIR, e->d_name); if (stat(full, &st) == 0 && S_ISDIR(st.st_mode)) list_add(out, e->d_name); free(full); } closedir(d); } /* ── file copying ────────────────────────────────────────────────────── */ static int files_identical(const char *a, const char *b) { struct stat sa, sb; int fa, fb, same = 1; char *ba, *bb; if (stat(a, &sa) != 0 || stat(b, &sb) != 0) return 0; if (sa.st_size != sb.st_size) return 0; /* * Size matched, so compare content. Timestamps are useless here: * vfat stores mtime at two-second granularity, so a comparison * against an ext4/nvme source is wrong roughly half the time. */ if ((fa = open(a, O_RDONLY)) < 0) return 0; if ((fb = open(b, O_RDONLY)) < 0) { close(fa); return 0; } ba = xmalloc(COPY_BUF); bb = xmalloc(COPY_BUF); for (;;) { ssize_t na = read(fa, ba, COPY_BUF); ssize_t nb = read(fb, bb, COPY_BUF); if (na != nb || na < 0) { same = 0; break; } if (na == 0) break; if (memcmp(ba, bb, (size_t)na) != 0) { same = 0; break; } } free(ba); free(bb); close(fa); close(fb); return same; } /* * Straight copy, no temp file. A staging copy would double peak ESP * usage for a 15MB kernel image on a partition sized in hundreds of * megabytes. The failure mode is handled instead: a partial write is * unlinked, so the ESP holds either a whole kernel or nothing, and * the next run copies it again. */ static int copy_file(const char *src, const char *dst) { int fs, fd; char *buf; ssize_t n; int ok = 1; if ((fs = open(src, O_RDONLY)) < 0) { warn_("open %s: %s", src, strerror(errno)); return 0; } if ((fd = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { warn_("create %s: %s", dst, strerror(errno)); close(fs); return 0; } buf = xmalloc(COPY_BUF); while ((n = read(fs, buf, COPY_BUF)) > 0) { if (write(fd, buf, (size_t)n) != n) { warn_("write %s: %s", dst, strerror(errno)); ok = 0; break; } } if (n < 0) { warn_("read %s: %s", src, strerror(errno)); ok = 0; } free(buf); if (ok && fsync(fd) != 0) { warn_("fsync %s: %s", dst, strerror(errno)); ok = 0; } close(fs); close(fd); if (!ok) unlink(dst); return ok; } /* * Copy via a temp file in the destination directory, then rename. * Reserved for the bootloader itself: a partial vmlinuz costs one menu * entry, a partial BOOTX64.EFI costs the machine. ~150KB, so the * doubled peak ESP usage that ruled this out for kernels is irrelevant. */ static int copy_file_atomic(const char *src, const char *dst) { char tmp[1024]; snprintf(tmp, sizeof(tmp), "%s.tmp", dst); if (!copy_file(src, tmp)) return 0; if (rename(tmp, dst) != 0) { warn_("rename %s: %s", dst, strerror(errno)); unlink(tmp); return 0; } return 1; } /* mkdir -p, for EFI/BOOT on a freshly formatted ESP */ static int mkdir_p(const char *path) { char buf[1024], *p; snprintf(buf, sizeof(buf), "%s", path); for (p = buf + 1; *p; p++) { if (*p != '/') continue; *p = '\0'; if (mkdir(buf, 0755) != 0 && errno != EEXIST) return 0; *p = '/'; } return mkdir(buf, 0755) == 0 || errno == EEXIST; } /* * Write only if the content differs. Regenerating identical bytes on * every trigger means an ESP write per package install — pointless * flash traffic, and every write is a window in which a power loss * leaves a half-written boot config. Returns 1 if it wrote. */ /* * Config files are small, so these do get a temp-and-rename. A * truncated limine.conf costs you the whole menu, not one entry. */ static int write_atomic(const char *path, const char *data) { char tmp[1024]; int fd; size_t len = strlen(data); snprintf(tmp, sizeof(tmp), "%s.tmp", path); if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0644)) < 0) { warn_("create %s: %s", tmp, strerror(errno)); return 0; } if (write(fd, data, len) != (ssize_t)len || fsync(fd) != 0) { warn_("write %s: %s", tmp, strerror(errno)); close(fd); unlink(tmp); return 0; } close(fd); if (rename(tmp, path) != 0) { warn_("rename %s: %s", path, strerror(errno)); unlink(tmp); return 0; } return 1; } /* ── growable output buffer ──────────────────────────────────────────── */ typedef struct { char *buf; size_t len; size_t cap; } buf_t; static void bufcat(buf_t *b, const char *fmt, ...) { va_list ap; int n; for (;;) { size_t space = b->cap - b->len; va_start(ap, fmt); n = vsnprintf(b->buf ? b->buf + b->len : NULL, space, fmt, ap); va_end(ap); if (n < 0) die("vsnprintf failed"); if ((size_t)n < space) break; b->cap = b->cap ? b->cap * 2 : 4096; while (b->cap - b->len <= (size_t)n) b->cap *= 2; b->buf = realloc(b->buf, b->cap); if (!b->buf) die("out of memory"); } b->len += (size_t)n; } /* ── config file ─────────────────────────────────────────────────────── */ static void unquote(char *s) { size_t n = strlen(s); if (n >= 2 && (s[0] == '"' || s[0] == '\'') && s[n - 1] == s[0]) { memmove(s, s + 1, n - 2); s[n - 2] = '\0'; } } static void load_conf(void) { FILE *fp = fopen(CONF_FILE, "r"); char line[MAXLINE]; if (!fp) return; /* compiled-in defaults are a working configuration */ while (fgets(line, sizeof(line), fp)) { char *key, *val, *p; if ((p = strchr(line, '#')) != NULL) *p = '\0'; if ((p = strchr(line, '\n')) != NULL) *p = '\0'; key = line; while (*key && isspace((unsigned char)*key)) key++; if (!(val = strchr(key, '='))) continue; *val++ = '\0'; p = key + strlen(key); while (p > key && isspace((unsigned char)p[-1])) *--p = '\0'; while (*val && isspace((unsigned char)*val)) val++; p = val + strlen(val); while (p > val && isspace((unsigned char)p[-1])) *--p = '\0'; unquote(val); if (!strcmp(key, "CMDLINE")) snprintf(cf_cmdline, MAXLINE, "%s", val); else if (!strcmp(key, "TITLE")) snprintf(cf_title, MAXLINE, "%s", val); else if (!strcmp(key, "MICROCODE")) snprintf(cf_microcode, MAXLINE, "%s", val); else if (!strcmp(key, "INITRD")) snprintf(cf_initrd, MAXLINE, "%s", val); else if (!strcmp(key, "XEN_KERNEL")) snprintf(cf_xen_kernel, MAXLINE, "%s", val); else if (!strcmp(key, "XEN_CMDLINE")) snprintf(cf_xen_cmdline, MAXLINE, "%s", val); else if (!strcmp(key, "XEN_OPTIONS")) snprintf(cf_xen_options, MAXLINE, "%s", val); else if (!strcmp(key, "XEN_UCODE")) snprintf(cf_xen_ucode, MAXLINE, "%s", val); else warn_("%s: unknown key %s", CONF_FILE, key); } fclose(fp); } static char *read_file(const char *path) { FILE *fp = fopen(path, "r"); char *buf; long n; if (!fp) return NULL; if (fseek(fp, 0, SEEK_END) != 0) { fclose(fp); return NULL; } n = ftell(fp); rewind(fp); if (n < 0) { fclose(fp); return NULL; } buf = xmalloc((size_t)n + 1); if (fread(buf, 1, (size_t)n, fp) != (size_t)n) { free(buf); fclose(fp); return NULL; } buf[n] = '\0'; fclose(fp); return buf; } static int write_if_changed(const char *path, const char *data) { char *cur = read_file(path); int same = cur && strcmp(cur, data) == 0; free(cur); if (same) return 0; if (!opt_dry && !write_atomic(path, data)) die("could not write %s", path); return 1; } /* ── main ────────────────────────────────────────────────────────────── */ static void usage(void) { fputs("usage: limine-sync [-n] [-q]\n" " -n dry run, print what would change\n" " -q quiet; suppress the record of what changed\n", stderr); exit(1); } int main(int argc, char **argv) { list_t boot = {0}, esp = {0}, mods = {0}, kernels = {0}; buf_t entries = {0}, out = {0}; char *tmpl; const char *marker, *marker_end = NULL; const char *newest_xen = NULL; const char *xen_kernel = NULL; size_t i; int have_ucode, have_initrd, opt; while ((opt = getopt(argc, argv, "nqvh")) != -1) { switch (opt) { case 'n': opt_dry = 1; break; case 'q': opt_quiet = 1; break; case 'v': break; /* accepted, now the default */ default: usage(); } } load_conf(); /* ── interlocks ──────────────────────────────────────────────── */ if (!*cf_cmdline) die("no CMDLINE in %s.\n" " Generated entries need a root device; there is no sane " "default.\n" " On an install target: setup-boot /mnt\n" " By hand: echo 'CMDLINE=root=/dev/sdXn ro quiet' " ">> %s", CONF_FILE, CONF_FILE); switch (is_mountpoint(ESP_DIR)) { case -1: die("%s: %s", ESP_DIR, strerror(errno)); case 0: die("%s is not a mountpoint — refusing to write to the root fs", ESP_DIR); } scan_managed(BOOT_DIR, &boot); scan_managed(ESP_DIR, &esp); scan_modules(&mods); for (i = 0; i < boot.count; i++) if (has_prefix(boot.item[i], "vmlinuz-")) list_add(&kernels, boot.item[i] + strlen("vmlinuz-")); if (kernels.count == 0) die("no vmlinuz-* in %s — refusing to prune the ESP.\n" " Seed it once by hand: cp %s/vmlinuz-* %s/", BOOT_DIR, ESP_DIR, BOOT_DIR); qsort(kernels.item, kernels.count, sizeof(char *), ver_cmp_desc); /* ── mirror /boot -> ESP ─────────────────────────────────────── */ for (i = 0; i < boot.count; i++) { char *src = joinpath(BOOT_DIR, boot.item[i]); char *dst = joinpath(ESP_DIR, boot.item[i]); if (!files_identical(src, dst)) { info("copy %s", boot.item[i]); if (!opt_dry && !copy_file(src, dst)) die("mirroring %s failed — ESP left inconsistent", boot.item[i]); } free(src); free(dst); } /* * ── mirror the bootloader itself ───────────────────────────── * * The limine port installs only /usr/share/limine/BOOTX64.EFI; it * does not write to the ESP, so no package does. Never pruned: * removing the limine package must not remove the bootloader from * a running machine. */ if (access(LIMINE_EFI, R_OK) == 0) { char *dst = joinpath(ESP_DIR, ESP_EFI); if (!files_identical(LIMINE_EFI, dst)) { info("install bootloader %s", ESP_EFI); if (!opt_dry) { char *dir = joinpath(ESP_DIR, "EFI/BOOT"); if (!mkdir_p(dir)) die("cannot create %s: %s", dir, strerror(errno)); free(dir); if (!copy_file_atomic(LIMINE_EFI, dst)) die("installing the bootloader failed — ESP may have " "no loader; fix by hand before rebooting"); } } free(dst); } else { warn_("%s not found — leaving the ESP bootloader alone", LIMINE_EFI); } /* ── prune ESP ───────────────────────────────────────────────── */ for (i = 0; i < esp.count; i++) { const struct class *c = classify(esp.item[i]); size_t j; int class_populated = 0; if (!c || !c->prune || list_has(&boot, esp.item[i])) continue; for (j = 0; j < boot.count; j++) if (classify(boot.item[j]) == c) class_populated = 1; if (!class_populated) { /* * Dry run only. This reports a non-action, and a normal run * must stay silent when nothing changed — that silence is * what makes any line under "trigger: limine-sync" mean the * boot path really moved. */ if (opt_dry) info("keep %s (no %s staged in %s yet)", esp.item[i], c->name, BOOT_DIR); continue; } info("delete %s", esp.item[i]); if (!opt_dry) { char *p = joinpath(ESP_DIR, esp.item[i]); if (unlink(p) != 0) warn_("unlink %s: %s", p, strerror(errno)); free(p); } } /* ── generate the managed entry block ────────────────────────── */ /* * Entries reference boot():/ — the ESP — so they must describe the * ESP as it now stands, not what /boot staged. Files in a dormant * class (a xen-*.efi or microcode image not yet owned by any * package) are on the ESP and bootable; reading /boot would drop * their entries and silently shorten the menu. */ list_free(&esp); scan_managed(ESP_DIR, &esp); have_ucode = list_has(&esp, cf_microcode); have_initrd = *cf_initrd && list_has(&esp, cf_initrd); /* * An encrypted root with no initramfs on the ESP is a kernel panic * at the next reboot, and the cmdline that causes it is in a file * nobody reads again. Say so while there is still a shell. */ if (strstr(cf_cmdline, "cryptroot=") && !have_initrd) warn_("cmdline has cryptroot= but %s is not on the ESP — " "the generated entries cannot unlock root", cf_initrd); for (i = 0; i < kernels.count; i++) { const char *v = kernels.item[i]; bufcat(&entries, "/%s %s\n", cf_title, v); if (!list_has(&mods, v)) bufcat(&entries, " comment: no module tree in %s/%s" " — modular hardware will not work\n", MOD_DIR, v); bufcat(&entries, " protocol: linux\n"); bufcat(&entries, " path: boot():/vmlinuz-%s\n", v); bufcat(&entries, " cmdline: %s\n", cf_cmdline); if (have_ucode) bufcat(&entries, " module_path: boot():/%s\n", cf_microcode); /* * After the microcode, always. The kernel only applies an * early microcode update from the head of the initrd chain, so * an initramfs ahead of it silently disables it. */ if (have_initrd) bufcat(&entries, " module_path: boot():/%s\n", cf_initrd); if (i + 1 < kernels.count) bufcat(&entries, "\n"); } /* newest xen-*.efi on the ESP, if any, gets a chainload entry */ for (i = 0; i < esp.count; i++) { if (!has_prefix(esp.item[i], "xen-") || !has_suffix(esp.item[i], ".efi")) continue; if (!newest_xen || ver_cmp(esp.item[i], newest_xen) > 0) newest_xen = esp.item[i]; } if (newest_xen) bufcat(&entries, "\n/%s Xen\n" " protocol: efi\n" " path: boot():/%s\n", cf_title, newest_xen); /* ── splice into the template ────────────────────────────────── */ if (!(tmpl = read_file(TEMPLATE))) die("cannot read %s: %s", TEMPLATE, strerror(errno)); /* * Only real option lines count. A plain strstr() over the whole * file also matches the word inside a comment explaining why not to * set it — which is exactly what the shipped template does. * limine.conf comments start with '#' and are always on their own * line, so skipping leading space and rejecting '#' is sufficient. */ { const char *ln = tmpl; while (ln && *ln) { const char *t = ln; while (*t == ' ' || *t == '\t') t++; if (*t != '#' && has_prefix(t, "default_entry")) die("%s sets default_entry, which is a 1-based index.\n" " Generated entries shift it whenever a kernel is " "added or\n" " removed. Use remember_last_entry, or rely on " "entry 1.", TEMPLATE); ln = strchr(ln, '\n'); if (ln) ln++; } } /* * The marker must be alone on a non-comment line. A bare strstr() * also matches the marker named inside a comment describing it — * which the shipped template does — and splices the entire menu * into the middle of that sentence. */ { const char *ln = tmpl; marker = NULL; while (*ln) { const char *t = ln, *e; while (*t == ' ' || *t == '\t') t++; if (*t != '#' && has_prefix(t, MARKER)) { e = t + strlen(MARKER); while (*e == ' ' || *e == '\t' || *e == '\r') e++; if (*e == '\n' || *e == '\0') { marker = ln; marker_end = *e == '\n' ? e + 1 : e; break; } } ln = strchr(ln, '\n'); if (!ln) break; ln++; } } if (!marker) die("%s has no line containing only %s", TEMPLATE, MARKER); bufcat(&out, "# Generated by limine-sync. Edit %s, not this file.\n", TEMPLATE); bufcat(&out, "%.*s", (int)(marker - tmpl), tmpl); bufcat(&out, "%s", entries.buf ? entries.buf : ""); bufcat(&out, "%s", marker_end); { char *dst = joinpath(ESP_DIR, "limine.conf"); if (write_if_changed(dst, out.buf)) info("write %s (%zu kernels)", dst, kernels.count); free(dst); } /* ── xen.cfg ─────────────────────────────────────────────────── */ if (*cf_xen_kernel) { for (i = 0; i < kernels.count; i++) { if (!in_series(kernels.item[i], cf_xen_kernel)) continue; if (!list_has(&mods, kernels.item[i])) { warn_("xen: skipping %s, no module tree", kernels.item[i]); continue; } xen_kernel = kernels.item[i]; break; /* kernels are sorted newest first */ } if (!xen_kernel) { warn_("no usable kernel in series %s — leaving xen.cfg alone", cf_xen_kernel); } else { buf_t x = {0}; char *dst = joinpath(ESP_DIR, "xen.cfg"); bufcat(&x, "# Generated by limine-sync. Edit %s, not this file.\n", CONF_FILE); bufcat(&x, "[global]\ndefault=%s\n", cf_title); bufcat(&x, "[%s]\n", cf_title); if (*cf_xen_options) bufcat(&x, "options=%s\n", cf_xen_options); bufcat(&x, "kernel=vmlinuz-%s%s%s\n", xen_kernel, *cf_xen_cmdline ? " " : "", cf_xen_cmdline); /* * Xen takes a single ramdisk, so microcode and an * initramfs are mutually exclusive here. An encrypted dom0 * root therefore cannot also get early microcode through * this path -- it would need Xen's own ucode= instead. */ if (!strcmp(cf_xen_ucode, "yes") && have_ucode) bufcat(&x, "ramdisk=%s\n", cf_microcode); else if (have_initrd) bufcat(&x, "ramdisk=%s\n", cf_initrd); else if (strstr(cf_xen_cmdline, "cryptroot=") || strstr(cf_cmdline, "cryptroot=")) warn_("xen: no ramdisk — dom0 cannot unlock an encrypted root"); if (write_if_changed(dst, x.buf)) info("write %s (kernel %s)", dst, xen_kernel); free(x.buf); free(dst); } } free(tmpl); free(entries.buf); free(out.buf); list_free(&boot); list_free(&esp); list_free(&mods); list_free(&kernels); return 0; }