λ

lumine

UEFI x86_64 bootloader for linux kernel images

settings

diff

f1caee61

+302 -0

.gitignore added +5 -0

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..c4a0218398f5391b8b66d5e765cef165c6d0c96a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+zig-pkg
+zig-out
+.zig-cache
+
+.ignore

LICENSE added +24 -0

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..1ab9636d8e5dc3d0324f2086c562307ac7cca4b1
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2026, dragsbruh
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md added +57 -0

diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..ff0fa827e83f09436ff27f746e5dfcc3875b079a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+<!-- working title -->
+# lumine
+
+cheapskate x86_64/aarch64 UEFI bootloader
+
+## config
+
+> [!WARNING]
+> - this rewrite is **EXTREMELY** incomplete, see `master` branch for a somewhat working c implementation.
+> - docs are written primarily for `x86_64`, just replace `BOOTX64` with `BOOTAA64` and the rest should be equivalent.
+
+config for lumine is currently written in zig object notation, see [default config](./config.zon) for reference.
+see [installation](#installation) for instructions on where to put config file.
+
+## compiling
+
+lumine uses the [zig build system](https://ziglang.org/learn/build-system/) version `0.16.0`, which
+you can download [here](https://ziglang.org/download/).
+
+```sh
+zig build -Doptimize=ReleaseSafe -Darch=x86_64
+# efi executable is now in `zig-out/build/BOOTX64.efi` 
+```
+
+## emulation
+
+dependencies like edk2 ovmf firmware are already downloaded as a zig dependency (hmmmm, even if you dont want emulation?)
+
+```sh
+# starts in serial console
+zig build -Darch=x86_64 run
+```
+
+## installation
+
+i dont want installation or setup or usage to be something convoluted or "hidden" away like grub does, so
+that you can literally do it manually and it works fine, or maybe automate finding of installations like
+grub's os-prober or whatever.
+
+therefore, lumine is only two files:
+- efi executable: can be placed anywhere, partition default `/EFI/BOOT/BOOTX64.EFI` or really any location
+  like `/lumine/lumine.efi` etc etc, you have the freedom.
+- config file: for now, config MUST be placed in `/lumine.zon`. this WILL change soon however, because i dont like it.
+
+assuming your efi directory is `/boot`:
+
+```sh
+cp zig-out/build/BOOTX64.efi /boot/lumine/BOOTX64.EFI
+cp config.zon /boot/lumine.zon
+```
+
+now just make a efi boot entry and set BootNext flag to try it out. i wont give you the steps because, well its incomplete
+as shit and might not work if you smile a little gay, if you really wanna, you might already know how to do it anyway.
+
+## license
+
+this project is licensed under [BSD-2-Clause](./LICENSE) license

build.zig added +75 -0

diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000000000000000000000000000000000000..869a3590671e19e6a3b9309fa46efa6c02b4f459
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,75 @@
+const std = @import("std");
+
+const Arch = enum {
+    x86_64,
+    aarch64,
+
+    pub fn stdlib(self: Arch) std.Target.Cpu.Arch {
+        return switch (self) {
+            .x86_64 => .x86_64,
+            .aarch64 => .aarch64,
+        };
+    }
+
+    pub fn efiName(self: Arch, b: *std.Build) []const u8 {
+        const suffix = switch (self) {
+            .x86_64 => "X64",
+            .aarch64 => "AA64",
+        };
+        return b.fmt("BOOT{s}", .{suffix});
+    }
+};
+
+pub fn build(b: *std.Build) void {
+    const optimize = b.standardOptimizeOption(.{});
+
+    const cpu_arch = b.option(Arch, "arch", "cpu architecture") orelse .x86_64;
+
+    const efi_name = cpu_arch.efiName(b);
+
+    const lumine_exe = b.addExecutable(.{
+        .name = efi_name,
+        .root_module = b.createModule(.{
+            .root_source_file = b.path("src/main.zig"),
+            .target = b.resolveTargetQuery(.{
+                .os_tag = .uefi,
+                .cpu_arch = cpu_arch.stdlib(),
+            }),
+            .optimize = optimize,
+        }),
+    });
+
+    const install_step = b.getInstallStep();
+
+    const lumine_install = b.addInstallArtifact(lumine_exe, .{});
+    install_step.dependOn(&lumine_install.step);
+
+    const qemu_exe = b.option([]const u8, "qemu", "path to qemu binary") orelse b.fmt("qemu-system-{t}", .{cpu_arch});
+    const qemu_cmd = b.addSystemCommand(&.{ qemu_exe, "-m", "512M", "-serial", "mon:stdio", "-nographic", "-no-reboot", "-no-shutdown" });
+
+    {
+        const ovmf_dep = b.dependency("ovmf", .{});
+        const ovmf_code = ovmf_dep.path(b.fmt("ovmf-code-{t}.fd", .{cpu_arch}));
+
+        switch (cpu_arch) {
+            .aarch64 => {
+                qemu_cmd.addArgs(&.{ "-M", "virt", "-cpu", "max" });
+            },
+            .x86_64 => {},
+        }
+
+        qemu_cmd.addArg("-bios");
+        qemu_cmd.addFileArg(ovmf_code);
+    }
+
+    {
+        const boot_dir = b.addTempFiles();
+        _ = boot_dir.addCopyFile(lumine_exe.getEmittedBin(), b.fmt("EFI/BOOT/{s}.EFI", .{efi_name}));
+
+        qemu_cmd.addArg("-drive");
+        qemu_cmd.addPrefixedDirectoryArg("format=raw,file=fat:rw:", boot_dir.getDirectory());
+    }
+
+    const run_step = b.step("run", "run in qemu");
+    run_step.dependOn(&qemu_cmd.step);
+}

build.zig.zon added +13 -0

diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000000000000000000000000000000000000..451bc0dc58e73f1f5d0960cd3d61e20f4bcf5daf
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,13 @@
+.{
+    .name = .lumine,
+    .version = "0.0.1",
+    .minimum_zig_version = "0.16.0",
+    .fingerprint = 0xb72a0db2e975b735,
+    .dependencies = .{
+        .ovmf = .{
+            .url = "https://github.com/osdev0/edk2-ovmf-nightly/releases/download/20260531T041444Z/edk2-ovmf.tar.gz",
+            .hash = "N-V-__8AAAAAjAyHNKDbnzj1LQizupozkEzL21pfQiTzt8NQ",
+        },
+    },
+    .paths = .{""},
+}

config.zon added +15 -0

diff --git a/config.zon b/config.zon
new file mode 100644
index 0000000000000000000000000000000000000000..a50892956f699929983f43bfbd03a66d3c1af082
--- /dev/null
+++ b/config.zon
@@ -0,0 +1,15 @@
+// example config, since this is unimplemented
+// for now, paths must be absolute path and in boot partition
+.{
+    .boot = .{
+        .linux = .{
+            .kernel_path = "\\linux\\vmlinuz",
+            .kernel_cmdline = "root=/dev/vda1 rw",
+
+            .module_paths = .{
+                "\\linux\\initramfs.img",
+                "\\linux\\intel-ucode.img",
+            },
+        },
+    },
+}

src/main.zig added +41 -0

diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000000000000000000000000000000000000..2614d4619b2066bcb6ccd1c99d65258e0d34f101
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,41 @@
+const std = @import("std");
+const uefi = std.os.uefi;
+const log = std.log;
+
+const Writer = @import("./uefi/Writer.zig");
+
+pub fn bmain() !void {
+    log.info("hello lumine", .{});
+    while (true)
+        std.atomic.spinLoopHint();
+}
+
+pub fn main() uefi.Status {
+    bmain() catch |err| {
+        log.err("unrecoverable error in bmain: {}", .{err});
+        return .aborted;
+    };
+
+    return .success;
+}
+
+pub fn logFn(
+    comptime level: log.Level,
+    comptime scope: @EnumLiteral(),
+    comptime format: []const u8,
+    args: anytype,
+) void {
+    // stderr should be equivalent to stdout?
+    // but i dont see logs on my hardware if i use stderr
+    var stderr: Writer = .stdout();
+    stderr.interface.print("{t}({}): ", .{ level, scope }) catch return;
+    stderr.interface.print(format ++ "\r\n", args) catch return;
+}
+
+pub fn panicFn(msg: []const u8, _: ?usize) noreturn {
+    logFn(.err, .panic, "{s}", .{msg});
+    @trap();
+}
+
+pub const panic = std.debug.FullPanic(panicFn);
+pub const std_options: std.Options = .{ .logFn = logFn };

src/uefi/Writer.zig added +72 -0

diff --git a/src/uefi/Writer.zig b/src/uefi/Writer.zig
new file mode 100644
index 0000000000000000000000000000000000000000..43fd9726616a8b9e46aa4008c7cd5e33ff2fb13b
--- /dev/null
+++ b/src/uefi/Writer.zig
@@ -0,0 +1,72 @@
+const std = @import("std");
+const Io = std.Io;
+const uefi = std.os.uefi;
+
+const Writer = @This();
+
+interface: Io.Writer,
+protocol: *uefi.protocol.SimpleTextOutput,
+
+pub fn init(buffer: []u8, protocol: *uefi.protocol.SimpleTextOutput) Writer {
+    return .{
+        .protocol = protocol,
+        .interface = .{
+            .buffer = buffer,
+            .vtable = &.{ .drain = drainFn },
+        },
+    };
+}
+
+pub fn stdout() Writer {
+    return .init(&.{}, uefi.system_table.con_out.?);
+}
+
+pub fn stderr() Writer {
+    return .init(&.{}, uefi.system_table.std_err.?);
+}
+
+pub fn write(self: *Writer, data: []const u8) !void {
+    var buffer: [64:0]u16 = undefined;
+    var buflen: usize = 0;
+    for (data) |b| {
+        buffer[buflen] = @intCast(b);
+        buflen += 1;
+        if (buflen == buffer.len) {
+            _ = self.protocol.outputString(&buffer) catch unreachable;
+            buflen = 0;
+        }
+    }
+    if (buflen > 0) {
+        buffer[buflen] = 0;
+        _ = self.protocol.outputString(buffer[0..buflen :0]) catch unreachable;
+    }
+}
+
+fn drainFn(io_w: *Io.Writer, data: []const []const u8, splat: usize) Io.Writer.Error!usize {
+    const self: *Writer = @fieldParentPtr("interface", io_w);
+
+    {
+        const buf = io_w.buffered();
+        if (buf.len > 0) {
+            try self.write(buf);
+            _ = io_w.consumeAll();
+        }
+    }
+
+    var written: usize = 0;
+
+    if (data.len > 1) {
+        for (data[0 .. data.len - 1]) |buf| {
+            try self.write(buf);
+            written += buf.len;
+        }
+    }
+
+    const buf = data[data.len - 1];
+    for (0..splat) |_| {
+        try self.write(buf);
+        written += buf.len;
+    }
+
+    return written;
+}