Skip to content

Commit ca8ab3a

Browse files
feat: enable github release binary downloads for go tools
1 parent c2c5d99 commit ca8ab3a

File tree

5 files changed

+217
-10
lines changed

5 files changed

+217
-10
lines changed

Diff for: pkg/repos/get.go

+24-10
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const credentialHelpersRepo = "github.com/gptscript-ai/gptscript-credential-help
2727
type Runtime interface {
2828
ID() string
2929
Supports(tool types.Tool, cmd []string) bool
30+
Binary(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) (bool, []string, error)
3031
Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error)
3132
GetHash(tool types.Tool) (string, error)
3233
}
@@ -46,6 +47,10 @@ func (n noopRuntime) Supports(_ types.Tool, _ []string) bool {
4647
return false
4748
}
4849

50+
func (n noopRuntime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) {
51+
return false, nil, nil
52+
}
53+
4954
func (n noopRuntime) Setup(_ context.Context, _ types.Tool, _, _ string, _ []string) ([]string, error) {
5055
return nil, nil
5156
}
@@ -211,21 +216,30 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e
211216
_ = os.RemoveAll(doneFile)
212217
_ = os.RemoveAll(target)
213218

214-
if tool.Source.Repo.VCS == "git" {
215-
if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil {
216-
return "", nil, err
219+
var (
220+
newEnv []string
221+
isBinary bool
222+
)
223+
224+
if isBinary, newEnv, err = runtime.Binary(ctx, tool, m.runtimeDir, targetFinal, env); err != nil {
225+
return "", nil, err
226+
} else if !isBinary {
227+
if tool.Source.Repo.VCS == "git" {
228+
if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil {
229+
return "", nil, err
230+
}
231+
} else {
232+
if err := os.MkdirAll(target, 0755); err != nil {
233+
return "", nil, err
234+
}
217235
}
218-
} else {
219-
if err := os.MkdirAll(target, 0755); err != nil {
236+
237+
newEnv, err = runtime.Setup(ctx, tool, m.runtimeDir, targetFinal, env)
238+
if err != nil {
220239
return "", nil, err
221240
}
222241
}
223242

224-
newEnv, err := runtime.Setup(ctx, tool, m.runtimeDir, targetFinal, env)
225-
if err != nil {
226-
return "", nil, err
227-
}
228-
229243
out, err := os.Create(doneFile + ".tmp")
230244
if err != nil {
231245
return "", nil, err

Diff for: pkg/repos/runtimes/busybox/busybox.go

+4
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ func (r *Runtime) Supports(_ types.Tool, cmd []string) bool {
4949
return false
5050
}
5151

52+
func (r *Runtime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) {
53+
return false, nil, nil
54+
}
55+
5256
func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, _ string, env []string) ([]string, error) {
5357
binPath, err := r.getRuntime(ctx, dataRoot)
5458
if err != nil {

Diff for: pkg/repos/runtimes/golang/golang.go

+181
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7+
"crypto/sha256"
78
_ "embed"
9+
"encoding/hex"
810
"errors"
911
"fmt"
12+
"io"
1013
"io/fs"
14+
"net/http"
1115
"os"
1216
"path/filepath"
1317
"runtime"
@@ -44,6 +48,183 @@ func (r *Runtime) Supports(tool types.Tool, cmd []string) bool {
4448
len(cmd) > 0 && cmd[0] == "${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool"
4549
}
4650

51+
type release struct {
52+
account, repo, label string
53+
}
54+
55+
func (r release) checksumTxt() string {
56+
return fmt.Sprintf(
57+
"https://github.com/%s/%s/releases/download/%s/checksums.txt",
58+
r.account,
59+
r.repo,
60+
r.label)
61+
}
62+
63+
func (r release) binURL() string {
64+
return fmt.Sprintf(
65+
"https://github.com/%s/%s/releases/download/%s/%s",
66+
r.account,
67+
r.repo,
68+
r.label,
69+
r.srcBinName())
70+
}
71+
72+
func (r release) targetBinName() string {
73+
suffix := ""
74+
if runtime.GOOS == "windows" {
75+
suffix = ".exe"
76+
}
77+
78+
return "gptscript-go-tool" + suffix
79+
}
80+
81+
func (r release) srcBinName() string {
82+
suffix := ""
83+
if runtime.GOOS == "windows" {
84+
suffix = ".exe"
85+
}
86+
87+
return r.repo + "-" +
88+
runtime.GOOS + "-" +
89+
runtime.GOARCH + suffix
90+
}
91+
92+
func getLatestRelease(tool types.Tool) (*release, bool) {
93+
if tool.Source.Repo == nil || !strings.HasPrefix(tool.Source.Repo.Root, "https://github.com/") {
94+
return nil, false
95+
}
96+
97+
parts := strings.Split(strings.TrimPrefix(strings.TrimSuffix(tool.Source.Repo.Root, ".git"), "https://"), "/")
98+
if len(parts) != 3 {
99+
return nil, false
100+
}
101+
102+
client := http.Client{
103+
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
104+
return http.ErrUseLastResponse
105+
},
106+
}
107+
108+
resp, err := client.Get(fmt.Sprintf("https://github.com/%s/%s/releases/latest", parts[1], parts[2]))
109+
if err != nil || resp.StatusCode != http.StatusFound {
110+
// ignore error
111+
return nil, false
112+
}
113+
defer resp.Body.Close()
114+
115+
target := resp.Header.Get("Location")
116+
if target == "" {
117+
return nil, false
118+
}
119+
120+
account, repo := parts[1], parts[2]
121+
parts = strings.Split(target, "/")
122+
label := parts[len(parts)-1]
123+
124+
return &release{
125+
account: account,
126+
repo: repo,
127+
label: label,
128+
}, true
129+
}
130+
131+
func get(ctx context.Context, url string) (*http.Response, error) {
132+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
resp, err := http.DefaultClient.Do(req)
138+
if err != nil {
139+
return nil, err
140+
} else if resp.StatusCode != http.StatusOK {
141+
_ = resp.Body.Close()
142+
return nil, fmt.Errorf("bad HTTP status code: %d", resp.StatusCode)
143+
}
144+
145+
return resp, nil
146+
}
147+
148+
func downloadBin(ctx context.Context, checksum, src, url, bin string) error {
149+
resp, err := get(ctx, url)
150+
if err != nil {
151+
return err
152+
}
153+
defer resp.Body.Close()
154+
155+
if err := os.MkdirAll(filepath.Join(src, "bin"), 0755); err != nil {
156+
return err
157+
}
158+
159+
targetFile, err := os.Create(filepath.Join(src, "bin", bin))
160+
if err != nil {
161+
return err
162+
}
163+
164+
digest := sha256.New()
165+
166+
if _, err := io.Copy(io.MultiWriter(targetFile, digest), resp.Body); err != nil {
167+
return err
168+
}
169+
170+
if err := targetFile.Close(); err != nil {
171+
return nil
172+
}
173+
174+
if got := hex.EncodeToString(digest.Sum(nil)); got != checksum {
175+
return fmt.Errorf("checksum mismatch %s != %s", got, checksum)
176+
}
177+
178+
if err := os.Chmod(targetFile.Name(), 0755); err != nil {
179+
return err
180+
}
181+
182+
return nil
183+
}
184+
185+
func getChecksum(ctx context.Context, rel *release) string {
186+
resp, err := get(ctx, rel.checksumTxt())
187+
if err != nil {
188+
// ignore error
189+
return ""
190+
}
191+
defer resp.Body.Close()
192+
193+
scan := bufio.NewScanner(resp.Body)
194+
for scan.Scan() {
195+
fields := strings.Fields(scan.Text())
196+
if len(fields) != 2 || fields[1] != rel.srcBinName() {
197+
continue
198+
}
199+
return fields[0]
200+
}
201+
202+
return ""
203+
}
204+
205+
func (r *Runtime) Binary(ctx context.Context, tool types.Tool, _, toolSource string, env []string) (bool, []string, error) {
206+
if !tool.Source.IsGit() {
207+
return false, nil, nil
208+
}
209+
210+
rel, ok := getLatestRelease(tool)
211+
if !ok {
212+
return false, nil, nil
213+
}
214+
215+
checksum := getChecksum(ctx, rel)
216+
if checksum == "" {
217+
return false, nil, nil
218+
}
219+
220+
if err := downloadBin(ctx, checksum, toolSource, rel.binURL(), rel.targetBinName()); err != nil {
221+
// ignore error
222+
return false, nil, nil
223+
}
224+
225+
return true, env, nil
226+
}
227+
47228
func (r *Runtime) Setup(ctx context.Context, _ types.Tool, dataRoot, toolSource string, env []string) ([]string, error) {
48229
binPath, err := r.getRuntime(ctx, dataRoot)
49230
if err != nil {

Diff for: pkg/repos/runtimes/node/node.go

+4
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ func (r *Runtime) ID() string {
3939
return "node" + r.Version
4040
}
4141

42+
func (r *Runtime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) {
43+
return false, nil, nil
44+
}
45+
4246
func (r *Runtime) Supports(_ types.Tool, cmd []string) bool {
4347
for _, testCmd := range []string{"node", "npx", "npm"} {
4448
if r.supports(testCmd, cmd) {

Diff for: pkg/repos/runtimes/python/python.go

+4
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@ func (r *Runtime) getReleaseAndDigest() (string, string, error) {
175175
return "", "", fmt.Errorf("failed to find an python runtime for %s", r.Version)
176176
}
177177

178+
func (r *Runtime) Binary(_ context.Context, _ types.Tool, _, _ string, _ []string) (bool, []string, error) {
179+
return false, nil, nil
180+
}
181+
178182
func (r *Runtime) GetHash(tool types.Tool) (string, error) {
179183
if !tool.Source.IsGit() && tool.WorkingDir != "" {
180184
if _, ok := tool.MetaData[requirementsTxt]; ok {

0 commit comments

Comments
 (0)