// Package zeta models Zeta package artifacts: package.lua manifests // (manifest.go) and zeta-makepkg .recipe source-build recipes (recipe.go). package zeta import ( "fmt" "regexp" "strings" ) // Recipe models a declarative Zeta .recipe file — the input format for // zeta-makepkg source builds. It is purely data; zeta-makepkg handles the // *how* (fetch, extract, build, package). // // v1 of zeta-reconstruct always uses build_system = "custom" wrapping an // arbitrary PKGBUILD-derived build.sh, so BuildScript is always required. type Recipe struct { Name string Version string Summary string URL string SHA256 string // source checksum; empty means "omit the line" (see Emit) Deps []string // runtime dependencies BuildScript string // required: build_system is always "custom" in v1 } var ( // Mirrors path.sanitize_name (zeta-toolchain/toolchain/lib/path.lua:94-100): // alphanumeric, dots, underscores, plus, hyphen — no leading dot. recipeNameRE = regexp.MustCompile(`^[\w.+\-]+$`) // Mirrors recipe.lua HEX64 (toolchain/lib/recipe.lua:37). recipeHex64RE = regexp.MustCompile(`^[0-9a-f]{64}$`) ) func recipeNameValid(name string) bool { if name == "" || strings.HasPrefix(name, ".") { return false } return recipeNameRE.MatchString(name) } // Validate mirrors zeta-toolchain/toolchain/lib/recipe.lua normalize() // (:43-173) for the fields zeta-reconstruct v1 emits. build_system is fixed // to "custom", so a non-empty BuildScript is always required (:121-123). func (r *Recipe) Validate() error { if !recipeNameValid(r.Name) { return fmt.Errorf("recipe: missing or invalid package name") } if r.Version == "" { return fmt.Errorf("recipe %q: version is required and must be a non-empty string", r.Name) } if r.Summary == "" { return fmt.Errorf("recipe %q: summary is required", r.Name) } if r.URL == "" { return fmt.Errorf("recipe %q: url is required", r.Name) } if r.SHA256 != "" && !recipeHex64RE.MatchString(strings.ToLower(r.SHA256)) { return fmt.Errorf("recipe %q: sha256 must be exactly 64 hex characters or nil", r.Name) } for _, d := range r.Deps { if d == "" { return fmt.Errorf("recipe %q: each dep must be a non-empty string", r.Name) } } if r.BuildScript == "" { return fmt.Errorf("recipe %q: build_script is required when build_system is 'custom'", r.Name) } return nil } // escapeRecipeString escapes a value for a double-quoted Lua string literal. // Only double quotes and newlines are escaped, matching the emitter used by // zeta-toolchain's packager.lua. func escapeRecipeString(s string) string { s = strings.ReplaceAll(s, `"`, `\"`) s = strings.ReplaceAll(s, "\n", `\n`) return s } // Emit renders the recipe as .recipe Lua text: 2-space indent, field keys // padded to width 13 before " = " (replicating the alignment of recipe.lua // and examples/custom-demo/custom.recipe), fields in order name, version, // summary, url, [sha256,] deps, build_system, build_script. // // CRITICAL: the sha256 line is omitted ENTIRELY when SHA256 is empty. // recipe.lua checks `raw.sha256 ~= nil` (toolchain/lib/recipe.lua:74), so an // empty string is treated as present and fails the 64-hex check. Never emit // `sha256 = ""`. func (r *Recipe) Emit() (string, error) { if err := r.Validate(); err != nil { return "", err } var b strings.Builder b.WriteString("return {\n") fmt.Fprintf(&b, " %-13s= \"%s\",\n", "name", r.Name) fmt.Fprintf(&b, " %-13s= \"%s\",\n", "version", r.Version) fmt.Fprintf(&b, " %-13s= \"%s\",\n", "summary", escapeRecipeString(r.Summary)) fmt.Fprintf(&b, " %-13s= \"%s\",\n", "url", r.URL) if r.SHA256 != "" { // recipe.lua normalizes sha256 to lowercase on load (:78). fmt.Fprintf(&b, " %-13s= \"%s\",\n", "sha256", strings.ToLower(r.SHA256)) } if len(r.Deps) == 0 { b.WriteString(" deps = {},\n") } else { quoted := make([]string, len(r.Deps)) for i, d := range r.Deps { quoted[i] = `"` + escapeRecipeString(d) + `"` } fmt.Fprintf(&b, " %-13s= { %s },\n", "deps", strings.Join(quoted, ", ")) } fmt.Fprintf(&b, " %-13s= \"custom\",\n", "build_system") fmt.Fprintf(&b, " %-13s= \"%s\",\n", "build_script", r.BuildScript) b.WriteString("}\n") return b.String(), nil }