feat(platform): 多平台检测/路径/信号/系统信息(含跨平台构建验证)
This commit is contained in:
1 parent
efed5f05b1
commit
1d4bed415f
6 files changed
+276
No files matched your search
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !windows
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func shutdownSignals() []os.Signal {
|
||||
return []os.Signal{os.Interrupt, syscall.SIGTERM}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
Reference in new issue
Block a user