gosetup/cmd/update/main.go
sneak 7188d87c39 Initial commit: gosetup tool for installing Go dev binaries
Installs common Go development tools at pinned versions including
vim-go binaries, linters, formatters, and debugging tools.
2025-12-18 00:57:28 -08:00

170 lines
3.8 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
type Tool struct {
Name string `json:"name"`
Package string `json:"package"`
Version string `json:"version"`
Date string `json:"date"`
}
type ToolsFile struct {
Tools []Tool `json:"tools"`
}
type ModuleInfo struct {
Version string `json:"Version"`
Time string `json:"Time"`
}
func main() {
outputPath := flag.String("o", "tools.json", "output file path")
parallel := flag.Int("j", 8, "number of parallel lookups")
flag.Parse()
// Read existing tools.json to get the package list
data, err := os.ReadFile(*outputPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error reading %s: %v\n", *outputPath, err)
os.Exit(1)
}
var tf ToolsFile
if err := json.Unmarshal(data, &tf); err != nil {
fmt.Fprintf(os.Stderr, "error parsing %s: %v\n", *outputPath, err)
os.Exit(1)
}
fmt.Printf("Updating %d tools...\n\n", len(tf.Tools))
var wg sync.WaitGroup
sem := make(chan struct{}, *parallel)
results := make(chan result, len(tf.Tools))
for i, t := range tf.Tools {
wg.Add(1)
go func(idx int, tool Tool) {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
updated, err := fetchLatestVersion(tool)
results <- result{idx: idx, tool: tool, updated: updated, err: err}
}(i, t)
}
go func() {
wg.Wait()
close(results)
}()
updatedTools := make([]Tool, len(tf.Tools))
var succeeded, failed, changed int
for r := range results {
if r.err != nil {
fmt.Printf("✗ %s: %v\n", r.tool.Name, r.err)
updatedTools[r.idx] = r.tool // keep old version
failed++
} else {
updatedTools[r.idx] = r.updated
if r.tool.Version != r.updated.Version {
fmt.Printf("↑ %s: %s -> %s\n", r.tool.Name, r.tool.Version, r.updated.Version)
changed++
} else {
fmt.Printf("✓ %s: %s (unchanged)\n", r.tool.Name, r.tool.Version)
}
succeeded++
}
}
tf.Tools = updatedTools
// Write updated tools.json
output, err := json.MarshalIndent(tf, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "error marshaling JSON: %v\n", err)
os.Exit(1)
}
output = append(output, '\n')
absPath, _ := filepath.Abs(*outputPath)
if err := os.WriteFile(*outputPath, output, 0644); err != nil {
fmt.Fprintf(os.Stderr, "error writing %s: %v\n", *outputPath, err)
os.Exit(1)
}
fmt.Printf("\nDone: %d succeeded, %d failed, %d updated\n", succeeded, failed, changed)
fmt.Printf("Written to %s\n", absPath)
if failed > 0 {
os.Exit(1)
}
}
// getModulePath extracts the module path from a package path
// e.g., "golang.org/x/tools/cmd/goimports" -> "golang.org/x/tools"
func getModulePath(pkg string) string {
// Handle special cases where cmd is in a subpath
if strings.Contains(pkg, "/cmd/") {
parts := strings.Split(pkg, "/cmd/")
return parts[0]
}
// For packages like "github.com/cweill/gotests/gotests"
// we need the parent module
if strings.Contains(pkg, "/gojson") && strings.Contains(pkg, "ChimeraCoder") {
return "github.com/ChimeraCoder/gojson"
}
if strings.HasSuffix(pkg, "/gotests") {
return strings.TrimSuffix(pkg, "/gotests")
}
return pkg
}
func fetchLatestVersion(tool Tool) (Tool, error) {
modPath := getModulePath(tool.Package)
cmd := exec.Command("go", "list", "-m", "-json", modPath+"@latest")
output, err := cmd.Output()
if err != nil {
return Tool{}, fmt.Errorf("go list failed: %w", err)
}
var info ModuleInfo
if err := json.Unmarshal(output, &info); err != nil {
return Tool{}, fmt.Errorf("parse error: %w", err)
}
// Parse date
date := ""
if info.Time != "" {
if t, err := time.Parse(time.RFC3339, info.Time); err == nil {
date = t.Format("2006-01-02")
}
}
return Tool{
Name: tool.Name,
Package: tool.Package,
Version: info.Version,
Date: date,
}, nil
}
type result struct {
idx int
tool Tool
updated Tool
err error
}