sandbox/pythonRuntime.go
Star f9dcf07ba4 first version
supported macOS、linux
2026-03-23 00:35:27 +08:00

167 lines
5.9 KiB
Go

package sandbox
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"runtime"
"time"
"github.com/ssgo/httpclient"
"github.com/ssgo/log"
"github.com/ssgo/u"
)
func init() {
RegisterRuntime("python", &Runtime{
Check: func(runtimePath, venvPath, projectPath string, uid, gid int, cfg *RuntimeConfig) (*RuntimeSandboxConfig, error) {
if err := checkPythonRuntime(runtimePath, cfg); err != nil {
return nil, err
}
if err := checkPythonVenv(runtimePath, venvPath, uid, gid); err != nil {
return nil, err
}
if err := checkPythonProject(venvPath, projectPath, uid, gid, cfg); err != nil {
return nil, err
}
venvExe := getPythonExe(venvPath)
return &RuntimeSandboxConfig{
StartCmd: venvExe,
Envs: map[string]string{
"PATH": filepath.Dir(venvExe) + string(os.PathListSeparator) + "$PATH",
},
}, nil
},
})
}
func checkPythonRuntime(runtimePath string, cfg *RuntimeConfig) error {
runtimeVersion := filepath.Base(runtimePath)
lock := getRuntimeLock("python", "runtime", runtimePath)
lock.Lock()
defer lock.Unlock()
if !u.FileExists(runtimePath) {
osPart := u.StringIf(runtime.GOOS == "linux", "unknown-linux-gnu", u.StringIf(runtime.GOOS == "windows", "pc-windows-msvc-shared", "apple-darwin"))
archPart := u.StringIf(runtime.GOARCH == "arm64", "aarch64", "x86_64")
instFile := filepath.Join(filepath.Dir(runtimePath), ".installs", fmt.Sprintf("%s-%s-%s.tar.gz", runtimeVersion, osPart, archPart))
if !u.FileExists(instFile) {
downloadClient := httpclient.GetClient(3600 * time.Second)
downloadClient.EnableRedirect()
if cfg.HttpProxy != "" {
if proxyURL, err := url.Parse(cfg.HttpProxy); err == nil {
downloadClient.GetRawClient().Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
}
}
log.DefaultLogger.Info("[Sandbox] fetching python runtime release list", "from", "https://github.com/astral-sh/python-build-standalone/releases", "httpProxy", cfg.HttpProxy)
r1 := downloadClient.Get("https://github.com/astral-sh/python-build-standalone/releases")
if r1.Error != nil {
return r1.Error
}
if m1 := regexp.MustCompile(`data-deferred-src="([^"]+)"`).FindStringSubmatch(r1.String()); len(m1) >= 2 {
log.DefaultLogger.Info("[Sandbox] fetching python runtime download url", "from", m1[1], "httpProxy", cfg.HttpProxy)
r2 := downloadClient.Get(m1[1])
if r2.Error != nil {
return r2.Error
}
pattern := fmt.Sprintf(`href="([^"]+cpython-%s(?:\.\d+)*\+[^"]+-%s-%s-install_only\.tar\.gz)"`, regexp.QuoteMeta(runtimeVersion), archPart, osPart)
if m2 := regexp.MustCompile(pattern).FindStringSubmatch(r2.String()); len(m2) >= 2 {
log.DefaultLogger.Info("[Sandbox] downloading python runtime", "from", "https://github.com"+m2[1], "to", instFile)
if _, err := downloadClient.Download(instFile, "https://github.com"+m2[1], nil); err != nil {
return err
} else {
log.DefaultLogger.Info("[Sandbox] downloaded python runtime", "version", runtimeVersion, "os", osPart, "arch", archPart, "url", "https://github.com"+m2[1], "to", instFile, "httpProxy", cfg.HttpProxy)
}
} else {
return fmt.Errorf("no binary for version %s on %s-%s on %s", runtimeVersion, osPart, archPart, m1[1])
}
} else {
return errors.New("can't read release list on https://github.com/astral-sh/python-build-standalone/releases")
}
}
if err := u.Extract(instFile, runtimePath, true); err != nil {
log.DefaultLogger.Error("[Sandbox] unpack python runtime failed", "version", runtimeVersion, "os", osPart, "arch", archPart, "from", instFile, "err", err.Error())
return err
}
}
return nil
}
func checkPythonVenv(runtimePath, venvPath string, uid, gid int) error {
lock := getRuntimeLock("python", "venv", venvPath)
lock.Lock()
defer lock.Unlock()
if !u.FileExists(venvPath) {
pyExe := getPythonExe(runtimePath)
os.MkdirAll(venvPath, 0755)
if uid != 0 && runtime.GOOS == "linux" {
os.Chown(venvPath, uid, gid)
}
if out, err := RunCmd(venvPath, uid, gid, pyExe, "-m", "venv", venvPath); err != nil {
log.DefaultLogger.Error("[Sandbox] venv create failed", "runtime", runtimePath, "venv", venvPath, "err", err.Error(), "output", out)
return err
} else {
log.DefaultLogger.Info("[Sandbox] venv create success", "runtime", runtimePath, "venv", venvPath)
}
}
return nil
}
func checkPythonProject(venvPath, projectPath string, uid, gid int, cfg *RuntimeConfig) error {
lock := getRuntimeLock("python", "project", projectPath)
lock.Lock()
defer lock.Unlock()
if uid != 0 && runtime.GOOS == "linux" {
os.Chown(projectPath, uid, gid)
}
reqFile := filepath.Join(projectPath, "requirements.txt")
if u.FileExists(reqFile) {
shaFile := filepath.Join(projectPath, ".requirements.sha1")
currentSha := u.Sha1String(u.ReadFileN(reqFile))
oldSha := u.ReadFileN(shaFile)
if oldSha != currentSha {
pipExe := getPipExe(venvPath)
pipArgs := []string{"install", "-r", reqFile}
if cfg.Mirror != "" {
pipArgs = append(pipArgs, "-i", cfg.Mirror)
}
if out, err := RunCmd(projectPath, uid, gid, pipExe, pipArgs...); err != nil {
log.DefaultLogger.Error("[Sandbox] pip install failed", "venv", venvPath, "project", projectPath, "err", err.Error(), "output", out)
return err
} else {
log.DefaultLogger.Info("[Sandbox] pip install success", "venv", venvPath, "project", projectPath, "output", out)
u.WriteFile(shaFile, currentSha)
}
}
}
return nil
}
func getPythonExe(base string) string {
if runtime.GOOS == "windows" {
return filepath.Join(base, "python.exe")
}
exe := filepath.Join(base, "bin", "python")
if !u.FileExists(exe) {
exe = filepath.Join(base, "bin", "python3")
}
return exe
}
func getPipExe(base string) string {
if runtime.GOOS == "windows" {
return filepath.Join(base, "pip.exe")
}
exe := filepath.Join(base, "bin", "pip")
if !u.FileExists(exe) {
exe = filepath.Join(base, "bin", "pip3")
}
return exe
}