package execenv import ( "encoding/json" "sort" "strings" ) // Lock 是一层的不可变清单。ID 由规范 JSON 决定,与写盘时的空白无关。 type Lock struct { Schema int `json:"schema"` Kind string `json:"kind"` // base | extension | isolated PythonTag string `json:"pythonTag"` Platform string `json:"platform"` BaseID string `json:"baseId,omitempty"` Packages []LockedPkg `json:"packages"` } // LockedPkg 是一个 wheel。SHA256 是文件字节的哈希,不是文件名。 type LockedPkg struct { Name string `json:"name"` Version string `json:"version"` Filename string `json:"filename"` SHA256 string `json:"sha256"` Size int64 `json:"size,omitempty"` } // ID 返回 base- / x- / iso-。 func (l Lock) ID() string { canon := l canon.Packages = append([]LockedPkg(nil), l.Packages...) sort.Slice(canon.Packages, func(i, j int) bool { return normName(canon.Packages[i].Name) < normName(canon.Packages[j].Name) }) raw, _ := json.Marshal(canon) sum := HashBytes(raw) prefix := "x-" switch l.Kind { case "base": prefix = "base-" case "isolated": prefix = "iso-" } return prefix + sum[:16] } func normName(name string) string { var b strings.Builder prevDash := false for _, r := range strings.ToLower(name) { if r == '-' || r == '_' || r == '.' { if !prevDash && b.Len() > 0 { b.WriteByte('-') prevDash = true } continue } prevDash = false b.WriteRune(r) } return strings.Trim(b.String(), "-") }