// Real DEPENDS / SUB_DEPENDS parser. // // Source Mage DEPENDS files use these directives: // // depends [""] [""] [] // optional_depends "" "" "" // sub_depends // runtime_depends // // The 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 [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 }