// Package execenv 是系统托管的 Python 环境:一个解释器、一份不可变基础层、 // 按内容哈希缓存的扩展层。工作区不各自建 venv,也不往一个可变全局环境里装包。 package execenv import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "os" "path/filepath" "strings" ) // Root 是环境根目录。多个服务实例可以共用同一份。 type Root struct { Path string } // Open 创建环境根的目录骨架。已存在的层与锁不会被改写。 func Open(path string) (*Root, error) { if strings.TrimSpace(path) == "" { return nil, errors.New("execenv: empty root") } r := &Root{Path: path} for _, d := range []string{ r.PythonDir(), r.LayersDir(), r.Wheelhouse(), r.LocksDir(), r.BinDir(), } { if err := os.MkdirAll(d, 0o755); err != nil { return nil, err } } if err := r.ensureCatalog(); err != nil { return nil, err } if err := writePipShim(r.BinDir()); err != nil { return nil, err } return r, nil } func (r *Root) PythonDir() string { return filepath.Join(r.Path, "python") } func (r *Root) LayersDir() string { return filepath.Join(r.Path, "layers") } func (r *Root) Wheelhouse() string { return filepath.Join(r.Path, "wheelhouse") } func (r *Root) LocksDir() string { return filepath.Join(r.Path, "locks") } func (r *Root) BinDir() string { return filepath.Join(r.Path, "bin") } func (r *Root) CatalogPath() string { return filepath.Join(r.Path, "catalog.json") } func (r *Root) ensureCatalog() error { if _, err := os.Stat(r.CatalogPath()); err == nil { return nil } else if !errors.Is(err, os.ErrNotExist) { return err } raw, err := json.MarshalIndent(DefaultCatalog(), "", " ") if err != nil { return err } return os.WriteFile(r.CatalogPath(), append(raw, '\n'), 0o644) } // LayerDir 返回某一层的 site-packages 目录(不一定已经存在)。 func (r *Root) LayerDir(id string) string { return filepath.Join(r.LayersDir(), id, "site-packages") } // CurrentBase 读取 current 符号链接指向的基础层 ID。未建立时返回空串。 func (r *Root) CurrentBase() (string, error) { dest, err := os.Readlink(filepath.Join(r.Path, "current")) if errors.Is(err, os.ErrNotExist) { return "", nil } if err != nil { return "", err } id := filepath.Base(dest) if id == "" || id == "." { return "", fmt.Errorf("execenv: current link %q is empty", dest) } return id, nil } // SwitchCurrent 把 current 指到已存在的层。旧链接被替换,层目录本身不动。 func (r *Root) SwitchCurrent(id string) error { if _, err := os.Stat(r.LayerDir(id)); err != nil { return fmt.Errorf("execenv: layer %s: %w", id, err) } link := filepath.Join(r.Path, "current") tmp := link + ".tmp" _ = os.Remove(tmp) if err := os.Symlink(filepath.Join("layers", id), tmp); err != nil { return err } return os.Rename(tmp, link) } // ReadLock 读取某一层的锁。 func (r *Root) ReadLock(id string) (Lock, error) { raw, err := os.ReadFile(filepath.Join(r.LocksDir(), id+".json")) if err != nil { return Lock{}, err } var l Lock if err := json.Unmarshal(raw, &l); err != nil { return Lock{}, err } return l, nil } // WriteLock 写入锁文件。内容按哈希寻址,已存在且字节相同则跳过。 func (r *Root) WriteLock(l Lock) (string, error) { id := l.ID() path := filepath.Join(r.LocksDir(), id+".json") raw, err := json.MarshalIndent(l, "", " ") if err != nil { return "", err } raw = append(raw, '\n') if prev, err := os.ReadFile(path); err == nil { if string(prev) != string(raw) { return "", fmt.Errorf("execenv: lock %s already exists with different bytes", id) } return id, nil } return id, os.WriteFile(path, raw, 0o644) } // HashBytes 是层键与 wheel 去重共用的 sha256。 func HashBytes(b []byte) string { sum := sha256.Sum256(b) return hex.EncodeToString(sum[:]) }