Skip to content

Commit 11de5cb

Browse files
feat: add basic bash support for windows
1 parent ca203da commit 11de5cb

File tree

12 files changed

+223
-18
lines changed

12 files changed

+223
-18
lines changed

pkg/engine/cmd.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"os"
1010
"os/exec"
11+
"path"
1112
"path/filepath"
1213
"runtime"
1314
"sort"
@@ -192,6 +193,10 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T
192193
})
193194
}
194195

196+
if runtime.GOOS == "windows" && (args[0] == "/bin/bash" || args[0] == "/bin/sh") {
197+
args[0] = path.Base(args[0])
198+
}
199+
195200
if runtime.GOOS == "windows" && (args[0] == "/usr/bin/env" || args[0] == "/bin/env") {
196201
args = args[1:]
197202
}

pkg/repos/download/extract.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"net/http"
1010
"net/url"
1111
"os"
12+
"path"
1213
"path/filepath"
14+
"strings"
1315
"time"
1416

1517
"github.com/mholt/archiver/v4"
@@ -60,6 +62,18 @@ func Extract(ctx context.Context, downloadURL, digest, targetDir string) error {
6062
return err
6163
}
6264

65+
bin := path.Base(parsedURL.Path)
66+
if strings.HasSuffix(bin, ".exe") {
67+
dst, err := os.Create(filepath.Join(targetDir, bin))
68+
if err != nil {
69+
return err
70+
}
71+
defer dst.Close()
72+
73+
_, err = io.Copy(dst, tmpFile)
74+
return err
75+
}
76+
6377
format, input, err := archiver.Identify(filepath.Base(parsedURL.Path), tmpFile)
6478
if err != nil {
6579
return err

pkg/repos/get.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111

1212
"github.com/BurntSushi/locker"
13+
"github.com/gptscript-ai/gptscript/pkg/hash"
1314
"github.com/gptscript-ai/gptscript/pkg/repos/git"
1415
"github.com/gptscript-ai/gptscript/pkg/types"
1516
)
@@ -36,10 +37,15 @@ func (n noopRuntime) Setup(_ context.Context, _, _ string, _ []string) ([]string
3637
}
3738

3839
type Manager struct {
39-
storageDir string
40-
gitDir string
41-
runtimeDir string
42-
runtimes []Runtime
40+
storageDir string
41+
gitDir string
42+
runtimeDir string
43+
runtimes []Runtime
44+
supportLocal bool
45+
}
46+
47+
func (m *Manager) SetSupportLocal() {
48+
m.supportLocal = true
4349
}
4450

4551
func New(cacheDir string, runtimes ...Runtime) *Manager {
@@ -74,8 +80,14 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e
7480
_ = os.RemoveAll(doneFile)
7581
_ = os.RemoveAll(target)
7682

77-
if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil {
78-
return "", nil, err
83+
if tool.Source.Repo.VCS == "git" {
84+
if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil {
85+
return "", nil, err
86+
}
87+
} else {
88+
if err := os.MkdirAll(target, 0755); err != nil {
89+
return "", nil, err
90+
}
7991
}
8092

8193
newEnv, err := runtime.Setup(ctx, m.runtimeDir, targetFinal, env)
@@ -101,12 +113,23 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e
101113
}
102114

103115
func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error) {
104-
if tool.Source.Repo == nil {
105-
return tool.WorkingDir, env, nil
106-
}
116+
if !m.supportLocal {
117+
if tool.Source.Repo == nil {
118+
return tool.WorkingDir, env, nil
119+
}
107120

108-
if tool.Source.Repo.VCS != "git" {
109-
return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID)
121+
if tool.Source.Repo.VCS != "git" {
122+
return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID)
123+
}
124+
} else if tool.Source.Repo == nil {
125+
id := hash.Digest(tool)[:12]
126+
tool.Source.Repo = &types.Repo{
127+
VCS: "<local>",
128+
Root: id,
129+
Path: "/",
130+
Name: id,
131+
Revision: id,
132+
}
110133
}
111134

112135
for _, runtime := range m.runtimes {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
6d2dfd1c1412c3550a89071a1b36a6f6073844320e687343d1dfc72719ecb8d9 FRP-5301-gda71f7c57/busybox-w64-FRP-5301-gda71f7c57.exe

pkg/repos/runtimes/busybox/busybox.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package busybox
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"context"
7+
_ "embed"
8+
"errors"
9+
"fmt"
10+
"io/fs"
11+
"os"
12+
"os/exec"
13+
"path"
14+
"path/filepath"
15+
"runtime"
16+
"strings"
17+
18+
runtimeEnv "github.com/gptscript-ai/gptscript/pkg/env"
19+
"github.com/gptscript-ai/gptscript/pkg/hash"
20+
"github.com/gptscript-ai/gptscript/pkg/repos/download"
21+
)
22+
23+
//go:embed SHASUMS256.txt
24+
var releasesData []byte
25+
26+
const downloadURL = "https://github.com/gptscript-ai/busybox-w32/releases/download/%s"
27+
28+
type Runtime struct {
29+
}
30+
31+
func (r *Runtime) ID() string {
32+
return "busybox"
33+
}
34+
35+
func (r *Runtime) Supports(cmd []string) bool {
36+
if runtime.GOOS != "windows" {
37+
return false
38+
}
39+
for _, bin := range []string{"bash", "sh", "/bin/sh", "/bin/bash"} {
40+
if runtimeEnv.Matches(cmd, bin) {
41+
return true
42+
}
43+
}
44+
return false
45+
}
46+
47+
func (r *Runtime) Setup(ctx context.Context, dataRoot, _ string, env []string) ([]string, error) {
48+
binPath, err := r.getRuntime(ctx, dataRoot)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
newEnv := runtimeEnv.AppendPath(env, binPath)
54+
return newEnv, nil
55+
}
56+
57+
func (r *Runtime) getReleaseAndDigest() (string, string, error) {
58+
scanner := bufio.NewScanner(bytes.NewReader(releasesData))
59+
for scanner.Scan() {
60+
line := scanner.Text()
61+
fields := strings.Fields(line)
62+
return fmt.Sprintf(downloadURL, fields[1]), fields[0], nil
63+
}
64+
65+
return "", "", fmt.Errorf("failed to find %s release", r.ID())
66+
}
67+
68+
func (r *Runtime) getRuntime(ctx context.Context, cwd string) (string, error) {
69+
url, sha, err := r.getReleaseAndDigest()
70+
if err != nil {
71+
return "", err
72+
}
73+
74+
target := filepath.Join(cwd, "busybox", hash.ID(url, sha))
75+
if _, err := os.Stat(target); err == nil {
76+
return target, nil
77+
} else if !errors.Is(err, fs.ErrNotExist) {
78+
return "", err
79+
}
80+
81+
log.Infof("Downloading Busybox")
82+
tmp := target + ".download"
83+
defer os.RemoveAll(tmp)
84+
85+
if err := os.MkdirAll(tmp, 0755); err != nil {
86+
return "", err
87+
}
88+
89+
if err := download.Extract(ctx, url, sha, tmp); err != nil {
90+
return "", err
91+
}
92+
93+
bbExe := filepath.Join(tmp, path.Base(url))
94+
95+
cmd := exec.Command(bbExe, "--install", ".")
96+
cmd.Dir = filepath.Dir(bbExe)
97+
98+
if err := cmd.Run(); err != nil {
99+
return "", err
100+
}
101+
102+
if err := os.Rename(tmp, target); err != nil {
103+
return "", err
104+
}
105+
106+
return target, nil
107+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package busybox
2+
3+
import (
4+
"context"
5+
"errors"
6+
"io/fs"
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
"testing"
12+
13+
"github.com/adrg/xdg"
14+
"github.com/samber/lo"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
var (
19+
testCacheHome = lo.Must(xdg.CacheFile("gptscript-test-cache/runtime"))
20+
)
21+
22+
func firstPath(s []string) string {
23+
_, p, _ := strings.Cut(s[0], "=")
24+
return strings.Split(p, string(os.PathListSeparator))[0]
25+
}
26+
27+
func TestRuntime(t *testing.T) {
28+
if runtime.GOOS != "windows" {
29+
t.Skip()
30+
}
31+
32+
r := Runtime{}
33+
34+
s, err := r.Setup(context.Background(), testCacheHome, "testdata", os.Environ())
35+
require.NoError(t, err)
36+
_, err = os.Stat(filepath.Join(firstPath(s), "busybox.exe"))
37+
if errors.Is(err, fs.ErrNotExist) {
38+
_, err = os.Stat(filepath.Join(firstPath(s), "busybox"))
39+
}
40+
require.NoError(t, err)
41+
}

pkg/repos/runtimes/busybox/log.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package busybox
2+
3+
import "github.com/gptscript-ai/gptscript/pkg/mvl"
4+
5+
var log = mvl.Package()

pkg/repos/runtimes/default.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ 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/busybox"
67
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang"
78
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/node"
89
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/python"
910
)
1011

1112
var Runtimes = []repos.Runtime{
13+
&busybox.Runtime{},
1214
&python.Runtime{
1315
Version: "3.12",
1416
Default: true,

pkg/tests/runner_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -727,9 +727,6 @@ func TestGlobalErr(t *testing.T) {
727727
}
728728

729729
func TestContextArg(t *testing.T) {
730-
if runtime.GOOS == "windows" {
731-
t.Skip()
732-
}
733730
runner := tester.NewRunner(t)
734731
x, err := runner.Run("", `{
735732
"file": "foo.db"

pkg/tests/testdata/TestContextArg/other.gpt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ name: fromcontext
22
args: first: an arg
33
args: second: an arg
44

5-
#!/bin/bash
6-
echo this is from other context ${first} and then ${second}
5+
#!/usr/bin/env bash
6+
echo this is from other context ${FIRST} and then ${SECOND}

pkg/tests/testdata/TestContextArg/test.gpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ name: fromcontext
99
args: first: an arg
1010

1111
#!/bin/bash
12-
echo this is from context -- ${first}
12+
echo this is from context -- ${FIRST}

pkg/tests/tester/runner.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import (
88
"path/filepath"
99
"testing"
1010

11+
"github.com/adrg/xdg"
1112
"github.com/gptscript-ai/gptscript/pkg/loader"
13+
"github.com/gptscript-ai/gptscript/pkg/repos"
14+
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes"
1215
"github.com/gptscript-ai/gptscript/pkg/runner"
1316
"github.com/gptscript-ai/gptscript/pkg/types"
1417
"github.com/hexops/autogold/v2"
@@ -148,8 +151,15 @@ func NewRunner(t *testing.T) *Runner {
148151
t: t,
149152
}
150153

154+
cacheDir, err := xdg.CacheFile("gptscript-test-cache/runtime")
155+
require.NoError(t, err)
156+
157+
rm := runtimes.Default(cacheDir)
158+
rm.(*repos.Manager).SetSupportLocal()
159+
151160
run, err := runner.New(c, "default", runner.Options{
152-
Sequential: true,
161+
Sequential: true,
162+
RuntimeManager: rm,
153163
})
154164
require.NoError(t, err)
155165

0 commit comments

Comments
 (0)