Skip to content

Commit 9ae1a8a

Browse files
ibuildtheclouddrpebcak
authored andcommitted
feat: add basic bash support for windows
1 parent c8cf310 commit 9ae1a8a

File tree

12 files changed

+232
-18
lines changed

12 files changed

+232
-18
lines changed

pkg/engine/cmd.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"os"
1212
"os/exec"
13+
"path"
1314
"path/filepath"
1415
"runtime"
1516
"sort"
@@ -196,10 +197,9 @@ var ignoreENV = map[string]struct{}{
196197
}
197198

198199
func appendEnv(envs []string, k, v string) []string {
199-
for _, k := range []string{k, env.ToEnvLike(k)} {
200-
if _, ignore := ignoreENV[k]; !ignore {
201-
envs = append(envs, k+"="+v)
202-
}
200+
k = env.ToEnvLike(k)
201+
if _, ignore := ignoreENV[k]; !ignore {
202+
envs = append(envs, k+"="+v)
203203
}
204204
return envs
205205
}
@@ -262,6 +262,10 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T
262262
})
263263
}
264264

265+
if runtime.GOOS == "windows" && (args[0] == "/bin/bash" || args[0] == "/bin/sh") {
266+
args[0] = path.Base(args[0])
267+
}
268+
265269
if runtime.GOOS == "windows" && (args[0] == "/usr/bin/env" || args[0] == "/bin/env") {
266270
args = args[1:]
267271
}

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: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/BurntSushi/locker"
1616
"github.com/gptscript-ai/gptscript/pkg/config"
1717
"github.com/gptscript-ai/gptscript/pkg/credentials"
18+
"github.com/gptscript-ai/gptscript/pkg/hash"
1819
"github.com/gptscript-ai/gptscript/pkg/loader/github"
1920
"github.com/gptscript-ai/gptscript/pkg/repos/git"
2021
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes/golang"
@@ -51,13 +52,22 @@ type Manager struct {
5152
credHelperDirs credentials.CredentialHelperDirs
5253
runtimes []Runtime
5354
credHelperConfig *credHelperConfig
55+
supportLocal bool
5456
}
5557

5658
type credHelperConfig struct {
5759
lock sync.Mutex
5860
initialized bool
5961
cliCfg *config.CLIConfig
6062
env []string
63+
storageDir string
64+
gitDir string
65+
runtimeDir string
66+
runtimes []Runtime
67+
}
68+
69+
func (m *Manager) SetSupportLocal() {
70+
m.supportLocal = true
6171
}
6272

6373
func New(cacheDir string, runtimes ...Runtime) *Manager {
@@ -200,8 +210,14 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e
200210
_ = os.RemoveAll(doneFile)
201211
_ = os.RemoveAll(target)
202212

203-
if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil {
204-
return "", nil, err
213+
if tool.Source.Repo.VCS == "git" {
214+
if err := git.Checkout(ctx, m.gitDir, tool.Source.Repo.Root, tool.Source.Repo.Revision, target); err != nil {
215+
return "", nil, err
216+
}
217+
} else {
218+
if err := os.MkdirAll(target, 0755); err != nil {
219+
return "", nil, err
220+
}
205221
}
206222

207223
newEnv, err := runtime.Setup(ctx, m.runtimeDir, targetFinal, env)
@@ -227,12 +243,25 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e
227243
}
228244

229245
func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []string) (string, []string, error) {
230-
if tool.Source.Repo == nil {
231-
return tool.WorkingDir, env, nil
232-
}
246+
var isLocal bool
247+
if !m.supportLocal {
248+
if tool.Source.Repo == nil {
249+
return tool.WorkingDir, env, nil
250+
}
233251

234-
if tool.Source.Repo.VCS != "git" {
235-
return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID)
252+
if tool.Source.Repo.VCS != "git" {
253+
return "", nil, fmt.Errorf("only git is supported, found VCS %s for %s", tool.Source.Repo.VCS, tool.ID)
254+
}
255+
} else if tool.Source.Repo == nil {
256+
isLocal = true
257+
id := hash.Digest(tool)[:12]
258+
tool.Source.Repo = &types.Repo{
259+
VCS: "<local>",
260+
Root: id,
261+
Path: "/",
262+
Name: id,
263+
Revision: id,
264+
}
236265
}
237266

238267
for _, runtime := range m.runtimes {
@@ -242,5 +271,9 @@ func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []st
242271
}
243272
}
244273

274+
if isLocal {
275+
return tool.WorkingDir, env, nil
276+
}
277+
245278
return m.setup(ctx, &noopRuntime{}, tool, env)
246279
}
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
@@ -751,9 +751,6 @@ func TestGlobalErr(t *testing.T) {
751751
}
752752

753753
func TestContextArg(t *testing.T) {
754-
if runtime.GOOS == "windows" {
755-
t.Skip()
756-
}
757754
runner := tester.NewRunner(t)
758755
x, err := runner.Run("", `{
759756
"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,8 +8,11 @@ import (
88
"path/filepath"
99
"testing"
1010

11+
"github.com/adrg/xdg"
1112
"github.com/gptscript-ai/gptscript/pkg/credentials"
1213
"github.com/gptscript-ai/gptscript/pkg/loader"
14+
"github.com/gptscript-ai/gptscript/pkg/repos"
15+
"github.com/gptscript-ai/gptscript/pkg/repos/runtimes"
1316
"github.com/gptscript-ai/gptscript/pkg/runner"
1417
"github.com/gptscript-ai/gptscript/pkg/types"
1518
"github.com/hexops/autogold/v2"
@@ -171,8 +174,15 @@ func NewRunner(t *testing.T) *Runner {
171174
t: t,
172175
}
173176

177+
cacheDir, err := xdg.CacheFile("gptscript-test-cache/runtime")
178+
require.NoError(t, err)
179+
180+
rm := runtimes.Default(cacheDir)
181+
rm.(*repos.Manager).SetSupportLocal()
182+
174183
run, err := runner.New(c, credentials.NoopStore{}, runner.Options{
175-
Sequential: true,
184+
Sequential: true,
185+
RuntimeManager: rm,
176186
})
177187
require.NoError(t, err)
178188

0 commit comments

Comments
 (0)