From a336ebdea69b26c3d627440db5bd77040bfe5e92 Mon Sep 17 00:00:00 2001 From: GregoryLi360 Date: Tue, 23 Jun 2026 14:49:08 -0700 Subject: [PATCH 1/5] fix: point fix runner at vmm instead of deleted evaluator --- fix/.cargo/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/.cargo/config.toml b/fix/.cargo/config.toml index d1ab66c..34803b4 100644 --- a/fix/.cargo/config.toml +++ b/fix/.cargo/config.toml @@ -5,4 +5,4 @@ rustflags = [ "-C", "relocation-model=static", # "-C", "target-feature=+crt-static", ] -runner = "cargo run -p evaluator --bin fix-cli -- hybrid" +runner = "cargo run -p vmm --" From 0abc33397e8a1fccf41a52215d3ce48ca0f90cba Mon Sep 17 00:00:00 2001 From: GregoryLi360 Date: Tue, 23 Jun 2026 14:49:09 -0700 Subject: [PATCH 2/5] feat: add mkdir host filesystem effect --- common/src/protocol/control.rs | 2 ++ kernel/src/host.rs | 13 +++++++++++++ vmm/src/comm.rs | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/common/src/protocol/control.rs b/common/src/protocol/control.rs index b29173c..735a0d0 100644 --- a/common/src/protocol/control.rs +++ b/common/src/protocol/control.rs @@ -9,6 +9,7 @@ pub enum Request { GetArgs, Exit(i32), Open(String, FileMode), + Mkdir(String), Listen { ip: [u8; 4], port: u16 }, Connect { host: String, port: u16 }, } @@ -17,6 +18,7 @@ pub enum Request { pub enum Response { Args(Vec), Pipe(PipeData), + Ack, } #[derive(Debug, Serialize, Deserialize)] diff --git a/kernel/src/host.rs b/kernel/src/host.rs index 1a12ca6..c3965ac 100644 --- a/kernel/src/host.rs +++ b/kernel/src/host.rs @@ -244,6 +244,19 @@ pub mod fs { pipe: FilePipe, } + /// Recursively creates a directory and all of its parents on the host, like + /// `mkdir -p`. Returns `true` on success. Mirrors [`File::open`], but the + /// host has nothing to stream back so it replies with a bare ack rather than + /// a pipe. + pub fn mkdir(path: &str) -> bool { + let mut binding = crate::pipe::HOST.lock(); + let host = binding.get_mut().unwrap(); + matches!( + host.request(&control::Request::Mkdir(path.into())), + control::Response::Ack + ) + } + impl File { pub fn open( path: &str, diff --git a/vmm/src/comm.rs b/vmm/src/comm.rs index f5fd2aa..a3ba733 100644 --- a/vmm/src/comm.rs +++ b/vmm/src/comm.rs @@ -48,6 +48,10 @@ pub fn control_thread(argv: Vec, mut pipe: ControlPipe) { Err(x) => todo!("open: {x:?}"), } } + Request::Mkdir(path) => match std::fs::create_dir_all(&path) { + Ok(()) => Response::Ack, + Err(x) => todo!("mkdir: {x:?}"), + }, Request::Listen { ip, port } => { let listener = TcpListener::bind(SocketAddr::from((ip, port))).unwrap(); let (p, q) = common::pipe::pipe(1024); From 8fb17230eed216e305a53ce97c5ab660eb7f6fd6 Mon Sep 17 00:00:00 2001 From: GregoryLi360 Date: Tue, 23 Jun 2026 14:49:09 -0700 Subject: [PATCH 3/5] feat: add fix init and eval subcommands --- fix/src/main.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/fix/src/main.rs b/fix/src/main.rs index 1d8435e..c829daf 100644 --- a/fix/src/main.rs +++ b/fix/src/main.rs @@ -1,6 +1,6 @@ #![no_main] #![no_std] -use kernel::host::fs::{File, Whence}; +use kernel::host::fs::{self, File, Whence}; use kernel::host::os; use kernel::prelude::*; @@ -16,8 +16,33 @@ use derive_more::Unwrap; #[kmain] fn main() { let argv = os::argv(); - let filename = argv.get(1).expect("expected command file"); + // Subcommand dispatch: `fix init` | `fix eval `. + match argv.get(1).map(String::as_str) { + Some("init") => init(), + Some("eval") => { + let filename = argv.get(2).expect("fix eval: expected a command file"); + eval_file(filename); + } + Some(other) => panic!("fix: unknown command '{other}' (expected: init | eval )"), + None => panic!("fix: expected a command (init | eval )"), + } + + kernel::shutdown(); +} + +/// `fix init`: create the on-disk `.fix` store. +/// `mkdir` maps to host `create_dir_all`, so re-running on an existing store +/// is harmless (matches git's "reinitialized existing repository"). +fn init() { + if !fs::mkdir(".fix") { + panic!("fix init: failed to create .fix"); + } + println!("initialized empty fix store in .fix"); +} + +/// `fix eval `: read, parse, and evaluate a command file. +fn eval_file(filename: &str) { let mut file = File::open(filename, true, false, false, false, false).unwrap(); let len = file.seek(Whence::End(0)) as usize; file.seek(Whence::Start(0)); @@ -73,8 +98,6 @@ fn main() { } } } - - kernel::shutdown(); } #[derive(Clone, Debug, Unwrap)] From cb52bc5334fdba5a3e4d422c8e55360f79afb7b9 Mon Sep 17 00:00:00 2001 From: GregoryLi360 Date: Wed, 24 Jun 2026 12:05:29 -0700 Subject: [PATCH 4/5] feat: add objects and labels subdirs --- fix/src/main.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/fix/src/main.rs b/fix/src/main.rs index c829daf..112dd4a 100644 --- a/fix/src/main.rs +++ b/fix/src/main.rs @@ -31,12 +31,14 @@ fn main() { kernel::shutdown(); } -/// `fix init`: create the on-disk `.fix` store. -/// `mkdir` maps to host `create_dir_all`, so re-running on an existing store -/// is harmless (matches git's "reinitialized existing repository"). +/// `fix init`: create the on-disk `.fix` store with its `objects/` and +/// `labels/` subdirs. `mkdir` maps to host `create_dir_all`, so re-running on an +/// existing store is harmless (matches git's "reinitialized existing repository"). fn init() { - if !fs::mkdir(".fix") { - panic!("fix init: failed to create .fix"); + for dir in [".fix/objects", ".fix/labels"] { + if !fs::mkdir(dir) { + panic!("fix init: failed to create {dir}"); + } } println!("initialized empty fix store in .fix"); } From ec073cf82a27d2646d65d30bdb830535ef54d058 Mon Sep 17 00:00:00 2001 From: GregoryLi360 Date: Wed, 24 Jun 2026 13:57:29 -0700 Subject: [PATCH 5/5] refactor: surface open/mkdir errors as embedded_io::ErrorKind --- common/Cargo.toml | 1 + common/src/protocol/control.rs | 52 ++++++++++++++++++++++++++++++++++ fix/src/main.rs | 5 ++-- kernel/src/host.rs | 40 ++++++++++++++------------ vmm/src/comm.rs | 4 +-- 5 files changed, 80 insertions(+), 22 deletions(-) diff --git a/common/Cargo.toml b/common/Cargo.toml index 8022201..bad944a 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -25,6 +25,7 @@ nix = { version = "0.30.1", features = ["mman"], optional = true } async-channel = { version = "2.5.0", default-features = false } async-lock = { version = "3.4.1", default-features = false } serde = { version = "1.0.228", default-features = false, features = ["alloc", "derive"] } +embedded-io = { version = "0.6", default-features = false } [target.'cfg(target_os="linux")'.dev-dependencies] rand = "0.9.2" diff --git a/common/src/protocol/control.rs b/common/src/protocol/control.rs index 735a0d0..a6d315c 100644 --- a/common/src/protocol/control.rs +++ b/common/src/protocol/control.rs @@ -4,6 +4,8 @@ use alloc::vec::Vec; use serde::{Deserialize, Serialize}; +pub use embedded_io::ErrorKind; + #[derive(Debug, Serialize, Deserialize)] pub enum Request { GetArgs, @@ -19,6 +21,56 @@ pub enum Response { Args(Vec), Pipe(PipeData), Ack, + Err(IoErrorKind), +} + +/// Wire-serializable mirror of [`embedded_io::ErrorKind`], which is +/// `#[non_exhaustive]` and not `serde`-derivable. Carries a host I/O failure +/// back to the guest, where it converts into an `embedded_io::ErrorKind`. +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum IoErrorKind { + NotFound, + PermissionDenied, + AlreadyExists, + InvalidInput, + InvalidData, + Unsupported, + OutOfMemory, + Interrupted, + Other, +} + +impl From for ErrorKind { + fn from(e: IoErrorKind) -> Self { + match e { + IoErrorKind::NotFound => ErrorKind::NotFound, + IoErrorKind::PermissionDenied => ErrorKind::PermissionDenied, + IoErrorKind::AlreadyExists => ErrorKind::AlreadyExists, + IoErrorKind::InvalidInput => ErrorKind::InvalidInput, + IoErrorKind::InvalidData => ErrorKind::InvalidData, + IoErrorKind::Unsupported => ErrorKind::Unsupported, + IoErrorKind::OutOfMemory => ErrorKind::OutOfMemory, + IoErrorKind::Interrupted => ErrorKind::Interrupted, + IoErrorKind::Other => ErrorKind::Other, + } + } +} + +#[cfg(feature = "std")] +impl From for IoErrorKind { + fn from(e: std::io::ErrorKind) -> Self { + match e { + std::io::ErrorKind::NotFound => IoErrorKind::NotFound, + std::io::ErrorKind::PermissionDenied => IoErrorKind::PermissionDenied, + std::io::ErrorKind::AlreadyExists => IoErrorKind::AlreadyExists, + std::io::ErrorKind::InvalidInput => IoErrorKind::InvalidInput, + std::io::ErrorKind::InvalidData => IoErrorKind::InvalidData, + std::io::ErrorKind::Unsupported => IoErrorKind::Unsupported, + std::io::ErrorKind::OutOfMemory => IoErrorKind::OutOfMemory, + std::io::ErrorKind::Interrupted => IoErrorKind::Interrupted, + _ => IoErrorKind::Other, + } + } } #[derive(Debug, Serialize, Deserialize)] diff --git a/fix/src/main.rs b/fix/src/main.rs index 112dd4a..a3b96db 100644 --- a/fix/src/main.rs +++ b/fix/src/main.rs @@ -36,8 +36,9 @@ fn main() { /// existing store is harmless (matches git's "reinitialized existing repository"). fn init() { for dir in [".fix/objects", ".fix/labels"] { - if !fs::mkdir(dir) { - panic!("fix init: failed to create {dir}"); + if let Err(e) = fs::mkdir(dir) { + println!("fix init: failed to create {dir}: {e:?}"); + kernel::exit(1); } } println!("initialized empty fix store in .fix"); diff --git a/kernel/src/host.rs b/kernel/src/host.rs index c3965ac..e43a7fb 100644 --- a/kernel/src/host.rs +++ b/kernel/src/host.rs @@ -238,23 +238,26 @@ pub mod fs { use super::get_pipe; use crate::pipe::*; pub use common::protocol::file::Whence; - use common::{protocol::control::FileMode, protocol::*}; + use common::{ + protocol::control::{ErrorKind, FileMode}, + protocol::*, + }; pub struct File { pipe: FilePipe, } /// Recursively creates a directory and all of its parents on the host, like - /// `mkdir -p`. Returns `true` on success. Mirrors [`File::open`], but the - /// host has nothing to stream back so it replies with a bare ack rather than - /// a pipe. - pub fn mkdir(path: &str) -> bool { + /// `mkdir -p`. Mirrors [`File::open`], but the host has nothing to stream + /// back, so success is a bare ack and failure carries an [`ErrorKind`]. + pub fn mkdir(path: &str) -> Result<(), ErrorKind> { let mut binding = crate::pipe::HOST.lock(); let host = binding.get_mut().unwrap(); - matches!( - host.request(&control::Request::Mkdir(path.into())), - control::Response::Ack - ) + match host.request(&control::Request::Mkdir(path.into())) { + control::Response::Ack => Ok(()), + control::Response::Err(e) => Err(e.into()), + _ => Err(ErrorKind::Other), + } } impl File { @@ -265,10 +268,10 @@ pub mod fs { create: bool, append: bool, truncate: bool, - ) -> Option { + ) -> Result { let mut binding = crate::pipe::HOST.lock(); let host = binding.get_mut().unwrap(); - let control::Response::Pipe(id) = host.request(&control::Request::Open( + match host.request(&control::Request::Open( path.into(), FileMode { read, @@ -277,13 +280,14 @@ pub mod fs { append, truncate, }, - )) else { - return None; - }; - unsafe { - Some(File { - pipe: FilePipe::new(get_pipe(id)), - }) + )) { + control::Response::Pipe(id) => unsafe { + Ok(File { + pipe: FilePipe::new(get_pipe(id)), + }) + }, + control::Response::Err(e) => Err(e.into()), + _ => Err(ErrorKind::Other), } } diff --git a/vmm/src/comm.rs b/vmm/src/comm.rs index a3ba733..860d1b1 100644 --- a/vmm/src/comm.rs +++ b/vmm/src/comm.rs @@ -45,12 +45,12 @@ pub fn control_thread(argv: Vec, mut pipe: ControlPipe) { }); Response::Pipe(decompose_pipe(p)) } - Err(x) => todo!("open: {x:?}"), + Err(e) => Response::Err(e.kind().into()), } } Request::Mkdir(path) => match std::fs::create_dir_all(&path) { Ok(()) => Response::Ack, - Err(x) => todo!("mkdir: {x:?}"), + Err(e) => Response::Err(e.kind().into()), }, Request::Listen { ip, port } => { let listener = TcpListener::bind(SocketAddr::from((ip, port))).unwrap();