From 3279a6ca5307398e9141918a2dcd34ac31f70668 Mon Sep 17 00:00:00 2001 From: henrygd Date: Mon, 12 Jan 2026 15:38:13 -0500 Subject: [PATCH] agent: add separate glibc build with NVML support (#1618) purego requires dynamic linking, so split the agent builds: - Default: static binary without NVML (works on musl/alpine) - Glibc: dynamic binary with NVML support via purego Changes: - Add glibc build tag to conditionally include NVML code - Add beszel-agent-linux-amd64-glibc build/archive in goreleaser - Update ghupdate to use glibc binary on glibc systems - Switch nvidia dockerfile to golang:bookworm with -tags glibc --- .goreleaser.yml | 21 +++++++++++++++++++++ agent/gpu.go | 3 +-- agent/gpu_nvml.go | 2 +- agent/gpu_nvml_linux.go | 2 +- agent/gpu_nvml_unsupported.go | 2 +- agent/systemd_nonlinux_test.go | 4 ++-- internal/dockerfile_agent_nvidia | 4 ++-- internal/ghupdate/ghupdate.go | 28 ++++++++++++++++++++++++++++ 8 files changed, 57 insertions(+), 9 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 88787019..c14f6e5d 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -76,6 +76,18 @@ builds: - goos: windows goarch: riscv64 + - id: beszel-agent-linux-amd64-glibc + binary: beszel-agent + main: internal/cmd/agent/agent.go + env: + - CGO_ENABLED=0 + flags: + - -tags=glibc + goos: + - linux + goarch: + - amd64 + archives: - id: beszel-agent formats: [tar.gz] @@ -89,6 +101,15 @@ archives: - goos: windows formats: [zip] + - id: beszel-agent-linux-amd64-glibc + formats: [tar.gz] + ids: + - beszel-agent-linux-amd64-glibc + name_template: >- + {{ .Binary }}_ + {{- .Os }}_ + {{- .Arch }}_glibc + - id: beszel formats: [tar.gz] ids: diff --git a/agent/gpu.go b/agent/gpu.go index dc5a6530..06752cdb 100644 --- a/agent/gpu.go +++ b/agent/gpu.go @@ -5,6 +5,7 @@ import ( "bytes" "encoding/json" "fmt" + "log/slog" "maps" "os/exec" "regexp" @@ -14,8 +15,6 @@ import ( "time" "github.com/henrygd/beszel/internal/entities/system" - - "log/slog" ) const ( diff --git a/agent/gpu_nvml.go b/agent/gpu_nvml.go index bf444745..a90c9c04 100644 --- a/agent/gpu_nvml.go +++ b/agent/gpu_nvml.go @@ -1,4 +1,4 @@ -//go:build (linux || windows) && amd64 +//go:build amd64 && (windows || (linux && glibc)) package agent diff --git a/agent/gpu_nvml_linux.go b/agent/gpu_nvml_linux.go index 44ad2b12..a2a95123 100644 --- a/agent/gpu_nvml_linux.go +++ b/agent/gpu_nvml_linux.go @@ -1,4 +1,4 @@ -//go:build linux && amd64 +//go:build glibc && linux && amd64 package agent diff --git a/agent/gpu_nvml_unsupported.go b/agent/gpu_nvml_unsupported.go index f32401d6..b8a6d40a 100644 --- a/agent/gpu_nvml_unsupported.go +++ b/agent/gpu_nvml_unsupported.go @@ -1,4 +1,4 @@ -//go:build (!linux && !windows) || !amd64 +//go:build (!linux && !windows) || !amd64 || (linux && !glibc) package agent diff --git a/agent/systemd_nonlinux_test.go b/agent/systemd_nonlinux_test.go index af4cda9e..a4e5b602 100644 --- a/agent/systemd_nonlinux_test.go +++ b/agent/systemd_nonlinux_test.go @@ -19,11 +19,11 @@ func TestSystemdManagerGetServiceStats(t *testing.T) { assert.NoError(t, err) // Test with refresh = true - result := manager.getServiceStats(true) + result := manager.getServiceStats("any-service", true) assert.Nil(t, result) // Test with refresh = false - result = manager.getServiceStats(false) + result = manager.getServiceStats("any-service", false) assert.Nil(t, result) } diff --git a/internal/dockerfile_agent_nvidia b/internal/dockerfile_agent_nvidia index 2602f467..58fa6020 100644 --- a/internal/dockerfile_agent_nvidia +++ b/internal/dockerfile_agent_nvidia @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM golang:alpine AS builder +FROM --platform=$BUILDPLATFORM golang:bookworm AS builder WORKDIR /app @@ -10,7 +10,7 @@ COPY . ./ # Build ARG TARGETOS TARGETARCH -RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-w -s" -o /agent ./internal/cmd/agent +RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -tags glibc -ldflags "-w -s" -o /agent ./internal/cmd/agent # -------------------------- # Smartmontools builder stage diff --git a/internal/ghupdate/ghupdate.go b/internal/ghupdate/ghupdate.go index 5cf9a175..8020583e 100644 --- a/internal/ghupdate/ghupdate.go +++ b/internal/ghupdate/ghupdate.go @@ -11,6 +11,7 @@ import ( "log/slog" "net/http" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -345,5 +346,32 @@ func archiveSuffix(binaryName, goos, goarch string) string { if goos == "windows" { return fmt.Sprintf("%s_%s_%s.zip", binaryName, goos, goarch) } + // Use glibc build for agent on glibc systems (includes NVML support via purego) + if binaryName == "beszel-agent" && goos == "linux" && goarch == "amd64" && isGlibc() { + return fmt.Sprintf("%s_%s_%s_glibc.tar.gz", binaryName, goos, goarch) + } return fmt.Sprintf("%s_%s_%s.tar.gz", binaryName, goos, goarch) } + +func isGlibc() bool { + for _, path := range []string{ + "/lib64/ld-linux-x86-64.so.2", // common on many distros + "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", // Debian/Ubuntu + "/lib/ld-linux-x86-64.so.2", // alternate + } { + if _, err := os.Stat(path); err == nil { + return true + } + } + // Fallback to ldd output when present (musl ldd reports musl, glibc reports GNU libc/glibc). + if lddPath, err := exec.LookPath("ldd"); err == nil { + out, err := exec.Command(lddPath, "--version").CombinedOutput() + if err == nil { + s := strings.ToLower(string(out)) + if strings.Contains(s, "gnu libc") || strings.Contains(s, "glibc") { + return true + } + } + } + return false +}