refactor: split platform-specific update code into build-tagged files

Amolith created

Moves binary validation and binaryName const into platform-specific files
(update_unix.go, update_darwin.go, update_windows.go) to avoid compiling
debug/elf, debug/macho, debug/pe on platforms that don't need them.

Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>

Change summary

internal/update/update.go         | 60 ---------------------------------
internal/update/update_darwin.go  | 23 ++++++++++++
internal/update/update_unix.go    | 23 ++++++++++++
internal/update/update_windows.go | 21 +++++++++++
4 files changed, 67 insertions(+), 60 deletions(-)

Detailed changes

internal/update/update.go 🔗

@@ -7,9 +7,6 @@ import (
 	"compress/gzip"
 	"context"
 	"crypto/sha256"
-	"debug/elf"
-	"debug/macho"
-	"debug/pe"
 	"encoding/hex"
 	"encoding/json"
 	"fmt"
@@ -285,53 +282,6 @@ func Download(ctx context.Context, asset *Asset, release *Release) (string, erro
 	return binaryPath, nil
 }
 
-// validateBinary checks that the file at path is a valid executable binary
-// for the current platform using the standard library debug packages.
-func validateBinary(path string) error {
-	switch runtime.GOOS {
-	case "windows":
-		return validatePE(path)
-	case "darwin":
-		return validateMachO(path)
-	default:
-		return validateELF(path)
-	}
-}
-
-func validateELF(path string) error {
-	f, err := elf.Open(path)
-	if err != nil {
-		return fmt.Errorf("not a valid ELF binary: %w", err)
-	}
-	defer f.Close()
-	if f.Type != elf.ET_EXEC && f.Type != elf.ET_DYN {
-		return fmt.Errorf("ELF file is not an executable (type: %v)", f.Type)
-	}
-	return nil
-}
-
-func validateMachO(path string) error {
-	f, err := macho.Open(path)
-	if err != nil {
-		return fmt.Errorf("not a valid Mach-O binary: %w", err)
-	}
-	defer f.Close()
-	if f.Type != macho.TypeExec {
-		return fmt.Errorf("Mach-O file is not an executable (type: %v)", f.Type)
-	}
-	return nil
-}
-
-func validatePE(path string) error {
-	f, err := pe.Open(path)
-	if err != nil {
-		return fmt.Errorf("not a valid PE binary: %w", err)
-	}
-	defer f.Close()
-	// PE files opened successfully are valid executables.
-	return nil
-}
-
 // downloadChecksums downloads and parses the checksums.txt file.
 // Returns a map of filename to sha256 checksum.
 func downloadChecksums(ctx context.Context, client *http.Client, asset *Asset) (map[string]string, error) {
@@ -396,11 +346,6 @@ func extractZip(archivePath string) (string, error) {
 	}
 	defer r.Close()
 
-	binaryName := "crush"
-	if runtime.GOOS == "windows" {
-		binaryName = "crush.exe"
-	}
-
 	for _, f := range r.File {
 		// Path traversal protection.
 		cleanName := filepath.Clean(f.Name)
@@ -477,11 +422,6 @@ func extractTarGz(archivePath string) (string, error) {
 
 	tr := tar.NewReader(gzr)
 
-	binaryName := "crush"
-	if runtime.GOOS == "windows" {
-		binaryName = "crush.exe"
-	}
-
 	for {
 		header, err := tr.Next()
 		if err == io.EOF {

internal/update/update_darwin.go 🔗

@@ -0,0 +1,23 @@
+//go:build darwin
+
+package update
+
+import (
+	"debug/macho"
+	"fmt"
+)
+
+const binaryName = "crush"
+
+// validateBinary checks that the file at path is a valid Mach-O executable.
+func validateBinary(path string) error {
+	f, err := macho.Open(path)
+	if err != nil {
+		return fmt.Errorf("not a valid Mach-O binary: %w", err)
+	}
+	defer f.Close()
+	if f.Type != macho.TypeExec {
+		return fmt.Errorf("Mach-O file is not an executable (type: %v)", f.Type)
+	}
+	return nil
+}

internal/update/update_unix.go 🔗

@@ -0,0 +1,23 @@
+//go:build !windows && !darwin
+
+package update
+
+import (
+	"debug/elf"
+	"fmt"
+)
+
+const binaryName = "crush"
+
+// validateBinary checks that the file at path is a valid ELF executable.
+func validateBinary(path string) error {
+	f, err := elf.Open(path)
+	if err != nil {
+		return fmt.Errorf("not a valid ELF binary: %w", err)
+	}
+	defer f.Close()
+	if f.Type != elf.ET_EXEC && f.Type != elf.ET_DYN {
+		return fmt.Errorf("ELF file is not an executable (type: %v)", f.Type)
+	}
+	return nil
+}

internal/update/update_windows.go 🔗

@@ -0,0 +1,21 @@
+//go:build windows
+
+package update
+
+import (
+	"debug/pe"
+	"fmt"
+)
+
+const binaryName = "crush.exe"
+
+// validateBinary checks that the file at path is a valid PE executable.
+func validateBinary(path string) error {
+	f, err := pe.Open(path)
+	if err != nil {
+		return fmt.Errorf("not a valid PE binary: %w", err)
+	}
+	defer f.Close()
+	// PE files opened successfully are valid executables.
+	return nil
+}