From 472dbfc7e7545873dce9d826ec07283c2b655972 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 17:29:49 +0300 Subject: [PATCH 01/11] treewide: going no_std Signed-off-by: NotAShelf Change-Id: Ia1c001eb099ea8cae9bdf76642b873376a6a6964 --- .cargo/config.toml | 2 +- Cargo.lock | 6 + Cargo.toml | 2 + crates/alloc/Cargo.toml | 12 + crates/alloc/src/lib.rs | 106 +++++++++ crates/asm/src/lib.rs | 39 ++++ crates/lib/src/colors.rs | 68 ++++-- crates/lib/src/desktop.rs | 25 +- crates/lib/src/lib.rs | 477 +++++++++++++++++++++++++++++++++----- crates/lib/src/release.rs | 19 +- crates/lib/src/system.rs | 164 +++++++++---- crates/lib/src/uptime.rs | 12 +- microfetch/Cargo.toml | 6 +- microfetch/src/main.rs | 61 ++++- 14 files changed, 856 insertions(+), 143 deletions(-) create mode 100644 crates/alloc/Cargo.toml create mode 100644 crates/alloc/src/lib.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index c7125dc..c406f76 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ # https://github.com/rui314/mold?tab=readme-ov-file#how-to-use [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold" ] +rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-lc", "-C", "link-arg=-lgcc_s" ] diff --git a/Cargo.lock b/Cargo.lock index fa82609..2f5d4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,9 +582,15 @@ name = "microfetch" version = "0.4.13" dependencies = [ "hotpath", + "microfetch-alloc", + "microfetch-asm", "microfetch-lib", ] +[[package]] +name = "microfetch-alloc" +version = "0.4.13" + [[package]] name = "microfetch-asm" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index f8bd2a2..da548d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ rust-version = "1.92.0" version = "0.4.13" [workspace.dependencies] +microfetch-alloc = { path = "./crates/alloc" } microfetch-asm = { path = "./crates/asm" } microfetch-lib = { path = "./crates/lib" } @@ -18,6 +19,7 @@ criterion-cycles-per-byte = "0.8.0" [profile.dev] opt-level = 1 +panic = "abort" [profile.release] codegen-units = 1 diff --git a/crates/alloc/Cargo.toml b/crates/alloc/Cargo.toml new file mode 100644 index 0000000..47cd1d2 --- /dev/null +++ b/crates/alloc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "microfetch-alloc" +description = "Simple, std-free bump allocator for Microfetch" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/alloc/src/lib.rs b/crates/alloc/src/lib.rs new file mode 100644 index 0000000..06e3f07 --- /dev/null +++ b/crates/alloc/src/lib.rs @@ -0,0 +1,106 @@ +//! Simple bump allocator for `no_std` environments. Uses a statically allocated +//! 32KB buffer and provides O(1) allocation with no deallocation support +//! (memory is never freed). +#![no_std] +use core::{ + alloc::{GlobalAlloc, Layout}, + cell::UnsafeCell, + ptr::null_mut, +}; + +/// Default heap size is 32KB, should be plenty for Microfetch. Technically it +/// can be invoked with more (or less) depending on our needs but I am quite +/// sure 32KB is more than enough. +pub const DEFAULT_HEAP_SIZE: usize = 32 * 1024; + +/// A simple bump allocator that never frees memory. +/// +/// This allocator maintains a static buffer and a bump pointer. Allocations are +/// fast (just bump the pointer), but memory is never reclaimed. While you might +/// be inclined to point out that this is ugly, it's suitable for a short-lived +/// program with bounded memory usage. +pub struct BumpAllocator { + heap: UnsafeCell<[u8; N]>, + next: UnsafeCell, +} + +// SAFETY: BumpAllocator is thread-safe because it uses UnsafeCell +// and the allocator is only used in single-threaded contexts (i.e., no_std). +unsafe impl Sync for BumpAllocator {} + +impl BumpAllocator { + /// Creates a new bump allocator with the specified heap size. + #[must_use] + pub const fn new() -> Self { + Self { + heap: UnsafeCell::new([0; N]), + next: UnsafeCell::new(0), + } + } + + /// Returns the number of bytes currently allocated. + #[must_use] + pub fn used(&self) -> usize { + // SAFETY: We're just reading the value, and this is only called + // in single-threaded contexts. + unsafe { *self.next.get() } + } + + /// Returns the total heap size. + #[must_use] + pub const fn capacity(&self) -> usize { + N + } + + /// Returns the number of bytes remaining. + #[must_use] + pub fn remaining(&self) -> usize { + N - self.used() + } +} + +impl Default for BumpAllocator { + fn default() -> Self { + Self::new() + } +} + +unsafe impl GlobalAlloc for BumpAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + unsafe { + let next = self.next.get(); + let heap = self.heap.get(); + + // Align the current position + let align = layout.align(); + let start = (*next + align - 1) & !(align - 1); + let end = start + layout.size(); + + if end > N { + // Out of memory + null_mut() + } else { + *next = end; + (*heap).as_mut_ptr().add(start) + } + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + // Bump allocator doesn't support deallocation + // Memory is reclaimed when the program exits + } +} + +/// Static bump allocator instance with 32KB heap. +/// +/// # Example +/// +/// Use this with `#[global_allocator]` in your binary: +/// +/// +/// ```rust,ignore +/// #[global_allocator] +/// static ALLOCATOR: BumpAllocator = BumpAllocator::new(); +/// ``` +pub type BumpAlloc = BumpAllocator; diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 63014d4..87c76b2 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -507,6 +507,7 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> Result { let _ = sys_close(fd); if bytes_read < 0 { + #[allow(clippy::cast_possible_truncation)] return Err(bytes_read as i32); } @@ -598,3 +599,41 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { ret } } + +/// Direct syscall to exit the process +/// +/// # Safety +/// +/// This syscall never returns. The process will terminate immediately. +#[inline] +pub unsafe fn sys_exit(code: i32) -> ! { + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!( + "syscall", + in("rax") 60i64, // SYS_exit + in("rdi") code, + options(noreturn, nostack) + ); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!( + "svc #0", + in("x8") 93i64, // SYS_exit + in("x0") code, + options(noreturn, nostack) + ); + } + + #[cfg(target_arch = "riscv64")] + unsafe { + core::arch::asm!( + "ecall", + in("a7") 93i64, // SYS_exit + in("a0") code, + options(noreturn, nostack) + ); + } +} diff --git a/crates/lib/src/colors.rs b/crates/lib/src/colors.rs index 0fca89e..9f012f9 100644 --- a/crates/lib/src/colors.rs +++ b/crates/lib/src/colors.rs @@ -1,5 +1,6 @@ -use std::sync::LazyLock; +use alloc::string::String; +/// Color codes for terminal output pub struct Colors { pub reset: &'static str, pub blue: &'static str, @@ -11,7 +12,8 @@ pub struct Colors { } impl Colors { - const fn new(is_no_color: bool) -> Self { + #[must_use] + pub const fn new(is_no_color: bool) -> Self { if is_no_color { Self { reset: "", @@ -36,46 +38,68 @@ impl Colors { } } -pub static COLORS: LazyLock = LazyLock::new(|| { - // Only presence matters; value is irrelevant per the NO_COLOR spec - let is_no_color = std::env::var_os("NO_COLOR").is_some(); - Colors::new(is_no_color) -}); +use core::sync::atomic::{AtomicBool, Ordering}; + +// Check if NO_COLOR is set (only once, lazily) +// Only presence matters; value is irrelevant per the NO_COLOR spec +static NO_COLOR_CHECKED: AtomicBool = AtomicBool::new(false); +static NO_COLOR_SET: AtomicBool = AtomicBool::new(false); + +/// Checks if `NO_COLOR` environment variable is set. +pub(crate) fn is_no_color() -> bool { + // Fast path: already checked + if NO_COLOR_CHECKED.load(Ordering::Acquire) { + return NO_COLOR_SET.load(Ordering::Relaxed); + } + + // Slow path: check environment + let is_set = crate::env_exists("NO_COLOR"); + NO_COLOR_SET.store(is_set, Ordering::Relaxed); + NO_COLOR_CHECKED.store(true, Ordering::Release); + is_set +} #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn print_dots() -> String { - // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color const GLYPH: &str = ""; - let capacity = COLORS.blue.len() - + COLORS.cyan.len() - + COLORS.green.len() - + COLORS.yellow.len() - + COLORS.red.len() - + COLORS.magenta.len() - + COLORS.reset.len() + + let colors = if is_no_color() { + Colors::new(true) + } else { + Colors::new(false) + }; + + // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color + let capacity = colors.blue.len() + + colors.cyan.len() + + colors.green.len() + + colors.yellow.len() + + colors.red.len() + + colors.magenta.len() + + colors.reset.len() + (GLYPH.len() + 2) * 6; let mut result = String::with_capacity(capacity); - result.push_str(COLORS.blue); + result.push_str(colors.blue); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.cyan); + result.push_str(colors.cyan); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.green); + result.push_str(colors.green); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.yellow); + result.push_str(colors.yellow); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.red); + result.push_str(colors.red); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.magenta); + result.push_str(colors.magenta); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.reset); + result.push_str(colors.reset); result } diff --git a/crates/lib/src/desktop.rs b/crates/lib/src/desktop.rs index ea863b4..b9aa362 100644 --- a/crates/lib/src/desktop.rs +++ b/crates/lib/src/desktop.rs @@ -1,18 +1,15 @@ -use std::{ffi::OsStr, fmt::Write}; +use alloc::string::String; + +use crate::getenv_str; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { - let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); - let session_os = std::env::var_os("XDG_SESSION_TYPE"); + let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP").unwrap_or("Unknown"); + let session_raw = getenv_str("XDG_SESSION_TYPE").unwrap_or(""); - let desktop_raw = desktop_os - .as_deref() - .and_then(OsStr::to_str) - .unwrap_or("Unknown"); let desktop_str = desktop_raw.strip_prefix("none+").unwrap_or(desktop_raw); - let session_raw = session_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); let backend_str = if session_raw.is_empty() { "Unknown" } else { @@ -27,9 +24,15 @@ pub fn get_desktop_info() -> String { result.push_str(" ("); // Capitalize first character of backend - if let Some(first_char) = backend_str.chars().next() { - let _ = write!(result, "{}", first_char.to_ascii_uppercase()); - result.push_str(&backend_str[first_char.len_utf8()..]); + if let Some(first_byte) = backend_str.as_bytes().first() { + // Convert first byte to uppercase if it's ASCII lowercase + let upper = if first_byte.is_ascii_lowercase() { + (first_byte - b'a' + b'A') as char + } else { + *first_byte as char + }; + result.push(upper); + result.push_str(&backend_str[1..]); } result.push(')'); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 28147f7..7da92a0 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -1,13 +1,17 @@ +#![no_std] +extern crate alloc; + pub mod colors; pub mod desktop; pub mod release; pub mod system; pub mod uptime; -use std::{ +use alloc::string::String; +use core::{ ffi::CStr, - io::{self, Cursor, Write}, mem::MaybeUninit, + sync::atomic::{AtomicPtr, Ordering}, }; pub use microfetch_asm as syscall; @@ -25,6 +29,200 @@ pub use microfetch_asm::{ sys_write, }; +/// A simple error type for microfetch operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + /// An OS error occurred, containing the errno value. + OsError(i32), + /// Invalid data or encoding error. + InvalidData, + /// Not found. + NotFound, + /// Write operation failed or partial write. + WriteError, +} + +impl Error { + /// Creates an error from the last OS error (reads errno). + #[inline] + #[must_use] + pub const fn last_os_error() -> Self { + // This is a simplified version - in a real implementation, + // we'd need to get the actual errno from the syscall return + Self::OsError(0) + } + + /// Creates an error from a raw OS error code (negative errno from syscall). + #[inline] + #[must_use] + pub const fn from_raw_os_error(errno: i32) -> Self { + Self::OsError(-errno) + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::OsError(errno) => write!(f, "OS error: {errno}"), + Self::InvalidData => write!(f, "Invalid data"), + Self::NotFound => write!(f, "Not found"), + Self::WriteError => write!(f, "Write error"), + } + } +} + +// Simple OnceLock implementation for no_std +pub struct OnceLock { + ptr: AtomicPtr, +} + +impl Default for OnceLock { + fn default() -> Self { + Self::new() + } +} + +impl OnceLock { + #[must_use] + pub const fn new() -> Self { + Self { + ptr: AtomicPtr::new(core::ptr::null_mut()), + } + } + + pub fn get_or_init(&self, f: F) -> &T + where + F: FnOnce() -> T, + { + // Load the current pointer + let mut ptr = self.ptr.load(Ordering::Acquire); + + if ptr.is_null() { + // Need to initialize + let value = f(); + let boxed = alloc::boxed::Box::new(value); + let new_ptr = alloc::boxed::Box::into_raw(boxed); + + // Try to set the pointer + match self.ptr.compare_exchange( + core::ptr::null_mut(), + new_ptr, + Ordering::Release, + Ordering::Acquire, + ) { + Ok(_) => { + // We successfully set it + ptr = new_ptr; + }, + Err(existing) => { + // Someone else set it first, free our allocation + // SAFETY: We just allocated this and no one else has seen it + unsafe { + let _ = alloc::boxed::Box::from_raw(new_ptr); + } + ptr = existing; + }, + } + } + + // SAFETY: We know ptr is non-null and points to a valid T + unsafe { &*ptr } + } +} + +impl Drop for OnceLock { + fn drop(&mut self) { + let ptr = self.ptr.load(Ordering::Acquire); + if !ptr.is_null() { + // SAFETY: We know this was allocated via Box::into_raw + unsafe { + let _ = alloc::boxed::Box::from_raw(ptr); + } + } + } +} + +// Access to the environ pointer (provided by libc startup code) +unsafe extern "C" { + static environ: *const *const u8; +} + +/// Gets an environment variable by name (without using std). +/// +/// # Safety +/// +/// This function reads from the environ global which is initialized +/// by the C runtime before `main()` is called. +#[must_use] +pub fn getenv(name: &str) -> Option<&'static [u8]> { + // SAFETY: environ is set up by the C runtime before main() runs + // and remains valid for the lifetime of the program + let envp = unsafe { environ }; + if envp.is_null() { + return None; + } + + let name_bytes = name.as_bytes(); + + // Walk through environment variables + let mut i = 0; + loop { + // SAFETY: environ is null-terminated array of pointers + let entry = unsafe { *envp.add(i) }; + if entry.is_null() { + break; + } + + // Check if this entry starts with our variable name followed by '=' + let mut matches = true; + for (j, &b) in name_bytes.iter().enumerate() { + // SAFETY: entry is a valid C string + let entry_byte = unsafe { *entry.add(j) }; + if entry_byte != b { + matches = false; + break; + } + } + + if matches { + // Check for '=' after the name + // SAFETY: entry is a valid C string + let eq_byte = unsafe { *entry.add(name_bytes.len()) }; + if eq_byte == b'=' { + // Found it! Calculate the value length + let value_start = unsafe { entry.add(name_bytes.len() + 1) }; + let mut len = 0; + loop { + // SAFETY: entry is a valid C string + let b = unsafe { *value_start.add(len) }; + if b == 0 { + break; + } + len += 1; + } + // SAFETY: We calculated the exact length + return Some(unsafe { core::slice::from_raw_parts(value_start, len) }); + } + } + + i += 1; + } + + None +} + +/// Gets an environment variable as a UTF-8 string. +#[must_use] +pub fn getenv_str(name: &str) -> Option<&'static str> { + getenv(name).and_then(|bytes| core::str::from_utf8(bytes).ok()) +} + +/// Checks if an environment variable exists (regardless of its value). +#[must_use] +pub fn env_exists(name: &str) -> bool { + getenv(name).is_some() +} + /// Wrapper for `utsname` with safe accessor methods pub struct UtsName(UtsNameBuf); @@ -34,10 +232,10 @@ impl UtsName { /// # Errors /// /// Returns an error if the `uname` syscall fails - pub fn uname() -> Result { + pub fn uname() -> Result { let mut uts = MaybeUninit::uninit(); if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { - return Err(std::io::Error::last_os_error()); + return Err(Error::last_os_error()); } Ok(Self(unsafe { uts.assume_init() })) } @@ -79,9 +277,7 @@ struct Fields { } #[cfg_attr(feature = "hotpath", hotpath::measure)] -fn print_system_info( - fields: &Fields, -) -> Result<(), Box> { +fn print_system_info(fields: &Fields) -> Result<(), Error> { let Fields { user_info, os_name, @@ -94,69 +290,242 @@ fn print_system_info( colors, } = fields; - let cyan = colors::COLORS.cyan; - let blue = colors::COLORS.blue; - let reset = colors::COLORS.reset; + let no_color = colors::is_no_color(); + let colors_obj = colors::Colors::new(no_color); + let cyan = colors_obj.cyan; + let blue = colors_obj.blue; + let reset = colors_obj.reset; + // Build output string let mut buf = [0u8; 2048]; - let mut cursor = Cursor::new(&mut buf[..]); - - write!( - cursor, - " - {blue} ▟█▖ {cyan}▝█▙ ▗█▛ {user_info} ~{reset} - {blue} ▗▄▄▟██▄▄▄▄▄{cyan}▝█▙█▛ {blue}▖ {cyan} {blue}System{reset}  {os_name} - {blue} ▀▀▀▀▀▀▀▀▀▀▀▘{cyan}▝██ {blue}▟█▖ {cyan} {blue}Kernel{reset}  {kernel_version} - {cyan} ▟█▛ {cyan}▝█▘{blue}▟█▛ {cyan} {blue}Shell{reset}  {shell} - {cyan}▟█████▛ {blue}▟█████▛ {cyan} {blue}Uptime{reset}  {uptime} - {cyan} ▟█▛{blue}▗█▖ {blue}▟█▛ {cyan} {blue}Desktop{reset}  {desktop} - {cyan} ▝█▛ {blue}██▖{cyan}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan}󰍛 {blue}Memory{reset}  {memory_usage} - {cyan} ▝ {blue}▟█▜█▖{cyan}▀▀▀▀▀██▛▀▀▘ {cyan}󱥎 {blue}Storage (/){reset}  {storage} - {blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n\n" - )?; - - let len = - usize::try_from(cursor.position()).expect("cursor position fits usize"); + let mut pos = 0usize; + + // Helper to write to buffer + let mut write_str = |s: &str| { + let bytes = s.as_bytes(); + let remaining = buf.len() - pos; + let to_write = bytes.len().min(remaining); + buf[pos..pos + to_write].copy_from_slice(&bytes[..to_write]); + pos += to_write; + }; + + write_str("\n "); + write_str(blue); + write_str(" ▟█▖ "); + write_str(cyan); + write_str("▝█▙ ▗█▛ "); + write_str(user_info); + write_str(" ~"); + write_str(reset); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▗▄▄▟██▄▄▄▄▄"); + write_str(cyan); + write_str("▝█▙█▛ "); + write_str(blue); + write_str("▖ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("System"); + write_str(reset); + write_str("  "); + write_str(os_name); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▀▀▀▀▀▀▀▀▀▀▀▘"); + write_str(cyan); + write_str("▝██ "); + write_str(blue); + write_str("▟█▖ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Kernel"); + write_str(reset); + write_str("  "); + write_str(kernel_version); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▟█▛ "); + write_str(cyan); + write_str("▝█▘"); + write_str(blue); + write_str("▟█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Shell"); + write_str(reset); + write_str("  "); + write_str(shell); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str("▟█████▛ "); + write_str(blue); + write_str("▟█████▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Uptime"); + write_str(reset); + write_str("  "); + write_str(uptime); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▟█▛"); + write_str(blue); + write_str("▗█▖ "); + write_str(blue); + write_str("▟█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Desktop"); + write_str(reset); + write_str("  "); + write_str(desktop); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▝█▛ "); + write_str(blue); + write_str("██▖"); + write_str(cyan); + write_str("▗▄▄▄▄▄▄▄▄▄▄▄ "); + write_str(cyan); + write_str("󰍛 "); + write_str(blue); + write_str("Memory"); + write_str(reset); + write_str("  "); + write_str(memory_usage); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▝ "); + write_str(blue); + write_str("▟█▜█▖"); + write_str(cyan); + write_str("▀▀▀▀▀██▛▀▀▘ "); + write_str(cyan); + write_str("󱥎 "); + write_str(blue); + write_str("Storage (/)"); + write_str(reset); + write_str("  "); + write_str(storage); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▟█▘ ▜█▖ "); + write_str(cyan); + write_str("▝█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Colors"); + write_str(reset); + write_str("  "); + write_str(colors); + write_str("\n\n"); + // Direct syscall to avoid stdout buffering allocation - let written = unsafe { sys_write(1, buf.as_ptr(), len) }; + let written = unsafe { sys_write(1, buf.as_ptr(), pos) }; if written < 0 { - return Err(io::Error::last_os_error().into()); + #[allow(clippy::cast_possible_truncation)] + return Err(Error::OsError(written as i32)); } - #[allow(clippy::cast_sign_loss)] // non-negative verified by the guard above - if written as usize != len { - return Err( - io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout") - .into(), - ); + #[allow(clippy::cast_sign_loss)] + if written as usize != pos { + return Err(Error::WriteError); } Ok(()) } +/// Print version information using direct syscall. +fn print_version() { + const VERSION: &str = concat!("Microfetch ", env!("CARGO_PKG_VERSION"), "\n"); + unsafe { + let _ = sys_write(1, VERSION.as_ptr(), VERSION.len()); + } +} + +/// Check if --version was passed via argc/argv. +/// +/// # Safety +/// +/// This function must be called with valid argc and argv from the program entry +/// point. +unsafe fn check_version_flag(argc: i32, argv: *const *const u8) -> bool { + if argc < 2 { + return false; + } + // SAFETY: argv is a valid array of argc pointers + let arg1 = unsafe { *argv.add(1) }; + if arg1.is_null() { + return false; + } + // Check if arg1 is "--version" + let version_flag = b"--version\0"; + for (i, &b) in version_flag.iter().enumerate() { + // SAFETY: arg1 is a valid C string + let arg_byte = unsafe { *arg1.add(i) }; + if arg_byte != b { + return false; + } + } + true +} + /// Main entry point for microfetch - can be called by the binary crate -/// or by other consumers of the library +/// or by other consumers of the library. +/// +/// # Arguments +/// +/// * `argc` - Argument count from main +/// * `argv` - Argument vector from main /// /// # Errors /// /// Returns an error if any system call fails +/// +/// # Safety +/// +/// argv must be a valid null-terminated array of C strings. #[cfg_attr(feature = "hotpath", hotpath::main)] -pub fn run() -> Result<(), Box> { - if Some("--version") == std::env::args().nth(1).as_deref() { - println!("Microfetch {}", env!("CARGO_PKG_VERSION")); - } else { - let utsname = UtsName::uname()?; - let fields = Fields { - user_info: system::get_username_and_hostname(&utsname), - os_name: release::get_os_pretty_name()?, - kernel_version: release::get_system_info(&utsname), - shell: system::get_shell(), - desktop: desktop::get_desktop_info(), - uptime: uptime::get_current()?, - memory_usage: system::get_memory_usage()?, - storage: system::get_root_disk_usage()?, - colors: colors::print_dots(), - }; - print_system_info(&fields)?; +pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { + if unsafe { check_version_flag(argc, argv) } { + print_version(); + return Ok(()); } + let utsname = UtsName::uname()?; + let fields = Fields { + user_info: system::get_username_and_hostname(&utsname), + os_name: release::get_os_pretty_name()?, + kernel_version: release::get_system_info(&utsname), + shell: system::get_shell(), + desktop: desktop::get_desktop_info(), + uptime: uptime::get_current()?, + memory_usage: system::get_memory_usage()?, + storage: system::get_root_disk_usage()?, + colors: colors::print_dots(), + }; + print_system_info(&fields)?; + Ok(()) } diff --git a/crates/lib/src/release.rs b/crates/lib/src/release.rs index 41a55c3..299c1c9 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -1,6 +1,6 @@ -use std::{fmt::Write as _, io}; +use alloc::string::String; -use crate::{UtsName, syscall::read_file_fast}; +use crate::{Error, UtsName, syscall::read_file_fast}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -13,7 +13,14 @@ pub fn get_system_info(utsname: &UtsName) -> String { let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1; let mut result = String::with_capacity(capacity); - write!(result, "{sysname} {release} ({machine})").unwrap(); + // Manual string construction instead of write! macro + result.push_str(sysname); + result.push(' '); + result.push_str(release); + result.push_str(" ("); + result.push_str(machine); + result.push(')'); + result } @@ -23,7 +30,7 @@ pub fn get_system_info(utsname: &UtsName) -> String { /// /// Returns an error if `/etc/os-release` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_os_pretty_name() -> Result { +pub fn get_os_pretty_name() -> Result { // Fast byte-level scanning for PRETTY_NAME= const PREFIX: &[u8] = b"PRETTY_NAME="; @@ -31,7 +38,7 @@ pub fn get_os_pretty_name() -> Result { // Use fast syscall-based file reading let bytes_read = read_file_fast("/etc/os-release", &mut buffer) - .map_err(|e| io::Error::from_raw_os_error(-e))?; + .map_err(Error::from_raw_os_error)?; let content = &buffer[..bytes_read]; let mut offset = 0; @@ -66,5 +73,5 @@ pub fn get_os_pretty_name() -> Result { offset += line_end + 1; } - Ok("Unknown".to_owned()) + Ok(String::from("Unknown")) } diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index 53ab38a..b902dac 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -1,37 +1,39 @@ -use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit}; +use alloc::string::String; +use core::mem::MaybeUninit; use crate::{ + Error, UtsName, - colors::COLORS, + colors::Colors, syscall::{StatfsBuf, read_file_fast, sys_statfs}, }; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_username_and_hostname(utsname: &UtsName) -> String { - let username_os = std::env::var_os("USER"); - let username = username_os - .as_deref() - .and_then(OsStr::to_str) - .unwrap_or("unknown_user"); + let username = crate::getenv_str("USER").unwrap_or("unknown_user"); let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); - let capacity = COLORS.yellow.len() + // Get colors (checking NO_COLOR only once) + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); + + let capacity = colors.yellow.len() + username.len() - + COLORS.red.len() + + colors.red.len() + 1 - + COLORS.green.len() + + colors.green.len() + hostname.len() - + COLORS.reset.len(); + + colors.reset.len(); let mut result = String::with_capacity(capacity); - result.push_str(COLORS.yellow); + result.push_str(colors.yellow); result.push_str(username); - result.push_str(COLORS.red); + result.push_str(colors.red); result.push('@'); - result.push_str(COLORS.green); + result.push_str(colors.green); result.push_str(hostname); - result.push_str(COLORS.reset); + result.push_str(colors.reset); result } @@ -39,13 +41,12 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - let shell_os = std::env::var_os("SHELL"); - let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let shell = crate::getenv_str("SHELL").unwrap_or(""); let start = shell.rfind('/').map_or(0, |i| i + 1); if shell.is_empty() { - "unknown_shell".into() + String::from("unknown_shell") } else { - shell[start..].into() + String::from(&shell[start..]) } } @@ -56,12 +57,12 @@ pub fn get_shell() -> String { /// Returns an error if the filesystem information cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] #[allow(clippy::cast_precision_loss)] -pub fn get_root_disk_usage() -> Result { +pub fn get_root_disk_usage() -> Result { let mut vfs = MaybeUninit::::uninit(); let path = b"/\0"; if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); + return Err(Error::last_os_error()); } let vfs = unsafe { vfs.assume_init() }; @@ -77,18 +78,96 @@ pub fn get_root_disk_usage() -> Result { let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let usage = (used_size / total_size) * 100.0; + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); + let mut result = String::with_capacity(64); - write!( - result, - "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", - cyan = COLORS.cyan, - reset = COLORS.reset, - ) - .unwrap(); + + // Manual float formatting + write_float(&mut result, used_size, 2); + result.push_str(" GiB / "); + write_float(&mut result, total_size, 2); + result.push_str(" GiB ("); + result.push_str(colors.cyan); + write_float(&mut result, usage, 0); + result.push('%'); + result.push_str(colors.reset); + result.push(')'); Ok(result) } +/// Write a float to string with specified decimal places +#[allow( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_precision_loss +)] +fn write_float(s: &mut String, val: f64, decimals: u32) { + // Handle integer part + let int_part = val as u64; + write_u64(s, int_part); + + if decimals > 0 { + s.push('.'); + + // Calculate fractional part + let mut frac = val - int_part as f64; + for _ in 0..decimals { + frac *= 10.0; + let digit = frac as u8; + s.push((b'0' + digit) as char); + frac -= f64::from(digit); + } + } +} + +/// Round an f64 to nearest integer (`f64::round` is not in core) +#[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss +)] +fn round_f64(x: f64) -> f64 { + if x >= 0.0 { + let int_part = x as u64 as f64; + let frac = x - int_part; + if frac >= 0.5 { + int_part + 1.0 + } else { + int_part + } + } else { + let int_part = (-x) as u64 as f64; + let frac = -x - int_part; + if frac >= 0.5 { + -(int_part + 1.0) + } else { + -int_part + } + } +} + +/// Write a u64 to string +fn write_u64(s: &mut String, mut n: u64) { + if n == 0 { + s.push('0'); + return; + } + + let mut buf = [0u8; 20]; + let mut i = 20; + + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + // SAFETY: buf contains only ASCII digits + s.push_str(unsafe { core::str::from_utf8_unchecked(&buf[i..]) }); +} + /// Fast integer parsing without stdlib overhead #[inline] fn parse_u64_fast(s: &[u8]) -> u64 { @@ -109,16 +188,16 @@ fn parse_u64_fast(s: &[u8]) -> u64 { /// /// Returns an error if `/proc/meminfo` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_memory_usage() -> Result { +pub fn get_memory_usage() -> Result { #[cfg_attr(feature = "hotpath", hotpath::measure)] - fn parse_memory_info() -> Result<(f64, f64), io::Error> { + fn parse_memory_info() -> Result<(f64, f64), Error> { let mut total_memory_kb = 0u64; let mut available_memory_kb = 0u64; let mut buffer = [0u8; 1024]; // Use fast syscall-based file reading let bytes_read = read_file_fast("/proc/meminfo", &mut buffer) - .map_err(|e| io::Error::from_raw_os_error(-e))?; + .map_err(Error::from_raw_os_error)?; let meminfo = &buffer[..bytes_read]; // Fast scanning for MemTotal and MemAvailable @@ -168,17 +247,22 @@ pub fn get_memory_usage() -> Result { let (used_memory, total_memory) = parse_memory_info()?; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let percentage_used = (used_memory / total_memory * 100.0).round() as u64; + let percentage_used = round_f64(used_memory / total_memory * 100.0) as u64; + + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); let mut result = String::with_capacity(64); - write!( - result, - "{used_memory:.2} GiB / {total_memory:.2} GiB \ - ({cyan}{percentage_used}%{reset})", - cyan = COLORS.cyan, - reset = COLORS.reset, - ) - .unwrap(); + + write_float(&mut result, used_memory, 2); + result.push_str(" GiB / "); + write_float(&mut result, total_memory, 2); + result.push_str(" GiB ("); + result.push_str(colors.cyan); + write_u64(&mut result, percentage_used); + result.push('%'); + result.push_str(colors.reset); + result.push(')'); Ok(result) } diff --git a/crates/lib/src/uptime.rs b/crates/lib/src/uptime.rs index b529f53..4ed2200 100644 --- a/crates/lib/src/uptime.rs +++ b/crates/lib/src/uptime.rs @@ -1,6 +1,7 @@ -use std::{io, mem::MaybeUninit}; +use alloc::string::String; +use core::mem::MaybeUninit; -use crate::syscall::sys_sysinfo; +use crate::{Error, syscall::sys_sysinfo}; /// Faster integer to string conversion without the formatting overhead. #[inline] @@ -16,7 +17,8 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { n /= 10; } - unsafe { std::str::from_utf8_unchecked(&buf[i..]) } + // SAFETY: We only wrote ASCII digits + unsafe { core::str::from_utf8_unchecked(&buf[i..]) } } /// Gets the current system uptime. @@ -25,11 +27,11 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { /// /// Returns an error if the system uptime cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_current() -> Result { +pub fn get_current() -> Result { let uptime_seconds = { let mut info = MaybeUninit::uninit(); if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); + return Err(Error::last_os_error()); } #[allow(clippy::cast_sign_loss)] unsafe { diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index 4680aa8..b29b420 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,12 +11,14 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] -hotpath = { optional = true, version = "0.14.0" } +hotpath = { optional = true, version = "0.14.0" } +microfetch-alloc.workspace = true microfetch-lib.workspace = true +microfetch-asm.workspace = true [features] hotpath = [ "dep:hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] [lints] -workspace = true +workspace = true \ No newline at end of file diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 8f22041..0bdc022 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -1,3 +1,60 @@ -fn main() -> Result<(), Box> { - microfetch_lib::run() +#![no_std] +#![no_main] + +extern crate alloc; + +use microfetch_alloc::BumpAlloc; +use microfetch_asm::sys_write; +#[cfg(not(test))] +use {core::panic::PanicInfo, microfetch_asm::sys_exit}; + +#[global_allocator] +static ALLOCATOR: BumpAlloc = BumpAlloc::new(); + +/// Receives argc and argv directly. The C runtime will call this after +/// initializing the environment. Cool right? +/// +/// # Safety +/// +/// argv must be a valid pointer to an array of argc C strings. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { + // SAFETY: argc and argv are provided by the C runtime and are valid + unsafe { + match microfetch_lib::run(argc, argv) { + Ok(()) => 0, + Err(e) => { + // Print error message to stderr (fd 2) + let msg = alloc::format!("Error: {e}\n"); + let _ = sys_write(2, msg.as_ptr(), msg.len()); + 1 + }, + } + } +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + // Write "panic" to stderr and exit + const PANIC_MSG: &[u8] = b"panic\n"; + unsafe { + let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len()); + sys_exit(1) + } +} + +// FIXME: Stubs for Rust exception handling symbols needed when using alloc with +// panic=abort These are normally provided by the unwinding runtime, but we're +// using panic=abort. I don't actually think this is the correct approach, but I +// cannot think of anything better. + +#[cfg(not(test))] +#[unsafe(no_mangle)] +const extern "C" fn rust_eh_personality() {} + +#[cfg(not(test))] +#[unsafe(no_mangle)] +extern "C" fn _Unwind_Resume() -> ! { + unsafe { sys_exit(1) } } From 0f5fc124da127c1a55cdd510d681e0a1acdc7117 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:06:42 +0300 Subject: [PATCH 02/11] crates/asm: replicate some libc runtime symbols manually, unsafely Signed-off-by: NotAShelf Change-Id: I6df95b1f0a9f117c8e108d8755389a4d6a6a6964 --- crates/asm/src/lib.rs | 242 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 87c76b2..1589a1d 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -19,6 +19,248 @@ compile_error!( "Unsupported architecture: only x86_64, aarch64, and riscv64 are supported" ); +/// Copies `n` bytes from `src` to `dest`. +/// +/// # Safety +/// +/// `dest` and `src` must be valid pointers to non-overlapping regions of +/// memory of at least `n` bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memcpy( + dest: *mut u8, + src: *const u8, + n: usize, +) -> *mut u8 { + for i in 0..n { + unsafe { + *dest.add(i) = *src.add(i); + } + } + dest +} + +/// Fills memory region with a byte value. +/// +/// # Safety +/// +/// `s` must be a valid pointer to memory of at least `n` bytes. +/// The value in `c` is treated as unsigned (lower 8 bits used). +#[unsafe(no_mangle)] +pub unsafe extern "C" fn memset(s: *mut u8, c: i32, n: usize) -> *mut u8 { + for i in 0..n { + unsafe { + *s.add(i) = u8::try_from(c).unwrap_or(0); + } + } + s +} + +/// Calculates the length of a null-terminated string. +/// +/// # Safety +/// +/// `s` must be a valid pointer to a null-terminated string. +#[unsafe(no_mangle)] +pub const unsafe extern "C" fn strlen(s: *const u8) -> usize { + let mut len = 0; + while unsafe { *s.add(len) } != 0 { + len += 1; + } + len +} + +/// Function pointer type for the main application entry point. +/// The function receives argc and argv and should return an exit code. +pub type MainFn = unsafe extern "C" fn(i32, *const *const u8) -> i32; + +static mut MAIN_FN: Option = None; + +/// Register the main function to be called from the entry point. +/// This must be called before the program starts (e.g., in a constructor). +pub fn register_main(main_fn: MainFn) { + unsafe { + MAIN_FN = Some(main_fn); + } +} + +/// Rust entry point called from `_start` assembly. +/// +/// The `stack` pointer points to: +/// `[rsp]` = argc +/// `[rsp+8]` = argv[0] +/// etc. +/// +/// # Safety +/// +/// The `stack` pointer must point to valid stack memory set up by the kernel +/// AND the binary must define a `main` function with the following signature: +/// +/// ```rust,ignore +/// unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32` +/// ``` +#[unsafe(no_mangle)] +pub unsafe extern "C" fn entry_rust(stack: *const usize) -> i32 { + // Read argc and argv from stack + let argc = unsafe { *stack }; + let argv = unsafe { stack.add(1).cast::<*const u8>() }; + + // SAFETY: argc is unlikely to exceed i32::MAX on real systems + let argc_i32 = i32::try_from(argc).unwrap_or(i32::MAX); + + // Call the main function (defined by the binary crate) + unsafe { main(argc_i32, argv) } +} + +// External main function that must be defined by the binary using this crate. +// Signature: `unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> +// i32` +unsafe extern "C" { + fn main(argc: i32, argv: *const *const u8) -> i32; +} + +#[cfg(target_arch = "x86_64")] +mod entry { + use core::arch::naked_asm; + + /// Entry point that receives stack pointer directly from kernel. + /// On `x86_64` Linux at program start: + /// + /// - `[rsp]` = argc + /// - `[rsp+8]` = argv[0] + /// - `[rsp+16]` = argv[1] + /// - ... + /// - `[rsp+8n]` = NULL + /// - `[rsp+8n+8]` = envp[0] + /// + /// # Safety + /// + /// This is a naked function with no prologue or epilogue. It directly + /// manipulates the stack pointer (`rsp`) and assumes it was called by the + /// kernel with a valid stack containing argc and argv. The function: + /// + /// - Reads from `[rsp]` without validating the pointer + /// - Modifies `rsp` directly (16-byte alignment) + /// - Does not preserve any registers + /// - Does not return normally (exits via syscall) + /// + /// This function MUST only be used as the program entry point (`_start`). + /// Calling it from any other context is undefined behavior. This has been + /// your safety notice. I WILL put UB in your Rust program. + #[unsafe(no_mangle)] + #[unsafe(naked)] + pub unsafe extern "C" fn _start() { + naked_asm!( + // Move stack pointer to first argument register + "mov rdi, rsp", + // Align stack to 16-byte boundary (System V AMD64 ABI requirement) + "and rsp, -16", + // Call into Rust code + "call {entry_rust}", + // Move return code to syscall argument + "mov rdi, rax", + // Exit syscall + "mov rax, 60", // SYS_exit + "syscall", + entry_rust = sym super::entry_rust, + ); + } +} + +#[cfg(target_arch = "aarch64")] +mod entry { + use core::arch::naked_asm; + + /// Entry point that receives stack pointer directly from kernel. + /// On `aarch64` Linux at program start, the stack layout is identical + /// to x86_64: + /// + /// - `[sp]` = argc + /// - `[sp+8]` = argv[0] + /// - ... + /// + /// # Safety + /// + /// This is a naked function with no prologue or epilogue. It directly + /// manipulates the stack pointer (`sp`) and assumes it was called by the + /// kernel with a valid stack containing argc and argv. The function: + /// + /// - Reads from `[sp]` without validating the pointer + /// - Modifies `sp` directly (16-byte alignment) + /// - Does not preserve any registers + /// - Does not return normally (exits via SVC instruction) + /// + /// This function MUST only be used as the program entry point (`_start`). + /// Calling it from any other context is undefined behavior. + #[unsafe(no_mangle)] + #[unsafe(naked)] + pub unsafe extern "C" fn _start() { + naked_asm!( + // Move stack pointer to first argument register + "mov x0, sp", + // Align stack to 16-byte boundary (AArch64 ABI requirement) + "and sp, sp, -16", + // Call into Rust code + "bl {entry_rust}", + // Move return code to syscall argument + "mov x0, x0", + // Exit syscall + "mov x8, 93", // SYS_exit + "svc #0", + entry_rust = sym super::entry_rust, + ); + } +} + +#[cfg(target_arch = "riscv64")] +mod entry { + use core::arch::naked_asm; + + /// Entry point that receives stack pointer directly from kernel. + /// On `riscv64` Linux at program start, the stack layout is identical + /// to x86_64: + /// + /// - `[sp]` = argc + /// - `[sp+8]` = argv[0] + /// - ... + /// + /// # Safety + /// + /// This is a naked function with no prologue or epilogue. It directly + /// manipulates the stack pointer (`sp`) and assumes it was called by the + /// kernel with a valid stack containing argc and argv. The function: + /// + /// - Reads from `[sp]` without validating the pointer + /// - Modifies `sp` directly (16-byte alignment) + /// - Does not preserve any registers + /// - Does not return normally (exits via ECALL instruction) + /// + /// This function MUST only be used as the program entry point (`_start`). + /// Calling it from any other context is undefined behavior. + #[unsafe(no_mangle)] + #[unsafe(naked)] + pub unsafe extern "C" fn _start() { + naked_asm!( + // Move stack pointer to first argument register + "mv a0, sp", + // Align stack to 16-byte boundary (RISC-V ABI requirement) + "andi sp, sp, -16", + // Call into Rust code + "call {entry_rust}", + // Move return code to syscall argument + "mv a0, a0", + // Exit syscall + "li a7, 93", // SYS_exit + "ecall", + entry_rust = sym super::entry_rust, + ); + } +} + +// Re-export the entry point +#[cfg(target_arch = "x86_64")] pub use entry::_start; +#[cfg(target_arch = "aarch64")] pub use entry::_start; +#[cfg(target_arch = "riscv64")] pub use entry::_start; + /// Direct syscall to open a file /// /// # Returns From d6977bafe59b871bb928d5c17ada5c07b91d8d76 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:45:30 +0300 Subject: [PATCH 03/11] various: (ab)use the new syscall wrappers and symbols; drop libc entirely Signed-off-by: NotAShelf Change-Id: I19ecd9801cf6e04adcedd3003d9fc59d6a6a6964 --- .cargo/config.toml | 6 +++-- crates/lib/src/lib.rs | 33 ++++++++++++++++++--------- microfetch/src/main.rs | 51 +++++++++++++++++++++++------------------- 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index c406f76..7a7a98f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,5 @@ -# https://github.com/rui314/mold?tab=readme-ov-file#how-to-use +# Link with Mold, and without libc! We use nostartfiles to avoid the C runtime +# See: +# [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-lc", "-C", "link-arg=-lgcc_s" ] +rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-nostartfiles" ] diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 7da92a0..84fe5ab 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -142,22 +142,34 @@ impl Drop for OnceLock { } } -// Access to the environ pointer (provided by libc startup code) -unsafe extern "C" { - static environ: *const *const u8; -} +// Store the environment pointer internally,initialized from `main()`. This +// helps avoid the libc dependency *completely*. +static ENVP: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut()); -/// Gets an environment variable by name (without using std). +/// Initialize the environment pointer. Must be called before any `getenv()` +/// calls. This is called from `main()` with the calculated `envp`. /// /// # Safety /// -/// This function reads from the environ global which is initialized -/// by the C runtime before `main()` is called. +/// envp must be a valid null-terminated array of C strings, or null if +/// no environment is available. +#[inline] +pub unsafe fn init_env(envp: *const *const u8) { + ENVP.store(envp.cast_mut(), Ordering::Release); +} + +/// Gets the current environment pointer. +#[inline] +#[must_use] +fn get_envp() -> *const *const u8 { + ENVP.load(Ordering::Acquire) +} + +/// Gets an environment variable by name without using std or libc by reading +/// from the environment pointer set by [`init_env`]. #[must_use] pub fn getenv(name: &str) -> Option<&'static [u8]> { - // SAFETY: environ is set up by the C runtime before main() runs - // and remains valid for the lifetime of the program - let envp = unsafe { environ }; + let envp = get_envp(); if envp.is_null() { return None; } @@ -237,6 +249,7 @@ impl UtsName { if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { return Err(Error::last_os_error()); } + Ok(Self(unsafe { uts.assume_init() })) } diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 0bdc022..25e855c 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -3,40 +3,49 @@ extern crate alloc; -use microfetch_alloc::BumpAlloc; -use microfetch_asm::sys_write; -#[cfg(not(test))] -use {core::panic::PanicInfo, microfetch_asm::sys_exit}; +use core::panic::PanicInfo; + +use microfetch_alloc::BumpAllocator; +// Re-export libc replacement functions from asm crate +pub use microfetch_asm::{memcpy, memset, strlen}; +use microfetch_asm::{sys_exit, sys_write}; +// Global allocator #[global_allocator] -static ALLOCATOR: BumpAlloc = BumpAlloc::new(); +static ALLOCATOR: BumpAllocator = BumpAllocator::new(); -/// Receives argc and argv directly. The C runtime will call this after -/// initializing the environment. Cool right? +/// Main application entry point. Called by the asm crate's entry point +/// after setting up argc, argv, and envp. /// /// # Safety /// /// argv must be a valid pointer to an array of argc C strings. #[unsafe(no_mangle)] pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { - // SAFETY: argc and argv are provided by the C runtime and are valid + // Calculate envp from argv. On Linux, envp is right after argv on the stack + // but I bet 12 cents that there will be at least one exception. + let argc_usize = usize::try_from(argc).unwrap_or(0); + let envp = unsafe { argv.add(argc_usize + 1) }; + + // Initialize the environment pointer unsafe { - match microfetch_lib::run(argc, argv) { - Ok(()) => 0, - Err(e) => { - // Print error message to stderr (fd 2) - let msg = alloc::format!("Error: {e}\n"); - let _ = sys_write(2, msg.as_ptr(), msg.len()); - 1 - }, - } + microfetch_lib::init_env(envp); + } + + // Run the main application logic + match unsafe { microfetch_lib::run(argc, argv) } { + Ok(()) => 0, + Err(e) => { + let msg = alloc::format!("Error: {e}\n"); + let _ = unsafe { sys_write(2, msg.as_ptr(), msg.len()) }; + 1 + }, } } #[cfg(not(test))] #[panic_handler] fn panic(_info: &PanicInfo) -> ! { - // Write "panic" to stderr and exit const PANIC_MSG: &[u8] = b"panic\n"; unsafe { let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len()); @@ -44,11 +53,7 @@ fn panic(_info: &PanicInfo) -> ! { } } -// FIXME: Stubs for Rust exception handling symbols needed when using alloc with -// panic=abort These are normally provided by the unwinding runtime, but we're -// using panic=abort. I don't actually think this is the correct approach, but I -// cannot think of anything better. - +// Stubs for Rust exception handling #[cfg(not(test))] #[unsafe(no_mangle)] const extern "C" fn rust_eh_personality() {} From 5ce0f3b1e8b87c6e802f8ead41eb779f82fe6e1e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:48:19 +0300 Subject: [PATCH 04/11] nix: use mold on all Linux targets; fix source filters Signed-off-by: NotAShelf Change-Id: Ib5925097dd366505d01f9448e7e2ee926a6a6964 --- nix/package.nix | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index de8bdf8..3892b41 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,22 +1,13 @@ { lib, - stdenv, - stdenvAdapters, rustPlatform, llvm, - useMold ? stdenv.isLinux, }: let toml = (lib.importTOML ../Cargo.toml).package; pname = toml.name; inherit (toml) version; - - # Select stdenv based on useMold flag - stdenv = - if useMold - then stdenvAdapters.useMoldLinker llvm.stdenv - else llvm.stdenv; in - rustPlatform.buildRustPackage.override {inherit stdenv;} { + rustPlatform.buildRustPackage.override {inherit (llvm) stdenv;} { inherit pname version; src = let fs = lib.fileset; @@ -25,10 +16,11 @@ in fs.toSource { root = s; fileset = fs.unions [ - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /crates)) + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /microfetch)) + (s + /.cargo) (s + /Cargo.lock) (s + /Cargo.toml) - (s + /benches) ]; }; @@ -37,12 +29,6 @@ in buildNoDefaultFeatures = true; doCheck = false; - # Only set RUSTFLAGS for mold if useMold is enabled - env = lib.optionalAttrs useMold { - CARGO_LINKER = "clang"; - RUSTFLAGS = "-C link-arg=-fuse-ld=mold"; - }; - meta = { description = "Microscopic fetch script in Rust, for NixOS systems"; homepage = "https://github.com/NotAShelf/microfetch"; From 781de52ca6edf1271e10a59dc8eb9cec6decfd45 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Fri, 27 Mar 2026 23:53:04 +0300 Subject: [PATCH 05/11] build: be more aggressive with linker optimizations; wrap mold Signed-off-by: NotAShelf Change-Id: I0e3132ab1499684eda715c3cee9b27a16a6a6964 --- .cargo/config.toml | 30 ++++++++++++++++++++++++++++-- Cargo.toml | 6 +++--- microfetch/Cargo.toml | 8 ++++---- scripts/ld-wrapper | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100755 scripts/ld-wrapper diff --git a/.cargo/config.toml b/.cargo/config.toml index 7a7a98f..d9f659e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,31 @@ -# Link with Mold, and without libc! We use nostartfiles to avoid the C runtime +# Use a linker wrapper that invokes mold then strips junk sections with objcopy. +# mold cannot discard .eh_frame/.dynstr/.comment via linker scripts, so we do +# it as a post-link step. # See: # [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-nostartfiles" ] +linker = "scripts/ld-wrapper" +rustflags = [ + # No C runtime, we provide _start ourselves + "-C", + "link-arg=-nostartfiles", + # Fully static, no dynamic linker, no .interp/.dynsym/.dynamic overhead + "-C", + "link-arg=-static", + # Static PIE is incompatible with -static :( + "-C", + "relocation-model=static", + # Suppress .eh_frame emission from our own codegen (does not cover compiler_builtins; + # those remnants are removed by the linker wrapper via objcopy post-link) + "-C", + "force-unwind-tables=no", + # Linker flags + "-C", + "link-arg=-Wl,--gc-sections", # remove unreferenced input sections + "-C", + "link-arg=-Wl,--strip-all", # strip all symbol table entries + "-C", + "link-arg=-Wl,--build-id=none", # omit the .note.gnu.build-id section + "-C", + "link-arg=-Wl,-z,norelro", # disable RELRO (removes relro_padding) +] diff --git a/Cargo.toml b/Cargo.toml index da548d7..9500909 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,15 @@ version = "0.4.13" [workspace.dependencies] microfetch-alloc = { path = "./crates/alloc" } -microfetch-asm = { path = "./crates/asm" } -microfetch-lib = { path = "./crates/lib" } +microfetch-asm = { path = "./crates/asm" } +microfetch-lib = { path = "./crates/lib" } criterion = { default-features = false, features = [ "cargo_bench_support" ], version = "0.8.2" } criterion-cycles-per-byte = "0.8.0" [profile.dev] opt-level = 1 -panic = "abort" +panic = "abort" [profile.release] codegen-units = 1 diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index b29b420..43d4f9c 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,14 +11,14 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] -hotpath = { optional = true, version = "0.14.0" } +hotpath = { optional = true, version = "0.14.0" } microfetch-alloc.workspace = true -microfetch-lib.workspace = true -microfetch-asm.workspace = true +microfetch-asm.workspace = true +microfetch-lib.workspace = true [features] hotpath = [ "dep:hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/scripts/ld-wrapper b/scripts/ld-wrapper new file mode 100755 index 0000000..cf2cb21 --- /dev/null +++ b/scripts/ld-wrapper @@ -0,0 +1,34 @@ +#!/usr/bin/env sh +# Invoke mold, then strip junk sections from the output binary with objcopy. +# This (more or less) removes sections that mold cannot discard itself, suck as: +# - .eh_frame / .eh_frame_hdr - unwind tables from compiler_builtins +# - dynstr - mold emits this, even for fully static binaries +# - .comment - compiler version string +# +# We forward everything to mold via -fuse-ld, then post-process the output in place. + +set -eu + +# Locate the output file +OUTPUT="" +prev="" +for arg in "$@"; do + if [ "$prev" = "-o" ]; then + OUTPUT="$arg" + break + fi + prev="$arg" +done + +# Invoke mold via the cc driver, forward all original arguments +cc -fuse-ld=mold "$@" + +# Remove sections that mold cannot discard +if [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then + objcopy \ + --remove-section=.eh_frame \ + --remove-section=.eh_frame_hdr \ + --remove-section=.dynstr \ + --remove-section=.comment \ + "$OUTPUT" +fi From 3b823a098214a6e256455847811cc44e992e376e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 28 Mar 2026 00:05:02 +0300 Subject: [PATCH 06/11] nix: fix Signed-off-by: NotAShelf Change-Id: Ibdd7b1deaff9489cedcbbc05ce7956d86a6a6964 --- nix/package.nix | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 3892b41..2658d73 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -3,8 +3,8 @@ rustPlatform, llvm, }: let - toml = (lib.importTOML ../Cargo.toml).package; - pname = toml.name; + pname = "microfetch"; + toml = (lib.importTOML ../Cargo.toml).workspace.package; inherit (toml) version; in rustPlatform.buildRustPackage.override {inherit (llvm) stdenv;} { @@ -16,9 +16,10 @@ in fs.toSource { root = s; fileset = fs.unions [ - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /crates)) - (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /microfetch)) + (s + /crates) + (s + /microfetch) (s + /.cargo) + (s + /scripts/ld-wrapper) (s + /Cargo.lock) (s + /Cargo.toml) ]; From d49e75b6d23dfee2cbfcc937b884912f462580c5 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 17:08:13 -0400 Subject: [PATCH 07/11] ci: stop overriding .cargo/config.toml rustflags in CI workflows --- .github/workflows/hotpath-profile.yml | 2 ++ .github/workflows/rust.yml | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 244fb2b..9909986 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -16,6 +16,8 @@ jobs: fetch-depth: 0 - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Create metrics directory run: mkdir -p /tmp/metrics diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index deb45e3..945ed5c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,6 +29,7 @@ jobs: uses: actions-rust-lang/setup-rust-toolchain@v1 with: target: ${{ matrix.target }} + rustflags: "" - name: "Make Mold the default linker" uses: rui314/setup-mold@v1 From 308ca53dd8100a6025ce87b92e0ace8830082300 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Fri, 27 Mar 2026 17:08:28 -0400 Subject: [PATCH 08/11] ci: add missing mold linker setup to hotpath-profile workflow --- .github/workflows/hotpath-profile.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 9909986..dfb09c1 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -19,6 +19,9 @@ jobs: with: rustflags: "" + - name: Make Mold the default linker + uses: rui314/setup-mold@v1 + - name: Create metrics directory run: mkdir -p /tmp/metrics From 14fc728181d3783c7e4499aa45e0d3271ff45c0a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 28 Mar 2026 08:10:11 +0300 Subject: [PATCH 09/11] docs: clarify project state after the `no_std` rework Signed-off-by: NotAShelf Change-Id: Ib44a2dce66938d1100972a9137c01dff6a6a6964 --- docs/README.md | 77 ++++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/docs/README.md b/docs/README.md index 2e8830b..c0c8d54 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,12 +19,12 @@ [fastfetch]: https://github.com/fastfetch-cli/fastfetch -Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust -for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and -displays _most_ of the nonsense you'd see posted on r/unixporn or other internet -communities. Aims to replace [fastfetch] on my personal system, but -[probably not yours](#customizing). Though, you are more than welcome to use it -on your system: it is pretty _[fast](#benchmarks)_... +Stupidly small and simple, laughably fast, and pretty fetch tool. Written +(mostly) in Rust for speed and ease of maintainability. Runs in a _fraction of a +millisecond_ and displays _most_ of the nonsense you'd see posted on r/unixporn +or other internet communities. Aims to replace [fastfetch] on my personal +system, but [probably not yours](#customizing). Though, you are more than +welcome to use it on your system: it is pretty _[fast](#benchmarks)_...


@@ -40,9 +40,9 @@ on your system: it is pretty _[fast](#benchmarks)_... - Fast - Really fast -- Minimal dependencies -- Tiny binary (~370kb [^1]) -- Actually really fast +- No dependencies (not even libc!) +- Tiny binary (~25kb) +- Actually _really_ fast - Cool NixOS logo (other, inferior, distros are not supported) - Reliable detection of following info: - Hostname/Username @@ -57,12 +57,9 @@ on your system: it is pretty _[fast](#benchmarks)_... - Shell Colors - Did I mention fast? - Respects [`NO_COLOR` spec](https://no-color.org/) -- Funny [^2] +- Funny [^1] -[^1]: With the Mold linker, which is enabled by default in the Flake package, - the binary size is roughly 350kb. That's nearly 20kb reduction in size :) - -[^2]: I don't know how else to describe the (unhealthy) amount of handwritten +[^1]: I don't know how else to describe the (unhealthy) amount of handwritten assembly that was written in order to make Microfetch faster. ## Motivation @@ -70,12 +67,12 @@ on your system: it is pretty _[fast](#benchmarks)_... [Rube-Goldmark Machine]: https://en.wikipedia.org/wiki/Rube_Goldberg_machine Fastfetch, as its name _probably_ already hinted, is a very fast fetch tool -written in C. I used to use Fastfetch on my systems, but I eventually came to +written in C. I _used to_ use Fastfetch on my systems, but I eventually came to the realization that I am _not interested in any of its additional features_. I don't use Sixel, I don't change my configuration more than maybe once a year and -I don't even display most of the fields that it does. Sure the configurability -is nice and I can configure the defaults that I do not like but how often do I -really do that? +I don't even display most of the fields that it has. Sure, the configurability +is nice and _I could_ configure the defaults that I do not like... but how often +do I really do that? Since I already enjoy programming challenges, and don't use a fetch program that often, I eventually came to try and answer the question _how fast can I make my @@ -84,24 +81,29 @@ and put in my `~/.bashrc` but is _actually_ incredibly fast because it opts out of all the customization options provided by tools such as Fastfetch. Since Fetch scripts are kind of a coming-of-age ritual for most Linux users, I've decided to use it on my system. You also might be interested if you like the -defaults and like speed. - -Ultimately, it's a small, opinionated binary with a nice size that doesn't -bother me, and incredible speed. Customization? No thank you. I cannot -re-iterate it enough, Microfetch is _annoyingly fast_. It does not, however, -solve a technical problem. The "problem" Microfetch solves is entirely -self-imposed. On the matter of _size_, the project is written in Rust, which -comes at the cost of "bloated" dependency trees and the increased build times, -but we make an extended effort to keep the dependencies minimal and build times -manageable. The latter is also very easily mitigated with Nix's binary cache -systems. Since Microfetch is already in Nixpkgs, you are recommended to use it -to utilize the binary cache properly. The usage of Rust _is_ nice, however, -since it provides us with incredible tooling and a very powerful language that -allows for Microfetch to be as fast as possible. ~~Sure C could've been used -here as well, but do you think I hate myself?~~ Microfetch now features -handwritten assembly to unsafely optimize some areas. In hindsight you all -should have seen this coming. Is it faster? Yes. Should you use this? If you -want to. +defaults and like speed. Ultimately, Microfetch a small, opinionated binary with +a nice size that doesn't bother me, and _incredible_ speed. Customization? No +thank you. + +I cannot re-iterate it enough, Microfetch is _annoyingly fast_. It, however, +does not solve a real technical problem. The "problem" Microfetch "solves" is +entirely self-imposed. I want a fast, _almost_ zero-cost command invocation and +for it to not take that much space on my system. Thanks to the nature of Rust, +Microfetch is _fast_. Rust does, or well, _did_ mean "bloated" dependency trees +and slightly increased build times, though, as of 0.5.0 Microfetch has +(voluntarily) dropped both `std` and `libc`. You can go check the numbers for +the speed impact (hint: it's much better) but we also have little to no concerns +left about build times and the binary size. Build times are also _very easily_ +mitigated with Nix's binary cache systems, and since Microfetch is already in +Nixpkgs you are strongly encouraged to use `pkgs.microfetch` over the flake. The +usage of Rust _is_ quite nice, however, since it provides us with incredible +tooling and a very powerful language that allows for Microfetch to be as fast as +possible. + +Surely C would've been a smaller choice, but I like Rust more. Microfetch _also_ +features a whole bunch of handwritten assembly with per-platform support to +_unsafely_ optimize most syscalls. In hindsight you all should have seen this +coming. Is it faster? Yes. Is it better? Uh, yes. Should you use this? Yes. Also see: [Rube-Goldmark Machine] @@ -312,4 +314,5 @@ Microfetch. I might have missed your name here, but you have my thanks. ## License -Microfetch is licensed under [GPL3](LICENSE). See the license file for details. +This project is released under GNU Public Licence version 3 **only**. See the +[license](../LICENSE) for more details. From 8324b35661aa996f2954fc55b453051f294ecad4 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 29 Mar 2026 14:01:37 +0300 Subject: [PATCH 10/11] lib: collapse `print_system_info` into single `write!` call; custom logo support Signed-off-by: NotAShelf Change-Id: I589075f0e4b1629713c872397c602ac66a6a6964 --- crates/lib/src/lib.rs | 307 +++++++++++++++++++++--------------------- 1 file changed, 155 insertions(+), 152 deletions(-) diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 84fe5ab..1d89f8d 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -289,6 +289,49 @@ struct Fields { colors: String, } +/// Minimal, stack-allocated writer implementing `core::fmt::Write`. Avoids heap +/// allocation for the output buffer. +struct StackWriter<'a> { + buf: &'a mut [u8], + pos: usize, +} + +impl<'a> StackWriter<'a> { + #[inline] + const fn new(buf: &'a mut [u8]) -> Self { + Self { buf, pos: 0 } + } + + #[inline] + fn written(&self) -> &[u8] { + &self.buf[..self.pos] + } +} + +impl core::fmt::Write for StackWriter<'_> { + #[inline] + fn write_str(&mut self, s: &str) -> core::fmt::Result { + let bytes = s.as_bytes(); + let to_write = bytes.len().min(self.buf.len() - self.pos); + self.buf[self.pos..self.pos + to_write].copy_from_slice(&bytes[..to_write]); + self.pos += to_write; + Ok(()) + } +} + +/// Custom logo art embedded at compile time via the `MICROFETCH_LOGO` +/// environment variable. Set it to 9 newline-separated lines of ASCII/Unicode +/// art when building to replace the default NixOS logo: +/// +/// `MICROFETCH_LOGO="$(cat my_logo.txt)"` cargo build --release +/// +/// Each line maps to one info row. When unset, the built-in two-tone NixOS +/// logo is used. +const CUSTOM_LOGO: &str = match option_env!("MICROFETCH_LOGO") { + Some(s) => s, + None => "", +}; + #[cfg_attr(feature = "hotpath", hotpath::measure)] fn print_system_info(fields: &Fields) -> Result<(), Error> { let Fields { @@ -304,168 +347,128 @@ fn print_system_info(fields: &Fields) -> Result<(), Error> { } = fields; let no_color = colors::is_no_color(); - let colors_obj = colors::Colors::new(no_color); - let cyan = colors_obj.cyan; - let blue = colors_obj.blue; - let reset = colors_obj.reset; + let c = colors::Colors::new(no_color); - // Build output string let mut buf = [0u8; 2048]; - let mut pos = 0usize; - - // Helper to write to buffer - let mut write_str = |s: &str| { - let bytes = s.as_bytes(); - let remaining = buf.len() - pos; - let to_write = bytes.len().min(remaining); - buf[pos..pos + to_write].copy_from_slice(&bytes[..to_write]); - pos += to_write; - }; + let mut w = StackWriter::new(&mut buf); + + if CUSTOM_LOGO.is_empty() { + // Default two-tone NixOS logo rendered as a single write! pass. + core::fmt::write( + &mut w, + format_args!( + "\n {b} ▟█▖ {cy}▝█▙ ▗█▛ {user_info} ~{rs}\n {b} \ + ▗▄▄▟██▄▄▄▄▄{cy}▝█▙█▛ {b}▖ {cy}\u{F313} {b}System{rs} \ + {os_name}\n {b} ▀▀▀▀▀▀▀▀▀▀▀▘{cy}▝██ {b}▟█▖ {cy}\u{E712} \ + {b}Kernel{rs} {kernel_version}\n {cy} ▟█▛ \ + {cy}▝█▘{b}▟█▛ {cy}\u{E795} {b}Shell{rs} {shell}\n \ + {cy}▟█████▛ {b}▟█████▛ {cy}\u{F017} {b}Uptime{rs} \ + {uptime}\n {cy} ▟█▛{b}▗█▖ {b}▟█▛ {cy}\u{F2D2} \ + {b}Desktop{rs} {desktop}\n {cy} ▝█▛ \ + {b}██▖{cy}▗▄▄▄▄▄▄▄▄▄▄▄ {cy}\u{F035B} {b}Memory{rs} \ + {memory_usage}\n {cy} ▝ {b}▟█▜█▖{cy}▀▀▀▀▀██▛▀▀▘ \ + {cy}\u{F194E} {b}Storage (/){rs} {storage}\n {b} ▟█▘ ▜█▖ \ + {cy}▝█▛ {cy}\u{E22B} {b}Colors{rs} {colors}\n\n", + b = c.blue, + cy = c.cyan, + rs = c.reset, + user_info = user_info, + os_name = os_name, + kernel_version = kernel_version, + shell = shell, + uptime = uptime, + desktop = desktop, + memory_usage = memory_usage, + storage = storage, + colors = colors, + ), + ) + .ok(); + } else { + // Custom logo is 9 lines from MICROFETCH_LOGO env var, one per info row. + // Lines beyond 9 are ignored; missing lines render as empty. + let mut lines = CUSTOM_LOGO.split('\n'); + let logo_rows: [&str; 9] = + core::array::from_fn(|_| lines.next().unwrap_or("")); + + // Row format mirrors the default logo path exactly. + let rows: [(&str, &str, &str, &str, &str); 9] = [ + ("", "", user_info.as_str(), " ", " ~"), + ("\u{F313} ", "System", os_name.as_str(), " ", ""), + ( + "\u{E712} ", + "Kernel", + kernel_version.as_str(), + " ", + "", + ), + ("\u{E795} ", "Shell", shell.as_str(), " ", ""), + ("\u{F017} ", "Uptime", uptime.as_str(), " ", ""), + ("\u{F2D2} ", "Desktop", desktop.as_str(), " ", ""), + ( + "\u{F035B} ", + "Memory", + memory_usage.as_str(), + " ", + "", + ), + ("\u{F194E} ", "Storage (/)", storage.as_str(), " ", ""), + ("\u{E22B} ", "Colors", colors.as_str(), " ", ""), + ]; + + core::fmt::write(&mut w, format_args!("\n")).ok(); + for i in 0..9 { + let (icon, key, value, spacing, suffix) = rows[i]; + if key.is_empty() { + // Row 1 has no icon/key, just logo + user_info + core::fmt::write( + &mut w, + format_args!( + " {cy}{logo}{rs} {value}{suffix}\n", + cy = c.cyan, + rs = c.reset, + logo = logo_rows[i], + value = value, + suffix = suffix, + ), + ) + .ok(); + } else { + core::fmt::write( + &mut w, + format_args!( + " {cy}{logo}{rs} \ + {cy}{icon}{b}{key}{rs}{spacing}{value}{suffix}\n", + cy = c.cyan, + b = c.blue, + rs = c.reset, + logo = logo_rows[i], + icon = icon, + key = key, + spacing = spacing, + value = value, + suffix = suffix, + ), + ) + .ok(); + } + } + core::fmt::write(&mut w, format_args!("\n")).ok(); + } - write_str("\n "); - write_str(blue); - write_str(" ▟█▖ "); - write_str(cyan); - write_str("▝█▙ ▗█▛ "); - write_str(user_info); - write_str(" ~"); - write_str(reset); - write_str("\n"); - - write_str(" "); - write_str(blue); - write_str(" ▗▄▄▟██▄▄▄▄▄"); - write_str(cyan); - write_str("▝█▙█▛ "); - write_str(blue); - write_str("▖ "); - write_str(cyan); - write_str(" "); - write_str(blue); - write_str("System"); - write_str(reset); - write_str("  "); - write_str(os_name); - write_str("\n"); - - write_str(" "); - write_str(blue); - write_str(" ▀▀▀▀▀▀▀▀▀▀▀▘"); - write_str(cyan); - write_str("▝██ "); - write_str(blue); - write_str("▟█▖ "); - write_str(cyan); - write_str(" "); - write_str(blue); - write_str("Kernel"); - write_str(reset); - write_str("  "); - write_str(kernel_version); - write_str("\n"); - - write_str(" "); - write_str(cyan); - write_str(" ▟█▛ "); - write_str(cyan); - write_str("▝█▘"); - write_str(blue); - write_str("▟█▛ "); - write_str(cyan); - write_str(" "); - write_str(blue); - write_str("Shell"); - write_str(reset); - write_str("  "); - write_str(shell); - write_str("\n"); - - write_str(" "); - write_str(cyan); - write_str("▟█████▛ "); - write_str(blue); - write_str("▟█████▛ "); - write_str(cyan); - write_str(" "); - write_str(blue); - write_str("Uptime"); - write_str(reset); - write_str("  "); - write_str(uptime); - write_str("\n"); - - write_str(" "); - write_str(cyan); - write_str(" ▟█▛"); - write_str(blue); - write_str("▗█▖ "); - write_str(blue); - write_str("▟█▛ "); - write_str(cyan); - write_str(" "); - write_str(blue); - write_str("Desktop"); - write_str(reset); - write_str("  "); - write_str(desktop); - write_str("\n"); - - write_str(" "); - write_str(cyan); - write_str(" ▝█▛ "); - write_str(blue); - write_str("██▖"); - write_str(cyan); - write_str("▗▄▄▄▄▄▄▄▄▄▄▄ "); - write_str(cyan); - write_str("󰍛 "); - write_str(blue); - write_str("Memory"); - write_str(reset); - write_str("  "); - write_str(memory_usage); - write_str("\n"); - - write_str(" "); - write_str(cyan); - write_str(" ▝ "); - write_str(blue); - write_str("▟█▜█▖"); - write_str(cyan); - write_str("▀▀▀▀▀██▛▀▀▘ "); - write_str(cyan); - write_str("󱥎 "); - write_str(blue); - write_str("Storage (/)"); - write_str(reset); - write_str("  "); - write_str(storage); - write_str("\n"); - - write_str(" "); - write_str(blue); - write_str(" ▟█▘ ▜█▖ "); - write_str(cyan); - write_str("▝█▛ "); - write_str(cyan); - write_str(" "); - write_str(blue); - write_str("Colors"); - write_str(reset); - write_str("  "); - write_str(colors); - write_str("\n\n"); - - // Direct syscall to avoid stdout buffering allocation - let written = unsafe { sys_write(1, buf.as_ptr(), pos) }; + // Single syscall for the entire output. + let out = w.written(); + let written = unsafe { sys_write(1, out.as_ptr(), out.len()) }; if written < 0 { #[allow(clippy::cast_possible_truncation)] return Err(Error::OsError(written as i32)); } + #[allow(clippy::cast_sign_loss)] - if written as usize != pos { + if written as usize != out.len() { return Err(Error::WriteError); } + Ok(()) } From d118e591d07f4f05a1ea342692cd531b2943c490 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sun, 29 Mar 2026 14:47:12 +0300 Subject: [PATCH 11/11] docs: describe custom logo support; revise 'customizing' section Signed-off-by: NotAShelf Change-Id: I6e9b904fcd228aa74119e4c09c486f756a6a6964 --- docs/README.md | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/docs/README.md b/docs/README.md index c0c8d54..6c11dd2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,9 +41,9 @@ welcome to use it on your system: it is pretty _[fast](#benchmarks)_... - Fast - Really fast - No dependencies (not even libc!) -- Tiny binary (~25kb) +- Tiny binary (~24kb) - Actually _really_ fast -- Cool NixOS logo (other, inferior, distros are not supported) +- Cool NixOS logo, with support for custom logo text - Reliable detection of following info: - Hostname/Username - Kernel @@ -234,20 +234,11 @@ interested in Microfetch tailored to their distributions. ## Customizing -You can't*. - -### Why? - -Customization, of most kinds, is "expensive": I could try reading environment -variables, parse command-line arguments or read a configuration file to allow -configuring various fields but those inflate execution time and the resource -consumption by a lot. Since Microfetch is closer to a code golf challenge than a -program that attempts to fill a gap, I have elected not to make this trade. This -is, of course, not without a solution. +You can't* ### Really? -[main module]: ./src/main.rs +[main module]: ./microfetch/src/main.rs [discussions tab]: https://github.com/NotAShelf/microfetch/discussions To be fair, you _can_ customize Microfetch by, well, patching it. It is @@ -265,6 +256,36 @@ The Nix package allows passing patches in a streamlined manner by passing share your derivations with people. Feel free to use the [discussions tab] to share your own variants of Microfetch! +The real reason behind lack of customizability is that customization, of most +kinds, is "expensive": reading environment variables, parsing command-line +arguments or reading a configuration file inflates execution time and resource +consumption. Since Microfetch is closer to a code golf challenge than a program +that attempts to fill a gap, runtime configuration is not supported. Patching +the source is the only way to make changes that do not compromise on speed. The +exception to this, as described below, is the custom logo support. + +### Logo + +Microfetch used to be impossible to customize without patching. Fortunately it's +possible now to customize the logo to support your distribution. This is +best-effort, but should work for most cases as long as you adhere to the +constraints. + +To use a custom logo, set the `MICROFETCH_LOGO` environment variable to 9 +newline-separated lines of ASCII or Unicode art when building: + +```bash +# Pass your logo text with MICROFETCH_LOGO. +$ MICROFETCH_LOGO="$(cat my_logo.txt)" cargo build --release +``` + +Each line corresponds to one info row. Nerd Font glyphs for labels (System, +Kernel, Shell, etc.) are rendered automatically. An unset variable uses the +built-in two-tone NixOS logo. + +Keep in mind that the custom logo **is not padded**. You will need to mind the +spaces if you're providing a custom logo for some sort. + ## Contributing I will, mostly, reject feature additions. This is not to say you should avoid