Something went wrong. Try again.
Maps Linux distribution source packages to their upstream repositories, and through them to each other
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525package opensuse
import ( "net/url" "path" "regexp" "strconv" "strings")
// Spec is the subset of an RPM spec file that matters for upstream discovery: the preamble tags,// the Source/Patch lists, the `#Git-Clone:` comment and the `# PATCH-...` tags that openSUSE// packagers put above PatchN lines. Macros are expanded textually (see expand) and conditionals// are evaluated as far as possible with the Factory defaults; anything unknown is kept verbatim.type Spec struct { Name string Version string Release string Epoch string URL string GitClone string Sources []Source Patches []Patch // Macros is the final macro table (package-defined plus name/version/release/url). Macros map[string]string // Warnings lists things the parser could not make sense of. Warnings []string}
// Source is one SourceN tag.type Source struct { Tag string // "Source", "Source0", "Source1000" Raw string // unexpanded value // Value is the expanded value with the "#/rename" fragment stripped. Value string // Rename is the "#/name" fragment (the file name OBS stores the download under), if any. Rename string IsURL bool}
// Patch is one PatchN tag together with the comment lines directly above it.type Patch struct { Tag string Raw string Value string // expanded value (a file name or a URL) // File is the base name, i.e. the file as stored in the OBS package directory. File string IsURL bool // Comment is the "# PATCH-FIX-UPSTREAM ..." tag associated with this patch, if any. Comment *PatchTag // Comments are the comment lines directly above the Patch line (without "# "). Comments []string}
// PatchTag is a parsed "# PATCH-(FIX|FEATURE)-(UPSTREAM|OPENSUSE|SLE) [file] [refs] [email] [-- text]".type PatchTag struct { Raw string Kind string // FIX or FEATURE Scope string // UPSTREAM, OPENSUSE or SLE File string // Refs are bug references (bsc#N, gh#o/r#N, jsc#X) and URLs found before "--". Refs []string Email string // Text is the free text after "--" plus any tokens that were not recognised. Text string}
// IsUpstream reports whether the tag says the patch comes from or went upstream.func (t *PatchTag) IsUpstream() bool { return t != nil && t.Scope == "UPSTREAM" }
// IsLocal reports whether the tag marks a distro-specific patch.func (t *PatchTag) IsLocal() bool { return t != nil && (t.Scope == "OPENSUSE" || t.Scope == "SLE") }
var ( specTagRe = regexp.MustCompile(`(?i)^(Name|Version|Release|Epoch|URL|Source\d*|Patch\d*)\s*:\s*(.*?)\s*$`) patchTagRe = regexp.MustCompile(`(?i)^#\s*PATCH-(FIX|FEATURE)-(UPSTREAM|OPENSUSE|SLE)\b:?\s*(.*?)\s*$`) gitCloneRe = regexp.MustCompile(`(?i)^#\s*Git-Clone:\s*(\S+)`) defineRe = regexp.MustCompile(`^%(?:define|global)\s+([A-Za-z_][A-Za-z0-9_]*)(\([^)]*\))?\s*(.*)$`) bcondRe = regexp.MustCompile(`^%bcond_(with|without)\s+([A-Za-z_][A-Za-z0-9_]*)`) bcondNewRe = regexp.MustCompile(`^%bcond\s+([A-Za-z_][A-Za-z0-9_]*)\s+(\S+)`) condRe = regexp.MustCompile(`^%(if|ifarch|ifnarch|ifos|ifnos|elif|elifarch|elifos|else|endif)\b\s*(.*?)\s*$`) sectionRe = regexp.MustCompile(`^%(package|description|prep|build|install|check|files|changelog|pre|post|preun|postun|pretrans|posttrans|trigger\w*|generate_buildrequires|conf|sourcelist|patchlist)\b`) patchFileRe = regexp.MustCompile(`(?i)^[\w.+~-]+\.(?:patch|diff|dif)(?:\.(?:gz|bz2|xz|zst))?$`) tagRefTokenRe = regexp.MustCompile(`(?i)^(?:[a-z]+#\S+|https?://\S+|CVE-\d{4}-\d+)$`) emailTokenRe = regexp.MustCompile(`^<?[\w.+-]+@[\w.-]+\.\w+>?$`) shaTokenRe = regexp.MustCompile(`^[0-9a-f]{7,64}$`))
// factoryMacros are the values the Factory project config defines; they let the conditional// evaluator pick the openSUSE branch of `%if 0%{?suse_version}` style blocks.var factoryMacros = map[string]string{ "suse_version": "1699", "is_opensuse": "1", "nil": "", "_arch": "x86_64",}
// ParseSpec parses spec text. It never fails; problems are reported in Spec.Warnings.func ParseSpec(text string) *Spec { p := &specParser{spec: &Spec{Macros: map[string]string{}}} for k, v := range factoryMacros { p.spec.Macros[k] = v } p.run(joinContinuations(strings.Split(strings.ReplaceAll(text, "\r\n", "\n"), "\n"))) return p.spec}
// joinContinuations merges lines ending in a backslash with the following line.func joinContinuations(lines []string) []string { var out []string for i := 0; i < len(lines); i++ { l := lines[i] for strings.HasSuffix(l, "\\") && i+1 < len(lines) { l = strings.TrimRight(strings.TrimSuffix(l, "\\"), " \t") + " " + strings.TrimSpace(lines[i+1]) i++ } out = append(out, l) } return out}
type condState int
const ( condTrue condState = iota condFalse condUnknown)
type condFrame struct { state condState taken bool // a branch was definitely taken anyUnknown bool // a branch had an unknown condition}
type lineItem struct { comment bool text string // comment text without leading "#" patchIdx int // index into spec.Patches, or -1}
type specParser struct { spec *Spec frames []condFrame items []lineItem // tagState remembers whether a preamble tag was set inside an unknown branch so a later // definite branch can override it. tagState map[string]condState inSection bool}
func (p *specParser) active() condState { st := condTrue for _, f := range p.frames { switch f.state { case condFalse: return condFalse case condUnknown: st = condUnknown } } return st}
func (p *specParser) define(name, value string, force bool) { st := p.active() if st == condFalse { return } value = strings.ReplaceAll(value, "@BUILD_FLAVOR@", "") if _, ok := p.spec.Macros[name]; ok && st == condUnknown && !force { return // first definition wins inside branches we cannot evaluate } p.spec.Macros[name] = value}
func (p *specParser) run(lines []string) { p.tagState = map[string]condState{} for _, raw := range lines { line := strings.TrimRight(raw, " \t") trimmed := strings.TrimSpace(line) switch { case trimmed == "": p.items = append(p.items, lineItem{patchIdx: -1}) continue case strings.HasPrefix(trimmed, "#"): p.comment(trimmed) continue } if m := condRe.FindStringSubmatch(trimmed); m != nil { p.conditional(m[1], m[2]) p.items = append(p.items, lineItem{patchIdx: -1}) continue } if p.active() == condFalse && !strings.HasPrefix(trimmed, "%") { // Tags in a definitely-false branch: still collect Sources/Patches (the files exist // in the package) but never let them define name/version/url. if m := specTagRe.FindStringSubmatch(trimmed); m != nil { p.tag(m[1], m[2], condFalse) continue } } if strings.HasPrefix(trimmed, "%") { p.directive(trimmed) p.items = append(p.items, lineItem{patchIdx: -1}) continue } if m := specTagRe.FindStringSubmatch(trimmed); m != nil && !p.inSection { p.tag(m[1], m[2], p.active()) continue } p.items = append(p.items, lineItem{patchIdx: -1}) } if len(p.frames) > 0 { p.spec.Warnings = append(p.spec.Warnings, "unbalanced %if") } p.finish()}
func (p *specParser) comment(line string) { if m := gitCloneRe.FindStringSubmatch(line); m != nil && p.spec.GitClone == "" { p.spec.GitClone = strings.TrimSuffix(m[1], "/") } text := strings.TrimSpace(strings.TrimLeft(line, "#")) p.items = append(p.items, lineItem{comment: true, text: text, patchIdx: -1})}
func (p *specParser) directive(line string) { if m := defineRe.FindStringSubmatch(line); m != nil { if m[2] != "" { return // parametric macro: not textual } p.define(m[1], strings.TrimSpace(m[3]), false) return } if m := bcondRe.FindStringSubmatch(line); m != nil { if m[1] == "without" { p.define("with_"+m[2], "1", false) } return } if m := bcondNewRe.FindStringSubmatch(line); m != nil { v := p.expand(m[2]) if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n != 0 { p.define("with_"+m[1], "1", false) } return } if sectionRe.MatchString(line) { p.inSection = true }}
func (p *specParser) conditional(kw, expr string) { switch kw { case "if": st := p.eval(expr) p.frames = append(p.frames, condFrame{state: st, taken: st == condTrue, anyUnknown: st == condUnknown}) case "ifarch", "ifnarch", "ifos", "ifnos": p.frames = append(p.frames, condFrame{state: condUnknown, anyUnknown: true}) case "elif", "elifarch", "elifos": if len(p.frames) == 0 { return } f := &p.frames[len(p.frames)-1] switch { case f.taken: f.state = condFalse default: st := condUnknown if kw == "elif" { st = p.eval(expr) } if f.anyUnknown && st == condTrue { st = condUnknown } f.state = st if st == condTrue { f.taken = true } if st == condUnknown { f.anyUnknown = true } } case "else": if len(p.frames) == 0 { return } f := &p.frames[len(p.frames)-1] switch { case f.taken: f.state = condFalse case f.anyUnknown: f.state = condUnknown default: f.state = condTrue f.taken = true } case "endif": if len(p.frames) > 0 { p.frames = p.frames[:len(p.frames)-1] } }}
func (p *specParser) tag(name, value string, st condState) { lname := strings.ToLower(name) switch { case strings.HasPrefix(lname, "source"): p.spec.Sources = append(p.spec.Sources, Source{Tag: name, Raw: value}) p.items = append(p.items, lineItem{patchIdx: -1}) return case strings.HasPrefix(lname, "patch"): p.spec.Patches = append(p.spec.Patches, Patch{Tag: name, Raw: value}) p.items = append(p.items, lineItem{patchIdx: len(p.spec.Patches) - 1}) return } p.items = append(p.items, lineItem{patchIdx: -1}) if st == condFalse { return } prev, seen := p.tagState[lname] if seen && !(prev == condUnknown && st == condTrue) { return // first definite value wins } p.tagState[lname] = st exp := strings.TrimSpace(p.expand(value)) switch lname { case "name": p.spec.Name = exp case "version": p.spec.Version = exp case "release": p.spec.Release = exp case "epoch": p.spec.Epoch = exp case "url": p.spec.URL = exp } p.spec.Macros[lname] = exp}
// finish expands the collected Source/Patch values and associates patch tags.func (p *specParser) finish() { s := p.spec // Re-expand the preamble tags: macros defined after the tag (rare) become visible. s.Name = strings.TrimSpace(p.expand(s.Name)) s.Version = strings.TrimSpace(p.expand(s.Version)) s.Release = strings.TrimSpace(p.expand(s.Release)) s.URL = strings.TrimSpace(p.expand(s.URL)) s.Macros["name"], s.Macros["version"], s.Macros["release"], s.Macros["url"] = s.Name, s.Version, s.Release, s.URL for i := range s.Sources { src := &s.Sources[i] v := strings.TrimSpace(p.expand(src.Raw)) if j := strings.Index(v, "#/"); j >= 0 { src.Rename = v[j+2:] v = v[:j] } src.Value = v src.IsURL = strings.Contains(v, "://") } patchFiles := map[string]bool{} for i := range s.Patches { pt := &s.Patches[i] v := strings.TrimSpace(p.expand(pt.Raw)) if j := strings.Index(v, "#/"); j >= 0 { pt.File = v[j+2:] v = v[:j] } pt.Value = v pt.IsURL = strings.Contains(v, "://") if pt.File == "" { pt.File = v if pt.IsURL { if u, err := url.Parse(v); err == nil { pt.File = path.Base(u.Path) } } } patchFiles[pt.File] = true } p.associateTags(patchFiles)}
// associateTags links "# PATCH-..." comments to Patch lines: by file name anywhere in the// surrounding comment/Patch block first, then by adjacency (the nearest unclaimed tag directly// above the Patch line whose file name does not belong to another patch).func (p *specParser) associateTags(patchFiles map[string]bool) { type tagAt struct { line int tag *PatchTag claimed bool } isBlock := func(i int) bool { return p.items[i].comment || p.items[i].patchIdx >= 0 } for i, it := range p.items { if it.patchIdx < 0 { continue } pt := &p.spec.Patches[it.patchIdx] // Contiguous comment run directly above. for j := i - 1; j >= 0 && p.items[j].comment; j-- { if t := p.items[j].text; t != "" { pt.Comments = append([]string{t}, pt.Comments...) } } } // Walk blocks of comment/Patch lines. for start := 0; start < len(p.items); { if !isBlock(start) { start++ continue } end := start for end < len(p.items) && isBlock(end) { end++ } var tags []*tagAt for j := start; j < end; j++ { if p.items[j].comment { if t := parsePatchTag(p.items[j].text); t != nil { tags = append(tags, &tagAt{line: j, tag: t}) } } } if len(tags) > 0 { for j := start; j < end; j++ { idx := p.items[j].patchIdx if idx < 0 { continue } pt := &p.spec.Patches[idx] var pick *tagAt for _, t := range tags { if !t.claimed && t.tag.File != "" && t.tag.File == pt.File { pick = t break } } if pick == nil { for k := j - 1; k >= start && p.items[k].comment; k-- { for _, t := range tags { if t.line == k && !t.claimed && (t.tag.File == "" || !patchFiles[t.tag.File]) { pick = t } } if pick != nil { break } } } if pick != nil { pick.claimed = true pt.Comment = pick.tag } } } start = end }}
// parsePatchTag parses the text of a comment line (without the leading "#").func parsePatchTag(text string) *PatchTag { m := patchTagRe.FindStringSubmatch("#" + text) if m == nil { return nil } t := &PatchTag{Raw: text, Kind: strings.ToUpper(m[1]), Scope: strings.ToUpper(m[2])} rest := m[3] head, tail := rest, "" if i := strings.Index(rest, "--"); i >= 0 && (i == 0 || rest[i-1] == ' ') && (i+2 == len(rest) || rest[i+2] == ' ') { head, tail = rest[:i], strings.TrimSpace(rest[i+2:]) } var extra []string for _, tok := range strings.Fields(head) { tok = strings.Trim(tok, "[](),;") switch { case tok == "": case t.File == "" && patchFileRe.MatchString(tok): t.File = tok case tagRefTokenRe.MatchString(tok): t.Refs = append(t.Refs, tok) case t.Email == "" && emailTokenRe.MatchString(tok): t.Email = strings.Trim(tok, "<>") case shaTokenRe.MatchString(tok) && len(tok) >= 7: t.Refs = append(t.Refs, tok) default: extra = append(extra, tok) } } if len(extra) > 0 { if tail != "" { tail = strings.Join(extra, " ") + " " + tail } else { tail = strings.Join(extra, " ") } } t.Text = tail // Commit ids and URLs in the free text are references too ("-- based on commit abc123", // "-- https://.../merge-requests/1/"); short hex words are ignored to avoid English words. for _, tok := range strings.Fields(tail) { tok = strings.Trim(tok, "[](),;") if strings.HasPrefix(tok, "http://") || strings.HasPrefix(tok, "https://") || (shaTokenRe.MatchString(tok) && len(tok) >= 12) { t.Refs = appendUnique(t.Refs, tok) } } return t}
// Expand expands macros in text against the spec's macro table.func (s *Spec) Expand(text string) string { p := &specParser{spec: s} return p.expand(text)}
// SourceURLs returns the expanded Source values that are URLs.func (s *Spec) SourceURLs() []string { var out []string for _, src := range s.Sources { if src.IsURL { out = append(out, src.Value) } } return out}