diff options
| author | rawnix ports <ports@rawnix.org> | 2026-09-26 18:19:05 +0000 |
|---|---|---|
| committer | rawnix ports <ports@rawnix.org> | 2026-09-26 18:19:05 +0000 |
| commit | c82e88dd00d1b0b7fcfb4e5628d99611388cef6f (patch) | |
| tree | 56d864b5a5c6c145edd96178ae219a8f90e191a6 /opt/limine/limine-sync.c | |
| download | ports-c82e88dd00d1b0b7fcfb4e5628d99611388cef6f.tar.gz | |
sync 2026-09-26 18:19 UTC
1672 files changed, 151396 insertions(+)
Diffstat (limited to 'opt/limine/limine-sync.c')
| -rw-r--r-- | opt/limine/limine-sync.c | 1058 |
1 files changed, 1058 insertions, 0 deletions
diff --git a/opt/limine/limine-sync.c b/opt/limine/limine-sync.c new file mode 100644 index 0000000..47ba483 --- /dev/null +++ b/opt/limine/limine-sync.c @@ -0,0 +1,1058 @@ +/* + * 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 <ctype.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#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; +} |
