Skip to content

Commit c2c5d99

Browse files
Merge pull request #759 from ibuildthecloud/local-dev
feat: manage node/python runtime for local files in dev
2 parents 3bc07ac + cb46358 commit c2c5d99

20 files changed

+589
-258
lines changed

Diff for: pkg/engine/cmd.go

+36-109
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"os"
1212
"os/exec"
1313
"path"
14-
"path/filepath"
1514
"runtime"
1615
"sort"
1716
"strings"
@@ -121,7 +120,7 @@ func (e *Engine) runCommand(ctx Context, tool types.Tool, input string, toolCate
121120
var extraEnv = []string{
122121
strings.TrimSpace("GPTSCRIPT_CONTEXT=" + strings.Join(instructions, "\n")),
123122
}
124-
cmd, stop, err := e.newCommand(ctx.Ctx, extraEnv, tool, input)
123+
cmd, stop, err := e.newCommand(ctx.Ctx, extraEnv, tool, input, true)
125124
if err != nil {
126125
if toolCategory == NoCategory {
127126
return fmt.Sprintf("ERROR: got (%v) while parsing command", err), nil
@@ -244,7 +243,11 @@ func appendInputAsEnv(env []string, input string) []string {
244243
return env
245244
}
246245

247-
func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.Tool, input string) (*exec.Cmd, func(), error) {
246+
func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.Tool, input string, useShell bool) (*exec.Cmd, func(), error) {
247+
if runtime.GOOS == "windows" {
248+
useShell = false
249+
}
250+
248251
envvars := append(e.Env[:], extraEnv...)
249252
envvars = appendInputAsEnv(envvars, input)
250253
if log.IsDebug() {
@@ -254,9 +257,17 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T
254257
interpreter, rest, _ := strings.Cut(tool.Instructions, "\n")
255258
interpreter = strings.TrimSpace(interpreter)[2:]
256259

257-
args, err := shlex.Split(interpreter)
258-
if err != nil {
259-
return nil, nil, err
260+
var (
261+
args []string
262+
err error
263+
)
264+
if useShell {
265+
args = strings.Fields(interpreter)
266+
} else {
267+
args, err = shlex.Split(interpreter)
268+
if err != nil {
269+
return nil, nil, err
270+
}
260271
}
261272

262273
envvars, err = e.getRuntimeEnv(ctx, tool, args, envvars)
@@ -265,17 +276,6 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T
265276
}
266277

267278
envvars, envMap := envAsMapAndDeDup(envvars)
268-
for i, arg := range args {
269-
args[i] = os.Expand(arg, func(s string) string {
270-
return envMap[s]
271-
})
272-
}
273-
274-
// After we determined the interpreter we again interpret the args by env vars
275-
args, err = replaceVariablesForInterpreter(interpreter, envMap)
276-
if err != nil {
277-
return nil, nil, err
278-
}
279279

280280
if runtime.GOOS == "windows" && (args[0] == "/bin/bash" || args[0] == "/bin/sh") {
281281
args[0] = path.Base(args[0])
@@ -286,8 +286,7 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T
286286
}
287287

288288
var (
289-
cmdArgs = args[1:]
290-
stop = func() {}
289+
stop = func() {}
291290
)
292291

293292
if strings.TrimSpace(rest) != "" {
@@ -305,105 +304,33 @@ func (e *Engine) newCommand(ctx context.Context, extraEnv []string, tool types.T
305304
stop()
306305
return nil, nil, err
307306
}
308-
cmdArgs = append(cmdArgs, f.Name())
309-
}
310-
311-
// This is a workaround for Windows, where the command interpreter is constructed with unix style paths
312-
// It converts unix style paths to windows style paths
313-
if runtime.GOOS == "windows" {
314-
parts := strings.Split(args[0], "/")
315-
if parts[len(parts)-1] == "gptscript-go-tool" {
316-
parts[len(parts)-1] = "gptscript-go-tool.exe"
317-
}
318-
319-
args[0] = filepath.Join(parts...)
307+
args = append(args, f.Name())
320308
}
321309

322-
cmd := exec.CommandContext(ctx, env.Lookup(envvars, args[0]), cmdArgs...)
323-
cmd.Env = compressEnv(envvars)
324-
return cmd, stop, nil
325-
}
326-
327-
func replaceVariablesForInterpreter(interpreter string, envMap map[string]string) ([]string, error) {
328-
var parts []string
329-
for i, part := range splitByQuotes(interpreter) {
330-
if i%2 == 0 {
331-
part = os.Expand(part, func(s string) string {
310+
// Expand and/or normalize env references
311+
for i, arg := range args {
312+
args[i] = os.Expand(arg, func(s string) string {
313+
if strings.HasPrefix(s, "!") {
314+
return envMap[s[1:]]
315+
}
316+
if !useShell {
332317
return envMap[s]
333-
})
334-
// We protect newly resolved env vars from getting replaced when we do the second Expand
335-
// after shlex. Yeah, crazy. I'm guessing this isn't secure, but just trying to avoid a foot gun.
336-
part = os.Expand(part, func(s string) string {
337-
return "${__" + s + "}"
338-
})
339-
}
340-
parts = append(parts, part)
341-
}
342-
343-
parts, err := shlex.Split(strings.Join(parts, ""))
344-
if err != nil {
345-
return nil, err
346-
}
347-
348-
for i, part := range parts {
349-
parts[i] = os.Expand(part, func(s string) string {
350-
if strings.HasPrefix(s, "__") {
351-
return "${" + s[2:] + "}"
352318
}
353-
return envMap[s]
319+
return "${" + s + "}"
354320
})
355321
}
356322

357-
return parts, nil
358-
}
359-
360-
// splitByQuotes will split a string by parsing matching double quotes (with \ as the escape character).
361-
// The return value conforms to the following properties
362-
// 1. s == strings.Join(result, "")
363-
// 2. Even indexes are strings that were not in quotes.
364-
// 3. Odd indexes are strings that were quoted.
365-
//
366-
// Example: s = `In a "quoted string" quotes can be escaped with \"`
367-
//
368-
// result = [`In a `, `"quoted string"`, ` quotes can be escaped with \"`]
369-
func splitByQuotes(s string) (result []string) {
370-
var (
371-
buf strings.Builder
372-
inEscape, inQuote bool
373-
)
374-
375-
for _, c := range s {
376-
if inEscape {
377-
buf.WriteRune(c)
378-
inEscape = false
379-
continue
380-
}
381-
382-
switch c {
383-
case '"':
384-
if inQuote {
385-
buf.WriteRune(c)
386-
}
387-
result = append(result, buf.String())
388-
buf.Reset()
389-
if !inQuote {
390-
buf.WriteRune(c)
391-
}
392-
inQuote = !inQuote
393-
case '\\':
394-
inEscape = true
395-
buf.WriteRune(c)
396-
default:
397-
buf.WriteRune(c)
398-
}
323+
if runtime.GOOS == "windows" {
324+
args[0] = strings.ReplaceAll(args[0], "/", "\\")
399325
}
400326

401-
if buf.Len() > 0 {
402-
if inQuote {
403-
result = append(result, "")
404-
}
405-
result = append(result, buf.String())
327+
if useShell {
328+
args = append([]string{"/bin/sh", "-c"}, strings.Join(args, " "))
329+
} else {
330+
args[0] = env.Lookup(envvars, args[0])
406331
}
407332

408-
return
333+
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
334+
cmd.Env = compressEnv(envvars)
335+
return cmd, stop, nil
409336
}

Diff for: pkg/engine/cmd_test.go

-135
This file was deleted.

Diff for: pkg/engine/daemon.go

+1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func (e *Engine) startDaemon(tool types.Tool) (string, error) {
133133
},
134134
tool,
135135
"{}",
136+
false,
136137
)
137138
if err != nil {
138139
return url, err

Diff for: pkg/repos/get.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type Runtime interface {
2828
ID() string
2929
Supports(tool types.Tool, cmd []string) bool
3030
Setup(ctx context.Context, tool types.Tool, dataRoot, toolSource string, env []string) ([]string, error)
31+
GetHash(tool types.Tool) (string, error)
3132
}
3233

3334
type noopRuntime struct {
@@ -37,6 +38,10 @@ func (n noopRuntime) ID() string {
3738
return "none"
3839
}
3940

41+
func (n noopRuntime) GetHash(_ types.Tool) (string, error) {
42+
return "", nil
43+
}
44+
4045
func (n noopRuntime) Supports(_ types.Tool, _ []string) bool {
4146
return false
4247
}
@@ -183,8 +188,13 @@ func (m *Manager) setup(ctx context.Context, runtime Runtime, tool types.Tool, e
183188
locker.Lock(tool.ID)
184189
defer locker.Unlock(tool.ID)
185190

191+
runtimeHash, err := runtime.GetHash(tool)
192+
if err != nil {
193+
return "", nil, err
194+
}
195+
186196
target := filepath.Join(m.storageDir, tool.Source.Repo.Revision, tool.Source.Repo.Path, tool.Source.Repo.Name, runtime.ID())
187-
targetFinal := filepath.Join(target, tool.Source.Repo.Path)
197+
targetFinal := filepath.Join(target, tool.Source.Repo.Path+runtimeHash)
188198
doneFile := targetFinal + ".done"
189199
envData, err := os.ReadFile(doneFile)
190200
if err == nil {
@@ -251,7 +261,11 @@ func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []st
251261
for _, runtime := range m.runtimes {
252262
if runtime.Supports(tool, cmd) {
253263
log.Debugf("Runtime %s supports %v", runtime.ID(), cmd)
254-
return m.setup(ctx, runtime, tool, env)
264+
wd, env, err := m.setup(ctx, runtime, tool, env)
265+
if isLocal {
266+
wd = tool.WorkingDir
267+
}
268+
return wd, env, err
255269
}
256270
}
257271

0 commit comments

Comments
 (0)