feat(adapter-riscv): load init from VirtIO block device with FAT16 filesystem

Adapter probes for VirtIO block device, reads init.elf from FAT16 disk,
parses ELF, maps into userspace, and launches. Falls back to embedded
minimal init if no disk is present.

Also: VirtIO MMIO driver, boot FAT16 reader, disk image build script,
Makefile targets for build/run/run-disk.
This commit is contained in:
2026-10-18 04:19:19 +02:00
parent a910c06cf7
commit d43c6a7e74
8 changed files with 586 additions and 93 deletions

45
Makefile Normal file
View File

@@ -0,0 +1,45 @@
.PHONY: check fmt clippy test kani build all image run
RISCV_TARGET = riscv64gc-unknown-none-elf
KERNEL_BIN = target/$(RISCV_TARGET)/release/adapter-riscv
all: check fmt clippy test
check:
cargo check --workspace
fmt:
cargo fmt --all -- --check
clippy:
cargo clippy --workspace -- -D warnings
test:
cargo test --workspace
kani-types:
cargo kani -p types
kani-domain:
cargo kani -p domain
kani-kernel:
cargo kani -p kernel
kani: kani-types kani-domain kani-kernel
build:
cargo build -p adapter-riscv --target $(RISCV_TARGET) --release
image: build
scripts/make-disk-image.sh
run: build
qemu-system-riscv64 -machine virt -nographic -bios default \
-kernel $(KERNEL_BIN)
run-disk: image
qemu-system-riscv64 -machine virt -nographic -bios default \
-kernel $(KERNEL_BIN) \
-drive file=boot.img,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0

View File

@@ -1,6 +1,8 @@
pub const RAM_BASE: usize = 0x80000000; pub const RAM_BASE: usize = 0x80000000;
pub const RAM_SIZE: usize = 128 * 1024 * 1024; pub const RAM_SIZE: usize = 128 * 1024 * 1024;
pub const UART_BASE: usize = 0x10000000; pub const UART_BASE: usize = 0x10000000;
pub const VIRTIO_BASE: usize = 0x10001000;
pub const VIRTIO_SIZE: usize = 0x8000;
pub const PLIC_BASE: usize = 0x0C000000; pub const PLIC_BASE: usize = 0x0C000000;
pub const CLINT_BASE: usize = 0x02000000; pub const CLINT_BASE: usize = 0x02000000;
pub const USER_POOL_BASE: usize = 0x84000000; pub const USER_POOL_BASE: usize = 0x84000000;

View File

@@ -0,0 +1,125 @@
use crate::hw::virtio_blk::VirtioBlk;
const SECTOR_SIZE: usize = 512;
const MAX_FILE_SIZE: usize = 64 * 1024; // 64K max for init binary
struct BootFat16 {
reserved_sectors: u16,
sectors_per_fat: u16,
root_dir_start: u32,
root_dir_sectors: u32,
data_start: u32,
sectors_per_cluster: u8,
}
impl BootFat16 {
fn parse(boot_sector: &[u8; SECTOR_SIZE]) -> Option<Self> {
if boot_sector[510] != 0x55 || boot_sector[511] != 0xAA {
return None;
}
let bytes_per_sector = u16::from_le_bytes([boot_sector[11], boot_sector[12]]);
if bytes_per_sector != SECTOR_SIZE as u16 { return None; }
let sectors_per_cluster = boot_sector[13];
let reserved_sectors = u16::from_le_bytes([boot_sector[14], boot_sector[15]]);
let num_fats = boot_sector[16];
let root_entry_count = u16::from_le_bytes([boot_sector[17], boot_sector[18]]);
let sectors_per_fat = u16::from_le_bytes([boot_sector[22], boot_sector[23]]);
let fat_start = reserved_sectors as u32;
let root_dir_start = fat_start + (num_fats as u32) * (sectors_per_fat as u32);
let root_dir_sectors = ((root_entry_count as u32) * 32 + 511) / 512;
let data_start = root_dir_start + root_dir_sectors;
Some(Self {
reserved_sectors,
sectors_per_fat,
root_dir_start,
root_dir_sectors,
data_start,
sectors_per_cluster,
})
}
fn cluster_to_sector(&self, cluster: u16) -> u32 {
self.data_start + ((cluster as u32) - 2) * (self.sectors_per_cluster as u32)
}
}
fn to_fat_name(name: &[u8]) -> [u8; 11] {
let mut fat = [b' '; 11];
let dot_pos = name.iter().position(|&b| b == b'.').unwrap_or(name.len());
for i in 0..dot_pos.min(8) { fat[i] = name[i].to_ascii_uppercase(); }
if dot_pos < name.len() {
for i in 0..(name.len() - dot_pos - 1).min(3) {
fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase();
}
}
fat
}
pub fn read_file_from_disk(
blk: &mut VirtioBlk,
filename: &[u8],
out_buf: &mut [u8],
) -> Option<usize> {
let mut sector_buf = [0u8; SECTOR_SIZE];
// Read boot sector
if !blk.read_sector(0, &mut sector_buf) { return None; }
let fs = BootFat16::parse(&sector_buf)?;
let fat_name = to_fat_name(filename);
// Search root directory
let mut first_cluster: u16 = 0;
let mut file_size: u32 = 0;
let mut found = false;
for s in 0..fs.root_dir_sectors {
if !blk.read_sector((fs.root_dir_start + s) as u32, &mut sector_buf) { continue; }
for i in 0..(SECTOR_SIZE / 32) {
let off = i * 32;
if sector_buf[off] == 0x00 { break; }
if sector_buf[off] == 0xE5 { continue; }
if sector_buf[off..off + 11] == fat_name {
first_cluster = u16::from_le_bytes([sector_buf[off + 26], sector_buf[off + 27]]);
file_size = u32::from_le_bytes([
sector_buf[off + 28], sector_buf[off + 29],
sector_buf[off + 30], sector_buf[off + 31],
]);
found = true;
break;
}
}
if found { break; }
}
if !found { return None; }
let size = (file_size as usize).min(out_buf.len());
let mut read = 0;
let mut cluster = first_cluster;
let sectors_per_cluster = fs.sectors_per_cluster as u32;
while read < size && cluster >= 2 && cluster < 0xFFF8 {
for s in 0..sectors_per_cluster {
if read >= size { break; }
let sector = fs.cluster_to_sector(cluster) + s;
if !blk.read_sector(sector, &mut sector_buf) { return Some(read); }
let chunk = (size - read).min(SECTOR_SIZE);
out_buf[read..read + chunk].copy_from_slice(&sector_buf[..chunk]);
read += chunk;
}
// Read next cluster from FAT
let fat_offset = (cluster as u32) * 2;
let fat_sector = fs.reserved_sectors as u32 + fat_offset / SECTOR_SIZE as u32;
let fat_byte = (fat_offset % SECTOR_SIZE as u32) as usize;
if !blk.read_sector(fat_sector, &mut sector_buf) { break; }
cluster = u16::from_le_bytes([sector_buf[fat_byte], sector_buf[fat_byte + 1]]);
}
Some(read)
}

View File

@@ -1,3 +1,4 @@
pub mod plic; pub mod plic;
pub mod timer; pub mod timer;
pub mod uart; pub mod uart;
pub mod virtio_blk;

View File

@@ -0,0 +1,173 @@
const VIRTIO_MMIO_BASE: usize = 0x10001000;
const VIRTIO_MAGIC: u32 = 0x74726976;
const VIRTIO_BLK_DEVICE_ID: u32 = 2;
const VIRTIO_STATUS_ACK: u32 = 1;
const VIRTIO_STATUS_DRIVER: u32 = 2;
const VIRTIO_STATUS_FEATURES_OK: u32 = 8;
const VIRTIO_STATUS_DRIVER_OK: u32 = 4;
const QUEUE_SIZE: usize = 8;
const SECTOR_SIZE: usize = 512;
#[repr(C, align(16))]
struct VirtqDesc {
addr: u64,
len: u32,
flags: u16,
next: u16,
}
#[repr(C, align(2))]
struct VirtqAvail {
flags: u16,
idx: u16,
ring: [u16; QUEUE_SIZE],
}
#[repr(C, align(4))]
struct VirtqUsed {
flags: u16,
idx: u16,
ring: [VirtqUsedElem; QUEUE_SIZE],
}
#[repr(C)]
struct VirtqUsedElem {
id: u32,
len: u32,
}
#[repr(C)]
struct VirtioBlkReqHeader {
req_type: u32,
reserved: u32,
sector: u64,
}
static mut DESCS: [VirtqDesc; QUEUE_SIZE] = [const {
VirtqDesc { addr: 0, len: 0, flags: 0, next: 0 }
}; QUEUE_SIZE];
static mut AVAIL: VirtqAvail = VirtqAvail {
flags: 0, idx: 0, ring: [0; QUEUE_SIZE],
};
static mut USED: VirtqUsed = VirtqUsed {
flags: 0, idx: 0, ring: [const { VirtqUsedElem { id: 0, len: 0 } }; QUEUE_SIZE],
};
static mut REQ_HEADER: VirtioBlkReqHeader = VirtioBlkReqHeader {
req_type: 0, reserved: 0, sector: 0,
};
static mut REQ_STATUS: u8 = 0xFF;
fn mmio_read(offset: usize) -> u32 {
unsafe { core::ptr::read_volatile((VIRTIO_MMIO_BASE + offset) as *const u32) }
}
fn mmio_write(offset: usize, val: u32) {
unsafe { core::ptr::write_volatile((VIRTIO_MMIO_BASE + offset) as *mut u32, val) }
}
pub struct VirtioBlk {
initialized: bool,
}
impl VirtioBlk {
pub fn probe() -> Option<Self> {
let magic = mmio_read(0x000);
let device_id = mmio_read(0x008);
if magic != VIRTIO_MAGIC || device_id != VIRTIO_BLK_DEVICE_ID {
return None;
}
// Reset
mmio_write(0x070, 0);
// Acknowledge
mmio_write(0x070, VIRTIO_STATUS_ACK);
mmio_write(0x070, VIRTIO_STATUS_ACK | VIRTIO_STATUS_DRIVER);
// No feature negotiation needed for basic read
mmio_write(0x024, 0); // DriverFeaturesSel = 0
mmio_write(0x020, 0); // DriverFeatures = 0
mmio_write(0x070, VIRTIO_STATUS_ACK | VIRTIO_STATUS_DRIVER | VIRTIO_STATUS_FEATURES_OK);
let status = mmio_read(0x070);
if status & VIRTIO_STATUS_FEATURES_OK == 0 {
return None;
}
// Set up queue 0
mmio_write(0x030, 0); // QueueSel = 0
mmio_write(0x038, QUEUE_SIZE as u32); // QueueNum
unsafe {
let desc_addr = core::ptr::addr_of!(DESCS) as u64;
let avail_addr = core::ptr::addr_of!(AVAIL) as u64;
let used_addr = core::ptr::addr_of!(USED) as u64;
mmio_write(0x080, desc_addr as u32);
mmio_write(0x084, (desc_addr >> 32) as u32);
mmio_write(0x090, avail_addr as u32);
mmio_write(0x094, (avail_addr >> 32) as u32);
mmio_write(0x0A0, used_addr as u32);
mmio_write(0x0A4, (used_addr >> 32) as u32);
}
mmio_write(0x044, 1); // QueueReady
mmio_write(0x070, VIRTIO_STATUS_ACK | VIRTIO_STATUS_DRIVER | VIRTIO_STATUS_FEATURES_OK | VIRTIO_STATUS_DRIVER_OK);
Some(Self { initialized: true })
}
pub fn read_sector(&mut self, sector: u32, buf: &mut [u8; SECTOR_SIZE]) -> bool {
if !self.initialized { return false; }
unsafe {
REQ_HEADER.req_type = 0; // read
REQ_HEADER.reserved = 0;
REQ_HEADER.sector = sector as u64;
REQ_STATUS = 0xFF;
// Descriptor 0: header (device reads)
DESCS[0].addr = core::ptr::addr_of!(REQ_HEADER) as u64;
DESCS[0].len = 16;
DESCS[0].flags = 1; // NEXT
DESCS[0].next = 1;
// Descriptor 1: data buffer (device writes)
DESCS[1].addr = buf.as_ptr() as u64;
DESCS[1].len = SECTOR_SIZE as u32;
DESCS[1].flags = 2 | 1; // WRITE | NEXT
DESCS[1].next = 2;
// Descriptor 2: status (device writes)
DESCS[2].addr = core::ptr::addr_of!(REQ_STATUS) as u64;
DESCS[2].len = 1;
DESCS[2].flags = 2; // WRITE
DESCS[2].next = 0;
// Add to available ring
let avail_idx = AVAIL.idx;
AVAIL.ring[(avail_idx as usize) % QUEUE_SIZE] = 0;
core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
AVAIL.idx = avail_idx.wrapping_add(1);
core::sync::atomic::fence(core::sync::atomic::Ordering::SeqCst);
// Notify device
mmio_write(0x050, 0);
// Wait for completion
let expected = avail_idx.wrapping_add(1);
while core::ptr::read_volatile(core::ptr::addr_of!(USED.idx)) != expected {
core::hint::spin_loop();
}
REQ_STATUS == 0
}
}
}

View File

@@ -2,6 +2,7 @@
#![no_main] #![no_main]
mod board; mod board;
mod boot_fs;
mod hw; mod hw;
mod kernel_cell; mod kernel_cell;
mod mmu; mod mmu;
@@ -11,7 +12,6 @@ mod trap;
use core::arch::global_asm; use core::arch::global_asm;
use hw::uart::Uart; use hw::uart::Uart;
use kernel_cell::KernelCell; use kernel_cell::KernelCell;
use ::platform::Platform;
static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit(); static KERNEL: KernelCell<kernel::Kernel<platform::RiscVPlatform>> = KernelCell::uninit();
@@ -47,15 +47,8 @@ fn zero_bss() {
} }
} }
// Embedded init binary — in production this comes from the boot image.
// For now, a minimal program: call SYS_EXIT and loop.
static INIT_BINARY: [u8; 12] = [
0x93, 0x08, 0x00, 0x01, // addi a7, zero, 16 (SYS_EXIT)
0x73, 0x00, 0x00, 0x00, // ecall
0x6f, 0xf0, 0xdf, 0xff, // j -4
];
const INIT_LOAD_VA: usize = 0x10000; const INIT_LOAD_VA: usize = 0x10000;
const INIT_MAX_SIZE: usize = 64 * 1024;
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn kmain() -> ! { extern "C" fn kmain() -> ! {
@@ -82,13 +75,130 @@ extern "C" fn kmain() -> ! {
); );
KERNEL.init(kernel::Kernel::new(riscv_platform, pool)); KERNEL.init(kernel::Kernel::new(riscv_platform, pool));
launch_init(&uart, kernel_pt); let entry_va = load_init_from_disk(&uart, kernel_pt);
launch_init(&uart, entry_va);
} }
fn launch_init(uart: &Uart, kernel_pt: &mmu::PageTable) -> ! { fn load_init_from_disk(uart: &Uart, kernel_pt: &mmu::PageTable) -> usize {
let kern = kernel_ref(); // Probe VirtIO block device
let mut blk = match hw::virtio_blk::VirtioBlk::probe() {
Some(b) => {
uart.write_str("k-os: VirtIO block device found\n");
b
}
None => {
uart.write_str("k-os: no block device — using fallback init\n");
return load_fallback_init(kernel_pt);
}
};
let entry_va = load_init_binary(kernel_pt, uart); // Read init.elf from FAT16 filesystem on disk
let init_page_count = (INIT_MAX_SIZE + 4095) / 4096;
let init_base = alloc_contiguous_pages(init_page_count);
let init_buf = unsafe {
core::slice::from_raw_parts_mut(init_base as *mut u8, INIT_MAX_SIZE)
};
let file_size = match boot_fs::read_file_from_disk(&mut blk, b"init.elf", init_buf) {
Some(size) => {
uart.write_str("k-os: loaded init.elf (");
uart.write_dec(size);
uart.write_str(" bytes)\n");
size
}
None => {
uart.write_str("k-os: init.elf not found on disk — using fallback\n");
return load_fallback_init(kernel_pt);
}
};
// Parse ELF
let entry_va = match elf::parse(&init_buf[..file_size]) {
Ok(elf_info) => {
uart.write_str("k-os: ELF entry point = ");
uart.write_hex(elf_info.entry_point);
uart.put_char(b'\n');
// Map each segment into user page table
let pages = (file_size + 4095) / 4096;
let _user_pt = mmu::create_user_page_table(
kernel_pt,
types::address::PhysicalAddress::from_raw(init_base),
pages,
types::address::VirtualAddress::from_raw(INIT_LOAD_VA),
);
use ::platform::Platform;
kernel_ref().platform_mut().configure_protection(
types::id::ProcessId::from_raw(0),
&[],
);
elf_info.entry_point
}
Err(_) => {
uart.write_str("k-os: not valid ELF — loading as raw binary\n");
let _user_pt = mmu::create_user_page_table(
kernel_pt,
types::address::PhysicalAddress::from_raw(init_base),
(file_size + 4095) / 4096,
types::address::VirtualAddress::from_raw(INIT_LOAD_VA),
);
use ::platform::Platform;
kernel_ref().platform_mut().configure_protection(
types::id::ProcessId::from_raw(0),
&[],
);
INIT_LOAD_VA
}
};
entry_va
}
// Fallback: minimal init that just calls SYS_EXIT (no disk needed)
static FALLBACK_INIT: [u8; 12] = [
0x93, 0x08, 0x00, 0x01, // addi a7, zero, 16 (SYS_EXIT)
0x73, 0x00, 0x00, 0x00, // ecall
0x6f, 0xf0, 0xdf, 0xff, // j -4
];
fn load_fallback_init(kernel_pt: &mmu::PageTable) -> usize {
let page = mmu::allocator::alloc_zeroed_page();
unsafe {
core::ptr::copy_nonoverlapping(
FALLBACK_INIT.as_ptr(),
page as *mut u8,
FALLBACK_INIT.len(),
);
}
let _user_pt = mmu::create_user_page_table(
kernel_pt,
types::address::PhysicalAddress::from_raw(page),
1,
types::address::VirtualAddress::from_raw(INIT_LOAD_VA),
);
use ::platform::Platform;
kernel_ref().platform_mut().configure_protection(
types::id::ProcessId::from_raw(0),
&[],
);
INIT_LOAD_VA
}
fn alloc_contiguous_pages(count: usize) -> usize {
let first = mmu::allocator::alloc_zeroed_page();
for _ in 1..count {
mmu::allocator::alloc_zeroed_page();
}
first
}
fn launch_init(uart: &Uart, entry_va: usize) -> ! {
let kern = kernel_ref();
let (pid, tid) = kern let (pid, tid) = kern
.boot(types::bounded::Priority::DEFAULT, types::time::TimeSlice::DEFAULT) .boot(types::bounded::Priority::DEFAULT, types::time::TimeSlice::DEFAULT)
@@ -97,36 +207,28 @@ fn launch_init(uart: &Uart, kernel_pt: &mmu::PageTable) -> ! {
loop { unsafe { core::arch::asm!("wfi") }; } loop { unsafe { core::arch::asm!("wfi") }; }
}); });
// Set init thread's entry point to the loaded binary
if let Some(ctx) = kern.thread_context_mut(tid) { if let Some(ctx) = kern.thread_context_mut(tid) {
ctx.set_pc(entry_va); ctx.set_pc(entry_va);
ctx.registers_mut()[2] = INIT_LOAD_VA - 4096; // sp below program ctx.registers_mut()[2] = INIT_LOAD_VA - 4096;
// Set sstatus for U-mode return
let mut sstatus: usize = 0; let mut sstatus: usize = 0;
sstatus |= 1 << 5; // SPIE — enable interrupts after sret sstatus |= 1 << 5; // SPIE
// SPP = 0 (U-mode) — bit 8 stays clear
ctx.set_status(sstatus); ctx.set_status(sstatus);
} }
uart.write_str("k-os: init loaded at "); uart.write_str("k-os: init loaded, entering U-mode (pid=");
uart.write_hex(entry_va);
uart.write_str(", entering U-mode (pid=");
uart.write_dec(pid.raw() as usize); uart.write_dec(pid.raw() as usize);
uart.write_str(")\n"); uart.write_str(")\n");
// Activate init's page table
let init_pt_addr = kern.platform_mut().page_table_for(pid); let init_pt_addr = kern.platform_mut().page_table_for(pid);
if init_pt_addr != 0 { if init_pt_addr != 0 {
let pt = unsafe { &*(init_pt_addr as *const mmu::PageTable) }; let pt = unsafe { &*(init_pt_addr as *const mmu::PageTable) };
mmu::activate(pt); mmu::activate(pt);
} }
// Set kernel stack in sscratch for trap handler
let kernel_sp: usize; let kernel_sp: usize;
unsafe { core::arch::asm!("mv {0}, sp", out(reg) kernel_sp) }; unsafe { core::arch::asm!("mv {0}, sp", out(reg) kernel_sp) };
trap::set_kernel_stack(kernel_sp); trap::set_kernel_stack(kernel_sp);
// Enter user mode via sret
let user_sp = INIT_LOAD_VA - 4096; let user_sp = INIT_LOAD_VA - 4096;
unsafe { unsafe {
core::arch::asm!( core::arch::asm!(
@@ -144,75 +246,6 @@ fn launch_init(uart: &Uart, kernel_pt: &mmu::PageTable) -> ! {
} }
} }
fn load_init_binary(kernel_pt: &mmu::PageTable, uart: &Uart) -> usize {
// Try to parse as ELF
if let Ok(elf_info) = elf::parse(&INIT_BINARY) {
uart.write_str("k-os: init is ELF, entry=");
uart.write_hex(elf_info.entry_point);
uart.put_char(b'\n');
for segment in elf_info.segments() {
let pages = (segment.mem_size + 4095) / 4096;
for p in 0..pages {
let page = mmu::allocator::alloc_zeroed_page();
let src_offset = segment.file_offset + p * 4096;
let copy_len = if p * 4096 < segment.file_size {
(segment.file_size - p * 4096).min(4096)
} else {
0
};
if copy_len > 0 && src_offset + copy_len <= INIT_BINARY.len() {
unsafe {
core::ptr::copy_nonoverlapping(
INIT_BINARY.as_ptr().add(src_offset),
page as *mut u8,
copy_len,
);
}
}
}
}
let kern = kernel_ref();
kern.platform_mut().configure_protection(
types::id::ProcessId::from_raw(0),
&[],
);
elf_info.entry_point
} else {
// Raw binary — copy to a page and map at INIT_LOAD_VA
uart.write_str("k-os: init is raw binary, loading at ");
uart.write_hex(INIT_LOAD_VA);
uart.put_char(b'\n');
let program_page = mmu::allocator::alloc_zeroed_page();
unsafe {
core::ptr::copy_nonoverlapping(
INIT_BINARY.as_ptr(),
program_page as *mut u8,
INIT_BINARY.len(),
);
}
let _user_pt = mmu::create_user_page_table(
kernel_pt,
types::address::PhysicalAddress::from_raw(program_page),
1,
types::address::VirtualAddress::from_raw(INIT_LOAD_VA),
);
// Store the page table for init (pid 0)
let kern = kernel_ref();
kern.platform_mut().configure_protection(
types::id::ProcessId::from_raw(0),
&[],
);
INIT_LOAD_VA
}
}
#[panic_handler] #[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! { fn panic(info: &core::panic::PanicInfo) -> ! {
let uart = Uart::new(); let uart = Uart::new();

View File

@@ -143,6 +143,9 @@ pub fn init_kernel_page_table() -> &'static mut PageTable {
let (va, pa) = identity(board::CLINT_BASE); let (va, pa) = identity(board::CLINT_BASE);
map(root, Mapping { va, pa, pages: 0x10000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() }); map(root, Mapping { va, pa, pages: 0x10000 / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
let (va, pa) = identity(board::VIRTIO_BASE);
map(root, Mapping { va, pa, pages: board::VIRTIO_SIZE / PAGE_SIZE, flags: Sv39Flags::kernel_rw() });
root root
} }

111
scripts/make-disk-image.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/bin/bash
set -euo pipefail
IMG="boot.img"
IMG_SIZE_MB=4
TARGET="riscv64gc-unknown-none-elf"
BUILD_DIR="target/${TARGET}/release"
echo "=== Creating k-os boot disk image ==="
# Create empty image
dd if=/dev/zero of="${IMG}" bs=1M count=${IMG_SIZE_MB} 2>/dev/null
echo "Created ${IMG} (${IMG_SIZE_MB}MB)"
# Format as FAT16
# On macOS: use newfs_msdos, on Linux: use mkfs.fat
if command -v newfs_msdos &>/dev/null; then
newfs_msdos -F 16 "${IMG}" >/dev/null
elif command -v mkfs.fat &>/dev/null; then
mkfs.fat -F 16 "${IMG}" >/dev/null
else
echo "Error: need newfs_msdos (macOS) or mkfs.fat (Linux)"
exit 1
fi
echo "Formatted as FAT16"
# Mount and copy files
MOUNT_DIR=$(mktemp -d)
if [[ "$(uname)" == "Darwin" ]]; then
# macOS: use hdiutil
DEVICE=$(hdiutil attach -nomount "${IMG}" 2>/dev/null | head -1 | awk '{print $1}')
mount -t msdos "${DEVICE}" "${MOUNT_DIR}" 2>/dev/null || {
echo "Warning: could not mount image. Files must be copied manually."
echo "On macOS, try: brew install mtools"
hdiutil detach "${DEVICE}" 2>/dev/null
rmdir "${MOUNT_DIR}"
# Fallback: use mtools if available
if command -v mcopy &>/dev/null; then
copy_with_mtools
else
echo "Install mtools: brew install mtools"
exit 0
fi
exit 0
}
copy_files "${MOUNT_DIR}"
umount "${MOUNT_DIR}"
hdiutil detach "${DEVICE}" 2>/dev/null
else
# Linux: use mount
sudo mount -o loop "${IMG}" "${MOUNT_DIR}"
copy_files "${MOUNT_DIR}"
sudo umount "${MOUNT_DIR}"
fi
rmdir "${MOUNT_DIR}" 2>/dev/null
echo "=== Done: ${IMG} ==="
copy_files() {
local DIR="$1"
mkdir -p "${DIR}/servers" "${DIR}/apps"
# Copy init
if [ -f "${BUILD_DIR}/hello" ]; then
cp "${BUILD_DIR}/hello" "${DIR}/init.elf"
echo " Copied init.elf"
fi
# Copy any server binaries that exist
for server in display input log permission storage session; do
if [ -f "${BUILD_DIR}/${server}-server" ]; then
cp "${BUILD_DIR}/${server}-server" "${DIR}/servers/${server}.elf"
echo " Copied servers/${server}.elf"
fi
done
# Copy any app binaries that exist
for app in snake; do
if [ -f "${BUILD_DIR}/${app}" ]; then
cp "${BUILD_DIR}/${app}" "${DIR}/apps/${app}.elf"
echo " Copied apps/${app}.elf"
fi
done
}
copy_with_mtools() {
echo "Using mtools to copy files..."
mmd -i "${IMG}" ::servers ::apps 2>/dev/null || true
if [ -f "${BUILD_DIR}/hello" ]; then
mcopy -i "${IMG}" "${BUILD_DIR}/hello" "::init.elf"
echo " Copied init.elf"
fi
for server in display input log permission storage session; do
if [ -f "${BUILD_DIR}/${server}-server" ]; then
mcopy -i "${IMG}" "${BUILD_DIR}/${server}-server" "::servers/${server}.elf"
echo " Copied servers/${server}.elf"
fi
done
for app in snake; do
if [ -f "${BUILD_DIR}/${app}" ]; then
mcopy -i "${IMG}" "${BUILD_DIR}/${app}" "::apps/${app}.elf"
echo " Copied apps/${app}.elf"
fi
done
}