sorcery-go/pkg/grimoire/depends.go

190 lines
4.7 KiB
Go
Executable File

// Real DEPENDS / SUB_DEPENDS parser.
//
// Source Mage DEPENDS files use these directives:
//
// depends <spell> ["<sub_depends>"] ["<configure_flag>"] [<type>]
// optional_depends <spell> "<sub>" "<flag>" "<description>"
// sub_depends <spell> <feature>
// runtime_depends <spell>
//
// The <type> field is optional and may be one of:
// "" (defaults to runtime)
// "build" (build-only)
// "missing" (a missing dep — flagged for the lint pass)
// "-optional" (legacy form of optional_depends)
// "-subdepends" (this dep is conditional on a sub_depends)
//
// We are deliberately permissive: anything we can't parse is skipped with
// a debug log rather than aborting the whole grimoire walk.
package grimoire
import (
"bufio"
"os"
"strings"
)
// DependEntry is one parsed `depends` / `optional_depends` line.
type DependEntry struct {
Name string
Type string // "runtime" | "build" | "optional"
SubDep string // optional sub_depends token
Flag string // optional configure flag
Desc string // optional human description (optional_depends only)
}
// ParseDepends reads a DEPENDS file and returns the parsed entries.
// Errors are returned only for I/O failures; malformed lines are skipped.
func ParseDepends(path string) ([]DependEntry, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var out []DependEntry
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
var entry DependEntry
switch {
case strings.HasPrefix(line, "optional_depends"):
entry = parseOptionalDepends(line)
case strings.HasPrefix(line, "runtime_depends"):
entry = parseRuntimeDepends(line)
case strings.HasPrefix(line, "depends"):
entry = parseDepends(line)
case strings.HasPrefix(line, "sub_depends"):
// handled by ParseSubDepends; skip here
continue
case strings.HasPrefix(line, "conflicts"):
continue
case strings.HasPrefix(line, "suggests"):
continue
default:
continue
}
if entry.Name != "" {
out = append(out, entry)
}
}
return out, scanner.Err()
}
// ParseSubDepends extracts just the sub_depends directives from a DEPENDS
// file. Returns a slice of "spell:feature" strings.
func ParseSubDepends(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
var out []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if !strings.HasPrefix(line, "sub_depends") {
continue
}
fields := tokenize(line)
if len(fields) >= 3 {
out = append(out, fields[1]+":"+fields[2])
}
}
return out, scanner.Err()
}
func parseDepends(line string) DependEntry {
fields := tokenize(line)
if len(fields) < 2 {
return DependEntry{}
}
entry := DependEntry{Name: fields[1], Type: "runtime"}
// Quoted fields come back from tokenize without their quotes.
// Pattern: depends <name> [sub] [flag] [type]
rest := fields[2:]
for _, f := range rest {
switch {
case f == "build" || f == "missing":
entry.Type = f
case f == "-optional":
entry.Type = "optional"
case f == "-subdepends":
// marks a conditional dep — keep type as-is
case strings.HasPrefix(f, "--"):
if entry.Flag == "" {
entry.Flag = f
}
default:
if entry.SubDep == "" {
entry.SubDep = f
} else if entry.Flag == "" {
entry.Flag = f
}
}
}
return entry
}
func parseOptionalDepends(line string) DependEntry {
fields := tokenize(line)
if len(fields) < 2 {
return DependEntry{}
}
entry := DependEntry{Name: fields[1], Type: "optional"}
if len(fields) > 2 {
entry.SubDep = fields[2]
}
if len(fields) > 3 {
entry.Flag = fields[3]
}
if len(fields) > 4 {
entry.Desc = strings.Join(fields[4:], " ")
}
return entry
}
func parseRuntimeDepends(line string) DependEntry {
fields := tokenize(line)
if len(fields) < 2 {
return DependEntry{}
}
return DependEntry{Name: fields[1], Type: "runtime"}
}
// tokenize splits a shell-like line into fields, respecting single and
// double quotes. Tokens are returned without their surrounding quotes.
func tokenize(line string) []string {
var out []string
var cur strings.Builder
var inSingle, inDouble bool
for i := 0; i < len(line); i++ {
c := line[i]
switch {
case c == '\\' && i+1 < len(line):
cur.WriteByte(line[i+1])
i++
case c == '\'' && !inDouble:
inSingle = !inSingle
case c == '"' && !inSingle:
inDouble = !inDouble
case (c == ' ' || c == '\t') && !inSingle && !inDouble:
if cur.Len() > 0 {
out = append(out, cur.String())
cur.Reset()
}
default:
cur.WriteByte(c)
}
}
if cur.Len() > 0 {
out = append(out, cur.String())
}
return out
}