package execenv import ( "archive/tar" "compress/gzip" "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" "time" ) // PythonBin 返回托管解释器。未安装时 ok=false。 func (r *Root) PythonBin() (string, bool) { bin := filepath.Join(r.PythonDir(), "current", "bin", "python3") st, err := os.Stat(bin) if err != nil || st.IsDir() { return "", false } return bin, true } // PythonPrefix 是需要在沙箱里重新挂载的前缀(current 的真实目录)。 func (r *Root) PythonPrefix() string { dest, err := filepath.EvalSymlinks(filepath.Join(r.PythonDir(), "current")) if err != nil { return "" } return dest } // LinkPython 把已有解释器收成托管前缀:python/host/bin/python3 -> 绝对路径。 // 这是下载独立 CPython 失败时的退化,仍然全系统只有一份,不是每个工作区一份。 func (r *Root) LinkPython(bin string) error { if !filepath.IsAbs(bin) { return fmt.Errorf("execenv: python bin must be absolute: %s", bin) } if _, err := os.Stat(bin); err != nil { return err } host := filepath.Join(r.PythonDir(), "host") bindir := filepath.Join(host, "bin") if err := os.MkdirAll(bindir, 0o755); err != nil { return err } link := filepath.Join(bindir, "python3") _ = os.Remove(link) if err := os.Symlink(bin, link); err != nil { return err } if err := os.WriteFile(filepath.Join(r.PythonDir(), "SOURCE"), []byte("link "+bin+"\n"), 0o644); err != nil { return err } return r.switchPython("host") } func (r *Root) switchPython(name string) error { link := filepath.Join(r.PythonDir(), "current") tmp := link + ".tmp" _ = os.Remove(tmp) if err := os.Symlink(name, tmp); err != nil { return err } return os.Rename(tmp, link) } // InstallStandalone 下载 python-build-standalone 的 install_only 包并解到 python/。 // url 为空时向 GitHub latest 要与本机架构匹配的 cpython 3.14 资产。 func (r *Root) InstallStandalone(ctx context.Context, url string) error { if url == "" { var err error url, err = latestStandaloneURL(ctx) if err != nil { return err } } req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } client := &http.Client{Timeout: 5 * time.Minute} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != 200 { return fmt.Errorf("execenv: download python: %s", resp.Status) } dest := filepath.Join(r.PythonDir(), "cpython-standalone") if err := os.RemoveAll(dest); err != nil { return err } if err := os.MkdirAll(dest, 0o755); err != nil { return err } if err := extractTarGz(resp.Body, dest); err != nil { return err } bin := filepath.Join(dest, "python", "bin", "python3") if _, err := os.Stat(bin); err != nil { return fmt.Errorf("execenv: standalone archive has no python/bin/python3") } // 让 current/bin/python3 成立:current -> cpython-standalone/python。 inner := filepath.Join("cpython-standalone", "python") if err := os.WriteFile(filepath.Join(r.PythonDir(), "SOURCE"), []byte("standalone "+url+"\n"), 0o644); err != nil { return err } return r.switchPython(inner) } func latestStandaloneURL(ctx context.Context) (string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest", nil) if err != nil { return "", err } req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("User-Agent", "agent-system-execenv") resp, err := (&http.Client{Timeout: 30 * time.Second}).Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { return "", fmt.Errorf("execenv: github releases: %s", resp.Status) } var doc struct { Assets []struct { Name string `json:"name"` URL string `json:"browser_download_url"` } `json:"assets"` } if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { return "", err } arch := "x86_64" if runtime.GOARCH == "arm64" { arch = "aarch64" } needle := arch + "-unknown-linux-gnu-install_only.tar.gz" var fallback string for _, a := range doc.Assets { if !strings.Contains(a.Name, needle) || !strings.HasPrefix(a.Name, "cpython-3.14") { continue } if strings.Contains(a.Name, "freethreaded") { fallback = a.URL continue } return a.URL, nil } if fallback != "" { return fallback, nil } return "", errors.New("execenv: no cpython 3.14 install_only asset") } func extractTarGz(rd io.Reader, dest string) error { gz, err := gzip.NewReader(rd) if err != nil { return err } defer gz.Close() tr := tar.NewReader(gz) for { hdr, err := tr.Next() if errors.Is(err, io.EOF) { return nil } if err != nil { return err } name := filepath.Clean(hdr.Name) if name == "." || strings.HasPrefix(name, "..") || filepath.IsAbs(name) { return fmt.Errorf("execenv: tar path escapes: %s", hdr.Name) } target := filepath.Join(dest, name) if !strings.HasPrefix(target, filepath.Clean(dest)+string(os.PathSeparator)) && target != filepath.Clean(dest) { return fmt.Errorf("execenv: tar path escapes: %s", hdr.Name) } switch hdr.Typeflag { case tar.TypeDir: if err := os.MkdirAll(target, 0o755); err != nil { return err } case tar.TypeReg: if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err } mode := os.FileMode(hdr.Mode) & 0o777 if mode == 0 { mode = 0o644 } f, err := os.OpenFile(target, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, mode) if err != nil { return err } _, cerr := io.Copy(f, tr) f.Close() if cerr != nil { return cerr } case tar.TypeSymlink: if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { return err } _ = os.Remove(target) if err := os.Symlink(hdr.Linkname, target); err != nil { return err } } } } // BootstrapVenv 用系统 python3 做一份唯一的托管前缀。解释器仍指向系统 python, // 但 pip 在这份前缀里,构建器可以下载 wheel。 func (r *Root) BootstrapVenv(python string) error { if python == "" { python = "/usr/bin/python3" } dest := filepath.Join(r.PythonDir(), "venv") if _, err := os.Stat(filepath.Join(dest, "bin", "python3")); err != nil { cmd := exec.Command(python, "-m", "venv", dest) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("execenv: venv: %v: %s", err, out) } } if err := os.WriteFile(filepath.Join(r.PythonDir(), "SOURCE"), []byte("venv "+python+"\n"), 0o644); err != nil { return err } return r.switchPython("venv") }