dev/block: remove extra copy on aligned ops

This commit is contained in:
Mark Poliakov 2024-07-30 19:51:14 +03:00
parent c7d5294f86
commit 6e07fa91db
16 changed files with 303 additions and 91 deletions

View File

@ -2,7 +2,7 @@ use core::mem::{size_of, MaybeUninit};
use libk_mm::{
address::{AsPhysicalAddress, PhysicalAddress},
PageBox,
PageBox, PageSlice,
};
use tock_registers::register_structs;
@ -85,7 +85,7 @@ impl AtaIdentify {
}
impl AtaReadDmaEx {
pub fn new(lba: u64, sector_count: usize, buffer: &PageBox<[MaybeUninit<u8>]>) -> Self {
pub fn new(lba: u64, sector_count: usize, buffer: &PageSlice<MaybeUninit<u8>>) -> Self {
assert_eq!(buffer.len() % SECTOR_SIZE, 0);
assert_ne!(buffer.len(), 0);

View File

@ -10,7 +10,7 @@ use async_trait::async_trait;
use bytemuck::Zeroable;
use futures_util::task::AtomicWaker;
use libk::vfs::block::NgBlockDevice;
use libk_mm::{address::AsPhysicalAddress, device::DeviceMemoryIo, PageBox};
use libk_mm::{address::AsPhysicalAddress, device::DeviceMemoryIo, PageBox, PageSlice};
use libk_util::{sync::IrqSafeSpinlock, waker::QueueWaker, OneTimeInit};
use tock_registers::interfaces::{Readable, Writeable};
@ -297,13 +297,13 @@ impl NgBlockDevice for AhciPort {
async fn read(
&self,
lba: u64,
buffer: &mut PageBox<[MaybeUninit<u8>]>,
buffer: &mut PageSlice<MaybeUninit<u8>>,
) -> Result<(), AhciError> {
let command = AtaReadDmaEx::new(lba, buffer.len() / SECTOR_SIZE, &buffer);
let command = AtaReadDmaEx::new(lba, buffer.len() / SECTOR_SIZE, buffer);
self.submit(&command).await?.wait_for_completion().await
}
async fn write(&self, _lba: u64, _buffer: PageBox<[u8]>) -> Result<(), AhciError> {
async fn write(&self, _lba: u64, _buffer: &PageSlice<u8>) -> Result<(), AhciError> {
// TODO AtaDmaWriteEx
Err(AhciError::FeatureNotImplemented)
}

View File

@ -4,7 +4,7 @@ use alloc::{boxed::Box, format};
use async_trait::async_trait;
use kernel_fs::devfs;
use libk::vfs::block::{probe_partitions, NgBlockDevice, NgBlockDeviceWrapper};
use libk_mm::{address::AsPhysicalAddress, PageBox};
use libk_mm::{address::AsPhysicalAddress, PageSlice};
use crate::{command::IdentifyNamespaceRequest, IoDirection};
@ -64,30 +64,34 @@ impl NgBlockDevice for NvmeDrive {
async fn read(
&self,
lba: u64,
buffer: &mut PageBox<[MaybeUninit<u8>]>,
buffer: &mut PageSlice<MaybeUninit<u8>>,
) -> Result<(), NvmeError> {
debug_assert_eq!(buffer.len() % self.block_size(), 0);
let lba_count = buffer.len() / self.block_size();
self.controller
.perform_io(
self.nsid,
lba,
lba_count,
unsafe { buffer.as_physical_address() },
IoDirection::Read,
)
.await?;
Ok(())
.await
}
async fn write(&self, lba: u64, buffer: PageBox<[u8]>) -> Result<(), NvmeError> {
async fn write(&self, lba: u64, buffer: &PageSlice<u8>) -> Result<(), NvmeError> {
debug_assert_eq!(buffer.len() % self.block_size(), 0);
let lba_count = buffer.len() / self.block_size();
self.controller
.perform_io(
self.nsid,
lba,
lba_count,
unsafe { buffer.as_physical_address() },
IoDirection::Write,
)
.await?;
Ok(())
.await
}
fn block_size(&self) -> usize {

View File

@ -260,6 +260,7 @@ impl NvmeController {
&'static self,
nsid: u32,
lba: u64,
lba_count: usize,
buffer_address: PhysicalAddress,
direction: IoDirection,
) -> Result<(), NvmeError> {
@ -280,7 +281,7 @@ impl NvmeController {
IoRead {
nsid,
lba,
count: 1,
count: lba_count as _,
},
&[buffer_address],
true,
@ -289,7 +290,7 @@ impl NvmeController {
IoWrite {
nsid,
lba,
count: 1,
count: lba_count as _,
},
&[buffer_address],
true,

View File

@ -1,19 +1,13 @@
use core::{any::Any, mem::MaybeUninit, ops::Range, str::FromStr};
use core::{any::Any, mem::MaybeUninit, str::FromStr};
use alloc::{sync::Arc, vec};
use alloc::sync::Arc;
use libk::{
block,
error::Error,
task::sync::{MappedAsyncMutexGuard, Mutex},
vfs::{
block::cache::{CachedBlock, CachedBlockRef},
CommonImpl, DirectoryImpl, DirectoryOpenPosition, InstanceData, Metadata, Node, NodeFlags,
NodeRef, RegularImpl,
},
vfs::{CommonImpl, DirectoryImpl, DirectoryOpenPosition, Metadata, Node, NodeFlags, NodeRef},
};
use libk_util::lru_hash_table::LruCache;
use yggdrasil_abi::{
io::{DirectoryEntry, FileMode, FileType, GroupId, OpenOptions, UserId},
io::{DirectoryEntry, FileMode, FileType, GroupId, UserId},
util::FixedString,
};
@ -22,16 +16,10 @@ use crate::{Dirent, Ext2Fs, Inode};
pub struct DirectoryNode {
fs: Arc<Ext2Fs>,
inode: Inode,
#[allow(unused)]
ino: u32,
}
struct DirentIterExt<'a> {
fs: &'a Ext2Fs,
inode: &'a Inode,
offset: usize,
current: Option<CachedBlockRef>,
}
struct DirentIter<'a> {
fs: &'a Ext2Fs,
block: &'a [u8],
@ -161,7 +149,7 @@ impl DirectoryNode {
}
impl CommonImpl for DirectoryNode {
fn size(&self, node: &NodeRef) -> Result<u64, Error> {
fn size(&self, _node: &NodeRef) -> Result<u64, Error> {
Ok(self.inode.size_lower as _)
}
@ -169,7 +157,7 @@ impl CommonImpl for DirectoryNode {
self
}
fn metadata(&self, node: &NodeRef) -> Result<Metadata, Error> {
fn metadata(&self, _node: &NodeRef) -> Result<Metadata, Error> {
Ok(Metadata {
uid: unsafe { UserId::from_raw(self.inode.uid as _) },
gid: unsafe { GroupId::from_raw(self.inode.gid as _) },

View File

@ -1,10 +1,10 @@
use core::any::Any;
use alloc::{sync::Arc, vec};
use alloc::sync::Arc;
use libk::{
block,
error::Error,
vfs::{block, CommonImpl, InstanceData, Metadata, Node, NodeFlags, NodeRef, RegularImpl},
vfs::{CommonImpl, InstanceData, Metadata, Node, NodeFlags, NodeRef, RegularImpl},
};
use yggdrasil_abi::io::{FileMode, GroupId, OpenOptions, UserId};
@ -13,6 +13,7 @@ use crate::{Ext2Fs, Inode};
pub struct RegularNode {
fs: Arc<Ext2Fs>,
inode: Inode,
#[allow(unused)]
ino: u32,
}
@ -53,7 +54,7 @@ impl RegularNode {
}
impl CommonImpl for RegularNode {
fn metadata(&self, node: &NodeRef) -> Result<Metadata, Error> {
fn metadata(&self, _node: &NodeRef) -> Result<Metadata, Error> {
Ok(Metadata {
uid: unsafe { UserId::from_raw(self.inode.uid as _) },
gid: unsafe { GroupId::from_raw(self.inode.gid as _) },
@ -65,7 +66,7 @@ impl CommonImpl for RegularNode {
self
}
fn size(&self, node: &NodeRef) -> Result<u64, Error> {
fn size(&self, _node: &NodeRef) -> Result<u64, Error> {
Ok(self.size())
}
}
@ -87,16 +88,16 @@ impl RegularImpl for RegularNode {
Ok(())
}
fn truncate(&self, node: &NodeRef, new_size: u64) -> Result<(), Error> {
fn truncate(&self, _node: &NodeRef, _new_size: u64) -> Result<(), Error> {
Err(Error::ReadOnly)
}
fn write(
&self,
node: &NodeRef,
instance: Option<&InstanceData>,
pos: u64,
buf: &[u8],
_node: &NodeRef,
_instance: Option<&InstanceData>,
_pos: u64,
_buf: &[u8],
) -> Result<usize, Error> {
Err(Error::ReadOnly)
}

View File

@ -2,16 +2,9 @@
extern crate alloc;
use core::{
any::Any,
mem::MaybeUninit,
ops::{Deref, DerefMut},
pin::Pin,
sync::atomic::AtomicBool,
task::{Context, Poll},
};
use core::ops::{Deref, DerefMut};
use alloc::{boxed::Box, sync::Arc, vec, vec::Vec};
use alloc::{boxed::Box, sync::Arc, vec};
use bytemuck::{Pod, Zeroable};
use dir::DirectoryNode;
use file::RegularNode;
@ -19,13 +12,12 @@ use libk::{
error::Error,
vfs::{
block::{
cache::{BlockCache, CachedBlock, CachedBlockRef},
cache::{BlockCache, CachedBlockRef},
BlockDevice,
},
CommonImpl, Metadata, NodeRef,
NodeRef,
},
};
use libk_mm::PageBox;
use libk_util::OneTimeInit;
use static_assertions::const_assert_eq;
@ -237,6 +229,7 @@ impl Ext2Fs {
return Err(Error::InvalidArgument);
}
let bgdt_offset = 1;
let block_size = 1024usize << superblock.block_size_log2;
let bgdt_entry_count = ((superblock.total_blocks + superblock.block_group_block_count - 1)
@ -261,7 +254,7 @@ impl Ext2Fs {
bgdt_entry_count,
);
for i in 0..bgdt_block_count {
let disk_offset = (i as u64 + 1) * block_size as u64;
let disk_offset = (i as u64 + bgdt_offset) * block_size as u64;
device
.read_exact(
disk_offset,

View File

@ -15,6 +15,7 @@ use core::{
fmt,
mem::{size_of, MaybeUninit},
ops::{Deref, DerefMut},
slice::SliceIndex,
};
use address::Virtualize;
@ -67,6 +68,10 @@ pub struct PageBox<T: ?Sized> {
page_count: usize,
}
pub struct PageSlice<T> {
data: [T],
}
impl<T> PageBox<T> {
#[inline]
fn alloc_slice(count: usize, zeroed: bool) -> Result<(PhysicalAddress, usize), Error> {
@ -197,6 +202,14 @@ impl<T> PageBox<[T]> {
let slice = unsafe { slice.assume_init_slice() };
Ok(slice)
}
pub fn as_slice(&self) -> &PageSlice<T> {
todo!()
}
pub fn as_slice_mut(&mut self) -> &mut PageSlice<T> {
unsafe { core::mem::transmute(&mut self[..]) }
}
}
impl<T> PageBox<MaybeUninit<T>> {
@ -331,3 +344,36 @@ impl<T: ?Sized + fmt::Display> fmt::Display for PageBox<T> {
unsafe impl<T: ?Sized + Send> Send for PageBox<T> {}
unsafe impl<T: ?Sized + Sync> Sync for PageBox<T> {}
impl<T> PageSlice<T> {
pub fn subslice_mut<R: SliceIndex<[T], Output = [T]>>(
&mut self,
index: R,
) -> &mut PageSlice<T> {
unsafe { core::mem::transmute(&mut self.data[index]) }
}
pub fn subslice<R: SliceIndex<[T], Output = [T]>>(&self, index: R) -> &PageSlice<T> {
unsafe { core::mem::transmute(&self.data[index]) }
}
}
impl<T> AsPhysicalAddress for PageSlice<T> {
unsafe fn as_physical_address(&self) -> PhysicalAddress {
PhysicalAddress::from_virtualized(self.data.as_ptr().addr())
}
}
impl<T> Deref for PageSlice<T> {
type Target = [T];
fn deref(&self) -> &Self::Target {
&self.data
}
}
impl<T> DerefMut for PageSlice<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.data
}
}

View File

@ -16,7 +16,7 @@ extern crate alloc;
use core::{
mem::MaybeUninit,
ops::{Deref, DerefMut},
ops::{Add, Deref, DerefMut, Mul, Range, Sub},
panic,
};
@ -37,6 +37,57 @@ pub trait IsTrue {}
impl IsTrue for ConstAssert<true> {}
pub trait RangeExt<N> {
fn chunked_range(self, step: N) -> ChunkedRangeIter<N>;
fn mul(self, mul: N) -> Self;
fn add(self, add: N) -> Self;
}
pub struct ChunkedRangeIter<N> {
pos: N,
end: N,
step: N,
}
impl<N> ChunkedRangeIter<N> {
pub fn from_range(range: Range<N>, step: N) -> Self {
Self {
pos: range.start,
end: range.end,
step,
}
}
}
impl<N: Copy + Ord + Add<Output = N> + Sub<Output = N>> Iterator for ChunkedRangeIter<N> {
type Item = Range<N>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.end {
return None;
}
let step = core::cmp::min(self.end - self.pos, self.step);
let range = self.pos..self.pos + step;
self.pos = self.pos + step;
Some(range)
}
}
impl<N: Mul<Output = N> + Add<Output = N> + Copy> RangeExt<N> for Range<N> {
fn chunked_range(self, step: N) -> ChunkedRangeIter<N> {
ChunkedRangeIter::from_range(self, step)
}
fn mul(self, mul: N) -> Self {
self.start * mul..self.end * mul
}
fn add(self, add: N) -> Self {
self.start + add..self.end + add
}
}
/// Statically-allocated "dynamic" vector
pub struct StaticVector<T, const N: usize> {
data: [MaybeUninit<T>; N],
@ -105,3 +156,25 @@ impl<T, const N: usize> DerefMut for StaticVector<T, N> {
unsafe { MaybeUninit::slice_assume_init_mut(&mut self.data[..self.len]) }
}
}
#[cfg(test)]
mod tests {
use crate::RangeExt;
#[test]
fn chunked_range() {
let r = 0..30;
let mut i = 0;
for range in r.chunked_range(10) {
assert_eq!(range, i..i + 10);
i += 10;
}
let r = 0..28;
let mut it = r.chunked_range(10);
assert_eq!(it.next(), Some(0..10));
assert_eq!(it.next(), Some(10..20));
assert_eq!(it.next(), Some(20..28));
assert_eq!(it.next(), None);
}
}

View File

@ -3,10 +3,7 @@ use core::{
ops::AsyncFnOnce,
};
use alloc::{
collections::{linked_list, LinkedList},
vec::Vec,
};
use alloc::{collections::LinkedList, vec::Vec};
use crate::hash_table::DefaultHashBuilder;

View File

@ -1,7 +1,6 @@
use core::{
cell::UnsafeCell,
future::poll_fn,
marker::PhantomData,
ops::{AsyncFnOnce, Deref, DerefMut},
sync::atomic::{AtomicBool, AtomicU32, Ordering},
task::{Context, Poll},

View File

@ -18,7 +18,7 @@ pub struct CachedBlock {
}
pub struct CachedBlockRef {
entry: Arc<IrqSafeRwLock<CachedBlock>>,
_entry: Arc<IrqSafeRwLock<CachedBlock>>,
lock: IrqSafeRwLockReadGuard<'static, CachedBlock>,
}
@ -48,12 +48,28 @@ impl BlockCache {
let read = block.read();
if read.dirty {
log::info!("Evict block {}", address);
if let Err(err) = self.device.write_exact(address, &read.data).await {
if let Err(err) = self
.device
.write_aligned(address, read.data.as_slice())
.await
{
log::error!("Disk error: flushing block {}: {:?}", address, err);
}
}
}
async fn fetch_block(&self, address: u64) -> Result<Arc<IrqSafeRwLock<CachedBlock>>, Error> {
let mut data = PageBox::new_uninit_slice(self.block_size)?;
self.device
.read_aligned(address, data.as_slice_mut())
.await?;
let data = unsafe { data.assume_init_slice() };
Ok(Arc::new(IrqSafeRwLock::new(CachedBlock {
data,
dirty: false,
})))
}
pub async fn get<'a>(&'a self, address: u64) -> Result<CachedBlockRef, Error> {
debug_assert_eq!(address % self.block_size as u64, 0);
@ -63,14 +79,7 @@ impl BlockCache {
.await
.try_map_guard_async::<_, Error, _>(|cache: &'a mut LruCache<_, _>| async move {
let (value, evicted) = cache
.try_get_or_insert_with_async(address, || async move {
let mut data = PageBox::new_slice(0, self.block_size)?;
self.device.read_exact(address, &mut data).await?;
Ok(Arc::new(IrqSafeRwLock::new(CachedBlock {
data,
dirty: false,
})))
})
.try_get_or_insert_with_async(address, || self.fetch_block(address))
.await?;
if evicted.is_some() {
@ -102,7 +111,10 @@ impl CachedBlockRef {
let entry = entry.clone();
// Safety: ok, Arc instance is still held
let lock = unsafe { core::mem::transmute(entry.read()) };
Self { lock, entry }
Self {
lock,
_entry: entry,
}
}
}

View File

@ -11,13 +11,16 @@ use core::{
use alloc::boxed::Box;
use async_trait::async_trait;
use futures_util::{task::AtomicWaker, Future};
use libk_mm::{address::PhysicalAddress, table::MapAttributes, PageBox, PageProvider};
use libk_util::waker::QueueWaker;
use libk_mm::{address::PhysicalAddress, table::MapAttributes, PageBox, PageProvider, PageSlice};
use libk_util::{waker::QueueWaker, RangeExt};
use yggdrasil_abi::{error::Error, io::DeviceRequest};
use crate::vfs::block::{
request::{IoOperation, IoRequest, IoSubmissionId},
BlockDevice,
use crate::{
task::debug,
vfs::block::{
request::{IoOperation, IoRequest, IoSubmissionId},
BlockDevice,
},
};
#[async_trait]
@ -27,9 +30,9 @@ pub trait NgBlockDevice: Sync {
async fn read(
&self,
lba: u64,
buffer: &mut PageBox<[MaybeUninit<u8>]>,
buffer: &mut PageSlice<MaybeUninit<u8>>,
) -> Result<(), Self::Error>;
async fn write(&self, lba: u64, buffer: PageBox<[u8]>) -> Result<(), Self::Error>;
async fn write(&self, lba: u64, buffer: &PageSlice<u8>) -> Result<(), Self::Error>;
fn block_size(&self) -> usize;
fn block_count(&self) -> usize;
@ -75,6 +78,52 @@ impl<'a, D: NgBlockDevice + 'a> NgBlockDeviceWrapper<'a, D> {
#[async_trait]
impl<'a, D: NgBlockDevice + 'a> BlockDevice for NgBlockDeviceWrapper<'a, D> {
async fn read_aligned(
&self,
pos: u64,
buf: &mut PageSlice<MaybeUninit<u8>>,
) -> Result<(), Error> {
if pos % self.block_size as u64 != 0 || buf.len() % self.block_size != 0 {
// TODO fallback to unaligned read
todo!()
}
let range = 0..buf.len() / self.block_size;
for chunk in range.chunked_range(self.max_blocks_per_request) {
let lba = chunk.start as u64 + pos / self.block_size as u64;
let buffer_range = chunk.mul(self.block_size);
self.device
.read(lba, buf.subslice_mut(buffer_range))
.await
.map_err(Self::handle_drive_error)?;
}
Ok(())
}
async fn write_aligned(&self, pos: u64, buf: &PageSlice<u8>) -> Result<(), Error> {
if pos % self.block_size as u64 != 0 || buf.len() % self.block_size != 0 {
// TODO fallback to unaligned write
todo!()
}
let range = 0..buf.len() / self.block_size;
for chunk in range.chunked_range(self.max_blocks_per_request) {
let lba = chunk.start as u64 + pos / self.block_size as u64;
let buffer_range = chunk.mul(self.block_size);
self.device
.write(lba, buf.subslice(buffer_range))
.await
.map_err(Self::handle_drive_error)?;
}
Ok(())
}
async fn read(&self, mut pos: u64, mut buf: &mut [u8]) -> Result<usize, Error> {
let len = buf.len();
let mut remaining = buf.len();
@ -88,7 +137,7 @@ impl<'a, D: NgBlockDevice + 'a> BlockDevice for NgBlockDeviceWrapper<'a, D> {
let amount = core::cmp::min(self.block_size - block_offset, buf.len());
self.device
.read(lba, &mut block)
.read(lba, block.as_slice_mut())
.await
.map_err(Self::handle_drive_error)?;

View File

@ -1,8 +1,10 @@
#![allow(missing_docs)]
use core::mem::MaybeUninit;
use alloc::{boxed::Box, vec::Vec};
use async_trait::async_trait;
use libk_mm::{PageBox, PageProvider};
use libk_mm::{PageProvider, PageSlice};
use partition::Partition;
use yggdrasil_abi::{error::Error, io::DeviceRequest};
@ -68,6 +70,18 @@ pub fn probe_partitions<
#[allow(unused)]
#[async_trait]
pub trait BlockDevice: PageProvider + Sync {
async fn read_aligned(
&self,
pos: u64,
buf: &mut PageSlice<MaybeUninit<u8>>,
) -> Result<(), Error> {
Err(Error::NotImplemented)
}
async fn write_aligned(&self, pos: u64, buf: &PageSlice<u8>) -> Result<(), Error> {
Err(Error::NotImplemented)
}
async fn read(&self, pos: u64, buf: &mut [u8]) -> Result<usize, Error> {
Err(Error::NotImplemented)
}

View File

@ -3,7 +3,7 @@ use core::mem::{size_of, MaybeUninit};
use alloc::{boxed::Box, vec, vec::Vec};
use async_trait::async_trait;
use bytemuck::{Pod, Zeroable};
use libk_mm::{address::PhysicalAddress, table::MapAttributes, PageBox, PageProvider};
use libk_mm::{address::PhysicalAddress, table::MapAttributes, PageBox, PageProvider, PageSlice};
use static_assertions::const_assert_eq;
use uuid::Uuid;
use yggdrasil_abi::{error::Error, io::DeviceRequest};
@ -59,26 +59,56 @@ impl<'a, D: NgBlockDevice + 'a> Partition<'a, D> {
}
impl<'a, D: NgBlockDevice + 'a> PageProvider for Partition<'a, D> {
fn get_page(&self, offset: u64) -> Result<PhysicalAddress, Error> {
fn get_page(&self, _offset: u64) -> Result<PhysicalAddress, Error> {
todo!()
}
fn clone_page(
&self,
offset: u64,
src_phys: PhysicalAddress,
src_attrs: MapAttributes,
_offset: u64,
_src_phys: PhysicalAddress,
_src_attrs: MapAttributes,
) -> Result<PhysicalAddress, Error> {
todo!()
}
fn release_page(&self, offset: u64, phys: PhysicalAddress) -> Result<(), Error> {
fn release_page(&self, _offset: u64, _phys: PhysicalAddress) -> Result<(), Error> {
todo!()
}
}
#[async_trait]
impl<'a, D: NgBlockDevice + 'a> BlockDevice for Partition<'a, D> {
async fn read_aligned(
&self,
pos: u64,
buf: &mut PageSlice<MaybeUninit<u8>>,
) -> Result<(), Error> {
let start = self.start_byte() + pos;
if start % self.device.block_size as u64 != 0
|| start + buf.len() as u64 >= self.end_byte()
|| buf.len() % self.device.block_size != 0
{
// TODO fallback to unaligned read
todo!()
}
self.device.read_aligned(start, buf).await
}
async fn write_aligned(&self, pos: u64, buf: &PageSlice<u8>) -> Result<(), Error> {
let start = self.start_byte() + pos;
if start % self.device.block_size as u64 != 0
|| start + buf.len() as u64 >= self.end_byte()
|| buf.len() % self.device.block_size != 0
{
// TODO fallback to unaligned write
todo!()
}
self.device.write_aligned(start, buf).await
}
async fn read(&self, pos: u64, buf: &mut [u8]) -> Result<usize, Error> {
if pos >= self.end_byte() {
return Ok(0);

View File

@ -116,7 +116,12 @@ pub fn check_all(env: BuildEnv, action: CheckAction) -> Result<(), Error> {
}
pub fn test_all(env: BuildEnv) -> Result<(), Error> {
for path in ["kernel/driver/fs/memfs", "lib/abi", "kernel/libk"] {
for path in [
"kernel/driver/fs/memfs",
"lib/abi",
"kernel/libk",
"kernel/libk-util",
] {
CargoBuilder::Host(env.verbose).run(env.workspace_root.join(path), "test")?;
}
Ok(())