summaryrefslogtreecommitdiff
path: root/opt/limine
diff options
context:
space:
mode:
authorrawnix ports <ports@rawnix.org>2026-09-26 18:19:05 +0000
committerrawnix ports <ports@rawnix.org>2026-09-26 18:19:05 +0000
commitc82e88dd00d1b0b7fcfb4e5628d99611388cef6f (patch)
tree56d864b5a5c6c145edd96178ae219a8f90e191a6 /opt/limine
downloadports-c82e88dd00d1b0b7fcfb4e5628d99611388cef6f.tar.gz
sync 2026-09-26 18:19 UTC
1672 files changed, 151396 insertions(+)
Diffstat (limited to 'opt/limine')
-rw-r--r--opt/limine/.footprint14
-rw-r--r--opt/limine/.signature9
-rw-r--r--opt/limine/MAKEPKG48
-rw-r--r--opt/limine/limine-sync.c1058
-rw-r--r--opt/limine/limine-sync.conf.example108
-rw-r--r--opt/limine/limine.conf.in13
6 files changed, 1250 insertions, 0 deletions
diff --git a/opt/limine/.footprint b/opt/limine/.footprint
new file mode 100644
index 0000000..5ae359f
--- /dev/null
+++ b/opt/limine/.footprint
@@ -0,0 +1,14 @@
+drwxr-xr-x root/root etc/
+-rw-r--r-- root/root etc/limine-sync.conf.example
+-rw-r--r-- root/root etc/limine.conf.in
+drwxr-xr-x root/root sbin/
+-rwxr-xr-x root/root sbin/limine-sync
+drwxr-xr-x root/root usr/
+drwxr-xr-x root/root usr/bin/
+-rwxr-xr-x root/root usr/bin/limine
+drwxr-xr-x root/root usr/share/
+drwxr-xr-x root/root usr/share/limine/
+-rw-r--r-- root/root usr/share/limine/BOOTX64.EFI
+drwxr-xr-x root/root usr/share/man/
+drwxr-xr-x root/root usr/share/man/man1/
+-rw-r--r-- root/root usr/share/man/man1/limine.1.gz
diff --git a/opt/limine/.signature b/opt/limine/.signature
new file mode 100644
index 0000000..eab0f00
--- /dev/null
+++ b/opt/limine/.signature
@@ -0,0 +1,9 @@
+RWTZ9IduCSQ/mKJUwlhzbex++FtsZZvrBCEEZCKaqD9iLjxt52oolmtxlSbtS7hYLqIATN2EIVY2nfwb5ccTdDndnAs0ZQoZhgA=
+
+SHA256 (MAKEPKG) = cc65c7e40b19ef4b6bab670ea0ca6f11511d99dcb5e14bf7134859700fc9b512
+SHA256 (.footprint) = 2926db941c365b0ea5bc296d2a396b79a15582bee3c7549c6d1c13a9d47e0d49
+SHA256 (limine-12.9.0.tar.gz) = adea922af3b9c8179a4676bcecc8e4df2f3ef72ad36b3f4afab44cbf5f265e36
+SHA256 (limine-sync.c) = 54fb6a89ba0d07b6177777dcc96bb372e3014479c552a1a353d182e58b357a16
+SHA256 (limine.conf.in) = 53d3dbcf9db0d56dabf5d8ca2430e5de5de30d0e79f9d01dbf52f81dbd86a69b
+SHA256 (limine-sync.conf.example) = 5a7952ba6eaa21cbf071bc676478f262768b8b2e2cf5e4e4de12975d88ac49c2
+SHA256 (limine#12.9.0-2.pkg.tar.gz) = c04afbc26813238d5dca4b7d559f1d2a6b400839a6f6cebad05ea4435988da7b
diff --git a/opt/limine/MAKEPKG b/opt/limine/MAKEPKG
new file mode 100644
index 0000000..322e9be
--- /dev/null
+++ b/opt/limine/MAKEPKG
@@ -0,0 +1,48 @@
+#!/bin/mkpkg
+# description: Modern multiprotocol EFI bootloader
+# url: https://limine-bootloader.org/
+
+name=limine
+version=12.9.0
+release=2
+depends=()
+makedeps=(nasm)
+source=(https://github.com/Limine-Bootloader/Limine/releases/download/v${version}/${name}-${version}.tar.gz
+ limine-sync.c
+ limine.conf.in
+ limine-sync.conf.example
+)
+sha256sums=(
+ "adea922af3b9c8179a4676bcecc8e4df2f3ef72ad36b3f4afab44cbf5f265e36"
+ "54fb6a89ba0d07b6177777dcc96bb372e3014479c552a1a353d182e58b357a16"
+ "53d3dbcf9db0d56dabf5d8ca2430e5de5de30d0e79f9d01dbf52f81dbd86a69b"
+ "5a7952ba6eaa21cbf071bc676478f262768b8b2e2cf5e4e4de12975d88ac49c2"
+)
+
+build() {
+ cd limine-$version
+
+ ./configure \
+ --prefix=/usr \
+ --enable-uefi-x86-64
+
+ make $MKPKG_MAKEFLAGS
+
+ make DESTDIR=$PKG install
+
+ # Build and install limine-sync
+ cc -o "$SRC/limine-sync" "$SRC/limine-sync.c"
+ install -d "$PKG/sbin" "$PKG/etc"
+ install -m755 "$SRC/limine-sync" "$PKG/sbin/"
+ install -m644 "$SRC/limine.conf.in" "$PKG/etc"
+ install -m644 "$SRC/limine-sync.conf.example" "$PKG/etc"
+
+ rm -rf $PKG/usr/share/doc
+}
+
+signify() {
+ untrusted comment: public key
+ RWTZ9IduCSQ/mL8337TEUinPwT92xFEUpD92hkS7IxcOnzTt9QdpohT3
+}
+
+# vim: filetype=sh
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;
+}
diff --git a/opt/limine/limine-sync.conf.example b/opt/limine/limine-sync.conf.example
new file mode 100644
index 0000000..4e66397
--- /dev/null
+++ b/opt/limine/limine-sync.conf.example
@@ -0,0 +1,108 @@
+# /etc/limine-sync.conf.example — copy to /etc/limine-sync.conf and edit.
+#
+# limine-sync generates the kernel entries of the boot menu from what is
+# in /boot, and splices them into /etc/limine.conf.in at @KERNELS@. This
+# file supplies the few things a directory listing cannot tell it.
+#
+# Everything below except CMDLINE is optional and shown at its built-in
+# default; an absent file means "all defaults", which is why CMDLINE
+# being required is what makes the file required.
+#
+# Syntax: KEY=value, one per line. Keys are uppercase. Surrounding
+# quotes are stripped. '#' starts a comment anywhere on the line, so a
+# value cannot contain one. An unrecognised key is warned about and
+# ignored — check the output after editing.
+#
+# Nothing here is read at boot. Changes take effect when limine-sync
+# next runs: automatically on the boot/ trigger, or by hand. Always
+# look before you leap:
+#
+# limine-sync -n # print what would change, touch nothing
+# limine-sync
+#
+
+# ── CMDLINE (required) ───────────────────────────────────────────────
+#
+# The kernel command line, used verbatim for every generated entry.
+# There is no default: the root device is the one setting that differs
+# on every machine, and a guess would produce a bootable-looking menu
+# pointing at the wrong disk. limine-sync refuses to run without it.
+#
+# On an install target `setup-boot /mnt` writes this line for you, and
+# reruns are how you change it afterwards.
+#
+# Unencrypted root:
+#
+#CMDLINE=root=/dev/nvme0n1p2 ro quiet
+#
+# LUKS root. cryptroot= is the UUID of the *LUKS partition* (from
+# `cryptsetup luksUUID /dev/nvme0n1p2`), not of the filesystem inside
+# it. tinyrd creates the mapping under exactly the name given in
+# root=, so the name you passed to `cryptsetup open` belongs here and
+# is recorded nowhere else:
+#
+#CMDLINE=cryptroot=UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx root=/dev/mapper/croot ro quiet
+#
+# Serial console as well as the screen (VPS, or `qemu -nographic`):
+#
+#CMDLINE=root=/dev/vda2 ro quiet console=tty0 console=ttyS0,115200n8
+
+# ── TITLE ────────────────────────────────────────────────────────────
+#
+# Menu label. Entries read "<TITLE> <version>", e.g. "rawnix 6.18.50",
+# and the Xen chainload entry reads "<TITLE> Xen".
+#
+#TITLE=rawnix
+
+# ── INITRD ───────────────────────────────────────────────────────────
+#
+# Filename in /boot of the initramfs, added as a module_path after the
+# microcode. tinyrd carries no modules and takes the LUKS UUID from
+# the cmdline, so one image serves every kernel and this is a plain
+# name with no version in it.
+#
+# Set it empty to boot with no initramfs. That is fine for an
+# unencrypted root and impossible for a LUKS one — limine-sync warns if
+# CMDLINE has cryptroot= and the image is not on the ESP.
+#
+#INITRD=tinyrd.img
+
+# ── MICROCODE ────────────────────────────────────────────────────────
+#
+# Filename in /boot of the early microcode image, added as the *first*
+# module_path. The kernel only applies an early update from the head
+# of the initrd chain, so the order is not adjustable.
+#
+# Set empty on a machine that needs none, or on Intel once there is an
+# intel-ucode port. An entry is only emitted if the file is actually
+# on the ESP.
+#
+#MICROCODE=amd-ucode.img
+
+# ── Xen ──────────────────────────────────────────────────────────────
+#
+# Only relevant if a xen-*.efi is on the ESP. The newest one always
+# gets a chainload entry in limine.conf; these keys additionally
+# generate /boot/efi/xen.cfg, which is what picks dom0's kernel.
+#
+# XEN_KERNEL pins a *series*, not a version: "6.12" selects the newest
+# 6.12.x that has a module tree. Empty means leave xen.cfg alone,
+# which is the default and what you want if you maintain it by hand.
+#
+#XEN_KERNEL=
+#
+# Appended to dom0's kernel line. Note dom0 usually wants fewer CPUs
+# and less memory than the cmdline above gives a bare-metal boot.
+#
+#XEN_CMDLINE=
+#
+# The hypervisor's own options line.
+#
+#XEN_OPTIONS=dom0_mem=4G,max:4G dom0_max_vcpus=4
+#
+# Xen takes a single ramdisk, so microcode and an initramfs are
+# mutually exclusive here. yes = microcode; anything else = the
+# INITRD image, if there is one. An encrypted dom0 root therefore
+# cannot get early microcode this way and needs Xen's own ucode=.
+#
+#XEN_UCODE=no
diff --git a/opt/limine/limine.conf.in b/opt/limine/limine.conf.in
new file mode 100644
index 0000000..5ad4a1e
--- /dev/null
+++ b/opt/limine/limine.conf.in
@@ -0,0 +1,13 @@
+# /etc/limine.conf.in — hand-written half of the boot menu.
+# limine-sync splices generated kernel entries in at @KERNELS@ and
+# writes the result to /boot/efi/limine.conf. Edit this file, never
+# that one.
+#
+# Do not set default_entry: it is a 1-based index, so it shifts every
+# time a kernel is added or removed. limine-sync refuses the file if
+# it finds one.
+
+timeout: 5
+remember_last_entry: yes
+
+@KERNELS@