Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
check:
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04-arm]
os: [ubuntu-latest, ubuntu-24.04-arm, macos-15, macos-15-intel]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
Expand All @@ -20,15 +20,22 @@ jobs:
- run: make lint test build
- name: Run without root and verify JSON
run: |
case "$(uname -m)" in aarch64) arch=arm64 ;; x86_64) arch=amd64 ;; esac
case "$(uname -m)" in aarch64|arm64) arch=arm64 ;; x86_64) arch=amd64 ;; esac
case "$(uname -s)" in Linux) platform=linux ;; Darwin) platform=darwin ;; esac
version=$(sed -n 's/^const Version = "\([^"]*\)"/\1/p' version.go)
./dist/sysinfo-$version-linux-$arch > /tmp/sysinfo.json
./dist/sysinfo-$version-$platform-$arch > /tmp/sysinfo.json
python3 - <<'PYTHON'
import json, os
import json, os, sys
with open('/tmp/sysinfo.json') as f:
report = json.load(f)
assert report['runtime']['page_size_bytes'] == os.sysconf('SC_PAGE_SIZE')
assert report['runtime']['resource_limits']
assert report['runtime']['libc']
assert report['runtime']['vm']['overcommit_memory'] in ['0', '1', '2']
if sys.platform == 'linux':
assert report['runtime']['libc']
assert report['runtime']['vm']['overcommit_memory'] in ['0', '1', '2']
else:
assert report['os']['name'] == 'macOS'
assert report['os']['version']
assert report['kernel']['release']
assert report['memory']['size'] > 0
PYTHON
7 changes: 4 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

## 项目定位与边界

Linux 系统信息 CLI,基于 zcalusic/sysinfo 增强。直接执行输出 JSON,辅助判断二进制构建与运行环境的兼容性;不自动作兼容性结论。
Linux / macOS 系统信息 CLI,基于 zcalusic/sysinfo 增强。直接执行输出 JSON,辅助判断二进制构建与运行环境的兼容性;不自动作兼容性结论。

## 代码地图

```text
cmd/sysinfo/ # CLI 入口,输出一次 JSON
*.go # 沿用上游系统信息采集结构
runtime.go # 基础页大小、资源限制、内存与内核策略
runtime*.go # 基础页大小与平台运行环境
platform_*.go # Linux / macOS 采集入口
libc.go # 静态读取已安装 libc 的版本证据
cpuid/ # 上游 CPU 指令封装
```

## 关键约定

- 采集只读,不创建或修复系统文件,不执行外部程序。
- 标准库实现;发布使用 CGO_ENABLED=0,不依赖客户机的 libc 或语言运行时。
- 标准库实现;发布使用 CGO_ENABLED=0,Linux 静态链接,macOS 仅使用系统自带库,不依赖额外语言运行时。
- 缺失证据不转换为正常默认值;新增运行环境采集错误保存在 runtime.errors。
- 保留上游版权及许可证。版本由 version.go 管理。
- 验证入口:make fix、make lint、make test、make build;build 一次产出全部支持架构,跨编译不等于客户机运行验证。
Expand Down
12 changes: 10 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ TEST_WORKERS ?= 4
LINT_WORKERS ?= 4

.DEFAULT_GOAL := build
.PHONY: fix lint test build build-all build-linux-amd64 build-linux-arm64
.PHONY: fix lint test build build-all build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64
fix:
$(GO) fmt ./...

Expand All @@ -16,13 +16,15 @@ lint:
$(GO) vet -p $(LINT_WORKERS) ./...
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) vet -p $(LINT_WORKERS) ./...
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) vet -p $(LINT_WORKERS) ./...
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 $(GO) vet -p $(LINT_WORKERS) ./...
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GO) vet -p $(LINT_WORKERS) ./...

test:
$(GO) test -p $(TEST_WORKERS) -parallel 1 $(TEST_PACKAGES)

# One static binary per architecture serves supported Linux distributions and
# kernels. Do not create kernel/glibc-labelled copies of the same executable.
build: build-linux-amd64 build-linux-arm64
build: build-linux-amd64 build-linux-arm64 build-darwin-amd64 build-darwin-arm64

build-all: build

Expand All @@ -31,3 +33,9 @@ build-linux-amd64:

build-linux-arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM64=v8.0 $(GO) build -trimpath -ldflags='-s -w' -o "$(DIST_DIR)/$(ARTIFACT_PREFIX)-linux-arm64" ./cmd/sysinfo

build-darwin-amd64:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 GOAMD64=v1 $(GO) build -trimpath -ldflags='-s -w' -o "$(DIST_DIR)/$(ARTIFACT_PREFIX)-darwin-amd64" ./cmd/sysinfo

build-darwin-arm64:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 GOARM64=v8.0 $(GO) build -trimpath -ldflags='-s -w' -o "$(DIST_DIR)/$(ARTIFACT_PREFIX)-darwin-arm64" ./cmd/sysinfo
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# sysinfo

sysinfo is a standalone, read-only Linux CLI for collecting the system facts needed
sysinfo is a standalone, read-only Linux and macOS CLI for collecting the system facts needed
to build, distribute and troubleshoot native applications. It gathers OS, kernel,
CPU, memory, libc and process-limit information into a single JSON report, so users
can compare environments, choose suitable build targets and investigate startup failures.
Expand All @@ -14,23 +14,36 @@ Run it without arguments to print the report; redirect stdout to save it.

```sh
./sysinfo-1.1.4-linux-arm64 > sysinfo.json
# On x86-64:
# On Linux x86-64:
./sysinfo-1.1.4-linux-amd64 > sysinfo.json
# On Apple Silicon macOS:
./sysinfo-1.1.4-darwin-arm64 > sysinfo.json
```

The binary needs no Bun, Node, Python, shell utilities, or installed libc.
The binary needs no Bun, Node, Python or shell utilities. Linux builds are static;
macOS builds use only system libraries shipped with macOS.
It does not execute subprocesses or modify system files. Root is optional;
firmware/serial information may be unavailable without permission.

## Information collected

On Linux:

- OS distribution, kernel and userland architecture, CPU and common feature flags.
- Hardware inventory inherited from upstream: firmware, memory, storage and network.
- Actual base page size in bytes (including 64 KiB ARM64 systems), independently of huge pages.
- Installed libc candidates: resolved path, ELF machine, release banner and exported ABI versions.
- This process's soft/hard resource limits, including virtual address space and core dumps.
- `/proc/meminfo`, overcommit policy/ratio/size, mapping limits and cgroup membership.

On macOS, native system APIs collect the OS version/build, Darwin kernel, host and
process architectures, CPU model/core counts, physical memory, base page size,
resource limits and network interfaces. Linux-specific libc, overcommit and cgroup
fields remain empty, with platform notes in the report. Firmware/storage inventory
and ARM CPU feature flags are not collected on macOS.
Under Rosetta, the report identifies translation and separates the ARM64 host
from the AMD64 process. Page size and resource limits describe the running process.

Reports describe the environment visible to the collector. Run it from the same
shell/container/service context as the failing program when investigating limits.
Memory data comes from procfs; cgroup membership is included as context, but cgroup
Expand Down Expand Up @@ -58,21 +71,23 @@ make test
make build
# dist/sysinfo-1.1.4-linux-amd64
# dist/sysinfo-1.1.4-linux-arm64
# dist/sysinfo-1.1.4-darwin-amd64
# dist/sysinfo-1.1.4-darwin-arm64

# Optional: build a single architecture, or choose an output directory.
make build-linux-arm64
make build DIST_DIR=/tmp/sysinfo-dist
```

`make` and `make build-all` also build both architectures. Artifact versions come
`make` and `make build-all` also build all four platform binaries. Artifact versions come
from `version.go`, matching the version in the JSON report. AMD64 uses the v1 CPU
baseline; ARM64 uses ARMv8.0. Both Linux builds use `CGO_ENABLED=0`, so there are no
separate Debian/Kylin, glibc or kernel-labelled binaries. Go's documented Linux
kernel requirements are listed in [Minimum Requirements](https://go.dev/wiki/MinimumRequirements);
the collector also needs access to procfs/sysfs for the corresponding fields.

macOS can cross-compile and run parser tests;
the CLI itself supports Linux only. Kernel/distribution and page-size combinations
All four builds use `CGO_ENABLED=0`. macOS binaries use native sysctl and resource-limit
APIs without invoking external commands. Kernel/distribution and page-size combinations
still require runtime validation on those systems.

## Origin and license
Expand Down
6 changes: 1 addition & 5 deletions cmd/sysinfo/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ import (
"fmt"
"log"
"os"
"runtime"

"github.com/compforge/sysinfo"
)

func main() {
if len(os.Args) != 1 {
fmt.Fprintln(os.Stderr, "usage: sysinfo (prints Linux system information as JSON)")
fmt.Fprintln(os.Stderr, "usage: sysinfo (prints system information as JSON)")
os.Exit(2)
}
if runtime.GOOS != "linux" {
log.Fatal("sysinfo supports Linux only")
}

var si sysinfo.SysInfo

Expand Down
12 changes: 12 additions & 0 deletions kernel_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package sysinfo

import "syscall"

// Kernel information.
type Kernel struct {
Release string `json:"release,omitempty"`
Expand All @@ -11,4 +13,14 @@ type Kernel struct {
}

func (si *SysInfo) getKernelInfo() {
si.Kernel.Release = si.sysctlString("kern.osrelease")
si.Kernel.Version = si.sysctlString("kern.version")
si.Kernel.Architecture = si.sysctlString("hw.machine")
// Under Rosetta hw.machine may describe the translated process. ARM64
// support or the translation flag identifies the native host;
// runtime.process_architecture still describes the running executable.
arm64, _ := syscall.SysctlUint32("hw.optional.arm64")
if arm64 == 1 || si.Runtime.Translation == "rosetta2" {
si.Kernel.Architecture = "arm64"
}
}
130 changes: 130 additions & 0 deletions platform_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package sysinfo

import (
"encoding/binary"
"fmt"
"net"
"os"
"strconv"
"strings"
"syscall"
"time"
)

func (si *SysInfo) sysctlString(name string) string {
value, err := syscall.Sysctl(name)
if err != nil {
si.Runtime.Errors[name] = err.Error()
}
return strings.TrimSpace(value)
}

func (si *SysInfo) sysctlUint32(name string) uint32 {
value, err := syscall.SysctlUint32(name)
if err != nil {
si.Runtime.Errors[name] = err.Error()
}
return value
}

func (si *SysInfo) getPlatformInfo() {
si.getKernelInfo()
si.OS = OS{Name: "macOS", Vendor: "Apple", Version: si.sysctlString("kern.osproductversion"),
Release: si.sysctlString("kern.osversion"), Architecture: si.Kernel.Architecture}
var err error
si.Node.Hostname, err = os.Hostname()
if err != nil {
si.Runtime.Errors["hostname"] = err.Error()
}
si.Node.Timezone, _ = time.Now().Zone()
si.Product.Vendor = "Apple"
si.Product.Name = si.sysctlString("hw.model")
si.CPU.Model = si.sysctlString("machdep.cpu.brand_string")
si.CPU.Cpus = uint(si.sysctlUint32("hw.packages"))
si.CPU.Cores = uint(si.sysctlUint32("hw.physicalcpu"))
si.CPU.Threads = uint(si.sysctlUint32("hw.logicalcpu"))
// x86 feature strings are optional on Apple Silicon. Do not translate
// unrelated ARM capability keys into x86 instruction-set claims.
if value, err := syscall.Sysctl("machdep.cpu.vendor"); err == nil {
si.CPU.Vendor = value
}
for _, name := range []string{"machdep.cpu.features", "machdep.cpu.leaf7_features", "machdep.cpu.extfeatures"} {
if value, err := syscall.Sysctl(name); err == nil {
si.CPU.Features = append(si.CPU.Features, strings.Fields(strings.ToLower(value))...)
}
}
if value, err := sysctlUint64("hw.memsize"); err == nil {
si.Memory.Size = uint(value / (1024 * 1024))
si.Runtime.Memory["physical_bytes"] = strconv.FormatUint(value, 10)
} else {
si.Runtime.Errors["hw.memsize"] = err.Error()
}
interfaces, err := net.Interfaces()
if err != nil {
si.Runtime.Errors["network_interfaces"] = err.Error()
return
}
for _, iface := range interfaces {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
si.Network = append(si.Network, NetworkDevice{Name: iface.Name, MACAddress: iface.HardwareAddr.String()})
}
}

func sysctlUint64(name string) (uint64, error) {
value, err := syscall.Sysctl(name)
if err != nil {
return 0, err
}
return decodeSysctlUint64(value)
}

func decodeSysctlUint64(value string) (uint64, error) {
// syscall.Sysctl removes one trailing NUL even for binary data. Restore
// that byte for uint64 values. Both supported Darwin architectures are LE.
if len(value) != 7 && len(value) != 8 {
return 0, fmt.Errorf("expected 8-byte sysctl value, got %d bytes", len(value))
}
var data [8]byte
copy(data[:], value)
return binary.LittleEndian.Uint64(data[:]), nil
}

func (si *SysInfo) getPlatformRuntime() {
if translated, err := syscall.SysctlUint32("sysctl.proc_translated"); err == nil && translated == 1 {
si.Runtime.Translation = "rosetta2"
}
si.Runtime.Notes = []string{
"macOS uses the system libSystem runtime; Linux libc, overcommit and cgroup fields do not apply.",
"macOS firmware, storage inventory and ARM CPU feature flags are not collected.",
}
for _, limit := range []struct {
name string
resource int
unit string
}{
{"Max cpu time", syscall.RLIMIT_CPU, "seconds"},
{"Max file size", syscall.RLIMIT_FSIZE, "bytes"},
{"Max data size", syscall.RLIMIT_DATA, "bytes"},
{"Max stack size", syscall.RLIMIT_STACK, "bytes"},
{"Max core file size", syscall.RLIMIT_CORE, "bytes"},
{"Max address space", syscall.RLIMIT_AS, "bytes"},
{"Max open files", syscall.RLIMIT_NOFILE, "files"},
} {
var value syscall.Rlimit
if err := syscall.Getrlimit(limit.resource, &value); err != nil {
si.Runtime.Errors[limit.name] = err.Error()
continue
}
si.Runtime.Limits = append(si.Runtime.Limits, ResourceLimit{Name: limit.name,
Soft: darwinLimit(value.Cur), Hard: darwinLimit(value.Max), Unit: limit.unit})
}
}

func darwinLimit(value uint64) string {
if value == uint64(syscall.RLIM_INFINITY) {
return "unlimited"
}
return strconv.FormatUint(value, 10)
}
Loading
Loading