Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <[email protected]>
119 lines
4.2 KiB
Go
119 lines
4.2 KiB
Go
// 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
|
|
}
|