commit 7188d87c39f4f468754c69a1e669d607771af22e Author: sneak Date: Thu Dec 18 00:57:28 2025 -0800 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d443b94 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# gosetup + +Installs common Go development tools at pinned versions. + +## Install + +```bash +go install sneak.berlin/go/gosetup@COMMIT_HASH +``` + +## Usage + +```bash +# Install all tools at pinned versions +gosetup + +# List tools and versions without installing +gosetup -l + +# Dry run (show commands without executing) +gosetup -n + +# Verbose output +gosetup -v + +# Control parallelism (default: 4) +gosetup -j 8 +``` + +## Updating Pinned Versions + +To update `tools.json` with the latest versions: + +```bash +go run ./cmd/update -o tools.json +``` + +## Included Tools + +- **Language Server**: gopls +- **Formatters**: gofumpt, goimports, golines +- **Linters**: golangci-lint, staticcheck, errcheck, revive +- **vim-go tools**: godef, guru, gorename, gotests, gomodifytags, impl, fillstruct, fillswitch, motion, iferr, keyify, asmfmt, gotags +- **Debugger**: dlv (delve) +- **And more**: gocyclo, ineffassign, misspell, unconvert, gopkgs, go-outline, go-symbols diff --git a/cmd/update/main.go b/cmd/update/main.go new file mode 100644 index 0000000..b4b629f --- /dev/null +++ b/cmd/update/main.go @@ -0,0 +1,169 @@ +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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dccfadd --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module sneak.berlin/go/gosetup + +go 1.21 diff --git a/main.go b/main.go new file mode 100644 index 0000000..b901a3f --- /dev/null +++ b/main.go @@ -0,0 +1,108 @@ +package main + +import ( + _ "embed" + "encoding/json" + "flag" + "fmt" + "os" + "os/exec" + "sync" +) + +//go:embed tools.json +var toolsJSON []byte + +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"` +} + +func main() { + parallel := flag.Int("j", 4, "number of parallel installs") + dryRun := flag.Bool("n", false, "dry run (print commands without executing)") + verbose := flag.Bool("v", false, "verbose output") + list := flag.Bool("l", false, "list tools and versions without installing") + flag.Parse() + + var tf ToolsFile + if err := json.Unmarshal(toolsJSON, &tf); err != nil { + fmt.Fprintf(os.Stderr, "error parsing embedded tools.json: %v\n", err) + os.Exit(1) + } + + if *list { + for _, t := range tf.Tools { + fmt.Printf("%-20s %s (%s)\n", t.Name, t.Version, t.Date) + } + return + } + + fmt.Printf("Installing %d Go development tools...\n\n", len(tf.Tools)) + + if *dryRun { + for _, t := range tf.Tools { + fmt.Printf("go install %s@%s\n", t.Package, t.Version) + } + return + } + + var wg sync.WaitGroup + sem := make(chan struct{}, *parallel) + results := make(chan result, len(tf.Tools)) + + for _, t := range tf.Tools { + wg.Add(1) + go func(tool Tool) { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + + if *verbose { + fmt.Printf("Installing %s@%s...\n", tool.Name, tool.Version) + } + + pkg := fmt.Sprintf("%s@%s", tool.Package, tool.Version) + cmd := exec.Command("go", "install", pkg) + cmd.Env = os.Environ() + output, err := cmd.CombinedOutput() + results <- result{tool: tool, err: err, output: string(output)} + }(t) + } + + go func() { + wg.Wait() + close(results) + }() + + var succeeded, failed int + for r := range results { + if r.err != nil { + fmt.Printf("✗ %s@%s: %v\n", r.tool.Name, r.tool.Version, r.err) + if *verbose && r.output != "" { + fmt.Printf(" %s\n", r.output) + } + failed++ + } else { + fmt.Printf("✓ %s@%s\n", r.tool.Name, r.tool.Version) + succeeded++ + } + } + + fmt.Printf("\nDone: %d succeeded, %d failed\n", succeeded, failed) + if failed > 0 { + os.Exit(1) + } +} + +type result struct { + tool Tool + err error + output string +} diff --git a/tools.json b/tools.json new file mode 100644 index 0000000..db5001d --- /dev/null +++ b/tools.json @@ -0,0 +1,196 @@ +{ + "tools": [ + { + "name": "gopls", + "package": "golang.org/x/tools/gopls", + "version": "v0.21.0", + "date": "2025-12-05" + }, + { + "name": "gofumpt", + "package": "mvdan.cc/gofumpt", + "version": "v0.9.2", + "date": "2025-10-21" + }, + { + "name": "goimports", + "package": "golang.org/x/tools/cmd/goimports", + "version": "v0.40.0", + "date": "2025-12-08" + }, + { + "name": "golangci-lint", + "package": "github.com/golangci/golangci-lint/cmd/golangci-lint", + "version": "v1.64.8", + "date": "2025-03-17" + }, + { + "name": "staticcheck", + "package": "honnef.co/go/tools/cmd/staticcheck", + "version": "v0.6.1", + "date": "2025-03-05" + }, + { + "name": "errcheck", + "package": "github.com/kisielk/errcheck", + "version": "v1.9.0", + "date": "2025-02-19" + }, + { + "name": "revive", + "package": "github.com/mgechev/revive", + "version": "v1.13.0", + "date": "2025-11-13" + }, + { + "name": "godef", + "package": "github.com/rogpeppe/godef", + "version": "v1.1.2", + "date": "2020-03-03" + }, + { + "name": "guru", + "package": "golang.org/x/tools/cmd/guru", + "version": "v0.40.0", + "date": "2025-12-08" + }, + { + "name": "gorename", + "package": "golang.org/x/tools/cmd/gorename", + "version": "v0.40.0", + "date": "2025-12-08" + }, + { + "name": "gotests", + "package": "github.com/cweill/gotests/gotests", + "version": "v1.9.0", + "date": "2025-10-23" + }, + { + "name": "gomodifytags", + "package": "github.com/fatih/gomodifytags", + "version": "v1.17.0", + "date": "2024-07-15" + }, + { + "name": "impl", + "package": "github.com/josharian/impl", + "version": "v1.5.0", + "date": "2025-12-09" + }, + { + "name": "fillstruct", + "package": "github.com/davidrjenni/reftools/cmd/fillstruct", + "version": "v0.0.0-20250907133731-34b10582faa4", + "date": "2025-09-07" + }, + { + "name": "fillswitch", + "package": "github.com/davidrjenni/reftools/cmd/fillswitch", + "version": "v0.0.0-20250907133731-34b10582faa4", + "date": "2025-09-07" + }, + { + "name": "fixplurals", + "package": "github.com/davidrjenni/reftools/cmd/fixplurals", + "version": "v0.0.0-20250907133731-34b10582faa4", + "date": "2025-09-07" + }, + { + "name": "dlv", + "package": "github.com/go-delve/delve/cmd/dlv", + "version": "v1.25.2", + "date": "2025-08-27" + }, + { + "name": "gotags", + "package": "github.com/jstemmer/gotags", + "version": "v1.4.1", + "date": "2017-04-03" + }, + { + "name": "gogetdoc", + "package": "github.com/zmb3/gogetdoc", + "version": "v0.0.0-20190228002656-b37376c5da6a", + "date": "2019-02-28" + }, + { + "name": "iferr", + "package": "github.com/koron/iferr", + "version": "v0.0.0-20240122035601-9c3e2fbe4bd1", + "date": "2024-01-22" + }, + { + "name": "keyify", + "package": "honnef.co/go/tools/cmd/keyify", + "version": "v0.6.1", + "date": "2025-03-05" + }, + { + "name": "asmfmt", + "package": "github.com/klauspost/asmfmt/cmd/asmfmt", + "version": "v1.3.2", + "date": "2022-03-30" + }, + { + "name": "motion", + "package": "github.com/fatih/motion", + "version": "v1.2.0", + "date": "2023-02-16" + }, + { + "name": "gojson", + "package": "github.com/ChimeraCoder/gojson/gojson", + "version": "v1.1.0", + "date": "2018-08-18" + }, + { + "name": "golines", + "package": "github.com/segmentio/golines", + "version": "v0.13.0", + "date": "2025-08-21" + }, + { + "name": "gocyclo", + "package": "github.com/fzipp/gocyclo/cmd/gocyclo", + "version": "v0.6.0", + "date": "2022-06-15" + }, + { + "name": "ineffassign", + "package": "github.com/gordonklaus/ineffassign", + "version": "v0.2.0", + "date": "2025-08-24" + }, + { + "name": "misspell", + "package": "github.com/client9/misspell/cmd/misspell", + "version": "v0.3.4", + "date": "2018-03-09" + }, + { + "name": "unconvert", + "package": "github.com/mdempsky/unconvert", + "version": "v0.0.0-20250216222326-4a038b3d31f5", + "date": "2025-02-16" + }, + { + "name": "gopkgs", + "package": "github.com/uudashr/gopkgs/v2/cmd/gopkgs", + "version": "v2.1.2", + "date": "2020-02-14" + }, + { + "name": "go-outline", + "package": "github.com/ramya-rao-a/go-outline", + "version": "v0.0.0-20210608161538-9736a4bde949", + "date": "2021-06-08" + }, + { + "name": "go-symbols", + "package": "github.com/acroca/go-symbols", + "version": "v0.1.1", + "date": "2019-01-14" + } + ] +}