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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201// Package alpine resolves Alpine Linux origins (source packages) to their upstream// repositories.//// Data sources (all read through fetch.Client)://// - the APKINDEX of edge/{main,community} (dl-cdn.alpinelinux.org) lists every binary// package with its origin, version and upstream url;// - the aports tree, read in one streamed download of the archive (aports.go), gives each// origin's APKBUILD. Alpine has no .SRCINFO, so APKBUILD is parsed directly with a small// bash-variable evaluator (ParseAPKBUILD) that handles the substitutions aports uses.package alpine
import ( "archive/tar" "bufio" "bytes" "compress/gzip" "context" "errors" "fmt" "io" "strings" "time"
"tangled.org/odd.computer/isomorph/internal/distro" "tangled.org/odd.computer/isomorph/internal/fetch" "tangled.org/odd.computer/isomorph/internal/model" "tangled.org/odd.computer/isomorph/internal/repourl")
const ( // DefaultIndexBase is the package mirror serving the APKINDEX files. DefaultIndexBase = "https://dl-cdn.alpinelinux.org/alpine" // edgeRelease is the APKINDEX release directory of aports' master branch. edgeRelease = "edge" // indexArch is the architecture whose APKINDEX enumerates packages (every arch-independent // source package builds for x86_64). indexArch = "x86_64")
// Evidence sources and weights (prior trust in the field).const ( srcSourceVCS = "APKBUILD:source:git" srcSource = "APKBUILD:source" srcURL = "APKBUILD:url" // srcRepoURL is the _repo_url variable KDE's APKBUILDs set to the project's git repository. srcRepoURL = "APKBUILD:_repo_url"
weightSourceVCS = 0.9 weightSource = 0.5 weightURL = 0.5 weightRepoURL = 0.9)
// Resolver reads Alpine's package indexes.type Resolver struct { c *fetch.Client // IndexBase is the package mirror; APKINDEX.tar.gz is read from // IndexBase/edge/<section>/x86_64/. IndexBase string}
// New returns a Resolver using c, with the default mirror.func New(c *fetch.Client) *Resolver { return &Resolver{c: c, IndexBase: DefaultIndexBase}}
func (r *Resolver) indexURL(section string) string { return r.IndexBase + "/" + edgeRelease + "/" + section + "/" + indexArch + "/APKINDEX.tar.gz"}
// Candidates returns the upstream candidates an APKBUILD supports: every remote source (VCS// sources weigh most) and the url field. One observation per (kind, key) from the source list,// so that a series of files from one host does not inflate the score. Not yet ranked.func Candidates(ab *APKBUILD) []distro.Candidate { var cands []distro.Candidate seen := map[string]bool{} for _, s := range ab.Sources { if s.Local || isSignatureFile(s.URL) { continue } c, err := repourl.Parse(s.Remote()) if err != nil { continue } k := string(c.Kind) + ":" + c.Key if seen[k] { continue } seen[k] = true source, weight := srcSource, weightSource if s.VCS != "" { source, weight = srcSourceVCS, weightSourceVCS if s.VCS != "git" { source = "APKBUILD:source:" + s.VCS } } cands = distro.AddCandidate(cands, s.Remote(), model.Evidence{Source: source, Value: s.Raw, Weight: weight}) } if u := strings.TrimSpace(ab.Vars["_repo_url"]); u != "" { cands = distro.AddCandidate(cands, u, model.Evidence{Source: srcRepoURL, Value: u, Weight: weightRepoURL}) } if ab.URL != "" { cands = distro.AddCandidate(cands, ab.URL, model.Evidence{Source: srcURL, Value: ab.URL, Weight: weightURL}) } return cands}
// Index returns the edge APKINDEX stanzas of section (main, community).func (r *Resolver) Index(ctx context.Context, section string) ([]IndexEntry, error) { b, err := r.c.Get(ctx, r.indexURL(section), fetch.WithTTL(6*time.Hour)) if err != nil { return nil, fmt.Errorf("alpine: %s APKINDEX: %w", section, err) } return ParseAPKINDEX(b)}
// IndexEntry is one stanza of an APKINDEX.type IndexEntry struct { Name string // P: Version string // V: Origin string // o: (the source package / APKBUILD directory) URL string // U:}
// ParseAPKINDEX reads an APKINDEX.tar.gz (a signature tar segment followed by the index tar,// as concatenated gzip streams) and returns its stanzas.func ParseAPKINDEX(tgz []byte) ([]IndexEntry, error) { gz, err := gzip.NewReader(bytes.NewReader(tgz)) if err != nil { return nil, fmt.Errorf("apkindex: gzip: %w", err) } defer gz.Close() tr := tar.NewReader(gz) for { hdr, err := tr.Next() if err == io.EOF { break } if err != nil { return nil, fmt.Errorf("apkindex: tar: %w", err) } if hdr.Name != "APKINDEX" { continue } b, err := io.ReadAll(io.LimitReader(tr, 256<<20)) if err != nil { return nil, fmt.Errorf("apkindex: read: %w", err) } return parseIndexText(b), nil } return nil, errors.New("apkindex: no APKINDEX member in archive")}
// parseIndexText parses the "K:value" stanzas of an APKINDEX.func parseIndexText(b []byte) []IndexEntry { var out []IndexEntry var cur IndexEntry flush := func() { if cur.Name != "" { out = append(out, cur) } cur = IndexEntry{} } sc := bufio.NewScanner(bytes.NewReader(b)) sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) for sc.Scan() { line := sc.Text() if line == "" { flush() continue } if len(line) < 2 || line[1] != ':' { continue } v := line[2:] switch line[0] { case 'P': cur.Name = v case 'V': cur.Version = v case 'o': cur.Origin = v case 'U': cur.URL = v } } flush() return out}
func isSignatureFile(u string) bool { l := strings.ToLower(u) for _, ext := range []string{".sig", ".asc", ".sign", ".gpg", ".minisig"} { if strings.HasSuffix(l, ext) { return true } } return false}