diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4622d60 --- /dev/null +++ b/Makefile @@ -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 diff --git a/crates/adapter-riscv/src/board.rs b/crates/adapter-riscv/src/board.rs index 8ce59bf..f48cc1a 100644 --- a/crates/adapter-riscv/src/board.rs +++ b/crates/adapter-riscv/src/board.rs @@ -1,6 +1,8 @@ pub const RAM_BASE: usize = 0x80000000; pub const RAM_SIZE: usize = 128 * 1024 * 1024; 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 CLINT_BASE: usize = 0x02000000; pub const USER_POOL_BASE: usize = 0x84000000; diff --git a/crates/adapter-riscv/src/boot_fs.rs b/crates/adapter-riscv/src/boot_fs.rs new file mode 100644 index 0000000..00cd694 --- /dev/null +++ b/crates/adapter-riscv/src/boot_fs.rs @@ -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 { + 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 { + let mut sector_buf = [0u8; SECTOR_SIZE]; + + // Read boot sector + if !blk.read_sector(0, &mut sector_buf) { return None; } + let fs = BootFat16::parse(§or_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(§or_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) +} diff --git a/crates/adapter-riscv/src/hw/mod.rs b/crates/adapter-riscv/src/hw/mod.rs index 88a95ab..679c465 100644 --- a/crates/adapter-riscv/src/hw/mod.rs +++ b/crates/adapter-riscv/src/hw/mod.rs @@ -1,3 +1,4 @@ pub mod plic; pub mod timer; pub mod uart; +pub mod virtio_blk; diff --git a/crates/adapter-riscv/src/hw/virtio_blk.rs b/crates/adapter-riscv/src/hw/virtio_blk.rs new file mode 100644 index 0000000..ecf3e8c --- /dev/null +++ b/crates/adapter-riscv/src/hw/virtio_blk.rs @@ -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 { + 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 + } + } +} diff --git a/crates/adapter-riscv/src/main.rs b/crates/adapter-riscv/src/main.rs index 76eae8a..6a6c2db 100644 --- a/crates/adapter-riscv/src/main.rs +++ b/crates/adapter-riscv/src/main.rs @@ -2,6 +2,7 @@ #![no_main] mod board; +mod boot_fs; mod hw; mod kernel_cell; mod mmu; @@ -11,7 +12,6 @@ mod trap; use core::arch::global_asm; use hw::uart::Uart; use kernel_cell::KernelCell; -use ::platform::Platform; static KERNEL: KernelCell> = 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_MAX_SIZE: usize = 64 * 1024; #[unsafe(no_mangle)] extern "C" fn kmain() -> ! { @@ -82,13 +75,130 @@ extern "C" fn kmain() -> ! { ); 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) -> ! { - let kern = kernel_ref(); +fn load_init_from_disk(uart: &Uart, kernel_pt: &mmu::PageTable) -> usize { + // 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 .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") }; } }); - // Set init thread's entry point to the loaded binary if let Some(ctx) = kern.thread_context_mut(tid) { ctx.set_pc(entry_va); - ctx.registers_mut()[2] = INIT_LOAD_VA - 4096; // sp below program - // Set sstatus for U-mode return + ctx.registers_mut()[2] = INIT_LOAD_VA - 4096; let mut sstatus: usize = 0; - sstatus |= 1 << 5; // SPIE — enable interrupts after sret - // SPP = 0 (U-mode) — bit 8 stays clear + sstatus |= 1 << 5; // SPIE ctx.set_status(sstatus); } - uart.write_str("k-os: init loaded at "); - uart.write_hex(entry_va); - uart.write_str(", entering U-mode (pid="); + uart.write_str("k-os: init loaded, entering U-mode (pid="); uart.write_dec(pid.raw() as usize); uart.write_str(")\n"); - // Activate init's page table let init_pt_addr = kern.platform_mut().page_table_for(pid); if init_pt_addr != 0 { let pt = unsafe { &*(init_pt_addr as *const mmu::PageTable) }; mmu::activate(pt); } - // Set kernel stack in sscratch for trap handler let kernel_sp: usize; unsafe { core::arch::asm!("mv {0}, sp", out(reg) kernel_sp) }; trap::set_kernel_stack(kernel_sp); - // Enter user mode via sret let user_sp = INIT_LOAD_VA - 4096; unsafe { 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] fn panic(info: &core::panic::PanicInfo) -> ! { let uart = Uart::new(); diff --git a/crates/adapter-riscv/src/mmu/page_table.rs b/crates/adapter-riscv/src/mmu/page_table.rs index 7184d13..57f226f 100644 --- a/crates/adapter-riscv/src/mmu/page_table.rs +++ b/crates/adapter-riscv/src/mmu/page_table.rs @@ -143,6 +143,9 @@ pub fn init_kernel_page_table() -> &'static mut PageTable { let (va, pa) = identity(board::CLINT_BASE); 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 } diff --git a/scripts/make-disk-image.sh b/scripts/make-disk-image.sh new file mode 100755 index 0000000..d9b742c --- /dev/null +++ b/scripts/make-disk-image.sh @@ -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 +}