Files
mesh-delivery/modules/mesh-delivery-indexer/internal/index/requirements.go
T

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]
}