mirror of
https://github.com/NotAShelf/microfetch.git
synced 2026-04-12 12:57:41 +00:00
Merge pull request #57 from NotAShelf/notashelf/push-pynzzylozqql
NO MORE STDS!!!!!
This commit is contained in:
commit
bf25c77b8f
20 changed files with 1219 additions and 221 deletions
|
|
@ -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")']
|
||||
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",
|
||||
]
|
||||
|
|
|
|||
5
.github/workflows/hotpath-profile.yml
vendored
5
.github/workflows/hotpath-profile.yml
vendored
|
|
@ -16,6 +16,11 @@ 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
|
||||
|
|
|
|||
12
.github/workflows/rust.yml
vendored
12
.github/workflows/rust.yml
vendored
|
|
@ -29,16 +29,18 @@ 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: "Install cross"
|
||||
run: cargo install cross --git https://github.com/cross-rs/cross
|
||||
- name: "Setup cross-compilation toolchain"
|
||||
uses: taiki-e/setup-cross-toolchain-action@v1
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
|
||||
- name: "Build"
|
||||
run: cross build --verbose --target ${{ matrix.target }}
|
||||
run: cargo build --verbose
|
||||
|
||||
- name: "Run tests"
|
||||
if: matrix.target == 'x86_64-unknown-linux-gnu'
|
||||
run: cross test --verbose --target ${{ matrix.target }}
|
||||
run: cargo test --workspace --exclude microfetch --verbose
|
||||
|
|
|
|||
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -582,9 +582,15 @@ name = "microfetch"
|
|||
version = "0.4.13"
|
||||
dependencies = [
|
||||
"hotpath",
|
||||
"microfetch-alloc",
|
||||
"microfetch-asm",
|
||||
"microfetch-lib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "microfetch-alloc"
|
||||
version = "0.4.13"
|
||||
|
||||
[[package]]
|
||||
name = "microfetch-asm"
|
||||
version = "0.4.13"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ rust-version = "1.92.0"
|
|||
version = "0.4.13"
|
||||
|
||||
[workspace.dependencies]
|
||||
microfetch-alloc = { path = "./crates/alloc" }
|
||||
microfetch-asm = { path = "./crates/asm" }
|
||||
microfetch-lib = { path = "./crates/lib" }
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ criterion-cycles-per-byte = "0.8.0"
|
|||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
panic = "abort"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
|
|
|
|||
12
crates/alloc/Cargo.toml
Normal file
12
crates/alloc/Cargo.toml
Normal 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
106
crates/alloc/src/lib.rs
Normal 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>;
|
||||
|
|
@ -19,6 +19,137 @@ 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<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
|
||||
///
|
||||
/// # Returns
|
||||
|
|
@ -507,6 +638,7 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> Result<usize, i32> {
|
|||
let _ = sys_close(fd);
|
||||
|
||||
if bytes_read < 0 {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
return Err(bytes_read as i32);
|
||||
}
|
||||
|
||||
|
|
@ -598,3 +730,41 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 {
|
|||
ret
|
||||
}
|
||||
}
|
||||
|
||||
/// Direct syscall to exit the process
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This syscall never returns. The process will terminate immediately.
|
||||
#[inline]
|
||||
pub unsafe fn sys_exit(code: i32) -> ! {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"syscall",
|
||||
in("rax") 60i64, // SYS_exit
|
||||
in("rdi") code,
|
||||
options(noreturn, nostack)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "aarch64")]
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"svc #0",
|
||||
in("x8") 93i64, // SYS_exit
|
||||
in("x0") code,
|
||||
options(noreturn, nostack)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "riscv64")]
|
||||
unsafe {
|
||||
core::arch::asm!(
|
||||
"ecall",
|
||||
in("a7") 93i64, // SYS_exit
|
||||
in("a0") code,
|
||||
options(noreturn, nostack)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::sync::LazyLock;
|
||||
use alloc::string::String;
|
||||
|
||||
/// Color codes for terminal output
|
||||
pub struct Colors {
|
||||
pub reset: &'static str,
|
||||
pub blue: &'static str,
|
||||
|
|
@ -11,7 +12,8 @@ pub struct Colors {
|
|||
}
|
||||
|
||||
impl Colors {
|
||||
const fn new(is_no_color: bool) -> Self {
|
||||
#[must_use]
|
||||
pub const fn new(is_no_color: bool) -> Self {
|
||||
if is_no_color {
|
||||
Self {
|
||||
reset: "",
|
||||
|
|
@ -36,46 +38,68 @@ impl Colors {
|
|||
}
|
||||
}
|
||||
|
||||
pub static COLORS: LazyLock<Colors> = LazyLock::new(|| {
|
||||
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
|
||||
let is_no_color = std::env::var_os("NO_COLOR").is_some();
|
||||
Colors::new(is_no_color)
|
||||
});
|
||||
static NO_COLOR_CHECKED: AtomicBool = AtomicBool::new(false);
|
||||
static NO_COLOR_SET: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Checks if `NO_COLOR` environment variable is set.
|
||||
pub(crate) fn is_no_color() -> bool {
|
||||
// Fast path: already checked
|
||||
if NO_COLOR_CHECKED.load(Ordering::Acquire) {
|
||||
return NO_COLOR_SET.load(Ordering::Relaxed);
|
||||
}
|
||||
|
||||
// Slow path: check environment
|
||||
let is_set = crate::env_exists("NO_COLOR");
|
||||
NO_COLOR_SET.store(is_set, Ordering::Relaxed);
|
||||
NO_COLOR_CHECKED.store(true, Ordering::Release);
|
||||
is_set
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn print_dots() -> String {
|
||||
// Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color
|
||||
const GLYPH: &str = "";
|
||||
let capacity = COLORS.blue.len()
|
||||
+ COLORS.cyan.len()
|
||||
+ COLORS.green.len()
|
||||
+ COLORS.yellow.len()
|
||||
+ COLORS.red.len()
|
||||
+ COLORS.magenta.len()
|
||||
+ COLORS.reset.len()
|
||||
|
||||
let colors = if is_no_color() {
|
||||
Colors::new(true)
|
||||
} else {
|
||||
Colors::new(false)
|
||||
};
|
||||
|
||||
// Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color
|
||||
let capacity = colors.blue.len()
|
||||
+ colors.cyan.len()
|
||||
+ colors.green.len()
|
||||
+ colors.yellow.len()
|
||||
+ colors.red.len()
|
||||
+ colors.magenta.len()
|
||||
+ colors.reset.len()
|
||||
+ (GLYPH.len() + 2) * 6;
|
||||
|
||||
let mut result = String::with_capacity(capacity);
|
||||
result.push_str(COLORS.blue);
|
||||
result.push_str(colors.blue);
|
||||
result.push_str(GLYPH);
|
||||
result.push_str(" ");
|
||||
result.push_str(COLORS.cyan);
|
||||
result.push_str(colors.cyan);
|
||||
result.push_str(GLYPH);
|
||||
result.push_str(" ");
|
||||
result.push_str(COLORS.green);
|
||||
result.push_str(colors.green);
|
||||
result.push_str(GLYPH);
|
||||
result.push_str(" ");
|
||||
result.push_str(COLORS.yellow);
|
||||
result.push_str(colors.yellow);
|
||||
result.push_str(GLYPH);
|
||||
result.push_str(" ");
|
||||
result.push_str(COLORS.red);
|
||||
result.push_str(colors.red);
|
||||
result.push_str(GLYPH);
|
||||
result.push_str(" ");
|
||||
result.push_str(COLORS.magenta);
|
||||
result.push_str(colors.magenta);
|
||||
result.push_str(GLYPH);
|
||||
result.push_str(" ");
|
||||
result.push_str(COLORS.reset);
|
||||
result.push_str(colors.reset);
|
||||
|
||||
result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
use std::{ffi::OsStr, fmt::Write};
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::getenv_str;
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_desktop_info() -> String {
|
||||
let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP");
|
||||
let session_os = std::env::var_os("XDG_SESSION_TYPE");
|
||||
let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP").unwrap_or("Unknown");
|
||||
let session_raw = getenv_str("XDG_SESSION_TYPE").unwrap_or("");
|
||||
|
||||
let desktop_raw = desktop_os
|
||||
.as_deref()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or("Unknown");
|
||||
let desktop_str = desktop_raw.strip_prefix("none+").unwrap_or(desktop_raw);
|
||||
|
||||
let session_raw = session_os.as_deref().and_then(OsStr::to_str).unwrap_or("");
|
||||
let backend_str = if session_raw.is_empty() {
|
||||
"Unknown"
|
||||
} else {
|
||||
|
|
@ -27,9 +24,15 @@ pub fn get_desktop_info() -> String {
|
|||
result.push_str(" (");
|
||||
|
||||
// Capitalize first character of backend
|
||||
if let Some(first_char) = backend_str.chars().next() {
|
||||
let _ = write!(result, "{}", first_char.to_ascii_uppercase());
|
||||
result.push_str(&backend_str[first_char.len_utf8()..]);
|
||||
if let Some(first_byte) = backend_str.as_bytes().first() {
|
||||
// Convert first byte to uppercase if it's ASCII lowercase
|
||||
let upper = if first_byte.is_ascii_lowercase() {
|
||||
(first_byte - b'a' + b'A') as char
|
||||
} else {
|
||||
*first_byte as char
|
||||
};
|
||||
result.push(upper);
|
||||
result.push_str(&backend_str[1..]);
|
||||
}
|
||||
|
||||
result.push(')');
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
#![no_std]
|
||||
extern crate alloc;
|
||||
|
||||
pub mod colors;
|
||||
pub mod desktop;
|
||||
pub mod release;
|
||||
pub mod system;
|
||||
pub mod uptime;
|
||||
|
||||
use std::{
|
||||
use alloc::string::String;
|
||||
use core::{
|
||||
ffi::CStr,
|
||||
io::{self, Cursor, Write},
|
||||
mem::MaybeUninit,
|
||||
sync::atomic::{AtomicPtr, Ordering},
|
||||
};
|
||||
|
||||
pub use microfetch_asm as syscall;
|
||||
|
|
@ -25,6 +29,212 @@ 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<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
|
||||
pub struct UtsName(UtsNameBuf);
|
||||
|
||||
|
|
@ -34,11 +244,12 @@ impl UtsName {
|
|||
/// # Errors
|
||||
///
|
||||
/// 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();
|
||||
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() }))
|
||||
}
|
||||
|
||||
|
|
@ -78,10 +289,51 @@ 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<(), Box<dyn std::error::Error>> {
|
||||
fn print_system_info(fields: &Fields) -> Result<(), Error> {
|
||||
let Fields {
|
||||
user_info,
|
||||
os_name,
|
||||
|
|
@ -94,55 +346,189 @@ fn print_system_info(
|
|||
colors,
|
||||
} = fields;
|
||||
|
||||
let cyan = colors::COLORS.cyan;
|
||||
let blue = colors::COLORS.blue;
|
||||
let reset = colors::COLORS.reset;
|
||||
let no_color = colors::is_no_color();
|
||||
let c = colors::Colors::new(no_color);
|
||||
|
||||
let mut buf = [0u8; 2048];
|
||||
let mut cursor = Cursor::new(&mut buf[..]);
|
||||
let mut w = StackWriter::new(&mut buf);
|
||||
|
||||
write!(
|
||||
cursor,
|
||||
"
|
||||
{blue} ▟█▖ {cyan}▝█▙ ▗█▛ {user_info} ~{reset}
|
||||
{blue} ▗▄▄▟██▄▄▄▄▄{cyan}▝█▙█▛ {blue}▖ {cyan} {blue}System{reset} {os_name}
|
||||
{blue} ▀▀▀▀▀▀▀▀▀▀▀▘{cyan}▝██ {blue}▟█▖ {cyan} {blue}Kernel{reset} {kernel_version}
|
||||
{cyan} ▟█▛ {cyan}▝█▘{blue}▟█▛ {cyan} {blue}Shell{reset} {shell}
|
||||
{cyan}▟█████▛ {blue}▟█████▛ {cyan} {blue}Uptime{reset} {uptime}
|
||||
{cyan} ▟█▛{blue}▗█▖ {blue}▟█▛ {cyan} {blue}Desktop{reset} {desktop}
|
||||
{cyan} ▝█▛ {blue}██▖{cyan}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan} {blue}Memory{reset} {memory_usage}
|
||||
{cyan} ▝ {blue}▟█▜█▖{cyan}▀▀▀▀▀██▛▀▀▘ {cyan} {blue}Storage (/){reset} {storage}
|
||||
{blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset} {colors}\n\n"
|
||||
)?;
|
||||
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(""));
|
||||
|
||||
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) };
|
||||
// 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()) };
|
||||
if written < 0 {
|
||||
return Err(io::Error::last_os_error().into());
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
return Err(Error::OsError(written as i32));
|
||||
}
|
||||
#[allow(clippy::cast_sign_loss)] // non-negative verified by the guard above
|
||||
if written as usize != len {
|
||||
return Err(
|
||||
io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout")
|
||||
.into(),
|
||||
);
|
||||
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
if written as usize != out.len() {
|
||||
return Err(Error::WriteError);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Print version information using direct syscall.
|
||||
fn print_version() {
|
||||
const VERSION: &str = concat!("Microfetch ", env!("CARGO_PKG_VERSION"), "\n");
|
||||
unsafe {
|
||||
let _ = sys_write(1, VERSION.as_ptr(), VERSION.len());
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if --version was passed via argc/argv.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// This function must be called with valid argc and argv from the program entry
|
||||
/// point.
|
||||
unsafe fn check_version_flag(argc: i32, argv: *const *const u8) -> bool {
|
||||
if argc < 2 {
|
||||
return false;
|
||||
}
|
||||
// SAFETY: argv is a valid array of argc pointers
|
||||
let arg1 = unsafe { *argv.add(1) };
|
||||
if arg1.is_null() {
|
||||
return false;
|
||||
}
|
||||
// Check if arg1 is "--version"
|
||||
let version_flag = b"--version\0";
|
||||
for (i, &b) in version_flag.iter().enumerate() {
|
||||
// SAFETY: arg1 is a valid C string
|
||||
let arg_byte = unsafe { *arg1.add(i) };
|
||||
if arg_byte != b {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Main entry point for microfetch - can be called by the binary crate
|
||||
/// or by other consumers of the library
|
||||
/// or by other consumers of the library.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `argc` - Argument count from main
|
||||
/// * `argv` - Argument vector from main
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if any system call fails
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// argv must be a valid null-terminated array of C strings.
|
||||
#[cfg_attr(feature = "hotpath", hotpath::main)]
|
||||
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if Some("--version") == std::env::args().nth(1).as_deref() {
|
||||
println!("Microfetch {}", env!("CARGO_PKG_VERSION"));
|
||||
} else {
|
||||
pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> {
|
||||
if unsafe { check_version_flag(argc, argv) } {
|
||||
print_version();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let utsname = UtsName::uname()?;
|
||||
let fields = Fields {
|
||||
user_info: system::get_username_and_hostname(&utsname),
|
||||
|
|
@ -156,7 +542,6 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
colors: colors::print_dots(),
|
||||
};
|
||||
print_system_info(&fields)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::{fmt::Write as _, io};
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::{UtsName, syscall::read_file_fast};
|
||||
use crate::{Error, UtsName, syscall::read_file_fast};
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
|
|
@ -13,7 +13,14 @@ pub fn get_system_info(utsname: &UtsName) -> String {
|
|||
let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1;
|
||||
let mut result = String::with_capacity(capacity);
|
||||
|
||||
write!(result, "{sysname} {release} ({machine})").unwrap();
|
||||
// Manual string construction instead of write! macro
|
||||
result.push_str(sysname);
|
||||
result.push(' ');
|
||||
result.push_str(release);
|
||||
result.push_str(" (");
|
||||
result.push_str(machine);
|
||||
result.push(')');
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
|
@ -23,7 +30,7 @@ pub fn get_system_info(utsname: &UtsName) -> String {
|
|||
///
|
||||
/// Returns an error if `/etc/os-release` cannot be read.
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_os_pretty_name() -> Result<String, io::Error> {
|
||||
pub fn get_os_pretty_name() -> Result<String, Error> {
|
||||
// Fast byte-level scanning for 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
|
||||
let bytes_read = read_file_fast("/etc/os-release", &mut buffer)
|
||||
.map_err(|e| io::Error::from_raw_os_error(-e))?;
|
||||
.map_err(Error::from_raw_os_error)?;
|
||||
let content = &buffer[..bytes_read];
|
||||
|
||||
let mut offset = 0;
|
||||
|
|
@ -66,5 +73,5 @@ pub fn get_os_pretty_name() -> Result<String, io::Error> {
|
|||
offset += line_end + 1;
|
||||
}
|
||||
|
||||
Ok("Unknown".to_owned())
|
||||
Ok(String::from("Unknown"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,39 @@
|
|||
use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit};
|
||||
use alloc::string::String;
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use crate::{
|
||||
Error,
|
||||
UtsName,
|
||||
colors::COLORS,
|
||||
colors::Colors,
|
||||
syscall::{StatfsBuf, read_file_fast, sys_statfs},
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_username_and_hostname(utsname: &UtsName) -> String {
|
||||
let username_os = std::env::var_os("USER");
|
||||
let username = username_os
|
||||
.as_deref()
|
||||
.and_then(OsStr::to_str)
|
||||
.unwrap_or("unknown_user");
|
||||
let username = crate::getenv_str("USER").unwrap_or("unknown_user");
|
||||
let hostname = utsname.nodename().to_str().unwrap_or("unknown_host");
|
||||
|
||||
let capacity = COLORS.yellow.len()
|
||||
// Get colors (checking NO_COLOR only once)
|
||||
let no_color = crate::colors::is_no_color();
|
||||
let colors = Colors::new(no_color);
|
||||
|
||||
let capacity = colors.yellow.len()
|
||||
+ username.len()
|
||||
+ COLORS.red.len()
|
||||
+ colors.red.len()
|
||||
+ 1
|
||||
+ COLORS.green.len()
|
||||
+ colors.green.len()
|
||||
+ hostname.len()
|
||||
+ COLORS.reset.len();
|
||||
+ colors.reset.len();
|
||||
let mut result = String::with_capacity(capacity);
|
||||
|
||||
result.push_str(COLORS.yellow);
|
||||
result.push_str(colors.yellow);
|
||||
result.push_str(username);
|
||||
result.push_str(COLORS.red);
|
||||
result.push_str(colors.red);
|
||||
result.push('@');
|
||||
result.push_str(COLORS.green);
|
||||
result.push_str(colors.green);
|
||||
result.push_str(hostname);
|
||||
result.push_str(COLORS.reset);
|
||||
result.push_str(colors.reset);
|
||||
|
||||
result
|
||||
}
|
||||
|
|
@ -39,13 +41,12 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String {
|
|||
#[must_use]
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_shell() -> String {
|
||||
let shell_os = std::env::var_os("SHELL");
|
||||
let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or("");
|
||||
let shell = crate::getenv_str("SHELL").unwrap_or("");
|
||||
let start = shell.rfind('/').map_or(0, |i| i + 1);
|
||||
if shell.is_empty() {
|
||||
"unknown_shell".into()
|
||||
String::from("unknown_shell")
|
||||
} else {
|
||||
shell[start..].into()
|
||||
String::from(&shell[start..])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,12 +57,12 @@ pub fn get_shell() -> String {
|
|||
/// Returns an error if the filesystem information cannot be retrieved.
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn get_root_disk_usage() -> Result<String, io::Error> {
|
||||
pub fn get_root_disk_usage() -> Result<String, Error> {
|
||||
let mut vfs = MaybeUninit::<StatfsBuf>::uninit();
|
||||
let path = b"/\0";
|
||||
|
||||
if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
return Err(Error::last_os_error());
|
||||
}
|
||||
|
||||
let vfs = unsafe { vfs.assume_init() };
|
||||
|
|
@ -77,18 +78,96 @@ pub fn get_root_disk_usage() -> Result<String, io::Error> {
|
|||
let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
let usage = (used_size / total_size) * 100.0;
|
||||
|
||||
let no_color = crate::colors::is_no_color();
|
||||
let colors = Colors::new(no_color);
|
||||
|
||||
let mut result = String::with_capacity(64);
|
||||
write!(
|
||||
result,
|
||||
"{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})",
|
||||
cyan = COLORS.cyan,
|
||||
reset = COLORS.reset,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Manual float formatting
|
||||
write_float(&mut result, used_size, 2);
|
||||
result.push_str(" GiB / ");
|
||||
write_float(&mut result, total_size, 2);
|
||||
result.push_str(" GiB (");
|
||||
result.push_str(colors.cyan);
|
||||
write_float(&mut result, usage, 0);
|
||||
result.push('%');
|
||||
result.push_str(colors.reset);
|
||||
result.push(')');
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Write a float to string with specified decimal places
|
||||
#[allow(
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_precision_loss
|
||||
)]
|
||||
fn write_float(s: &mut String, val: f64, decimals: u32) {
|
||||
// Handle integer part
|
||||
let int_part = val as u64;
|
||||
write_u64(s, int_part);
|
||||
|
||||
if decimals > 0 {
|
||||
s.push('.');
|
||||
|
||||
// Calculate fractional part
|
||||
let mut frac = val - int_part as f64;
|
||||
for _ in 0..decimals {
|
||||
frac *= 10.0;
|
||||
let digit = frac as u8;
|
||||
s.push((b'0' + digit) as char);
|
||||
frac -= f64::from(digit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Round an f64 to nearest integer (`f64::round` is not in core)
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss
|
||||
)]
|
||||
fn round_f64(x: f64) -> f64 {
|
||||
if x >= 0.0 {
|
||||
let int_part = x as u64 as f64;
|
||||
let frac = x - int_part;
|
||||
if frac >= 0.5 {
|
||||
int_part + 1.0
|
||||
} else {
|
||||
int_part
|
||||
}
|
||||
} else {
|
||||
let int_part = (-x) as u64 as f64;
|
||||
let frac = -x - int_part;
|
||||
if frac >= 0.5 {
|
||||
-(int_part + 1.0)
|
||||
} else {
|
||||
-int_part
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a u64 to string
|
||||
fn write_u64(s: &mut String, mut n: u64) {
|
||||
if n == 0 {
|
||||
s.push('0');
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 20];
|
||||
let mut i = 20;
|
||||
|
||||
while n > 0 {
|
||||
i -= 1;
|
||||
buf[i] = b'0' + (n % 10) as u8;
|
||||
n /= 10;
|
||||
}
|
||||
|
||||
// SAFETY: buf contains only ASCII digits
|
||||
s.push_str(unsafe { core::str::from_utf8_unchecked(&buf[i..]) });
|
||||
}
|
||||
|
||||
/// Fast integer parsing without stdlib overhead
|
||||
#[inline]
|
||||
fn parse_u64_fast(s: &[u8]) -> u64 {
|
||||
|
|
@ -109,16 +188,16 @@ fn parse_u64_fast(s: &[u8]) -> u64 {
|
|||
///
|
||||
/// Returns an error if `/proc/meminfo` cannot be read.
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_memory_usage() -> Result<String, io::Error> {
|
||||
pub fn get_memory_usage() -> Result<String, Error> {
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
fn parse_memory_info() -> Result<(f64, f64), io::Error> {
|
||||
fn parse_memory_info() -> Result<(f64, f64), Error> {
|
||||
let mut total_memory_kb = 0u64;
|
||||
let mut available_memory_kb = 0u64;
|
||||
let mut buffer = [0u8; 1024];
|
||||
|
||||
// Use fast syscall-based file reading
|
||||
let bytes_read = read_file_fast("/proc/meminfo", &mut buffer)
|
||||
.map_err(|e| io::Error::from_raw_os_error(-e))?;
|
||||
.map_err(Error::from_raw_os_error)?;
|
||||
let meminfo = &buffer[..bytes_read];
|
||||
|
||||
// Fast scanning for MemTotal and MemAvailable
|
||||
|
|
@ -168,17 +247,22 @@ pub fn get_memory_usage() -> Result<String, io::Error> {
|
|||
|
||||
let (used_memory, total_memory) = parse_memory_info()?;
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
let percentage_used = (used_memory / total_memory * 100.0).round() as u64;
|
||||
let percentage_used = round_f64(used_memory / total_memory * 100.0) as u64;
|
||||
|
||||
let no_color = crate::colors::is_no_color();
|
||||
let colors = Colors::new(no_color);
|
||||
|
||||
let mut result = String::with_capacity(64);
|
||||
write!(
|
||||
result,
|
||||
"{used_memory:.2} GiB / {total_memory:.2} GiB \
|
||||
({cyan}{percentage_used}%{reset})",
|
||||
cyan = COLORS.cyan,
|
||||
reset = COLORS.reset,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
write_float(&mut result, used_memory, 2);
|
||||
result.push_str(" GiB / ");
|
||||
write_float(&mut result, total_memory, 2);
|
||||
result.push_str(" GiB (");
|
||||
result.push_str(colors.cyan);
|
||||
write_u64(&mut result, percentage_used);
|
||||
result.push('%');
|
||||
result.push_str(colors.reset);
|
||||
result.push(')');
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use std::{io, mem::MaybeUninit};
|
||||
use alloc::string::String;
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use crate::syscall::sys_sysinfo;
|
||||
use crate::{Error, syscall::sys_sysinfo};
|
||||
|
||||
/// Faster integer to string conversion without the formatting overhead.
|
||||
#[inline]
|
||||
|
|
@ -16,7 +17,8 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str {
|
|||
n /= 10;
|
||||
}
|
||||
|
||||
unsafe { std::str::from_utf8_unchecked(&buf[i..]) }
|
||||
// SAFETY: We only wrote ASCII digits
|
||||
unsafe { core::str::from_utf8_unchecked(&buf[i..]) }
|
||||
}
|
||||
|
||||
/// Gets the current system uptime.
|
||||
|
|
@ -25,11 +27,11 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str {
|
|||
///
|
||||
/// Returns an error if the system uptime cannot be retrieved.
|
||||
#[cfg_attr(feature = "hotpath", hotpath::measure)]
|
||||
pub fn get_current() -> Result<String, io::Error> {
|
||||
pub fn get_current() -> Result<String, Error> {
|
||||
let uptime_seconds = {
|
||||
let mut info = MaybeUninit::uninit();
|
||||
if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
return Err(Error::last_os_error());
|
||||
}
|
||||
#[allow(clippy::cast_sign_loss)]
|
||||
unsafe {
|
||||
|
|
|
|||
120
docs/README.md
120
docs/README.md
|
|
@ -19,12 +19,12 @@
|
|||
|
||||
[fastfetch]: https://github.com/fastfetch-cli/fastfetch
|
||||
|
||||
Stupidly small and simple, laughably fast and pretty fetch tool. Written in Rust
|
||||
for speed and ease of maintainability. Runs in a _fraction of a millisecond_ and
|
||||
displays _most_ of the nonsense you'd see posted on r/unixporn or other internet
|
||||
communities. Aims to replace [fastfetch] on my personal system, but
|
||||
[probably not yours](#customizing). Though, you are more than welcome to use it
|
||||
on your system: it is pretty _[fast](#benchmarks)_...
|
||||
Stupidly small and simple, laughably fast, and pretty fetch tool. Written
|
||||
(mostly) in Rust for speed and ease of maintainability. Runs in a _fraction of a
|
||||
millisecond_ and displays _most_ of the nonsense you'd see posted on r/unixporn
|
||||
or other internet communities. Aims to replace [fastfetch] on my personal
|
||||
system, but [probably not yours](#customizing). Though, you are more than
|
||||
welcome to use it on your system: it is pretty _[fast](#benchmarks)_...
|
||||
|
||||
<p align="center">
|
||||
<br/>
|
||||
|
|
@ -40,10 +40,10 @@ on your system: it is pretty _[fast](#benchmarks)_...
|
|||
|
||||
- Fast
|
||||
- Really fast
|
||||
- Minimal dependencies
|
||||
- Tiny binary (~370kb [^1])
|
||||
- Actually really fast
|
||||
- Cool NixOS logo (other, inferior, distros are not supported)
|
||||
- No dependencies (not even libc!)
|
||||
- Tiny binary (~24kb)
|
||||
- Actually _really_ fast
|
||||
- Cool NixOS logo, with support for custom logo text
|
||||
- Reliable detection of following info:
|
||||
- Hostname/Username
|
||||
- Kernel
|
||||
|
|
@ -57,12 +57,9 @@ on your system: it is pretty _[fast](#benchmarks)_...
|
|||
- Shell Colors
|
||||
- Did I mention fast?
|
||||
- Respects [`NO_COLOR` spec](https://no-color.org/)
|
||||
- Funny [^2]
|
||||
- Funny [^1]
|
||||
|
||||
[^1]: With the Mold linker, which is enabled by default in the Flake package,
|
||||
the binary size is roughly 350kb. That's nearly 20kb reduction in size :)
|
||||
|
||||
[^2]: I don't know how else to describe the (unhealthy) amount of handwritten
|
||||
[^1]: I don't know how else to describe the (unhealthy) amount of handwritten
|
||||
assembly that was written in order to make Microfetch faster.
|
||||
|
||||
## Motivation
|
||||
|
|
@ -70,12 +67,12 @@ on your system: it is pretty _[fast](#benchmarks)_...
|
|||
[Rube-Goldmark Machine]: https://en.wikipedia.org/wiki/Rube_Goldberg_machine
|
||||
|
||||
Fastfetch, as its name _probably_ already hinted, is a very fast fetch tool
|
||||
written in C. I used to use Fastfetch on my systems, but I eventually came to
|
||||
written in C. I _used to_ use Fastfetch on my systems, but I eventually came to
|
||||
the realization that I am _not interested in any of its additional features_. I
|
||||
don't use Sixel, I don't change my configuration more than maybe once a year and
|
||||
I don't even display most of the fields that it does. Sure the configurability
|
||||
is nice and I can configure the defaults that I do not like but how often do I
|
||||
really do that?
|
||||
I don't even display most of the fields that it has. Sure, the configurability
|
||||
is nice and _I could_ configure the defaults that I do not like... but how often
|
||||
do I really do that?
|
||||
|
||||
Since I already enjoy programming challenges, and don't use a fetch program that
|
||||
often, I eventually came to try and answer the question _how fast can I make my
|
||||
|
|
@ -84,24 +81,29 @@ and put in my `~/.bashrc` but is _actually_ incredibly fast because it opts out
|
|||
of all the customization options provided by tools such as Fastfetch. Since
|
||||
Fetch scripts are kind of a coming-of-age ritual for most Linux users, I've
|
||||
decided to use it on my system. You also might be interested if you like the
|
||||
defaults and like speed.
|
||||
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
|
||||
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.
|
||||
I cannot re-iterate it enough, Microfetch is _annoyingly fast_. It, however,
|
||||
does not solve a real technical problem. The "problem" Microfetch "solves" is
|
||||
entirely self-imposed. I want a fast, _almost_ zero-cost command invocation and
|
||||
for it to not take that much space on my system. Thanks to the nature of Rust,
|
||||
Microfetch is _fast_. Rust does, or well, _did_ mean "bloated" dependency trees
|
||||
and slightly increased build times, though, as of 0.5.0 Microfetch has
|
||||
(voluntarily) dropped both `std` and `libc`. You can go check the numbers for
|
||||
the speed impact (hint: it's much better) but we also have little to no concerns
|
||||
left about build times and the binary size. Build times are also _very easily_
|
||||
mitigated with Nix's binary cache systems, and since Microfetch is already in
|
||||
Nixpkgs you are strongly encouraged to use `pkgs.microfetch` over the flake. The
|
||||
usage of Rust _is_ quite nice, however, since it provides us with incredible
|
||||
tooling and a very powerful language that allows for Microfetch to be as fast as
|
||||
possible.
|
||||
|
||||
Surely C would've been a smaller choice, but I like Rust more. Microfetch _also_
|
||||
features a whole bunch of handwritten assembly with per-platform support to
|
||||
_unsafely_ optimize most syscalls. In hindsight you all should have seen this
|
||||
coming. Is it faster? Yes. Is it better? Uh, yes. Should you use this? Yes.
|
||||
|
||||
Also see: [Rube-Goldmark Machine]
|
||||
|
||||
|
|
@ -232,20 +234,11 @@ interested in Microfetch tailored to their distributions.
|
|||
|
||||
## Customizing
|
||||
|
||||
You can't*.
|
||||
|
||||
### Why?
|
||||
|
||||
Customization, of most kinds, is "expensive": I could try reading environment
|
||||
variables, parse command-line arguments or read a configuration file to allow
|
||||
configuring various fields but those inflate execution time and the resource
|
||||
consumption by a lot. Since Microfetch is closer to a code golf challenge than a
|
||||
program that attempts to fill a gap, I have elected not to make this trade. This
|
||||
is, of course, not without a solution.
|
||||
You can't*
|
||||
|
||||
### Really?
|
||||
|
||||
[main module]: ./src/main.rs
|
||||
[main module]: ./microfetch/src/main.rs
|
||||
[discussions tab]: https://github.com/NotAShelf/microfetch/discussions
|
||||
|
||||
To be fair, you _can_ customize Microfetch by, well, patching it. It is
|
||||
|
|
@ -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 own variants of Microfetch!
|
||||
|
||||
The real reason behind lack of customizability is that customization, of most
|
||||
kinds, is "expensive": reading environment variables, parsing command-line
|
||||
arguments or reading a configuration file inflates execution time and resource
|
||||
consumption. Since Microfetch is closer to a code golf challenge than a program
|
||||
that attempts to fill a gap, runtime configuration is not supported. Patching
|
||||
the source is the only way to make changes that do not compromise on speed. The
|
||||
exception to this, as described below, is the custom logo support.
|
||||
|
||||
### Logo
|
||||
|
||||
Microfetch used to be impossible to customize without patching. Fortunately it's
|
||||
possible now to customize the logo to support your distribution. This is
|
||||
best-effort, but should work for most cases as long as you adhere to the
|
||||
constraints.
|
||||
|
||||
To use a custom logo, set the `MICROFETCH_LOGO` environment variable to 9
|
||||
newline-separated lines of ASCII or Unicode art when building:
|
||||
|
||||
```bash
|
||||
# Pass your logo text with MICROFETCH_LOGO.
|
||||
$ MICROFETCH_LOGO="$(cat my_logo.txt)" cargo build --release
|
||||
```
|
||||
|
||||
Each line corresponds to one info row. Nerd Font glyphs for labels (System,
|
||||
Kernel, Shell, etc.) are rendered automatically. An unset variable uses the
|
||||
built-in two-tone NixOS logo.
|
||||
|
||||
Keep in mind that the custom logo **is not padded**. You will need to mind the
|
||||
spaces if you're providing a custom logo for some sort.
|
||||
|
||||
## Contributing
|
||||
|
||||
I will, mostly, reject feature additions. This is not to say you should avoid
|
||||
|
|
@ -312,4 +335,5 @@ Microfetch. I might have missed your name here, but you have my thanks.
|
|||
|
||||
## License
|
||||
|
||||
Microfetch is licensed under [GPL3](LICENSE). See the license file for details.
|
||||
This project is released under GNU Public Licence version 3 **only**. See the
|
||||
[license](../LICENSE) for more details.
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
hotpath = { optional = true, version = "0.14.0" }
|
||||
microfetch-alloc.workspace = true
|
||||
microfetch-asm.workspace = true
|
||||
microfetch-lib.workspace = true
|
||||
|
||||
[features]
|
||||
|
|
|
|||
17
microfetch/build.rs
Normal file
17
microfetch/build.rs
Normal 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");
|
||||
}
|
||||
|
|
@ -1,3 +1,112 @@
|
|||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
microfetch_lib::run()
|
||||
#![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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
{
|
||||
lib,
|
||||
stdenv,
|
||||
stdenvAdapters,
|
||||
rustPlatform,
|
||||
llvm,
|
||||
useMold ? stdenv.isLinux,
|
||||
}: let
|
||||
toml = (lib.importTOML ../Cargo.toml).package;
|
||||
pname = toml.name;
|
||||
pname = "microfetch";
|
||||
toml = (lib.importTOML ../Cargo.toml).workspace.package;
|
||||
inherit (toml) version;
|
||||
|
||||
# Select stdenv based on useMold flag
|
||||
stdenv =
|
||||
if useMold
|
||||
then stdenvAdapters.useMoldLinker llvm.stdenv
|
||||
else llvm.stdenv;
|
||||
in
|
||||
rustPlatform.buildRustPackage.override {inherit stdenv;} {
|
||||
rustPlatform.buildRustPackage.override {inherit (llvm) stdenv;} {
|
||||
inherit pname version;
|
||||
src = let
|
||||
fs = lib.fileset;
|
||||
|
|
@ -25,10 +16,12 @@ in
|
|||
fs.toSource {
|
||||
root = s;
|
||||
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.toml)
|
||||
(s + /benches)
|
||||
];
|
||||
};
|
||||
|
||||
|
|
@ -37,12 +30,6 @@ in
|
|||
buildNoDefaultFeatures = true;
|
||||
doCheck = false;
|
||||
|
||||
# Only set RUSTFLAGS for mold if useMold is enabled
|
||||
env = lib.optionalAttrs useMold {
|
||||
CARGO_LINKER = "clang";
|
||||
RUSTFLAGS = "-C link-arg=-fuse-ld=mold";
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Microscopic fetch script in Rust, for NixOS systems";
|
||||
homepage = "https://github.com/NotAShelf/microfetch";
|
||||
|
|
|
|||
38
scripts/ld-wrapper
Executable file
38
scripts/ld-wrapper
Executable 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue