190 lines
4.7 KiB
Go
Executable File
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
|
|
}
|