2026-09-04 02:49:02 +00:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# gather-hardware-info.sh — collects the fields ArchyHCL's report form asks
|
|
|
|
|
# for and prints a YAML block matching data/schema.json, ready to paste into
|
|
|
|
|
# https://hcl.archipelago-foundation.org/report.html or a PR.
|
|
|
|
|
#
|
|
|
|
|
# Read before you run, like any script from the internet:
|
|
|
|
|
# https://source.archipelago-foundation.org/ssmithx/ArchyHCL/src/branch/main/site/gather-hardware-info.sh
|
|
|
|
|
#
|
|
|
|
|
# Local-only: reads /proc, /sys, and a few common CLI tools. Makes no
|
|
|
|
|
# network calls and changes nothing on the system. Some fields (WiFi chip
|
|
|
|
|
# on non-PCI hardware, device model on some boards) can't be detected
|
|
|
|
|
# reliably everywhere — those are left as TODO rather than guessed wrong.
|
|
|
|
|
#
|
|
|
|
|
# Usage: ./gather-hardware-info.sh (or: bash gather-hardware-info.sh)
|
|
|
|
|
|
|
|
|
|
set -uo pipefail
|
|
|
|
|
|
|
|
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
|
|
|
|
|
|
# --- device model ---
|
|
|
|
|
device_model="TODO — e.g. \"Lenovo ThinkPad T430\""
|
|
|
|
|
if [ -r /proc/device-tree/model ]; then
|
|
|
|
|
# Raspberry Pi and most other ARM SBCs expose a clean model string here,
|
|
|
|
|
# null-terminated.
|
|
|
|
|
device_model="$(tr -d '\0' < /proc/device-tree/model)"
|
|
|
|
|
elif [ -r /sys/class/dmi/id/sys_vendor ] && [ -r /sys/class/dmi/id/product_name ]; then
|
|
|
|
|
vendor="$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null)"
|
|
|
|
|
product="$(cat /sys/class/dmi/id/product_name 2>/dev/null)"
|
|
|
|
|
if [ -n "$vendor" ] && [ -n "$product" ]; then
|
|
|
|
|
device_model="$vendor $product"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- form factor (best-effort guess — double-check this one) ---
|
|
|
|
|
form_factor="other # TODO: couldn't guess confidently, pick laptop|desktop|mini-pc|sbc|server|other"
|
|
|
|
|
if [ -r /proc/device-tree/model ]; then
|
|
|
|
|
form_factor="sbc"
|
|
|
|
|
elif ls /sys/class/power_supply/BAT* >/dev/null 2>&1; then
|
|
|
|
|
form_factor="laptop"
|
|
|
|
|
elif [ -r /sys/class/dmi/id/chassis_type ]; then
|
|
|
|
|
case "$(cat /sys/class/dmi/id/chassis_type 2>/dev/null)" in
|
|
|
|
|
8|9|10|14) form_factor="laptop" ;; # Portable/Laptop/Notebook/Sub-Notebook
|
|
|
|
|
3|4|6|7) form_factor="desktop" ;; # Desktop/Low Profile Desktop/Mini Tower/Tower
|
|
|
|
|
35|36) form_factor="mini-pc" ;; # Small Form Factor variants some vendors use
|
|
|
|
|
23) form_factor="server" ;; # Rack Mount Chassis
|
|
|
|
|
esac
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# --- CPU ---
|
|
|
|
|
if have lscpu; then
|
|
|
|
|
cpu="$(lscpu | awk -F: '/^Model name/ {print $2; exit}' | sed 's/^ *//')"
|
|
|
|
|
fi
|
|
|
|
|
if [ -z "${cpu:-}" ]; then
|
|
|
|
|
cpu="$(awk -F: '/^model name/ {print $2; exit}' /proc/cpuinfo | sed 's/^ *//')"
|
|
|
|
|
fi
|
|
|
|
|
if [ -z "${cpu:-}" ]; then
|
|
|
|
|
# ARM boards often don't set "model name" — fall back to the SoC line.
|
|
|
|
|
cpu="$(awk -F: '/^Hardware|^Model/ {print $2; exit}' /proc/cpuinfo | sed 's/^ *//')"
|
|
|
|
|
fi
|
|
|
|
|
[ -z "${cpu:-}" ] && cpu="TODO — could not detect, check \`lscpu\` or \`cat /proc/cpuinfo\`"
|
|
|
|
|
|
|
|
|
|
# --- RAM (rounded up to the nearest whole GB) ---
|
|
|
|
|
ram_kb="$(awk '/MemTotal/ {print $2}' /proc/meminfo)"
|
|
|
|
|
ram_gb=$(( (ram_kb + 1048575) / 1048576 ))
|
|
|
|
|
|
|
|
|
|
# --- root filesystem storage: type + size ---
|
|
|
|
|
storage_type="TODO"
|
|
|
|
|
storage_size_gb="TODO"
|
|
|
|
|
if have lsblk && have findmnt; then
|
|
|
|
|
root_src="$(findmnt -no SOURCE / 2>/dev/null || true)"
|
|
|
|
|
root_dev="$(lsblk -no PKNAME "$root_src" 2>/dev/null || true)"
|
|
|
|
|
[ -z "$root_dev" ] && root_dev="$(basename "${root_src:-}" 2>/dev/null | sed -E 's/p?[0-9]+$//')"
|
|
|
|
|
if [ -n "$root_dev" ] && [ -b "/dev/$root_dev" ]; then
|
|
|
|
|
size_bytes="$(lsblk -bno SIZE "/dev/$root_dev" 2>/dev/null | head -1)"
|
|
|
|
|
if [ -n "$size_bytes" ]; then
|
|
|
|
|
storage_size_gb=$(( size_bytes / 1073741824 ))
|
|
|
|
|
fi
|
|
|
|
|
case "$root_dev" in
|
|
|
|
|
nvme*) storage_type="nvme" ;;
|
|
|
|
|
mmcblk*) storage_type="emmc" ;;
|
|
|
|
|
*)
|
|
|
|
|
if [ -r "/sys/block/$root_dev/queue/rotational" ]; then
|
|
|
|
|
if [ "$(cat "/sys/block/$root_dev/queue/rotational")" = "0" ]; then
|
|
|
|
|
storage_type="ssd"
|
|
|
|
|
else
|
|
|
|
|
storage_type="hdd"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-04 02:58:51 +00:00
|
|
|
# --- network device naming: sysfs first, lspci/lsusb layered on top ---
|
|
|
|
|
# lspci/lsusb aren't guaranteed to be installed (pciutils/usbutils are
|
|
|
|
|
# separate packages, often missing on minimal/live-boot systems) — but
|
|
|
|
|
# /sys is always there. Same approach as finding a chip by hand: read
|
|
|
|
|
# vendor/device IDs straight from sysfs, and use lspci/lsusb only when
|
|
|
|
|
# present to turn a hex ID like 0x8086 into "Intel" instead of guessing.
|
|
|
|
|
|
|
|
|
|
pci_vendor_name() {
|
|
|
|
|
case "$1" in
|
|
|
|
|
0x8086) echo "Intel" ;;
|
|
|
|
|
0x14e4) echo "Broadcom" ;;
|
|
|
|
|
0x10ec) echo "Realtek" ;;
|
|
|
|
|
0x168c|0x1969) echo "Qualcomm Atheros" ;;
|
|
|
|
|
0x02df|0x0e8d) echo "MediaTek" ;;
|
|
|
|
|
0x1814) echo "Ralink" ;;
|
|
|
|
|
*) echo "" ;;
|
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
usb_vendor_name() {
|
|
|
|
|
case "$1" in
|
|
|
|
|
8086) echo "Intel" ;;
|
|
|
|
|
0a5c) echo "Broadcom" ;;
|
|
|
|
|
0bda) echo "Realtek" ;;
|
|
|
|
|
0cf3) echo "Qualcomm Atheros" ;;
|
|
|
|
|
148f) echo "Ralink/MediaTek" ;;
|
|
|
|
|
*) echo "" ;;
|
|
|
|
|
esac
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# describe_net_device IFACE — best-effort human description of the hardware
|
|
|
|
|
# backing a network interface. Walks /sys/class/net/$iface/device to find
|
|
|
|
|
# whether it's a PCI, USB, or SDIO device, reads the IDs directly, and only
|
|
|
|
|
# reaches for lspci/lsusb (if installed) to prettify the name.
|
|
|
|
|
describe_net_device() {
|
|
|
|
|
local iface="$1" devpath realpath driver="" name=""
|
|
|
|
|
devpath="/sys/class/net/$iface/device"
|
|
|
|
|
[ -e "$devpath" ] || return 0
|
|
|
|
|
realpath="$(readlink -f "$devpath")"
|
|
|
|
|
[ -r "$devpath/uevent" ] && driver="$(awk -F= '/^DRIVER=/ {print $2}' "$devpath/uevent")"
|
|
|
|
|
|
|
|
|
|
case "$realpath" in
|
|
|
|
|
*/pci*)
|
|
|
|
|
local vendor device pciid vname
|
|
|
|
|
vendor="$(cat "$devpath/vendor" 2>/dev/null)"
|
|
|
|
|
device="$(cat "$devpath/device" 2>/dev/null)"
|
|
|
|
|
# Not just basename: some drivers (virtio-net, and others that wrap a
|
|
|
|
|
# PCI function in a virtual sub-bus) resolve one level deeper than
|
|
|
|
|
# the actual PCI address, e.g. .../0000:00:12.0/virtio2 — the real
|
|
|
|
|
# slot is the LAST domain:bus:device.function-shaped path component,
|
|
|
|
|
# not necessarily the final one. Confirmed by hand: basename alone
|
|
|
|
|
# gave "virtio2", which lspci -s rejects as "Invalid slot number".
|
|
|
|
|
pciid="$(echo "$realpath" | grep -oE '[0-9a-f]{4}:[0-9a-f]{2}:[0-9a-f]{2}\.[0-9a-f]' | tail -1)"
|
|
|
|
|
if have lspci && [ -n "$pciid" ]; then
|
|
|
|
|
name="$(lspci -s "$pciid" 2>/dev/null | sed -E 's/^[0-9a-f:.]+ [^:]+: //')"
|
|
|
|
|
fi
|
|
|
|
|
if [ -z "$name" ] && [ -n "$vendor" ]; then
|
|
|
|
|
vname="$(pci_vendor_name "$vendor")"
|
|
|
|
|
name="${vname:-PCI vendor $vendor} device $device"
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
*/usb*)
|
|
|
|
|
local usbdir idv idp vname
|
|
|
|
|
usbdir="$realpath"
|
|
|
|
|
# The net device's "device" symlink usually points at the USB
|
|
|
|
|
# *interface*, not the device itself — walk up until we find the
|
|
|
|
|
# level that actually has idVendor/idProduct.
|
|
|
|
|
while [ -n "$usbdir" ] && [ "$usbdir" != "/" ] && [ ! -r "$usbdir/idVendor" ]; do
|
|
|
|
|
usbdir="$(dirname "$usbdir")"
|
|
|
|
|
done
|
|
|
|
|
if [ -r "$usbdir/idVendor" ]; then
|
|
|
|
|
idv="$(cat "$usbdir/idVendor" 2>/dev/null)"
|
|
|
|
|
idp="$(cat "$usbdir/idProduct" 2>/dev/null)"
|
|
|
|
|
if have lsusb; then
|
|
|
|
|
name="$(lsusb -d "${idv}:${idp}" 2>/dev/null | head -1 | sed -E 's/^Bus [0-9]+ Device [0-9]+: ID [0-9a-f]{4}:[0-9a-f]{4} //')"
|
|
|
|
|
fi
|
|
|
|
|
if [ -z "$name" ] && [ -n "$idv" ]; then
|
|
|
|
|
vname="$(usb_vendor_name "$idv")"
|
|
|
|
|
name="${vname:-USB vendor $idv} device $idp"
|
|
|
|
|
fi
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
*/sdio*)
|
|
|
|
|
# SDIO WiFi (common on Raspberry Pi and other ARM SBCs, e.g. Broadcom
|
|
|
|
|
# BCM43455) has no standard human-readable name in sysfs — the driver
|
|
|
|
|
# (brcmfmac, etc.) is usually the most reliable identifier here.
|
|
|
|
|
local vendor device
|
|
|
|
|
vendor="$(cat "$devpath/vendor" 2>/dev/null)"
|
|
|
|
|
device="$(cat "$devpath/device" 2>/dev/null)"
|
|
|
|
|
[ -n "$vendor" ] && name="SDIO vendor $vendor device $device"
|
|
|
|
|
;;
|
|
|
|
|
esac
|
|
|
|
|
|
|
|
|
|
if [ -n "$name" ] && [ -n "$driver" ]; then
|
|
|
|
|
echo "$name (driver: $driver)"
|
|
|
|
|
elif [ -n "$name" ]; then
|
|
|
|
|
echo "$name"
|
|
|
|
|
elif [ -n "$driver" ]; then
|
|
|
|
|
echo "driver: $driver (chip model not identified)"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# --- WiFi chip ---
|
2026-09-04 02:49:02 +00:00
|
|
|
wifi_chip="none"
|
|
|
|
|
wifi_iface=""
|
|
|
|
|
for w in /sys/class/net/*/wireless; do
|
|
|
|
|
[ -d "$w" ] || continue
|
|
|
|
|
wifi_iface="$(basename "$(dirname "$w")")"
|
|
|
|
|
break
|
|
|
|
|
done
|
|
|
|
|
if [ -n "$wifi_iface" ]; then
|
2026-09-04 02:58:51 +00:00
|
|
|
wifi_chip="$(describe_net_device "$wifi_iface")"
|
2026-09-04 02:49:02 +00:00
|
|
|
[ -z "$wifi_chip" ] && wifi_chip="TODO — wireless interface $wifi_iface found but chip not identified"
|
|
|
|
|
fi
|
|
|
|
|
|
2026-09-04 02:58:51 +00:00
|
|
|
# --- Ethernet chip: first non-wireless, non-loopback interface with a real
|
|
|
|
|
# device behind it (best-effort — on a box with a cellular modem and no
|
|
|
|
|
# wired NIC this could pick the modem; double-check if that's your setup) ---
|
2026-09-04 02:49:02 +00:00
|
|
|
ethernet=""
|
2026-09-04 02:58:51 +00:00
|
|
|
for e in /sys/class/net/*; do
|
|
|
|
|
iface="$(basename "$e")"
|
|
|
|
|
[ "$iface" = "lo" ] && continue
|
|
|
|
|
[ -d "$e/wireless" ] && continue
|
|
|
|
|
[ -e "$e/device" ] || continue
|
|
|
|
|
ethernet="$(describe_net_device "$iface")"
|
|
|
|
|
[ -n "$ethernet" ] && break
|
|
|
|
|
done
|
2026-09-04 02:49:02 +00:00
|
|
|
|
|
|
|
|
today="$(date +%F 2>/dev/null || echo "TODO")"
|
|
|
|
|
|
|
|
|
|
cat <<YAML
|
|
|
|
|
# Auto-gathered by gather-hardware-info.sh — check every field, especially
|
|
|
|
|
# any marked TODO, then paste this into:
|
|
|
|
|
# https://hcl.archipelago-foundation.org/report.html (fill the form instead — easiest)
|
|
|
|
|
# or add it directly as data/reports/<slug>.yml in a PR — see CONTRIBUTING.md
|
|
|
|
|
device_model: "$device_model"
|
|
|
|
|
form_factor: $form_factor
|
|
|
|
|
cpu: "$cpu"
|
|
|
|
|
ram_gb: $ram_gb
|
|
|
|
|
storage:
|
|
|
|
|
type: $storage_type
|
|
|
|
|
size_gb: $storage_size_gb
|
|
|
|
|
wifi_chip: "$wifi_chip"
|
|
|
|
|
$( [ -n "$ethernet" ] && echo "ethernet: \"$ethernet\"" )
|
|
|
|
|
archy_version: "TODO — e.g. 1.8.10-alpha"
|
|
|
|
|
install_method: TODO # usb-iso | netboot | existing-os-script | other
|
|
|
|
|
status: TODO # working | partial | broken
|
|
|
|
|
tested_date: "$today"
|
|
|
|
|
YAML
|