From 0492348131e26b4575d676982cceb013b31827fe Mon Sep 17 00:00:00 2001 From: Matheus Date: Tue, 9 Jun 2026 18:09:00 -0300 Subject: [PATCH] btop: add port --- patches/btop/jinx-working-patch.patch | 527 ++++++++++++++++++++++++++ recipes/btop | 27 ++ source-recipes/btop | 9 + 3 files changed, 563 insertions(+) create mode 100644 patches/btop/jinx-working-patch.patch create mode 100644 recipes/btop create mode 100644 source-recipes/btop diff --git a/patches/btop/jinx-working-patch.patch b/patches/btop/jinx-working-patch.patch new file mode 100644 index 00000000..deac76ff --- /dev/null +++ b/patches/btop/jinx-working-patch.patch @@ -0,0 +1,527 @@ +diff --git btop-clean/Makefile btop-workdir/Makefile +index 0bb9cbd..3dad342 100644 +--- btop-clean/Makefile ++++ btop-workdir/Makefile +@@ -132,6 +132,10 @@ else ifeq ($(PLATFORM_LC),netbsd) + override ADDFLAGS += -lkvm -lprop + export MAKE = gmake + SU_GROUP := wheel ++else ifeq ($(PLATFORM_LC),astral) ++ PLATFORM_DIR := astral ++ THREADS := $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) ++ SU_GROUP := root + else + $(error $(shell printf "\033[1;91mERROR: \033[97mUnsupported platform ($(PLATFORM))\033[0m")) + endif +diff --git btop-workdir/src/astral/btop_collect.cpp btop-workdir/src/astral/btop_collect.cpp +new file mode 100644 +index 0000000..97b9358 +--- /dev/null ++++ btop-workdir/src/astral/btop_collect.cpp +@@ -0,0 +1,472 @@ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "btop_config.hpp" ++#include "btop_shared.hpp" ++#include "btop_tools.hpp" ++ ++using namespace Tools; ++using std::cmp_greater; ++ ++namespace { ++ constexpr double load_scale = 1 << SI_LOAD_SHIFT; ++ ++ auto read_sysinfo() -> struct sysinfo { ++ struct sysinfo info {}; ++ if (sysinfo(&info) != 0) ++ throw std::runtime_error("sysinfo() failed"); ++ return info; ++ } ++ ++ auto mem_bytes(unsigned long pages, unsigned int unit) -> uint64_t { ++ return static_cast(pages) * static_cast(unit ? unit : 1); ++ } ++ ++ auto user_name(uid_t uid) -> string { ++ if (auto *pw = getpwuid(uid); pw != nullptr and pw->pw_name != nullptr) ++ return pw->pw_name; ++ return std::to_string(uid); ++ } ++ ++ auto proc_runtime_seconds(const sysctl_proc_info_t& info) -> uint64_t { ++ return static_cast(info.total_runtime.tv_sec); ++ } ++ ++ auto proc_start_seconds(const sysctl_proc_info_t& info) -> uint64_t { ++ return static_cast(info.start_timestamp.tv_sec); ++ } ++ ++ auto proc_state(int state) -> char { ++ (void)state; ++ // TODO: Astral sysctl currently exposes a placeholder process state. ++ return 'W'; ++ } ++ ++ auto get_processes() -> vector { ++ size_t limit = 256; ++ for (;;) { ++ int mib[] = { ++ SYS_CTL_KERN, ++ SYS_CTL_KERN_PROC, ++ SYS_CTL_KERN_PROC_ALL, ++ 0, ++ static_cast(sizeof(sysctl_proc_info_t)), ++ static_cast(limit), ++ }; ++ ++ size_t len = sizeof(sysctl_proc_info_t) * limit; ++ vector procs(limit); ++ if (sysctl(mib, std::size(mib), procs.data(), &len, nullptr, 0) == 0) { ++ procs.resize(len / sizeof(sysctl_proc_info_t)); ++ return procs; ++ } ++ ++ if (errno != ENOMEM) ++ throw std::runtime_error("Astral process sysctl failed"); ++ limit *= 2; ++ } ++ } ++} ++ ++namespace Cpu { ++ string cpuName = "Astral"; ++ string cpuHz; ++ vector available_fields = {"Auto", "total"}; ++ vector available_sensors; ++ bool got_sensors{}; ++ bool cpu_temp_only{}; ++ bool supports_watts{}; ++ bool has_battery = false; // TODO: no existing battery ABI. ++ tuple current_bat = {0, 0.0F, 0, ""}; ++ std::unordered_map core_mapping; ++ cpu_info current_cpu; ++ ++ auto get_cpuHz() -> string { ++ return ""; ++ } ++ ++ auto get_battery() -> tuple { ++ // TODO: no existing battery ABI. ++ return current_bat; ++ } ++ ++ auto get_core_mapping() -> std::unordered_map { ++ return {}; ++ } ++ ++ auto collect(bool no_update) -> cpu_info& { ++ if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) ++ return current_cpu; ++ ++ auto info = read_sysinfo(); ++ current_cpu.load_avg = { ++ static_cast(info.loads[0]) / load_scale, ++ static_cast(info.loads[1]) / load_scale, ++ static_cast(info.loads[2]) / load_scale, ++ }; ++ ++ for (auto& [field, values] : current_cpu.cpu_percent) { ++ // TODO: Astral does not expose CPU tick counters to userland yet. ++ values.push_back(0); ++ while (cmp_greater(values.size(), width * 2)) ++ values.pop_front(); ++ } ++ ++ if (current_cpu.core_percent.size() != static_cast(Shared::coreCount)) ++ current_cpu.core_percent.assign(Shared::coreCount, {}); ++ for (auto& values : current_cpu.core_percent) { ++ // TODO: no per-core CPU utilization API is currently available. ++ values.push_back(0); ++ while (cmp_greater(values.size(), width * 2)) ++ values.pop_front(); ++ } ++ ++ current_cpu.temp.assign(Shared::coreCount + 1, {}); ++ current_cpu.temp_max = 0; // TODO: no sensors API. ++ current_cpu.usage_watts = 0; // TODO: no power sensors API. ++ return current_cpu; ++ } ++} ++ ++namespace Mem { ++ bool has_swap{}; ++ int disk_ios{}; ++ mem_info current_mem {}; ++ ++ uint64_t get_totalMem() { ++ auto info = read_sysinfo(); ++ return mem_bytes(info.totalram, info.mem_unit); ++ } ++ ++ auto collect(bool no_update) -> mem_info& { ++ if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) ++ return current_mem; ++ ++ auto show_swap = Config::getB("show_swap"); ++ auto info = read_sysinfo(); ++ auto total = mem_bytes(info.totalram, info.mem_unit); ++ auto free = mem_bytes(info.freeram, info.mem_unit); ++ auto cached = mem_bytes(info.bufferram, info.mem_unit); ++ auto available = std::min(total, free + cached); ++ ++ current_mem.stats.at("free") = free; ++ current_mem.stats.at("cached") = cached; ++ current_mem.stats.at("available") = available; ++ current_mem.stats.at("used") = total > available ? total - available : 0; ++ current_mem.stats.at("swap_total") = mem_bytes(info.totalswap, info.mem_unit); ++ current_mem.stats.at("swap_free") = mem_bytes(info.freeswap, info.mem_unit); ++ current_mem.stats.at("swap_used") = current_mem.stats.at("swap_total") > current_mem.stats.at("swap_free") ++ ? current_mem.stats.at("swap_total") - current_mem.stats.at("swap_free") ++ : 0; ++ ++ for (const auto& name : mem_names) { ++ current_mem.percent.at(name).push_back(total == 0 ? 0 : std::round(static_cast(current_mem.stats.at(name)) * 100.0 / total)); ++ while (cmp_greater(current_mem.percent.at(name).size(), width * 2)) ++ current_mem.percent.at(name).pop_front(); ++ } ++ ++ has_swap = show_swap and current_mem.stats.at("swap_total") > 0; ++ if (has_swap) { ++ for (const auto& name : swap_names) { ++ current_mem.percent.at(name).push_back(std::round(static_cast(current_mem.stats.at(name)) * 100.0 / current_mem.stats.at("swap_total"))); ++ while (cmp_greater(current_mem.percent.at(name).size(), width * 2)) ++ current_mem.percent.at(name).pop_front(); ++ } ++ } ++ ++ current_mem.disks.clear(); ++ current_mem.disks_order.clear(); ++ disk_ios = 0; // TODO: no existing disk enumeration or disk IO API. ++ return current_mem; ++ } ++} ++ ++namespace Shared { ++ long pageSize, clkTck, coreCount, page_size, clk_tck; ++ ++ void init() { ++ coreCount = sysconf(_SC_NPROCESSORS_ONLN); ++ if (coreCount < 1) ++ coreCount = 1; ++ ++ pageSize = sysconf(_SC_PAGESIZE); ++ if (pageSize <= 0) ++ pageSize = 4096; ++ page_size = pageSize; ++ ++ clkTck = sysconf(_SC_CLK_TCK); ++ if (clkTck <= 0) ++ clkTck = 100; ++ clk_tck = clkTck; ++ ++ (void)read_sysinfo(); ++ ++ Cpu::current_cpu.core_percent.assign(coreCount, {}); ++ Cpu::current_cpu.temp.assign(coreCount + 1, {}); ++ Cpu::cpuName = "Astral"; ++ Cpu::cpuHz.clear(); ++ Cpu::has_battery = false; // TODO: no battery API. ++ Cpu::got_sensors = false; // TODO: no sensors API. ++ Cpu::supports_watts = false; // TODO: no power sensors API. ++ Cpu::available_fields.clear(); ++ for (const auto& [field, ignored] : Cpu::current_cpu.cpu_percent) ++ Cpu::available_fields.push_back(field); ++ Cpu::core_mapping = Cpu::get_core_mapping(); ++ Cpu::collect(); ++ ++ Mem::collect(); ++ Proc::collect(); ++ Net::collect(); ++ ++ Logger::debug("Shared::init() : Initialized Astral collector."); ++ } ++} ++ ++namespace Net { ++ std::unordered_map current_net; ++ net_info empty_net = {}; ++ vector interfaces; ++ string selected_iface; ++ std::unordered_map graph_max = {{"download", {}}, {"upload", {}}}; ++ bool rescale{true}; ++ ++ auto collect(bool no_update) -> net_info& { ++ if (Runner::stopping) ++ return empty_net; ++ if (no_update and not selected_iface.empty() and current_net.contains(selected_iface)) ++ return current_net.at(selected_iface); ++ ++ IfAddrsPtr if_addrs {}; ++ if (if_addrs.get_status() != 0) ++ return empty_net; ++ ++ interfaces.clear(); ++ char ip[INET6_ADDRSTRLEN] {}; ++ ++ for (auto *ifa = if_addrs.get(); ifa != nullptr; ifa = ifa->ifa_next) { ++ if (ifa->ifa_addr == nullptr or ifa->ifa_name == nullptr) ++ continue; ++ ++ const string iface = ifa->ifa_name; ++ if (not v_contains(interfaces, iface)) { ++ interfaces.push_back(iface); ++ current_net[iface].connected = (ifa->ifa_flags & IFF_RUNNING) != 0; ++ current_net[iface].ipv4.clear(); ++ current_net[iface].ipv6.clear(); ++ } ++ ++ if (ifa->ifa_addr->sa_family == AF_INET and current_net[iface].ipv4.empty()) { ++ auto *addr = reinterpret_cast(ifa->ifa_addr); ++ if (inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)) != nullptr) ++ current_net[iface].ipv4 = ip; ++ } ++ else if (ifa->ifa_addr->sa_family == AF_INET6 and current_net[iface].ipv6.empty()) { ++ auto *addr = reinterpret_cast(ifa->ifa_addr); ++ if (inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)) != nullptr) ++ current_net[iface].ipv6 = ip; ++ } ++ } ++ ++ for (const auto& iface : interfaces) { ++ for (const string dir : {"download", "upload"}) { ++ auto& netif = current_net[iface]; ++ auto& stat = netif.stat.at(dir); ++ auto& bandwidth = netif.bandwidth.at(dir); ++ // TODO: getifaddrs() exposes addresses, not Astral RX/TX byte counters. ++ stat.speed = 0; ++ stat.total = 0; ++ bandwidth.push_back(0); ++ while (cmp_greater(bandwidth.size(), width * 2)) ++ bandwidth.pop_front(); ++ } ++ } ++ ++ for (auto it = current_net.begin(); it != current_net.end();) { ++ if (not v_contains(interfaces, it->first)) ++ it = current_net.erase(it); ++ else ++ ++it; ++ } ++ ++ if (current_net.empty()) ++ return empty_net; ++ ++ auto& config_iface = Config::getS("net_iface"); ++ if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { ++ if (not config_iface.empty() and v_contains(interfaces, config_iface)) ++ selected_iface = config_iface; ++ else ++ selected_iface = interfaces.front(); ++ redraw = true; ++ rescale = true; ++ } ++ ++ return current_net.at(selected_iface); ++ } ++} ++ ++namespace Proc { ++ vector current_procs; ++ string current_sort; ++ string current_filter; ++ bool current_rev{}; ++ bool is_tree_mode{}; ++ atomic numpids{}; ++ int filter_found{}; ++ int collapse = -1, expand = -1, toggle_children = -1, collapse_all = -1; ++ detail_container detailed; ++ ++ static void collect_details(size_t pid, vector& procs) { ++ auto p_info = std::ranges::find(procs, pid, &proc_info::pid); ++ if (p_info == procs.end()) ++ return; ++ if (pid != detailed.last_pid) { ++ detailed = {}; ++ detailed.last_pid = pid; ++ } ++ detailed.entry = *p_info; ++ detailed.status = proc_states.contains(detailed.entry.state) ? proc_states.at(detailed.entry.state) : "Unknown"; ++ detailed.parent.clear(); ++ if (auto parent = std::ranges::find(procs, detailed.entry.ppid, &proc_info::pid); parent != procs.end()) ++ detailed.parent = parent->name; ++ detailed.memory = floating_humanizer(detailed.entry.mem); ++ detailed.mem_bytes.push_back(detailed.entry.mem); ++ while (cmp_greater(detailed.mem_bytes.size(), width)) ++ detailed.mem_bytes.pop_front(); ++ // TODO: no per-process IO counters are exposed by the current API. ++ detailed.io_read = "0 B"; ++ detailed.io_write = "0 B"; ++ } ++ ++ auto collect(bool no_update) -> vector& { ++ if (Runner::stopping) ++ return current_procs; ++ ++ const auto& sorting = Config::getS("proc_sorting"); ++ auto reverse = Config::getB("proc_reversed"); ++ const auto& filter = Config::getS("proc_filter"); ++ auto tree = Config::getB("proc_tree"); ++ auto show_detailed = Config::getB("show_detailed"); ++ const size_t detailed_pid = Config::getI("detailed_pid"); ++ bool should_filter = current_filter != filter; ++ bool sorted_change = sorting != current_sort or reverse != current_rev or should_filter; ++ bool tree_mode_change = tree != is_tree_mode; ++ if (should_filter) ++ current_filter = filter; ++ if (sorted_change) { ++ current_sort = sorting; ++ current_rev = reverse; ++ } ++ if (tree_mode_change) ++ is_tree_mode = tree; ++ ++ if (not no_update) { ++ auto total_mem = Mem::get_totalMem(); ++ vector found; ++ auto snapshot = get_processes(); ++ ++ for (const auto& proc : snapshot) { ++ found.push_back(proc.pid); ++ auto existing = std::ranges::find(current_procs, static_cast(proc.pid), &proc_info::pid); ++ if (existing == current_procs.end()) { ++ current_procs.push_back({static_cast(proc.pid)}); ++ existing = current_procs.end() - 1; ++ } ++ ++ auto& entry = *existing; ++ entry.name = proc.name[0] ? proc.name : std::to_string(proc.pid); ++ entry.cmd = entry.name; // TODO: Astral sysctl does not expose argv yet. ++ entry.short_cmd = entry.cmd; ++ entry.threads = proc.thread_count; ++ entry.user = user_name(proc.uid); ++ entry.ppid = proc.ppid; ++ entry.p_nice = proc.nice; ++ entry.state = proc_state(proc.state); ++ entry.cpu_s = proc_start_seconds(proc); ++ entry.cpu_t = proc_runtime_seconds(proc); ++ entry.cpu_p = 0.0; // TODO: no process CPU percentage ABI. ++ entry.cpu_c = 0.0; // TODO: no process CPU counters suitable for cumulative percentages. ++ entry.mem = static_cast(proc.physical_pages) * Shared::pageSize; ++ if (entry.mem == 0) ++ entry.mem = static_cast(proc.virtual_pages) * Shared::pageSize; ++ if (entry.mem > total_mem) ++ entry.mem = total_mem; ++ // TODO: RSS/virtual memory stay zero when Astral sysctl returns zero page counts. ++ } ++ ++ auto eraser = std::ranges::remove_if(current_procs, [&](const auto& entry) { ++ return not v_contains(found, entry.pid); ++ }); ++ current_procs.erase(eraser.begin(), eraser.end()); ++ should_filter = true; ++ } ++ ++ if (show_detailed) ++ collect_details(detailed_pid, current_procs); ++ ++ if (should_filter) { ++ filter_found = 0; ++ for (auto& proc : current_procs) { ++ if (not tree and not filter.empty() and not matches_filter(proc, filter)) { ++ proc.filtered = true; ++ filter_found++; ++ } ++ else ++ proc.filtered = false; ++ } ++ } ++ ++ if (sorted_change or tree_mode_change or not no_update) ++ proc_sorter(current_procs, sorting, reverse, tree); ++ ++ if (tree and not current_procs.empty() and (not no_update or should_filter or sorted_change)) { ++ if (collapse_all != -1) { ++ toggle_tree_collapse(current_procs); ++ collapse_all = -1; ++ } ++ if (auto find_pid = collapse != -1 ? collapse : expand; find_pid != -1) { ++ if (auto collapser = std::ranges::find(current_procs, static_cast(find_pid), &proc_info::pid); collapser != current_procs.end()) ++ collapser->collapsed = collapse != -1; ++ collapse = expand = -1; ++ } ++ ++ vector tree_procs; ++ tree_procs.reserve(current_procs.size()); ++ std::ranges::stable_sort(current_procs, std::ranges::less{}, &proc_info::ppid); ++ for (auto& proc : std::ranges::equal_range(current_procs, current_procs.front().ppid, std::ranges::less{}, &proc_info::ppid)) ++ _tree_gen(proc, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); ++ ++ int index = 0; ++ tree_sort(tree_procs, sorting, reverse, false, index, current_procs.size()); ++ for (auto t = tree_procs.begin(); t != tree_procs.end(); ++t) ++ _collect_prefixes(*t, t == tree_procs.end() - 1); ++ std::ranges::stable_sort(current_procs, std::ranges::less{}, &proc_info::tree_index); ++ } ++ ++ numpids = static_cast(current_procs.size()) - filter_found; ++ return current_procs; ++ } ++} ++ ++namespace Tools { ++ double system_uptime() { ++ return static_cast(read_sysinfo().uptime); ++ } ++} +diff --git btop-clean/src/btop.cpp btop-workdir/src/btop.cpp +index c04295c..e2c1772 100644 +--- btop-clean/src/btop.cpp ++++ btop-workdir/src/btop.cpp +@@ -214,7 +214,7 @@ void clean_quit(int sig) { + Global::quitting = true; + Runner::stop(); + if (Global::_runner_started) { +- #if defined __APPLE__ || defined __OpenBSD__ || defined __NetBSD__ ++ #if defined __APPLE__ || defined __OpenBSD__ || defined __NetBSD__ || defined __astral__ + if (pthread_join(Runner::runner_id, nullptr) != 0) { + Logger::warning("Failed to join _runner thread on exit!"); + pthread_cancel(Runner::runner_id); +diff --git btop-clean/src/btop_shared.cpp btop-workdir/src/btop_shared.cpp +index 462cba7..a88a9e4 100644 +--- btop-clean/src/btop_shared.cpp ++++ btop-workdir/src/btop_shared.cpp +@@ -95,10 +95,16 @@ namespace Gpu { + + namespace Proc { + bool set_priority(pid_t pid, int priority) { ++#ifdef __astral__ ++ (void)pid; ++ (void)priority; ++ return false; ++#else + if (setpriority(PRIO_PROCESS, pid, priority) == 0) { + return true; + } + return false; ++#endif + } + + void proc_sorter(vector& proc_vec, const string& sorting, bool reverse, bool tree) { diff --git a/recipes/btop b/recipes/btop new file mode 100644 index 00000000..059e3e4c --- /dev/null +++ b/recipes/btop @@ -0,0 +1,27 @@ +name=btop +revision=1 +from_source=btop +imagedeps="build-essential" +hostdeps="xgcc xbinutils libstdc++-v3 pkgconfig" +builddeps="ncurses" +deps="base ncurses" + +configure() { + cp -rp ${source_dir}/* . +} + +build() { + make \ + -j${parallelism} \ + PLATFORM=astral \ + CXX=x86_64-astral-g++ \ + PREFIX=${prefix} \ + GPU_SUPPORT=false +} + +package() { + make \ + DESTDIR=${dest_dir} \ + PREFIX=${prefix} \ + install +} diff --git a/source-recipes/btop b/source-recipes/btop new file mode 100644 index 00000000..0d1412ef --- /dev/null +++ b/source-recipes/btop @@ -0,0 +1,9 @@ +name=btop +version=1.4.7 +source_method=tarball +tarball_url="https://github.com/aristocratos/btop/archive/refs/tags/v${version}.tar.gz" +tarball_sha512="49f2d2aa4fa83816f4fdaebc7239b3036d6e776b45b059d40ec535867762f207dcd9e79df16e410b37965e10c758404bd6aab8106a8c8b8e0761d3950e8856ef" + +prepare() { + true +}