Merge pull request #57 from NotAShelf/notashelf/push-pynzzylozqql

NO MORE STDS!!!!!
This commit is contained in:
raf 2026-04-11 10:19:12 +03:00 committed by GitHub
commit bf25c77b8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1219 additions and 221 deletions

View file

@ -1,3 +1,16 @@
# https://github.com/rui314/mold?tab=readme-ov-file#how-to-use # 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:
# <https://github.com/rui314/mold?tab=readme-ov-file#how-to-use>
#
# Binary-specific link flags live in microfetch/build.rs via cargo:rustc-link-arg-bin
# so they only affect the final binary and don't break proc-macro or build-script linking.
[target.'cfg(target_os = "linux")'] [target.'cfg(target_os = "linux")']
rustflags = [ "-C", "link-arg=-fuse-ld=mold" ] linker = "scripts/ld-wrapper"
rustflags = [
# 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",
]

View file

@ -16,6 +16,11 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions-rust-lang/setup-rust-toolchain@v1 - uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ""
- name: Make Mold the default linker
uses: rui314/setup-mold@v1
- name: Create metrics directory - name: Create metrics directory
run: mkdir -p /tmp/metrics run: mkdir -p /tmp/metrics

View file

@ -29,16 +29,18 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
rustflags: ""
- name: "Make Mold the default linker" - name: "Make Mold the default linker"
uses: rui314/setup-mold@v1 uses: rui314/setup-mold@v1
- name: "Install cross" - name: "Setup cross-compilation toolchain"
run: cargo install cross --git https://github.com/cross-rs/cross uses: taiki-e/setup-cross-toolchain-action@v1
with:
target: ${{ matrix.target }}
- name: "Build" - name: "Build"
run: cross build --verbose --target ${{ matrix.target }} run: cargo build --verbose
- name: "Run tests" - name: "Run tests"
if: matrix.target == 'x86_64-unknown-linux-gnu' run: cargo test --workspace --exclude microfetch --verbose
run: cross test --verbose --target ${{ matrix.target }}

6
Cargo.lock generated
View file

@ -582,9 +582,15 @@ name = "microfetch"
version = "0.4.13" version = "0.4.13"
dependencies = [ dependencies = [
"hotpath", "hotpath",
"microfetch-alloc",
"microfetch-asm",
"microfetch-lib", "microfetch-lib",
] ]
[[package]]
name = "microfetch-alloc"
version = "0.4.13"
[[package]] [[package]]
name = "microfetch-asm" name = "microfetch-asm"
version = "0.4.13" version = "0.4.13"

View file

@ -10,6 +10,7 @@ rust-version = "1.92.0"
version = "0.4.13" version = "0.4.13"
[workspace.dependencies] [workspace.dependencies]
microfetch-alloc = { path = "./crates/alloc" }
microfetch-asm = { path = "./crates/asm" } microfetch-asm = { path = "./crates/asm" }
microfetch-lib = { path = "./crates/lib" } microfetch-lib = { path = "./crates/lib" }
@ -18,6 +19,7 @@ criterion-cycles-per-byte = "0.8.0"
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1
panic = "abort"
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1

12
crates/alloc/Cargo.toml Normal file
View file

@ -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

106
crates/alloc/src/lib.rs Normal file
View file

@ -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<const N: usize = DEFAULT_HEAP_SIZE> {
heap: UnsafeCell<[u8; N]>,
next: UnsafeCell<usize>,
}
// 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<const N: usize> Sync for BumpAllocator<N> {}
impl<const N: usize> BumpAllocator<N> {
/// 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<const N: usize> Default for BumpAllocator<N> {
fn default() -> Self {
Self::new()
}
}
unsafe impl<const N: usize> GlobalAlloc for BumpAllocator<N> {
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<DEFAULT_HEAP_SIZE>;

View file

@ -19,6 +19,137 @@ compile_error!(
"Unsupported architecture: only x86_64, aarch64, and riscv64 are supported" "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
}
/// Compares two byte sequences.
///
/// # Safety
///
/// `s1` and `s2` must be valid pointers to memory of at least `n` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn bcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 {
for i in 0..n {
let a = unsafe { *s1.add(i) };
let b = unsafe { *s2.add(i) };
if a != b {
return i32::from(a) - i32::from(b);
}
}
0
}
/// Compares two byte sequences.
///
/// # Safety
///
/// `s1` and `s2` must be valid pointers to memory of at least `n` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn memcmp(s1: *const u8, s2: *const u8, n: usize) -> i32 {
unsafe { bcmp(s1, s2, n) }
}
/// 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.
#[cfg(not(test))]
pub type MainFn = unsafe extern "C" fn(i32, *const *const u8) -> i32;
#[cfg(not(test))]
static mut MAIN_FN: Option<MainFn> = 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).
#[cfg(not(test))]
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`
/// ```
#[cfg(not(test))]
#[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`
#[cfg(not(test))]
unsafe extern "C" {
fn main(argc: i32, argv: *const *const u8) -> i32;
}
/// Direct syscall to open a file /// Direct syscall to open a file
/// ///
/// # Returns /// # Returns
@ -507,6 +638,7 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> Result<usize, i32> {
let _ = sys_close(fd); let _ = sys_close(fd);
if bytes_read < 0 { if bytes_read < 0 {
#[allow(clippy::cast_possible_truncation)]
return Err(bytes_read as i32); return Err(bytes_read as i32);
} }
@ -598,3 +730,41 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 {
ret 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)
);
}
}

View file

@ -1,5 +1,6 @@
use std::sync::LazyLock; use alloc::string::String;
/// Color codes for terminal output
pub struct Colors { pub struct Colors {
pub reset: &'static str, pub reset: &'static str,
pub blue: &'static str, pub blue: &'static str,
@ -11,7 +12,8 @@ pub struct Colors {
} }
impl 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 { if is_no_color {
Self { Self {
reset: "", reset: "",
@ -36,46 +38,68 @@ impl Colors {
} }
} }
pub static COLORS: LazyLock<Colors> = LazyLock::new(|| { use core::sync::atomic::{AtomicBool, Ordering};
// Only presence matters; value is irrelevant per the NO_COLOR spec
let is_no_color = std::env::var_os("NO_COLOR").is_some(); // Check if NO_COLOR is set (only once, lazily)
Colors::new(is_no_color) // 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] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn print_dots() -> String { pub fn print_dots() -> String {
// Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color
const GLYPH: &str = ""; const GLYPH: &str = "";
let capacity = COLORS.blue.len()
+ COLORS.cyan.len() let colors = if is_no_color() {
+ COLORS.green.len() Colors::new(true)
+ COLORS.yellow.len() } else {
+ COLORS.red.len() Colors::new(false)
+ COLORS.magenta.len() };
+ COLORS.reset.len()
// 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; + (GLYPH.len() + 2) * 6;
let mut result = String::with_capacity(capacity); let mut result = String::with_capacity(capacity);
result.push_str(COLORS.blue); result.push_str(colors.blue);
result.push_str(GLYPH); result.push_str(GLYPH);
result.push_str(" "); result.push_str(" ");
result.push_str(COLORS.cyan); result.push_str(colors.cyan);
result.push_str(GLYPH); result.push_str(GLYPH);
result.push_str(" "); result.push_str(" ");
result.push_str(COLORS.green); result.push_str(colors.green);
result.push_str(GLYPH); result.push_str(GLYPH);
result.push_str(" "); result.push_str(" ");
result.push_str(COLORS.yellow); result.push_str(colors.yellow);
result.push_str(GLYPH); result.push_str(GLYPH);
result.push_str(" "); result.push_str(" ");
result.push_str(COLORS.red); result.push_str(colors.red);
result.push_str(GLYPH); result.push_str(GLYPH);
result.push_str(" "); result.push_str(" ");
result.push_str(COLORS.magenta); result.push_str(colors.magenta);
result.push_str(GLYPH); result.push_str(GLYPH);
result.push_str(" "); result.push_str(" ");
result.push_str(COLORS.reset); result.push_str(colors.reset);
result result
} }

View file

@ -1,18 +1,15 @@
use std::{ffi::OsStr, fmt::Write}; use alloc::string::String;
use crate::getenv_str;
#[must_use] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_desktop_info() -> String { pub fn get_desktop_info() -> String {
let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP").unwrap_or("Unknown");
let session_os = std::env::var_os("XDG_SESSION_TYPE"); 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 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() { let backend_str = if session_raw.is_empty() {
"Unknown" "Unknown"
} else { } else {
@ -27,9 +24,15 @@ pub fn get_desktop_info() -> String {
result.push_str(" ("); result.push_str(" (");
// Capitalize first character of backend // Capitalize first character of backend
if let Some(first_char) = backend_str.chars().next() { if let Some(first_byte) = backend_str.as_bytes().first() {
let _ = write!(result, "{}", first_char.to_ascii_uppercase()); // Convert first byte to uppercase if it's ASCII lowercase
result.push_str(&backend_str[first_char.len_utf8()..]); 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(')'); result.push(')');

View file

@ -1,13 +1,17 @@
#![no_std]
extern crate alloc;
pub mod colors; pub mod colors;
pub mod desktop; pub mod desktop;
pub mod release; pub mod release;
pub mod system; pub mod system;
pub mod uptime; pub mod uptime;
use std::{ use alloc::string::String;
use core::{
ffi::CStr, ffi::CStr,
io::{self, Cursor, Write},
mem::MaybeUninit, mem::MaybeUninit,
sync::atomic::{AtomicPtr, Ordering},
}; };
pub use microfetch_asm as syscall; pub use microfetch_asm as syscall;
@ -25,6 +29,212 @@ pub use microfetch_asm::{
sys_write, 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<T> {
ptr: AtomicPtr<T>,
}
impl<T> Default for OnceLock<T> {
fn default() -> Self {
Self::new()
}
}
impl<T> OnceLock<T> {
#[must_use]
pub const fn new() -> Self {
Self {
ptr: AtomicPtr::new(core::ptr::null_mut()),
}
}
pub fn get_or_init<F>(&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<T> Drop for OnceLock<T> {
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);
}
}
}
}
// 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());
/// Initialize the environment pointer. Must be called before any `getenv()`
/// calls. This is called from `main()` with the calculated `envp`.
///
/// # Safety
///
/// 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]> {
let envp = get_envp();
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 /// Wrapper for `utsname` with safe accessor methods
pub struct UtsName(UtsNameBuf); pub struct UtsName(UtsNameBuf);
@ -34,11 +244,12 @@ impl UtsName {
/// # Errors /// # Errors
/// ///
/// Returns an error if the `uname` syscall fails /// Returns an error if the `uname` syscall fails
pub fn uname() -> Result<Self, std::io::Error> { pub fn uname() -> Result<Self, Error> {
let mut uts = MaybeUninit::uninit(); let mut uts = MaybeUninit::uninit();
if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { 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() })) Ok(Self(unsafe { uts.assume_init() }))
} }
@ -78,10 +289,51 @@ struct Fields {
colors: String, 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)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
fn print_system_info( fn print_system_info(fields: &Fields) -> Result<(), Error> {
fields: &Fields,
) -> Result<(), Box<dyn std::error::Error>> {
let Fields { let Fields {
user_info, user_info,
os_name, os_name,
@ -94,55 +346,189 @@ fn print_system_info(
colors, colors,
} = fields; } = fields;
let cyan = colors::COLORS.cyan; let no_color = colors::is_no_color();
let blue = colors::COLORS.blue; let c = colors::Colors::new(no_color);
let reset = colors::COLORS.reset;
let mut buf = [0u8; 2048]; let mut buf = [0u8; 2048];
let mut cursor = Cursor::new(&mut buf[..]); let mut w = StackWriter::new(&mut buf);
write!( if CUSTOM_LOGO.is_empty() {
cursor, // Default two-tone NixOS logo rendered as a single write! pass.
" core::fmt::write(
{blue} {cyan} {user_info} ~{reset} &mut w,
{blue} {cyan} {blue} {cyan} {blue}System{reset} {os_name} format_args!(
{blue} {cyan} {blue} {cyan} {blue}Kernel{reset} {kernel_version} "\n {b} ▟█▖ {cy}▝█▙ ▗█▛ {user_info} ~{rs}\n {b} \
{cyan} {cyan}{blue} {cyan} {blue}Shell{reset} {shell} {cy} {b} {cy}\u{F313} {b}System{rs} \
{cyan} {blue} {cyan} {blue}Uptime{reset} {uptime} {os_name}\n {b} {cy} {b} {cy}\u{E712} \
{cyan} {blue} {blue} {cyan} {blue}Desktop{reset} {desktop} {b}Kernel{rs} {kernel_version}\n {cy} \
{cyan} {blue}{cyan} {cyan}󰍛 {blue}Memory{reset} {memory_usage} {cy}{b} {cy}\u{E795} {b}Shell{rs} {shell}\n \
{cyan} {blue}{cyan} {cyan}󱥎 {blue}Storage (/){reset} {storage} {cy} {b} {cy}\u{F017} {b}Uptime{rs} \
{blue} {cyan} {cyan} {blue}Colors{reset} {colors}\n\n" {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(""));
let len = // Row format mirrors the default logo path exactly.
usize::try_from(cursor.position()).expect("cursor position fits usize"); let rows: [(&str, &str, &str, &str, &str); 9] = [
// Direct syscall to avoid stdout buffering allocation ("", "", user_info.as_str(), " ", " ~"),
let written = unsafe { sys_write(1, buf.as_ptr(), len) }; ("\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();
}
// Single syscall for the entire output.
let out = w.written();
let written = unsafe { sys_write(1, out.as_ptr(), out.len()) };
if written < 0 { 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 { #[allow(clippy::cast_sign_loss)]
return Err( if written as usize != out.len() {
io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout") return Err(Error::WriteError);
.into(),
);
} }
Ok(()) 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 /// 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 /// # Errors
/// ///
/// Returns an error if any system call fails /// 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)] #[cfg_attr(feature = "hotpath", hotpath::main)]
pub fn run() -> Result<(), Box<dyn std::error::Error>> { pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> {
if Some("--version") == std::env::args().nth(1).as_deref() { if unsafe { check_version_flag(argc, argv) } {
println!("Microfetch {}", env!("CARGO_PKG_VERSION")); print_version();
} else { return Ok(());
}
let utsname = UtsName::uname()?; let utsname = UtsName::uname()?;
let fields = Fields { let fields = Fields {
user_info: system::get_username_and_hostname(&utsname), user_info: system::get_username_and_hostname(&utsname),
@ -156,7 +542,6 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
colors: colors::print_dots(), colors: colors::print_dots(),
}; };
print_system_info(&fields)?; print_system_info(&fields)?;
}
Ok(()) Ok(())
} }

View file

@ -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] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[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 capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1;
let mut result = String::with_capacity(capacity); 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 result
} }
@ -23,7 +30,7 @@ pub fn get_system_info(utsname: &UtsName) -> String {
/// ///
/// Returns an error if `/etc/os-release` cannot be read. /// Returns an error if `/etc/os-release` cannot be read.
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_os_pretty_name() -> Result<String, io::Error> { pub fn get_os_pretty_name() -> Result<String, Error> {
// Fast byte-level scanning for PRETTY_NAME= // Fast byte-level scanning for PRETTY_NAME=
const PREFIX: &[u8] = b"PRETTY_NAME="; const PREFIX: &[u8] = b"PRETTY_NAME=";
@ -31,7 +38,7 @@ pub fn get_os_pretty_name() -> Result<String, io::Error> {
// Use fast syscall-based file reading // Use fast syscall-based file reading
let bytes_read = read_file_fast("/etc/os-release", &mut buffer) 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 content = &buffer[..bytes_read];
let mut offset = 0; let mut offset = 0;
@ -66,5 +73,5 @@ pub fn get_os_pretty_name() -> Result<String, io::Error> {
offset += line_end + 1; offset += line_end + 1;
} }
Ok("Unknown".to_owned()) Ok(String::from("Unknown"))
} }

View file

@ -1,37 +1,39 @@
use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit}; use alloc::string::String;
use core::mem::MaybeUninit;
use crate::{ use crate::{
Error,
UtsName, UtsName,
colors::COLORS, colors::Colors,
syscall::{StatfsBuf, read_file_fast, sys_statfs}, syscall::{StatfsBuf, read_file_fast, sys_statfs},
}; };
#[must_use] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_username_and_hostname(utsname: &UtsName) -> String { pub fn get_username_and_hostname(utsname: &UtsName) -> String {
let username_os = std::env::var_os("USER"); let username = crate::getenv_str("USER").unwrap_or("unknown_user");
let username = username_os
.as_deref()
.and_then(OsStr::to_str)
.unwrap_or("unknown_user");
let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); 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() + username.len()
+ COLORS.red.len() + colors.red.len()
+ 1 + 1
+ COLORS.green.len() + colors.green.len()
+ hostname.len() + hostname.len()
+ COLORS.reset.len(); + colors.reset.len();
let mut result = String::with_capacity(capacity); let mut result = String::with_capacity(capacity);
result.push_str(COLORS.yellow); result.push_str(colors.yellow);
result.push_str(username); result.push_str(username);
result.push_str(COLORS.red); result.push_str(colors.red);
result.push('@'); result.push('@');
result.push_str(COLORS.green); result.push_str(colors.green);
result.push_str(hostname); result.push_str(hostname);
result.push_str(COLORS.reset); result.push_str(colors.reset);
result result
} }
@ -39,13 +41,12 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String {
#[must_use] #[must_use]
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_shell() -> String { pub fn get_shell() -> String {
let shell_os = std::env::var_os("SHELL"); let shell = crate::getenv_str("SHELL").unwrap_or("");
let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or("");
let start = shell.rfind('/').map_or(0, |i| i + 1); let start = shell.rfind('/').map_or(0, |i| i + 1);
if shell.is_empty() { if shell.is_empty() {
"unknown_shell".into() String::from("unknown_shell")
} else { } 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. /// Returns an error if the filesystem information cannot be retrieved.
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
pub fn get_root_disk_usage() -> Result<String, io::Error> { pub fn get_root_disk_usage() -> Result<String, Error> {
let mut vfs = MaybeUninit::<StatfsBuf>::uninit(); let mut vfs = MaybeUninit::<StatfsBuf>::uninit();
let path = b"/\0"; let path = b"/\0";
if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 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() }; let vfs = unsafe { vfs.assume_init() };
@ -77,18 +78,96 @@ pub fn get_root_disk_usage() -> Result<String, io::Error> {
let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0);
let usage = (used_size / total_size) * 100.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); let mut result = String::with_capacity(64);
write!(
result, // Manual float formatting
"{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", write_float(&mut result, used_size, 2);
cyan = COLORS.cyan, result.push_str(" GiB / ");
reset = COLORS.reset, write_float(&mut result, total_size, 2);
) result.push_str(" GiB (");
.unwrap(); result.push_str(colors.cyan);
write_float(&mut result, usage, 0);
result.push('%');
result.push_str(colors.reset);
result.push(')');
Ok(result) 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 /// Fast integer parsing without stdlib overhead
#[inline] #[inline]
fn parse_u64_fast(s: &[u8]) -> u64 { 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. /// Returns an error if `/proc/meminfo` cannot be read.
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_memory_usage() -> Result<String, io::Error> { pub fn get_memory_usage() -> Result<String, Error> {
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[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 total_memory_kb = 0u64;
let mut available_memory_kb = 0u64; let mut available_memory_kb = 0u64;
let mut buffer = [0u8; 1024]; let mut buffer = [0u8; 1024];
// Use fast syscall-based file reading // Use fast syscall-based file reading
let bytes_read = read_file_fast("/proc/meminfo", &mut buffer) 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]; let meminfo = &buffer[..bytes_read];
// Fast scanning for MemTotal and MemAvailable // Fast scanning for MemTotal and MemAvailable
@ -168,17 +247,22 @@ pub fn get_memory_usage() -> Result<String, io::Error> {
let (used_memory, total_memory) = parse_memory_info()?; let (used_memory, total_memory) = parse_memory_info()?;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[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); let mut result = String::with_capacity(64);
write!(
result, write_float(&mut result, used_memory, 2);
"{used_memory:.2} GiB / {total_memory:.2} GiB \ result.push_str(" GiB / ");
({cyan}{percentage_used}%{reset})", write_float(&mut result, total_memory, 2);
cyan = COLORS.cyan, result.push_str(" GiB (");
reset = COLORS.reset, result.push_str(colors.cyan);
) write_u64(&mut result, percentage_used);
.unwrap(); result.push('%');
result.push_str(colors.reset);
result.push(')');
Ok(result) Ok(result)
} }

View file

@ -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. /// Faster integer to string conversion without the formatting overhead.
#[inline] #[inline]
@ -16,7 +17,8 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str {
n /= 10; 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. /// 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. /// Returns an error if the system uptime cannot be retrieved.
#[cfg_attr(feature = "hotpath", hotpath::measure)] #[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn get_current() -> Result<String, io::Error> { pub fn get_current() -> Result<String, Error> {
let uptime_seconds = { let uptime_seconds = {
let mut info = MaybeUninit::uninit(); let mut info = MaybeUninit::uninit();
if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 { 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)] #[allow(clippy::cast_sign_loss)]
unsafe { unsafe {

View file

@ -19,12 +19,12 @@
[fastfetch]: https://github.com/fastfetch-cli/fastfetch [fastfetch]: https://github.com/fastfetch-cli/fastfetch
Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust Stupidly small and simple, laughably fast, and pretty fetch tool. Written
for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and (mostly) in Rust for speed and ease of maintainability. Runs in a _fraction of a
displays _most_ of the nonsense you'd see posted on r/unixporn or other internet millisecond_ and displays _most_ of the nonsense you'd see posted on r/unixporn
communities. Aims to replace [fastfetch] on my personal system, but or other internet communities. Aims to replace [fastfetch] on my personal
[probably not yours](#customizing). Though, you are more than welcome to use it system, but [probably not yours](#customizing). Though, you are more than
on your system: it is pretty _[fast](#benchmarks)_... welcome to use it on your system: it is pretty _[fast](#benchmarks)_...
<p align="center"> <p align="center">
<br/> <br/>
@ -40,10 +40,10 @@ on your system: it is pretty _[fast](#benchmarks)_...
- Fast - Fast
- Really fast - Really fast
- Minimal dependencies - No dependencies (not even libc!)
- Tiny binary (~370kb [^1]) - Tiny binary (~24kb)
- Actually really fast - 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: - Reliable detection of following info:
- Hostname/Username - Hostname/Username
- Kernel - Kernel
@ -57,12 +57,9 @@ on your system: it is pretty _[fast](#benchmarks)_...
- Shell Colors - Shell Colors
- Did I mention fast? - Did I mention fast?
- Respects [`NO_COLOR` spec](https://no-color.org/) - 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, [^1]: I don't know how else to describe the (unhealthy) amount of handwritten
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
assembly that was written in order to make Microfetch faster. assembly that was written in order to make Microfetch faster.
## Motivation ## Motivation
@ -70,12 +67,12 @@ on your system: it is pretty _[fast](#benchmarks)_...
[Rube-Goldmark Machine]: https://en.wikipedia.org/wiki/Rube_Goldberg_machine [Rube-Goldmark Machine]: https://en.wikipedia.org/wiki/Rube_Goldberg_machine
Fastfetch, as its name _probably_ already hinted, is a very fast fetch tool 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 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 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 I don't even display most of the fields that it has. Sure, the configurability
is nice and I can configure the defaults that I do not like but how often do I is nice and _I could_ configure the defaults that I do not like... but how often
really do that? do I really do that?
Since I already enjoy programming challenges, and don't use a fetch program 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 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 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 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 decided to use it on my system. You also might be interested if you like the
defaults and like speed. 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.
Ultimately, it's a small, opinionated binary with a nice size that doesn't I cannot re-iterate it enough, Microfetch is _annoyingly fast_. It, however,
bother me, and incredible speed. Customization? No thank you. I cannot does not solve a real technical problem. The "problem" Microfetch "solves" is
re-iterate it enough, Microfetch is _annoyingly fast_. It does not, however, entirely self-imposed. I want a fast, _almost_ zero-cost command invocation and
solve a technical problem. The "problem" Microfetch solves is entirely for it to not take that much space on my system. Thanks to the nature of Rust,
self-imposed. On the matter of _size_, the project is written in Rust, which Microfetch is _fast_. Rust does, or well, _did_ mean "bloated" dependency trees
comes at the cost of "bloated" dependency trees and the increased build times, and slightly increased build times, though, as of 0.5.0 Microfetch has
but we make an extended effort to keep the dependencies minimal and build times (voluntarily) dropped both `std` and `libc`. You can go check the numbers for
manageable. The latter is also very easily mitigated with Nix's binary cache the speed impact (hint: it's much better) but we also have little to no concerns
systems. Since Microfetch is already in Nixpkgs, you are recommended to use it left about build times and the binary size. Build times are also _very easily_
to utilize the binary cache properly. The usage of Rust _is_ nice, however, mitigated with Nix's binary cache systems, and since Microfetch is already in
since it provides us with incredible tooling and a very powerful language that Nixpkgs you are strongly encouraged to use `pkgs.microfetch` over the flake. The
allows for Microfetch to be as fast as possible. ~~Sure C could've been used usage of Rust _is_ quite nice, however, since it provides us with incredible
here as well, but do you think I hate myself?~~ Microfetch now features tooling and a very powerful language that allows for Microfetch to be as fast as
handwritten assembly to unsafely optimize some areas. In hindsight you all possible.
should have seen this coming. Is it faster? Yes. Should you use this? If you
want to. 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] Also see: [Rube-Goldmark Machine]
@ -232,20 +234,11 @@ interested in Microfetch tailored to their distributions.
## Customizing ## Customizing
You can't*. 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.
### Really? ### Really?
[main module]: ./src/main.rs [main module]: ./microfetch/src/main.rs
[discussions tab]: https://github.com/NotAShelf/microfetch/discussions [discussions tab]: https://github.com/NotAShelf/microfetch/discussions
To be fair, you _can_ customize Microfetch by, well, patching it. It is To be fair, you _can_ customize Microfetch by, well, patching it. It is
@ -263,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 derivations with people. Feel free to use the [discussions tab] to
share your own variants of Microfetch! 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 ## Contributing
I will, mostly, reject feature additions. This is not to say you should avoid I will, mostly, reject feature additions. This is not to say you should avoid
@ -312,4 +335,5 @@ Microfetch. I might have missed your name here, but you have my thanks.
## License ## 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.

View file

@ -12,6 +12,8 @@ publish = false
[dependencies] [dependencies]
hotpath = { optional = true, version = "0.14.0" } hotpath = { optional = true, version = "0.14.0" }
microfetch-alloc.workspace = true
microfetch-asm.workspace = true
microfetch-lib.workspace = true microfetch-lib.workspace = true
[features] [features]

17
microfetch/build.rs Normal file
View file

@ -0,0 +1,17 @@
fn main() {
// These flags only apply to the microfetch binary, not to proc-macro crates
// or other host-compiled artifacts.
// No C runtime, we provide _start ourselves
println!("cargo:rustc-link-arg-bin=microfetch=-nostartfiles");
// Fully static, no dynamic linker, no .interp/.dynsym/.dynamic overhead
println!("cargo:rustc-link-arg-bin=microfetch=-static");
// Remove unreferenced input sections
println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--gc-sections");
// Strip all symbol table entries
println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--strip-all");
// Omit the .note.gnu.build-id section
println!("cargo:rustc-link-arg-bin=microfetch=-Wl,--build-id=none");
// Disable RELRO (removes relro_padding)
println!("cargo:rustc-link-arg-bin=microfetch=-Wl,-z,norelro");
}

View file

@ -1,3 +1,112 @@
fn main() -> Result<(), Box<dyn std::error::Error>> { #![no_std]
microfetch_lib::run() #![no_main]
extern crate alloc;
use core::{arch::naked_asm, panic::PanicInfo};
use microfetch_alloc::BumpAllocator;
// Re-export libc replacement functions from asm crate
pub use microfetch_asm::{memcpy, memset, strlen};
use microfetch_asm::{entry_rust, sys_exit, sys_write};
#[cfg(target_arch = "x86_64")]
#[unsafe(no_mangle)]
#[unsafe(naked)]
unsafe extern "C" fn _start() {
naked_asm!(
"mov rdi, rsp",
"and rsp, -16",
"call {entry_rust}",
"mov rdi, rax",
"mov rax, 60",
"syscall",
entry_rust = sym entry_rust,
);
}
#[cfg(target_arch = "aarch64")]
#[unsafe(no_mangle)]
#[unsafe(naked)]
unsafe extern "C" fn _start() {
naked_asm!(
"mov x0, sp",
"mov x9, sp",
"and x9, x9, #-16",
"mov sp, x9",
"bl {entry_rust}",
"mov x0, x0",
"mov x8, 93",
"svc #0",
entry_rust = sym entry_rust,
);
}
#[cfg(target_arch = "riscv64")]
#[unsafe(no_mangle)]
#[unsafe(naked)]
unsafe extern "C" fn _start() {
naked_asm!(
"mv a0, sp",
"andi sp, sp, -16",
"call {entry_rust}",
"mv a0, a0",
"li a7, 93",
"ecall",
entry_rust = sym entry_rust,
);
}
// Global allocator
#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator::new();
/// 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 {
// 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 {
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) -> ! {
const PANIC_MSG: &[u8] = b"panic\n";
unsafe {
let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len());
sys_exit(1)
}
}
// Stubs for Rust exception handling
#[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) }
} }

View file

@ -1,22 +1,13 @@
{ {
lib, lib,
stdenv,
stdenvAdapters,
rustPlatform, rustPlatform,
llvm, llvm,
useMold ? stdenv.isLinux,
}: let }: let
toml = (lib.importTOML ../Cargo.toml).package; pname = "microfetch";
pname = toml.name; toml = (lib.importTOML ../Cargo.toml).workspace.package;
inherit (toml) version; inherit (toml) version;
# Select stdenv based on useMold flag
stdenv =
if useMold
then stdenvAdapters.useMoldLinker llvm.stdenv
else llvm.stdenv;
in in
rustPlatform.buildRustPackage.override {inherit stdenv;} { rustPlatform.buildRustPackage.override {inherit (llvm) stdenv;} {
inherit pname version; inherit pname version;
src = let src = let
fs = lib.fileset; fs = lib.fileset;
@ -25,10 +16,12 @@ in
fs.toSource { fs.toSource {
root = s; root = s;
fileset = fs.unions [ fileset = fs.unions [
(fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) (s + /crates)
(s + /microfetch)
(s + /.cargo)
(s + /scripts/ld-wrapper)
(s + /Cargo.lock) (s + /Cargo.lock)
(s + /Cargo.toml) (s + /Cargo.toml)
(s + /benches)
]; ];
}; };
@ -37,12 +30,6 @@ in
buildNoDefaultFeatures = true; buildNoDefaultFeatures = true;
doCheck = false; 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 = { meta = {
description = "Microscopic fetch script in Rust, for NixOS systems"; description = "Microscopic fetch script in Rust, for NixOS systems";
homepage = "https://github.com/NotAShelf/microfetch"; homepage = "https://github.com/NotAShelf/microfetch";

38
scripts/ld-wrapper Executable file
View file

@ -0,0 +1,38 @@
#!/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 and detect static linking
IS_STATIC=0
OUTPUT=""
prev=""
for arg in "$@"; do
case "$arg" in
-static) IS_STATIC=1 ;;
esac
if [ "$prev" = "-o" ]; then
OUTPUT="$arg"
fi
prev="$arg"
done
# Invoke mold via the cc driver, forward all original arguments
cc -fuse-ld=mold "$@"
# Only strip sections from fully static binaries.
# Dynamic executables (i.e. build scripts, proc-macros) need .dynstr at runtime.
if [ "$IS_STATIC" = 1 ] && [ -n "$OUTPUT" ] && [ -f "$OUTPUT" ]; then
objcopy \
--remove-section=.eh_frame \
--remove-section=.eh_frame_hdr \
--remove-section=.dynstr \
--remove-section=.comment \
"$OUTPUT"
fi