230 lines
6.3 KiB
Go
230 lines
6.3 KiB
Go
package index
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-delivery/sdk/trunk"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
// The requirements state (DLV-I7, the lead's ruling on I7; REG S15, ADR 0606 §1): per mesh Go module, the highest
|
|
// version of it each consumer's go.sum names, filled at each merge from the directories whose go.sum the indexer reads.
|
|
// The builder's floor asks it through indexer_requirements, so a version is never numbered below what a consumer
|
|
// already requires.
|
|
const (
|
|
RequirementsState = "requirements"
|
|
RequirementsBucket = Module + "_" + RequirementsState
|
|
RequirementsTool = "indexer_requirements"
|
|
)
|
|
|
|
// Requirement is the value of one package's key in the requirements state: By maps each consumer, "<repository>:<dir>",
|
|
// to the highest version of Package its go.sum names.
|
|
type Requirement struct {
|
|
V int `json:"v"`
|
|
Package string `json:"package"`
|
|
By map[string]string `json:"by"`
|
|
}
|
|
|
|
// Versions are the distinct versions consumers require, highest last (CompareVersions).
|
|
func (r Requirement) Versions() []string {
|
|
seen := map[string]bool{}
|
|
for _, v := range r.By {
|
|
seen[v] = true
|
|
}
|
|
out := slices.Collect(maps.Keys(seen))
|
|
slices.SortFunc(out, CompareVersions)
|
|
return out
|
|
}
|
|
|
|
// RequiredOf is, per mesh Go module a go.sum names (as fingerprint.MeshPackagesOf takes them), the highest version it
|
|
// names, its module line or its go.mod line alike.
|
|
func RequiredOf(goSum []byte) map[string]string {
|
|
out := map[string]string{}
|
|
for _, line := range strings.Split(string(goSum), "\n") {
|
|
f := strings.Fields(line)
|
|
if len(f) < 2 || !strings.HasPrefix(f[0], trunk.ForgeModulePrefix) {
|
|
continue
|
|
}
|
|
if _, _, ok := trunk.PackageRepository(f[0]); !ok {
|
|
continue
|
|
}
|
|
v := strings.TrimSuffix(f[1], "/go.mod")
|
|
if cur, ok := out[f[0]]; !ok || CompareVersions(cur, v) < 0 {
|
|
out[f[0]] = v
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// CompareVersions orders two Go module versions as semantic versions (vMAJOR.MINOR.PATCH[-pre][+build]); a
|
|
// pseudo-version is a pre-release, so it sorts below the release it precedes. One that does not read sorts first.
|
|
func CompareVersions(a, b string) int {
|
|
pa, oka := semverParts(a)
|
|
pb, okb := semverParts(b)
|
|
switch {
|
|
case !oka || !okb:
|
|
if oka != okb {
|
|
if oka {
|
|
return 1
|
|
}
|
|
return -1
|
|
}
|
|
return strings.Compare(a, b)
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
if c := cmpNum(pa.core[i], pb.core[i]); c != 0 {
|
|
return c
|
|
}
|
|
}
|
|
switch {
|
|
case pa.pre == "" && pb.pre == "":
|
|
return 0
|
|
case pa.pre == "":
|
|
return 1
|
|
case pb.pre == "":
|
|
return -1
|
|
}
|
|
xa, xb := strings.Split(pa.pre, "."), strings.Split(pb.pre, ".")
|
|
for i := 0; i < len(xa) && i < len(xb); i++ {
|
|
na, ea := strconv.ParseUint(xa[i], 10, 64)
|
|
nb, eb := strconv.ParseUint(xb[i], 10, 64)
|
|
switch {
|
|
case ea == nil && eb == nil:
|
|
if na != nb {
|
|
return cmpNum(na, nb)
|
|
}
|
|
case ea == nil:
|
|
return -1
|
|
case eb == nil:
|
|
return 1
|
|
default:
|
|
if c := strings.Compare(xa[i], xb[i]); c != 0 {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
return cmpNum(uint64(len(xa)), uint64(len(xb)))
|
|
}
|
|
|
|
type semver struct {
|
|
core [3]uint64
|
|
pre string
|
|
}
|
|
|
|
func semverParts(v string) (semver, bool) {
|
|
var s semver
|
|
if !strings.HasPrefix(v, "v") {
|
|
return s, false
|
|
}
|
|
v, _, _ = strings.Cut(v[1:], "+")
|
|
core, pre, _ := strings.Cut(v, "-")
|
|
parts := strings.Split(core, ".")
|
|
if len(parts) != 3 {
|
|
return s, false
|
|
}
|
|
for i, p := range parts {
|
|
n, err := strconv.ParseUint(p, 10, 64)
|
|
if err != nil {
|
|
return s, false
|
|
}
|
|
s.core[i] = n
|
|
}
|
|
s.pre = pre
|
|
return s, true
|
|
}
|
|
|
|
func cmpNum(a, b uint64) int {
|
|
switch {
|
|
case a < b:
|
|
return -1
|
|
case a > b:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// keepRequirements writes what the go.sum of repository's directory dir requires into the requirements state: each
|
|
// package it names gets the consumer's version, and a package it no longer names loses the consumer. nil
|
|
// Requirements keeps nothing. A write that fails is said in the log; the next merge of the directory writes it again.
|
|
func (ix *Indexer) keepRequirements(ctx context.Context, repository, dir string, goSum []byte) {
|
|
if ix.Requirements == nil {
|
|
return
|
|
}
|
|
consumer := repository + ":" + dir
|
|
ix.reqMu.Lock()
|
|
if ix.reqKept == nil {
|
|
ix.reqKept = map[string]bool{}
|
|
}
|
|
ix.reqKept[consumer] = true
|
|
ix.reqMu.Unlock()
|
|
required := RequiredOf(goSum)
|
|
held, err := ix.Requirements.Keys(ctx)
|
|
if err != nil {
|
|
ix.logf("requirements: the state cannot be listed, so what %s requires is not kept: %v", consumer, err)
|
|
return
|
|
}
|
|
for _, pkg := range slices.Sorted(maps.Keys(required)) {
|
|
if !slices.Contains(held, pkg) {
|
|
held = append(held, pkg)
|
|
}
|
|
}
|
|
for _, pkg := range held {
|
|
version, names := required[pkg]
|
|
err := write(ctx, ix.Requirements, pkg, func(value []byte, found bool) ([]byte, bool, error) {
|
|
r := Requirement{V: keys.Version, Package: pkg, By: map[string]string{}}
|
|
if found && json.Unmarshal(value, &r) != nil {
|
|
r = Requirement{V: keys.Version, Package: pkg, By: map[string]string{}}
|
|
}
|
|
if r.By == nil {
|
|
r.By = map[string]string{}
|
|
}
|
|
if cur, has := r.By[consumer]; names && cur == version || !names && !has {
|
|
return nil, false, nil
|
|
}
|
|
if names {
|
|
r.By[consumer] = version
|
|
} else {
|
|
delete(r.By, consumer)
|
|
}
|
|
if len(r.By) == 0 {
|
|
return nil, true, nil
|
|
}
|
|
next, err := json.Marshal(r)
|
|
return next, false, err
|
|
})
|
|
if err != nil {
|
|
ix.logf("requirements: %s for %s is not kept: %v", pkg, consumer, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RequirementsOf answers indexer_requirements for one package from the state's value: the distinct versions its
|
|
// consumers require, highest last; none is an empty list.
|
|
func RequirementsOf(value []byte, found bool) ([]string, error) {
|
|
if !found {
|
|
return []string{}, nil
|
|
}
|
|
var r Requirement
|
|
if err := json.Unmarshal(value, &r); err != nil {
|
|
return nil, fmt.Errorf("the requirements value does not read: %w", err)
|
|
}
|
|
return r.Versions(), nil
|
|
}
|
|
|
|
// requirementsKept says this process has kept what the directory's go.sum requires: until it has, the go.sum is read
|
|
// even where its hash is unchanged, so a consumer not merged since the state appeared is still kept at its next message.
|
|
func (ix *Indexer) requirementsKept(repository, dir string) bool {
|
|
if ix.Requirements == nil {
|
|
return true
|
|
}
|
|
ix.reqMu.Lock()
|
|
defer ix.reqMu.Unlock()
|
|
return ix.reqKept[repository+":"+dir]
|
|
}
|