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 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ff0fa827e83f09436ff27f746e5dfcc3875b079a --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ + +# 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 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); +} 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 = .{""}, +} 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", + }, + }, + }, +} 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 }; 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; +}