feat(platform): 多平台检测/路径/信号/系统信息(含跨平台构建验证)

This commit is contained in:
Aska committed 2026-09-06 04:17:20 +08:00
1 parent efed5f05b1
commit 1d4bed415f
6 files changed
+276

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
package platform
import (
"os"
"path/filepath"
"runtime"
)
// DefaultDataDir returns the OS-appropriate default data directory when the
// user has not configured one. It never returns an empty path; on failure it
// falls back to a relative "./data" directory.
func DefaultDataDir() string {
home, err := os.UserHomeDir()
if err != nil {
return filepath.Join(".", "data")
}
switch runtime.GOOS {
case "windows":
if appdata := os.Getenv("APPDATA"); appdata != "" {
return filepath.Join(appdata, "Cyrene")
}
return filepath.Join(home, "AppData", "Roaming", "Cyrene")
case "darwin":
return filepath.Join(home, "Library", "Application Support", "Cyrene")
default:
if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" {
return filepath.Join(xdg, "cyrene")
}
return filepath.Join(home, ".local", "share", "cyrene")
}
}
+108
View File
@@ -0,0 +1,108 @@
// Package platform centralizes OS detection, path resolution, signal handling,
// and runtime system information. Other packages must never call syscall or
// hard-code OS paths; they resolve everything through this package.
package platform
import (
"os"
"runtime"
"strconv"
)
// Info describes the host platform.
type Info struct {
Name string // human-readable platform name
GOOS string
GOARCH string
Kind string // platform family: desktop / server / mobile / unknown
CPUs int
Host string
GoVer string
}
// Detect returns runtime-derived platform information.
func Detect() Info {
return Info{
Name: platformName(runtime.GOOS),
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
Kind: platformKind(runtime.GOOS),
CPUs: runtime.NumCPU(),
Host: hostname(),
GoVer: runtime.Version(),
}
}
// GOOS returns the operating system name (runtime.GOOS).
func GOOS() string { return runtime.GOOS }
// GOARCH returns the CPU architecture name (runtime.GOARCH).
func GOARCH() string { return runtime.GOARCH }
// CPUs returns the number of logical CPUs visible to the runtime.
func CPUs() int { return runtime.NumCPU() }
// IsWindows reports whether the host is Windows.
func IsWindows() bool { return runtime.GOOS == "windows" }
// IsDarwin reports whether the host is macOS.
func IsDarwin() bool { return runtime.GOOS == "darwin" }
// IsLinux reports whether the host is Linux.
func IsLinux() bool { return runtime.GOOS == "linux" }
// IsUnix reports whether the host is a Unix-like OS (linux, darwin, etc.).
func IsUnix() bool { return runtime.GOOS != "windows" }
// CPUArch converts runtime.GOARCH to a compact architecture label.
func CPUArch() string { return archLabel(runtime.GOARCH) }
// archLabel maps a GOARCH value to a compact human-readable label. Unknown
// values pass through unchanged.
func archLabel(goarch string) string {
switch goarch {
case "amd64":
return "x86_64"
case "386":
return "x86"
default:
return goarch
}
}
// Describe renders a short log-ready summary of the runtime.
func (i Info) Describe() string {
return i.Name + "/" + i.GOOS + "/" + i.GOARCH + " cpu=" + strconv.Itoa(i.CPUs) + " go=" + i.GoVer
}
func platformName(goos string) string {
switch goos {
case "windows":
return "Windows"
case "darwin":
return "macOS"
case "linux":
return "Linux"
default:
return goos
}
}
func platformKind(goos string) string {
switch goos {
case "windows", "darwin":
return "desktop"
case "linux":
return "server"
default:
return "unknown"
}
}
func hostname() string {
h, err := os.Hostname()
if err != nil {
return "unknown"
}
return h
}
+99
View File
@@ -0,0 +1,99 @@
package platform
import (
"context"
"path/filepath"
"runtime"
"testing"
)
func TestDetect_MatchesRuntime(t *testing.T) {
t.Parallel()
info := Detect()
if info.GOOS != runtime.GOOS {
t.Errorf("Detect().GOOS = %q, want %q", info.GOOS, runtime.GOOS)
}
if info.GOARCH != runtime.GOARCH {
t.Errorf("Detect().GOARCH = %q, want %q", info.GOARCH, runtime.GOARCH)
}
if info.CPUs <= 0 {
t.Errorf("Detect().CPUs = %d, want > 0", info.CPUs)
}
if info.Name == "" {
t.Error("Detect().Name is empty")
}
}
func TestOSPredicates_ConsistentWithRuntime(t *testing.T) {
t.Parallel()
wantWindows := runtime.GOOS == "windows"
wantDarwin := runtime.GOOS == "darwin"
wantLinux := runtime.GOOS == "linux"
if IsWindows() != wantWindows {
t.Errorf("IsWindows() = %v, want %v", IsWindows(), wantWindows)
}
if IsDarwin() != wantDarwin {
t.Errorf("IsDarwin() = %v, want %v", IsDarwin(), wantDarwin)
}
if IsLinux() != wantLinux {
t.Errorf("IsLinux() = %v, want %v", IsLinux(), wantLinux)
}
if IsUnix() != (runtime.GOOS != "windows") {
t.Errorf("IsUnix() = %v, want %v", IsUnix(), runtime.GOOS != "windows")
}
}
func TestArchLabel_Mapping(t *testing.T) {
t.Parallel()
tests := []struct {
goarch string
want string
}{
{goarch: "amd64", want: "x86_64"},
{goarch: "arm64", want: "arm64"},
{goarch: "386", want: "x86"},
{goarch: "riscv64", want: "riscv64"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.goarch, func(t *testing.T) {
t.Parallel()
if got := archLabel(tt.goarch); got != tt.want {
t.Fatalf("archLabel(%q) = %q, want %q", tt.goarch, got, tt.want)
}
})
}
}
func TestDefaultDataDir_XDGOnLinux(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("XDG_DATA_HOME only applies on linux")
}
t.Setenv("XDG_DATA_HOME", "/tmp/cyrene-xdg")
got := DefaultDataDir()
want := filepath.Join("/tmp/cyrene-xdg", "cyrene")
if got != want {
t.Errorf("DefaultDataDir() = %q, want %q", got, want)
}
}
func TestSignalContext_Cancelable(t *testing.T) {
t.Parallel()
ctx, cancel := SignalContext()
if ctx == nil {
t.Fatal("SignalContext() returned nil context")
}
if cancel == nil {
t.Fatal("SignalContext() returned nil cancel func")
}
cancel()
if ctx.Err() != context.Canceled {
t.Errorf("ctx.Err() = %v, want context.Canceled", ctx.Err())
}
}
+14
View File
@@ -0,0 +1,14 @@
package platform
import (
"context"
"os/signal"
)
// SignalContext returns a context canceled when an interactive termination
// signal arrives: Ctrl+C on every OS, plus SIGTERM on Unix. It is the single
// graceful-shutdown hook for the whole process.
func SignalContext() (context.Context, context.CancelFunc) {
ctx, cancel := signal.NotifyContext(context.Background(), shutdownSignals()...)
return ctx, cancel
}
+12
View File
@@ -0,0 +1,12 @@
//go:build !windows
package platform
import (
"os"
"syscall"
)
func shutdownSignals() []os.Signal {
return []os.Signal{os.Interrupt, syscall.SIGTERM}
}
+11
View File
@@ -0,0 +1,11 @@
//go:build windows
package platform
import "os"
func shutdownSignals() []os.Signal {
// Windows has no SIGTERM equivalent; Ctrl+C (os.Interrupt) is the only
// portable interactive termination signal.
return []os.Signal{os.Interrupt}
}