diff --git a/.cargo/config.toml b/.cargo/config.toml index 2623c5a..c7125dc 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,16 +1,3 @@ -# 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: -# -# -# 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. +# https://github.com/rui314/mold?tab=readme-ov-file#how-to-use [target.'cfg(target_os = "linux")'] -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", -] +rustflags = [ "-C", "link-arg=-fuse-ld=mold" ] diff --git a/.github/workflows/hotpath-profile.yml b/.github/workflows/hotpath-profile.yml index 5ebf1a9..98541e1 100644 --- a/.github/workflows/hotpath-profile.yml +++ b/.github/workflows/hotpath-profile.yml @@ -16,11 +16,6 @@ jobs: fetch-depth: 0 - 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 run: mkdir -p /tmp/metrics diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 99668ae..deb45e3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,18 +29,16 @@ 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 - - name: "Setup cross-compilation toolchain" - uses: taiki-e/setup-cross-toolchain-action@v1 - with: - target: ${{ matrix.target }} + - name: "Install cross" + run: cargo install cross --git https://github.com/cross-rs/cross - name: "Build" - run: cargo build --verbose + run: cross build --verbose --target ${{ matrix.target }} - name: "Run tests" - run: cargo test --workspace --exclude microfetch --verbose + if: matrix.target == 'x86_64-unknown-linux-gnu' + run: cross test --verbose --target ${{ matrix.target }} diff --git a/Cargo.lock b/Cargo.lock index 465a747..fa82609 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6" dependencies = [ "rustversion", ] @@ -123,9 +123,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -448,9 +448,9 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hotpath" -version = "0.14.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d9982fcb4356a5260502f0e646411ec1feb2962cc98c230777104a8d1c5ed3" +checksum = "2fde50be006a0fe95cc2fd6d25d884aa6932218e4055d7df2fa0d95c386acf8d" dependencies = [ "arc-swap", "cfg-if", @@ -475,9 +475,9 @@ dependencies = [ [[package]] name = "hotpath-macros" -version = "0.14.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4398eddb78466298f1ddcc739abbbd8a942e541d1972c7590bd83de364e626e0" +checksum = "dd884cee056e269e41e1127549458e1c4e309f31897ebbc1416982a74d40a5b5" dependencies = [ "proc-macro2", "quote", @@ -530,9 +530,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -546,15 +546,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ "libc", ] @@ -579,25 +579,19 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "microfetch" -version = "1.0.0" +version = "0.4.13" dependencies = [ "hotpath", - "microfetch-alloc", - "microfetch-asm", "microfetch-lib", ] -[[package]] -name = "microfetch-alloc" -version = "1.0.0" - [[package]] name = "microfetch-asm" -version = "1.0.0" +version = "0.4.13" [[package]] name = "microfetch-bench" -version = "1.0.0" +version = "0.4.13" dependencies = [ "criterion", "criterion-cycles-per-byte", @@ -606,7 +600,7 @@ dependencies = [ [[package]] name = "microfetch-lib" -version = "1.0.0" +version = "0.4.13" dependencies = [ "hotpath", "microfetch-asm", @@ -940,9 +934,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.51.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "pin-project-lite", ] @@ -983,9 +977,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -996,9 +990,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1006,9 +1000,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1019,18 +1013,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -1084,18 +1078,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 661cb8f..f8bd2a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,19 +7,17 @@ authors = [ "NotAShelf " ] edition = "2024" license = "GPL-3.0" rust-version = "1.92.0" -version = "1.0.0" +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" [profile.release] codegen-units = 1 diff --git a/crates/alloc/Cargo.toml b/crates/alloc/Cargo.toml deleted file mode 100644 index 47cd1d2..0000000 --- a/crates/alloc/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[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 deleted file mode 100644 index 06e3f07..0000000 --- a/crates/alloc/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! 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 19abc0e..63014d4 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -19,137 +19,6 @@ 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 -} - -/// 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 = 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 /// /// # Returns @@ -638,7 +507,6 @@ 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); } @@ -730,41 +598,3 @@ 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 9f012f9..0fca89e 100644 --- a/crates/lib/src/colors.rs +++ b/crates/lib/src/colors.rs @@ -1,6 +1,5 @@ -use alloc::string::String; +use std::sync::LazyLock; -/// Color codes for terminal output pub struct Colors { pub reset: &'static str, pub blue: &'static str, @@ -12,8 +11,7 @@ pub struct Colors { } impl Colors { - #[must_use] - pub const fn new(is_no_color: bool) -> Self { + const fn new(is_no_color: bool) -> Self { if is_no_color { Self { reset: "", @@ -38,68 +36,46 @@ impl Colors { } } -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 -} +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) +}); #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn print_dots() -> String { - const GLYPH: &str = ""; - - 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() + 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() + (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 b9aa362..ea863b4 100644 --- a/crates/lib/src/desktop.rs +++ b/crates/lib/src/desktop.rs @@ -1,15 +1,18 @@ -use alloc::string::String; - -use crate::getenv_str; +use std::{ffi::OsStr, fmt::Write}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { - let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP").unwrap_or("Unknown"); - let session_raw = getenv_str("XDG_SESSION_TYPE").unwrap_or(""); + let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); + let session_os = std::env::var_os("XDG_SESSION_TYPE"); + 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 { @@ -24,15 +27,9 @@ pub fn get_desktop_info() -> String { result.push_str(" ("); // Capitalize first character of backend - 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..]); + 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()..]); } result.push(')'); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 1d89f8d..28147f7 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -1,17 +1,13 @@ -#![no_std] -extern crate alloc; - pub mod colors; pub mod desktop; pub mod release; pub mod system; pub mod uptime; -use alloc::string::String; -use core::{ +use std::{ ffi::CStr, + io::{self, Cursor, Write}, mem::MaybeUninit, - sync::atomic::{AtomicPtr, Ordering}, }; pub use microfetch_asm as syscall; @@ -29,212 +25,6 @@ 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); - } - } - } -} - -// 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 pub struct UtsName(UtsNameBuf); @@ -244,12 +34,11 @@ 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(Error::last_os_error()); + return Err(std::io::Error::last_os_error()); } - Ok(Self(unsafe { uts.assume_init() })) } @@ -289,51 +78,10 @@ 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> { +fn print_system_info( + fields: &Fields, +) -> Result<(), Box> { let Fields { user_info, os_name, @@ -346,202 +94,69 @@ fn print_system_info(fields: &Fields) -> Result<(), Error> { colors, } = fields; - let no_color = colors::is_no_color(); - let c = colors::Colors::new(no_color); + let cyan = colors::COLORS.cyan; + let blue = colors::COLORS.blue; + let reset = colors::COLORS.reset; let mut buf = [0u8; 2048]; - let mut w = StackWriter::new(&mut buf); + let mut cursor = Cursor::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("")); + 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" + )?; - // 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(); - } - - // Single syscall for the entire output. - let out = w.written(); - let written = unsafe { sys_write(1, out.as_ptr(), out.len()) }; + let len = + usize::try_from(cursor.position()).expect("cursor position fits usize"); + // Direct syscall to avoid stdout buffering allocation + let written = unsafe { sys_write(1, buf.as_ptr(), len) }; if written < 0 { - #[allow(clippy::cast_possible_truncation)] - return Err(Error::OsError(written as i32)); + return Err(io::Error::last_os_error().into()); } - - #[allow(clippy::cast_sign_loss)] - if written as usize != out.len() { - return Err(Error::WriteError); + #[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(), + ); } - 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. -/// -/// # Arguments -/// -/// * `argc` - Argument count from main -/// * `argv` - Argument vector from main +/// or by other consumers of the library /// /// # 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 unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { - if unsafe { check_version_flag(argc, argv) } { - print_version(); - return Ok(()); +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)?; } - 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 299c1c9..41a55c3 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -1,6 +1,6 @@ -use alloc::string::String; +use std::{fmt::Write as _, io}; -use crate::{Error, UtsName, syscall::read_file_fast}; +use crate::{UtsName, syscall::read_file_fast}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -13,14 +13,7 @@ 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); - // 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(')'); - + write!(result, "{sysname} {release} ({machine})").unwrap(); result } @@ -30,7 +23,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="; @@ -38,7 +31,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(Error::from_raw_os_error)?; + .map_err(|e| io::Error::from_raw_os_error(-e))?; let content = &buffer[..bytes_read]; let mut offset = 0; @@ -73,5 +66,5 @@ pub fn get_os_pretty_name() -> Result { offset += line_end + 1; } - Ok(String::from("Unknown")) + Ok("Unknown".to_owned()) } diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index b902dac..53ab38a 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -1,39 +1,37 @@ -use alloc::string::String; -use core::mem::MaybeUninit; +use std::{ffi::OsStr, fmt::Write as _, io, 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 = crate::getenv_str("USER").unwrap_or("unknown_user"); + let username_os = std::env::var_os("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"); - // 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() + 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 } @@ -41,12 +39,13 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - let shell = crate::getenv_str("SHELL").unwrap_or(""); + let shell_os = std::env::var_os("SHELL"); + let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); let start = shell.rfind('/').map_or(0, |i| i + 1); if shell.is_empty() { - String::from("unknown_shell") + "unknown_shell".into() } else { - String::from(&shell[start..]) + shell[start..].into() } } @@ -57,12 +56,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(Error::last_os_error()); + return Err(io::Error::last_os_error()); } let vfs = unsafe { vfs.assume_init() }; @@ -78,96 +77,18 @@ 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); - - // 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(')'); + write!( + result, + "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", + cyan = COLORS.cyan, + reset = COLORS.reset, + ) + .unwrap(); 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 { @@ -188,16 +109,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), Error> { + fn parse_memory_info() -> Result<(f64, f64), io::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(Error::from_raw_os_error)?; + .map_err(|e| io::Error::from_raw_os_error(-e))?; let meminfo = &buffer[..bytes_read]; // Fast scanning for MemTotal and MemAvailable @@ -247,22 +168,17 @@ 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 = 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 percentage_used = (used_memory / total_memory * 100.0).round() as u64; let mut result = String::with_capacity(64); - - 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(')'); + write!( + result, + "{used_memory:.2} GiB / {total_memory:.2} GiB \ + ({cyan}{percentage_used}%{reset})", + cyan = COLORS.cyan, + reset = COLORS.reset, + ) + .unwrap(); Ok(result) } diff --git a/crates/lib/src/uptime.rs b/crates/lib/src/uptime.rs index 4ed2200..b529f53 100644 --- a/crates/lib/src/uptime.rs +++ b/crates/lib/src/uptime.rs @@ -1,7 +1,6 @@ -use alloc::string::String; -use core::mem::MaybeUninit; +use std::{io, mem::MaybeUninit}; -use crate::{Error, syscall::sys_sysinfo}; +use crate::syscall::sys_sysinfo; /// Faster integer to string conversion without the formatting overhead. #[inline] @@ -17,8 +16,7 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { n /= 10; } - // SAFETY: We only wrote ASCII digits - unsafe { core::str::from_utf8_unchecked(&buf[i..]) } + unsafe { std::str::from_utf8_unchecked(&buf[i..]) } } /// Gets the current system uptime. @@ -27,11 +25,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(Error::last_os_error()); + return Err(io::Error::last_os_error()); } #[allow(clippy::cast_sign_loss)] unsafe { diff --git a/docs/README.md b/docs/README.md index c62aecf..2e8830b 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 -(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)_... +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)_...


@@ -40,10 +40,10 @@ welcome to use it on your system: it is pretty _[fast](#benchmarks)_... - Fast - Really fast -- No dependencies (not even libc!) -- Tiny binary (~24kb) -- Actually _really_ fast -- Cool NixOS logo, with support for custom logo text +- Minimal dependencies +- Tiny binary (~370kb [^1]) +- Actually really fast +- Cool NixOS logo (other, inferior, distros are not supported) - Reliable detection of following info: - Hostname/Username - Kernel @@ -57,9 +57,12 @@ welcome to use it on your system: it is pretty _[fast](#benchmarks)_... - Shell Colors - Did I mention fast? - Respects [`NO_COLOR` spec](https://no-color.org/) -- Funny [^1] +- Funny [^2] -[^1]: I don't know how else to describe the (unhealthy) amount of handwritten +[^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 assembly that was written in order to make Microfetch faster. ## Motivation @@ -67,12 +70,12 @@ welcome to use it 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 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? +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? 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 @@ -81,29 +84,24 @@ 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, Microfetch a small, opinionated binary with -a nice size that doesn't bother me, and _incredible_ speed. Customization? No -thank you. +defaults and like speed. -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. +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. Also see: [Rube-Goldmark Machine] @@ -234,11 +232,20 @@ interested in Microfetch tailored to their distributions. ## 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? -[main module]: ../microfetch/src/main.rs +[main module]: ./src/main.rs [discussions tab]: https://github.com/NotAShelf/microfetch/discussions To be fair, you _can_ customize Microfetch by, well, patching it. It is @@ -256,36 +263,6 @@ 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 of some sort. - ## Contributing I will, mostly, reject feature additions. This is not to say you should avoid @@ -335,5 +312,4 @@ Microfetch. I might have missed your name here, but you have my thanks. ## License -This project is released under GNU Public Licence version 3 **only**. See the -[license](../LICENSE) for more details. +Microfetch is licensed under [GPL3](LICENSE). See the license file for details. diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index 43d4f9c..4680aa8 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,10 +11,8 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] -hotpath = { optional = true, version = "0.14.0" } -microfetch-alloc.workspace = true -microfetch-asm.workspace = true -microfetch-lib.workspace = true +hotpath = { optional = true, version = "0.14.0" } +microfetch-lib.workspace = true [features] hotpath = [ "dep:hotpath" ] diff --git a/microfetch/build.rs b/microfetch/build.rs deleted file mode 100644 index 3f43fef..0000000 --- a/microfetch/build.rs +++ /dev/null @@ -1,17 +0,0 @@ -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"); -} diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index fcefcc8..8f22041 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -1,112 +1,3 @@ -#![no_std] -#![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) } +fn main() -> Result<(), Box> { + microfetch_lib::run() } diff --git a/nix/package.nix b/nix/package.nix index 2658d73..de8bdf8 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,13 +1,22 @@ { lib, + stdenv, + stdenvAdapters, rustPlatform, llvm, + useMold ? stdenv.isLinux, }: let - pname = "microfetch"; - toml = (lib.importTOML ../Cargo.toml).workspace.package; + 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 (llvm) stdenv;} { + rustPlatform.buildRustPackage.override {inherit stdenv;} { inherit pname version; src = let fs = lib.fileset; @@ -16,12 +25,10 @@ in fs.toSource { root = s; fileset = fs.unions [ - (s + /crates) - (s + /microfetch) - (s + /.cargo) - (s + /scripts/ld-wrapper) + (fs.fileFilter (file: builtins.any file.hasExt ["rs"]) (s + /src)) (s + /Cargo.lock) (s + /Cargo.toml) + (s + /benches) ]; }; @@ -30,6 +37,12 @@ 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"; diff --git a/scripts/ld-wrapper b/scripts/ld-wrapper deleted file mode 100755 index a0337bb..0000000 --- a/scripts/ld-wrapper +++ /dev/null @@ -1,38 +0,0 @@ -#!/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, such 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