Skip to content

Commit 1cf101a

Browse files
Merge pull request #138 from ibuildthecloud/distribution
feat: Add golang tool support
2 parents ee7203d + e20b3c3 commit 1cf101a

File tree

9 files changed

+186
-1
lines changed

9 files changed

+186
-1
lines changed

pkg/engine/cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ func (e *Engine) runCommand(ctx context.Context, tool types.Tool, input string)
5757

5858
output := &bytes.Buffer{}
5959
all := &bytes.Buffer{}
60-
cmd.Stdin = strings.NewReader(input)
6160
cmd.Stderr = io.MultiWriter(all, os.Stderr)
6261
cmd.Stdout = io.MultiWriter(all, output)
6362

@@ -142,6 +141,7 @@ func appendInputAsEnv(env []string, input string) []string {
142141
}
143142
}
144143

144+
env = appendEnv(env, "GPTSCRIPT_INPUT", input)
145145
return env
146146
}
147147

pkg/repos/download/extract.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ func Extract(ctx context.Context, downloadURL, digest, targetDir string) error {
3030
}
3131
defer resp.Body.Close()
3232

33+
// NOTE: Because I'm validating the hash at the same time as extracting this isn't actually secure.
34+
// Security is still assumed the source is trusted. Which is bad and should be changed.
3335
digester := sha256.New()
3436
input := io.TeeReader(resp.Body, digester)
3537

pkg/repos/runtimes/default.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package runtimes
33
import (
44
"github.com/gptscript-ai/gptscript/pkg/engine"
55
"github.com/gptscript-ai/gptscript/pkg/repos"
6+
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang"
67
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/node"
78
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/python"
89
)
@@ -22,6 +23,9 @@ var Runtimes = []repos.Runtime{
2223
Version: "21",
2324
Default: true,
2425
},
26+
&golang.Runtime{
27+
Version: "1.22.1",
28+
},
2529
}
2630

2731
func Default(cacheDir string) engine.RuntimeManager {

pkg/repos/runtimes/golang/digests.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
3bc971772f4712fec0364f4bc3de06af22a00a12daab10b6f717fdcd13156cc0 go1.22.1.darwin-amd64.tar.gz
2+
943e4f9f038239f9911c44366f52ab9202f6ee13610322a668fe42406fb3deef go1.22.1.darwin-amd64.pkg
3+
f6a9cec6b8a002fcc9c0ee24ec04d67f430a52abc3cfd613836986bcc00d8383 go1.22.1.darwin-arm64.tar.gz
4+
5f10b95e2678618f85ba9d87fbed506b3b87efc9d5a8cafda939055cb97949ba go1.22.1.darwin-arm64.pkg
5+
8484df36d3d40139eaf0fe5e647b006435d826cc12f9ae72973bf7ec265e0ae4 go1.22.1.linux-386.tar.gz
6+
aab8e15785c997ae20f9c88422ee35d962c4562212bb0f879d052a35c8307c7f go1.22.1.linux-amd64.tar.gz
7+
e56685a245b6a0c592fc4a55f0b7803af5b3f827aaa29feab1f40e491acf35b8 go1.22.1.linux-arm64.tar.gz
8+
8cb7a90e48c20daed39a6ac8b8a40760030ba5e93c12274c42191d868687c281 go1.22.1.linux-armv6l.tar.gz
9+
0c5ebb7eb39b7884ec99f92b425d4c03a96a72443562aafbf6e7d15c42a3108a go1.22.1.windows-386.zip
10+
cf9c66a208a106402a527f5b956269ca506cfe535fc388e828d249ea88ed28ba go1.22.1.windows-amd64.zip

pkg/repos/runtimes/golang/golang.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package golang
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
_ "embed"
8+
"errors"
9+
"fmt"
10+
"io/fs"
11+
"os"
12+
"path/filepath"
13+
"runtime"
14+
"strings"
15+
16+
"github.com/gptscript-ai/gptscript/pkg/debugcmd"
17+
"github.com/gptscript-ai/gptscript/pkg/hash"
18+
"github.com/gptscript-ai/gptscript/pkg/repos/download"
19+
runtimeEnv "github.com/gptscript-ai/gptscript/pkg/repos/runtimes/env"
20+
)
21+
22+
//go:embed digests.txt
23+
var releasesData []byte
24+
25+
const downloadURL = "https://go.dev/dl/"
26+
27+
type Runtime struct {
28+
// version something like "1.22.1"
29+
Version string
30+
}
31+
32+
func (r *Runtime) ID() string {
33+
return "go" + r.Version
34+
}
35+
36+
func (r *Runtime) Supports(cmd []string) bool {
37+
return len(cmd) > 0 && cmd[0] == "${GPTSCRIPT_TOOL_DIR}/bin/gptscript-go-tool"
38+
}
39+
40+
func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env []string) ([]string, error) {
41+
binPath, err := r.getRuntime(ctx, dataRoot)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
newEnv := runtimeEnv.AppendPath(env, binPath)
47+
if err := r.runBuild(ctx, toolSource, binPath, append(env, newEnv...)); err != nil {
48+
return nil, err
49+
}
50+
51+
return newEnv, nil
52+
}
53+
54+
func (r *Runtime) getReleaseAndDigest() (string, string, error) {
55+
scanner := bufio.NewScanner(bytes.NewReader(releasesData))
56+
key := r.ID() + "." + runtime.GOOS + "-" + runtime.GOARCH
57+
for scanner.Scan() {
58+
line := strings.Split(scanner.Text(), " ")
59+
file, digest := strings.TrimSpace(line[1]), strings.TrimSpace(line[0])
60+
if strings.HasPrefix(file, key) {
61+
return downloadURL + file, digest, nil
62+
}
63+
}
64+
65+
return "", "", fmt.Errorf("failed to find %s release for os=%s arch=%s", r.ID(), runtime.GOOS, runtime.GOARCH)
66+
}
67+
68+
func stripGo(env []string) (result []string) {
69+
for _, env := range env {
70+
if strings.HasPrefix(env, "GO") {
71+
continue
72+
}
73+
result = append(result, env)
74+
}
75+
return
76+
}
77+
78+
func (r *Runtime) runBuild(ctx context.Context, toolSource, binDir string, env []string) error {
79+
cmd := debugcmd.New(ctx, filepath.Join(binDir, "go"), "build", "-o", "bin/gptscript-go-tool")
80+
cmd.Env = stripGo(env)
81+
cmd.Dir = toolSource
82+
return cmd.Run()
83+
}
84+
85+
func (r *Runtime) binDir(rel string) string {
86+
return filepath.Join(rel, "go", "bin")
87+
}
88+
89+
func (r *Runtime) getRuntime(ctx context.Context, cwd string) (string, error) {
90+
url, sha, err := r.getReleaseAndDigest()
91+
if err != nil {
92+
return "", err
93+
}
94+
95+
target := filepath.Join(cwd, "golang", hash.ID(url, sha))
96+
if _, err := os.Stat(target); err == nil {
97+
return r.binDir(target), nil
98+
} else if !errors.Is(err, fs.ErrNotExist) {
99+
return "", err
100+
}
101+
102+
log.Infof("Downloading Go %s", r.Version)
103+
tmp := target + ".download"
104+
defer os.RemoveAll(tmp)
105+
106+
if err := os.MkdirAll(tmp, 0755); err != nil {
107+
return "", err
108+
}
109+
110+
if err := download.Extract(ctx, url, sha, tmp); err != nil {
111+
return "", err
112+
}
113+
114+
if err := os.Rename(tmp, target); err != nil {
115+
return "", err
116+
}
117+
118+
return r.binDir(target), nil
119+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package golang
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"testing"
9+
10+
"github.com/adrg/xdg"
11+
"github.com/samber/lo"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
var (
17+
testCacheHome = lo.Must(xdg.CacheFile("gptscript-test-cache/runtime"))
18+
)
19+
20+
func TestRuntime(t *testing.T) {
21+
t.Cleanup(func() {
22+
os.RemoveAll("testdata/bin")
23+
})
24+
r := Runtime{
25+
Version: "1.22.1",
26+
}
27+
28+
s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ())
29+
require.NoError(t, err)
30+
p, v, _ := strings.Cut(s[0], "=")
31+
v, _, _ = strings.Cut(v, string(filepath.ListSeparator))
32+
assert.Equal(t, "PATH", p)
33+
_, err = os.Stat(filepath.Join(v, "gofmt"))
34+
assert.NoError(t, err)
35+
}

pkg/repos/runtimes/golang/log.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package golang
2+
3+
import "github.com/gptscript-ai/gptscript/pkg/mvl"
4+
5+
var log = mvl.Package()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module example.com
2+
3+
go 1.22.1
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package main
2+
3+
import "fmt"
4+
5+
func main() {
6+
fmt.Println("Hello AI World!")
7+
}

0 commit comments

Comments
 (0)