ScuttleBot

scuttlebot / internal / ergo / fetch.go
Source Blame History 163 lines
61c045e… lmata 1 package ergo
61c045e… lmata 2
61c045e… lmata 3 import (
61c045e… lmata 4 "archive/tar"
61c045e… lmata 5 "compress/gzip"
61c045e… lmata 6 "encoding/json"
61c045e… lmata 7 "fmt"
61c045e… lmata 8 "io"
61c045e… lmata 9 "net/http"
61c045e… lmata 10 "os"
61c045e… lmata 11 "path/filepath"
61c045e… lmata 12 "runtime"
61c045e… lmata 13 )
61c045e… lmata 14
61c045e… lmata 15 const ergoGitHubAPI = "https://api.github.com/repos/ergochat/ergo/releases/latest"
61c045e… lmata 16
61c045e… lmata 17 // EnsureBinary checks that the ergo binary exists at binaryPath. If it does
61c045e… lmata 18 // not, it downloads the latest release from GitHub into destDir and returns
61c045e… lmata 19 // the path to the installed binary.
61c045e… lmata 20 //
61c045e… lmata 21 // binaryPath is the configured path (may be just "ergo" meaning look in PATH).
61c045e… lmata 22 // destDir is where to install if not found.
61c045e… lmata 23 func EnsureBinary(binaryPath, destDir string) (string, error) {
61c045e… lmata 24 // If it's an absolute path or the caller set a specific path, check it first.
61c045e… lmata 25 if filepath.IsAbs(binaryPath) {
61c045e… lmata 26 if _, err := os.Stat(binaryPath); err == nil {
61c045e… lmata 27 return binaryPath, nil
61c045e… lmata 28 }
61c045e… lmata 29 }
61c045e… lmata 30
61c045e… lmata 31 // Check if ergo is already in our data dir.
61c045e… lmata 32 localPath := filepath.Join(destDir, "ergo")
61c045e… lmata 33 if _, err := os.Stat(localPath); err == nil {
61c045e… lmata 34 return localPath, nil
61c045e… lmata 35 }
61c045e… lmata 36
61c045e… lmata 37 // Download from GitHub releases.
61c045e… lmata 38 version, downloadURL, err := latestReleaseURL()
61c045e… lmata 39 if err != nil {
61c045e… lmata 40 return "", fmt.Errorf("ergo: fetch latest release info: %w", err)
61c045e… lmata 41 }
61c045e… lmata 42
61c045e… lmata 43 fmt.Fprintf(os.Stderr, "ergo binary not found — downloading %s...\n", version)
61c045e… lmata 44
61c045e… lmata 45 if err := os.MkdirAll(destDir, 0o700); err != nil {
61c045e… lmata 46 return "", fmt.Errorf("ergo: create data dir: %w", err)
61c045e… lmata 47 }
61c045e… lmata 48
61c045e… lmata 49 if err := downloadAndExtract(downloadURL, destDir); err != nil {
61c045e… lmata 50 return "", fmt.Errorf("ergo: download: %w", err)
61c045e… lmata 51 }
61c045e… lmata 52
61c045e… lmata 53 fmt.Fprintf(os.Stderr, "ergo %s installed to %s\n", version, localPath)
61c045e… lmata 54 return localPath, nil
61c045e… lmata 55 }
61c045e… lmata 56
61c045e… lmata 57 // latestReleaseURL queries GitHub for the latest ergo release and returns
61c045e… lmata 58 // the version string and the download URL for the current OS/arch.
61c045e… lmata 59 func latestReleaseURL() (string, string, error) {
61c045e… lmata 60 resp, err := http.Get(ergoGitHubAPI) //nolint:gosec // known GitHub API URL
61c045e… lmata 61 if err != nil {
61c045e… lmata 62 return "", "", err
61c045e… lmata 63 }
61c045e… lmata 64 defer resp.Body.Close()
61c045e… lmata 65
61c045e… lmata 66 var release struct {
61c045e… lmata 67 TagName string `json:"tag_name"`
61c045e… lmata 68 Assets []struct {
61c045e… lmata 69 Name string `json:"name"`
61c045e… lmata 70 BrowserDownloadURL string `json:"browser_download_url"`
61c045e… lmata 71 } `json:"assets"`
61c045e… lmata 72 }
61c045e… lmata 73 if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
61c045e… lmata 74 return "", "", err
61c045e… lmata 75 }
61c045e… lmata 76
61c045e… lmata 77 suffix := platformSuffix()
61c045e… lmata 78 for _, asset := range release.Assets {
61c045e… lmata 79 if matchesPlatform(asset.Name, suffix) {
61c045e… lmata 80 return release.TagName, asset.BrowserDownloadURL, nil
61c045e… lmata 81 }
61c045e… lmata 82 }
61c045e… lmata 83
61c045e… lmata 84 return "", "", fmt.Errorf("no release asset found for %s/%s (tag %s)", runtime.GOOS, runtime.GOARCH, release.TagName)
61c045e… lmata 85 }
61c045e… lmata 86
61c045e… lmata 87 // platformSuffix returns the OS-arch suffix used in ergo release filenames.
c8d9310… lmata 88 // Ergo uses "macos" instead of "darwin" and "x86_64" instead of "amd64".
61c045e… lmata 89 func platformSuffix() string {
c8d9310… lmata 90 goos := runtime.GOOS
c8d9310… lmata 91 if goos == "darwin" {
c8d9310… lmata 92 goos = "macos"
c8d9310… lmata 93 }
61c045e… lmata 94 arch := runtime.GOARCH
61c045e… lmata 95 if arch == "amd64" {
61c045e… lmata 96 arch = "x86_64"
61c045e… lmata 97 }
c8d9310… lmata 98 return goos + "-" + arch
61c045e… lmata 99 }
61c045e… lmata 100
61c045e… lmata 101 func matchesPlatform(name, suffix string) bool {
61c045e… lmata 102 // Ergo assets look like: ergo-v2.14.0-linux-x86_64.tar.gz
61c045e… lmata 103 return len(name) > 0 &&
61c045e… lmata 104 filepath.Ext(name) == ".gz" &&
61c045e… lmata 105 contains(name, suffix) &&
61c045e… lmata 106 contains(name, ".tar.")
61c045e… lmata 107 }
61c045e… lmata 108
61c045e… lmata 109 func contains(s, sub string) bool {
61c045e… lmata 110 return len(s) >= len(sub) && (s == sub || len(s) > 0 && containsStr(s, sub))
61c045e… lmata 111 }
61c045e… lmata 112
61c045e… lmata 113 func containsStr(s, sub string) bool {
61c045e… lmata 114 for i := 0; i <= len(s)-len(sub); i++ {
61c045e… lmata 115 if s[i:i+len(sub)] == sub {
61c045e… lmata 116 return true
61c045e… lmata 117 }
61c045e… lmata 118 }
61c045e… lmata 119 return false
61c045e… lmata 120 }
61c045e… lmata 121
61c045e… lmata 122 // downloadAndExtract downloads a .tar.gz and extracts the "ergo" binary into destDir.
61c045e… lmata 123 func downloadAndExtract(url, destDir string) error {
61c045e… lmata 124 resp, err := http.Get(url) //nolint:gosec // URL from GitHub API
61c045e… lmata 125 if err != nil {
61c045e… lmata 126 return err
61c045e… lmata 127 }
61c045e… lmata 128 defer resp.Body.Close()
61c045e… lmata 129
61c045e… lmata 130 gz, err := gzip.NewReader(resp.Body)
61c045e… lmata 131 if err != nil {
61c045e… lmata 132 return fmt.Errorf("gzip: %w", err)
61c045e… lmata 133 }
61c045e… lmata 134 defer gz.Close()
61c045e… lmata 135
61c045e… lmata 136 tr := tar.NewReader(gz)
61c045e… lmata 137 for {
61c045e… lmata 138 hdr, err := tr.Next()
61c045e… lmata 139 if err == io.EOF {
61c045e… lmata 140 break
61c045e… lmata 141 }
61c045e… lmata 142 if err != nil {
61c045e… lmata 143 return fmt.Errorf("tar: %w", err)
61c045e… lmata 144 }
61c045e… lmata 145 // Extract only the "ergo" binary (may be at root or in a subdirectory).
61c045e… lmata 146 if filepath.Base(hdr.Name) != "ergo" || hdr.Typeflag != tar.TypeReg {
61c045e… lmata 147 continue
61c045e… lmata 148 }
61c045e… lmata 149
61c045e… lmata 150 dest := filepath.Join(destDir, "ergo")
61c045e… lmata 151 f, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755)
61c045e… lmata 152 if err != nil {
61c045e… lmata 153 return err
61c045e… lmata 154 }
61c045e… lmata 155 if _, err := io.Copy(f, tr); err != nil { //nolint:gosec // size bounded by release binary
61c045e… lmata 156 f.Close()
61c045e… lmata 157 return err
61c045e… lmata 158 }
61c045e… lmata 159 return f.Close()
61c045e… lmata 160 }
61c045e… lmata 161
61c045e… lmata 162 return fmt.Errorf("ergo binary not found in archive")
61c045e… lmata 163 }

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button