// Package grimoire parses the real Source Mage spell files (DETAILS, // DEPENDS, SUB_DEPENDS, CONFIGURE, BUILD) into typed Go structs. // // The parser is "hybrid": a small bash subprocess sources the DETAILS file // (because real DETAILS files contain dynamic logic — `SOURCE_VERSION=$(...)`, // `if [[ ... ]]; then SOURCE_URL[0]=...; fi`, multi-source arrays, etc.) and // emits a JSON document on stdout. We then unmarshal that into a Spell // struct. This is the only way to be 100% compatible with an existing // a spell-format grimoire without reimplementing a Bash interpreter. // // For DEPENDS we use a pure-Go line parser because the format is simple // and spawning bash per file is wasteful when IndexAll walks thousands of // spells. package grimoire import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "sync" ) // Spell is the typed metadata extracted from a DETAILS file. type Spell struct { Name string `json:"name"` Version string `json:"version"` Patchlevel string `json:"patchlevel,omitempty"` Source string `json:"source"` SourceURLs []string `json:"source_urls"` SourceHash string `json:"source_hash"` SourceDir string `json:"source_directory"` Website string `json:"website"` Description string `json:"description"` LongDesc string `json:"long_desc"` License string `json:"license"` Entered string `json:"entered"` SecurityPatch string `json:"security_patch,omitempty"` BuildDeps []string `json:"build_deps"` RuntimeDeps []string `json:"runtime_deps"` OptionalDeps []string `json:"optional_deps"` SubDepends []string `json:"sub_depends"` Directory string `json:"-"` } // bridgeScript is the bash we exec to source DETAILS and emit JSON. // We deliberately list every standard SMGL variable so dynamic logic in // the sourced file is respected. Strings are JSON-escaped by jq-like // printf via python3 (available on every Source Mage box) — or, if // python3 is missing, by a tiny bash escaper. const bridgeScript = `#!/bin/bash source "$1" 2>/dev/null || exit 1 emit() { local v="$1"; shift printf '"%s":"%s"\n' "$v" "${!v//\"/\\\"}" } { printf '{' emit SPELL; printf ',' emit VERSION; printf ',' emit PATCHLEVEL; printf ',' emit SOURCE; printf ',' emit SOURCE_HASH; printf ',' emit SOURCE_DIRECTORY; printf ',' emit WEB_SITE; printf ',' emit ENTERED; printf ',' emit SECURITY_PATCH; printf ',' printf '"source_urls":[' i=0 while [[ -n "${SOURCE_URL[$i]:-}" ]]; do [ $i -gt 0 ] && printf ',' printf '"%s"' "${SOURCE_URL[$i]//\"/\\\"}" i=$((i+1)) done printf '],' printf '"license":"%s",' "${LICENSE[0]:-}" printf '"short":"%s",' "${SHORT//\"/\\\"}" printf '"long_desc":"%s"' "$(awk 'BEGIN{getline}{printf "%s\\n",$0}' < len(dirs) { workers = len(dirs) } if workers < 1 { workers = 1 } for w := 0; w < workers; w++ { wg.Add(1) go func() { defer wg.Done() for dir := range jobs { s, err := ParseDetails(dir) if err != nil || s == nil { results <- result{} continue } results <- result{name: s.Name, spell: s} } }() } for _, d := range dirs { jobs <- d } close(jobs) wg.Wait() close(results) index := make(map[string]*Spell, len(dirs)) for r := range results { if r.spell != nil { index[r.name] = r.spell } } return index, nil } // FindSpell locates a spell by name in the grimoire. Returns the directory // path or an error. Used by the CLI when the user runs `sorcery cast wget`. func FindSpell(grimoireRoot, name string) (string, error) { var found string err := filepath.Walk(grimoireRoot, func(path string, info os.FileInfo, err error) error { if err != nil || !info.IsDir() { return nil } if filepath.Base(path) == name { if _, statErr := os.Stat(filepath.Join(path, "DETAILS")); statErr == nil { found = path return filepath.SkipDir } } return nil }) if err != nil { return "", err } if found == "" { return "", fmt.Errorf("grimoire: spell %q not found under %s", name, grimoireRoot) } return found, nil } // Section returns the category (e.g., "libs", "utils") for a spell directory. func Section(spell *Spell) string { if spell.Directory == "" { return "" } rel, err := filepath.Rel(filepath.Dir(filepath.Dir(spell.Directory)), spell.Directory) if err != nil { return "" } return strings.SplitN(rel, string(filepath.Separator), 2)[0] }