diff --git a/pkg/env/env.go b/pkg/env/env.go index fcad5836..7402426d 100644 --- a/pkg/env/env.go +++ b/pkg/env/env.go @@ -39,6 +39,9 @@ func AppendPath(env []string, binPath string) []string { if ok { newEnv = append(newEnv, fmt.Sprintf("PATH=%s%s%s", binPath, string(os.PathListSeparator), v)) + } else if v, ok := strings.CutPrefix(path, "Path="); ok { + newEnv = append(newEnv, fmt.Sprintf("Path=%s%s%s", + binPath, string(os.PathListSeparator), v)) } } return newEnv @@ -53,10 +56,12 @@ func Lookup(env []string, bin string) string { if !ok { continue } + log.Debugf("Looking for %s in %s", bin, suffix) for _, path := range strings.Split(suffix, string(os.PathListSeparator)) { testPath := filepath.Join(path, bin) if stat, err := os.Stat(testPath); err == nil && !stat.IsDir() { + log.Debugf("Found %s for %s in %s", testPath, bin, suffix) return testPath } @@ -66,6 +71,7 @@ func Lookup(env []string, bin string) string { } if stat, err := os.Stat(testPath + ext); err == nil && !stat.IsDir() { + log.Debugf("Found %s for %s in %s", testPath+ext, bin, suffix) return testPath + ext } } diff --git a/pkg/env/log.go b/pkg/env/log.go new file mode 100644 index 00000000..e02c181e --- /dev/null +++ b/pkg/env/log.go @@ -0,0 +1,5 @@ +package env + +import "github.com/gptscript-ai/gptscript/pkg/mvl" + +var log = mvl.Package() diff --git a/pkg/repos/get.go b/pkg/repos/get.go index 3f25349e..b0555395 100644 --- a/pkg/repos/get.go +++ b/pkg/repos/get.go @@ -111,6 +111,7 @@ func (m *Manager) GetContext(ctx context.Context, tool types.Tool, cmd, env []st for _, runtime := range m.runtimes { if runtime.Supports(cmd) { + log.Debugf("Runtime %s supports %v", runtime.ID(), cmd) return m.setup(ctx, runtime, tool, env) } } diff --git a/pkg/repos/log.go b/pkg/repos/log.go new file mode 100644 index 00000000..e9559f3e --- /dev/null +++ b/pkg/repos/log.go @@ -0,0 +1,5 @@ +package repos + +import "github.com/gptscript-ai/gptscript/pkg/mvl" + +var log = mvl.Package() diff --git a/pkg/repos/runtimes/env.old/env.go b/pkg/repos/runtimes/env.old/env.go deleted file mode 100644 index 8c5c0ae3..00000000 --- a/pkg/repos/runtimes/env.old/env.go +++ /dev/null @@ -1,42 +0,0 @@ -package env - -import ( - "fmt" - "os" - "strings" -) - -func execEquals(bin, check string) bool { - return bin == check || - bin == check+".exe" -} - -func Matches(cmd []string, bin string) bool { - switch len(cmd) { - case 0: - return false - case 1: - return execEquals(cmd[0], bin) - } - if cmd[0] == bin { - return true - } - if cmd[0] == "/usr/bin/env" || cmd[0] == "/bin/env" { - return execEquals(cmd[1], bin) - } - return false -} - -func AppendPath(env []string, binPath string) []string { - var newEnv []string - for _, path := range env { - for _, prefix := range []string{"PATH=", "Path="} { - v, ok := strings.CutPrefix(path, prefix) - if ok { - newEnv = append(newEnv, fmt.Sprintf(prefix+"%s%s%s", - binPath, string(os.PathListSeparator), v)) - } - } - } - return newEnv -} diff --git a/pkg/repos/runtimes/python/python.go b/pkg/repos/runtimes/python/python.go index cb85e639..31840d53 100644 --- a/pkg/repos/runtimes/python/python.go +++ b/pkg/repos/runtimes/python/python.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/fs" "os" "path/filepath" @@ -21,7 +22,7 @@ import ( //go:embed python.json var releasesData []byte -const uvVersion = "uv==0.1.15" +const uvVersion = "uv==0.1.24" type Release struct { OS string `json:"os,omitempty"` @@ -52,13 +53,65 @@ func (r *Runtime) Supports(cmd []string) bool { return runtimeEnv.Matches(cmd, "python") || runtimeEnv.Matches(cmd, "python3") } +func pythonCmd(base string) string { + if runtime.GOOS == "windows" { + return filepath.Join(base, "python.exe") + } + return filepath.Join(base, "python3") +} + +func pythonBin(base string) string { + binDir := filepath.Join(base, "python") + if runtime.GOOS != "windows" { + binDir = filepath.Join(binDir, "bin") + } + return binDir +} + +func uvBin(binDir string) string { + if runtime.GOOS == "windows" { + return filepath.Join(binDir, "Scripts", "uv") + } + return filepath.Join(binDir, "uv") +} + func (r *Runtime) installVenv(ctx context.Context, binDir, venvPath string) error { log.Infof("Creating virtualenv in %s", venvPath) - cmd := debugcmd.New(ctx, filepath.Join(binDir, "uv"), "venv", "-p", - filepath.Join(binDir, "python3"), venvPath) + cmd := debugcmd.New(ctx, uvBin(binDir), "venv", "-p", pythonCmd(binDir), venvPath) return cmd.Run() } +func copyFile(to, from string) error { + in, err := os.Open(from) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(to) + if err != nil { + _ = out.Close() + return err + } + defer out.Close() + + if _, err := io.Copy(out, in); err != nil { + return fmt.Errorf("copying %s => %s", from, to) + } + + return nil +} + +func (r *Runtime) copyPythonForWindows(binDir string) error { + for _, targetBin := range []string{"python3.exe", "python" + r.ID() + ".exe"} { + err := copyFile(filepath.Join(binDir, targetBin), filepath.Join(binDir, "python.exe")) + if err != nil { + return err + } + } + return nil +} + func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env []string) ([]string, error) { binPath, err := r.getRuntime(ctx, dataRoot) if err != nil { @@ -67,6 +120,9 @@ func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env [] venvPath := filepath.Join(dataRoot, "venv", hash.ID(binPath, toolSource)) venvBinPath := filepath.Join(venvPath, "bin") + if runtime.GOOS == "windows" { + venvBinPath = filepath.Join(venvPath, "Scripts") + } // Cleanup failed runs if err := os.RemoveAll(venvPath); err != nil { @@ -80,6 +136,12 @@ func (r *Runtime) Setup(ctx context.Context, dataRoot, toolSource string, env [] newEnv := runtimeEnv.AppendPath(env, venvBinPath) newEnv = append(newEnv, "VIRTUAL_ENV="+venvPath) + if runtime.GOOS == "windows" { + if err := r.copyPythonForWindows(venvBinPath); err != nil { + return nil, err + } + } + if err := r.runPip(ctx, toolSource, binPath, append(env, newEnv...)); err != nil { return nil, err } @@ -110,7 +172,7 @@ func (r *Runtime) runPip(ctx context.Context, toolSource, binDir string, env []s for _, req := range []string{"requirements-gptscript.txt", "requirements.txt"} { reqFile := filepath.Join(toolSource, req) if s, err := os.Stat(reqFile); err == nil && !s.IsDir() { - cmd := debugcmd.New(ctx, filepath.Join(binDir, "uv"), "pip", "install", "-r", reqFile) + cmd := debugcmd.New(ctx, uvBin(binDir), "pip", "install", "-r", reqFile) cmd.Env = env return cmd.Run() } @@ -120,9 +182,7 @@ func (r *Runtime) runPip(ctx context.Context, toolSource, binDir string, env []s } func (r *Runtime) setupUV(ctx context.Context, tmp string) error { - cmd := debugcmd.New(ctx, filepath.Join(tmp, "python", "bin", "python3"), - filepath.Join(tmp, "python", "bin", "pip"), - "install", uvVersion) + cmd := debugcmd.New(ctx, pythonCmd(tmp), "-m", "pip", "install", uvVersion) return cmd.Run() } @@ -133,7 +193,7 @@ func (r *Runtime) getRuntime(ctx context.Context, cwd string) (string, error) { } target := filepath.Join(cwd, "python", hash.ID(url, sha, uvVersion)) - binDir := filepath.Join(target, "python", "bin") + binDir := pythonBin(target) if _, err := os.Stat(target); err == nil { return binDir, nil } else if !errors.Is(err, fs.ErrNotExist) { @@ -152,7 +212,7 @@ func (r *Runtime) getRuntime(ctx context.Context, cwd string) (string, error) { return "", err } - if err := r.setupUV(ctx, tmp); err != nil { + if err := r.setupUV(ctx, pythonBin(tmp)); err != nil { return "", err }