127 lines
5.6 KiB
Go
127 lines
5.6 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The published mesh packages (novox/hq ADR 0593 §2): the indexer alone reads each mesh Go module's package digests
|
|
// from the forge's listing at its merge and writes, in its own state, the latest published version of the module and
|
|
// the content digest of each package in it. The builder, the checker and the build modules read this state; no other
|
|
// part computes a package's version or digest. This is the contract only: the key, the value and its checks. How a
|
|
// digest is computed is the indexer's own.
|
|
|
|
const (
|
|
// PackagesState is the name of the indexer's state of published packages, as its manifest declares it, and
|
|
// PackagesBucket its bucket: one key per mesh Go module (PackagesKey).
|
|
PackagesState = "packages"
|
|
PackagesBucket = IndexerModule + "_" + PackagesState
|
|
// PackagesHistory is the revisions the packages state keeps per key: the module's newest publications.
|
|
PackagesHistory = 5
|
|
)
|
|
|
|
// PackagesKey is the key, in PackagesBucket, of a mesh Go module (its module path, such as
|
|
// git.novox.be/novox/mesh-sdk/go): the path as a key part (EscapeName). It refuses a path that is no mesh Go module's
|
|
// (PackageRepository).
|
|
func PackagesKey(module string) (string, error) {
|
|
if _, _, ok := PackageRepository(module); !ok {
|
|
return "", fmt.Errorf("delivery: %q is no mesh Go module's path (it starts with %s)", module, ForgeModulePrefix)
|
|
}
|
|
return EscapeName(module), nil
|
|
}
|
|
|
|
// PublishedPackages is the value of a packages key: a mesh Go module as its latest trunk merge published it. Module is
|
|
// its module path, Repository (`<owner>/<name>`) and Dir its repository and its directory there (RootPath for the
|
|
// root), Commit the merge commit (the version a build pins, StepAsk.Packages), Requires the digest of its go.mod and
|
|
// go.sum together, and Packages the content digest of each package of the module (its files, tests and sub-packages
|
|
// excluded), by import path, sorted and each once. Packages is a list, never a map keyed by import path: a state value
|
|
// refuses a field name ending in a credential word, and a package such as .../internal/token is one (issue 1930).
|
|
type PublishedPackages struct {
|
|
V int `json:"v"`
|
|
Module string `json:"module"`
|
|
Repository string `json:"repository"`
|
|
Dir string `json:"dir"`
|
|
Commit string `json:"commit"`
|
|
Requires string `json:"requires"`
|
|
Packages []PackageDigest `json:"packages"`
|
|
At time.Time `json:"at"`
|
|
}
|
|
|
|
// DigestOf is the content digest of the package at import path pkg, and whether p holds it.
|
|
func (p PublishedPackages) DigestOf(pkg string) (string, bool) {
|
|
i, found := slices.BinarySearchFunc(p.Packages, pkg, func(d PackageDigest, s string) int { return strings.Compare(d.Package, s) })
|
|
if !found {
|
|
return "", false
|
|
}
|
|
return p.Packages[i].Digest, true
|
|
}
|
|
|
|
func (p PublishedPackages) check() error {
|
|
repository, _, ok := PackageRepository(p.Module)
|
|
switch {
|
|
case !ok:
|
|
return fmt.Errorf("delivery: %q is no mesh Go module's path (it starts with %s)", p.Module, ForgeModulePrefix)
|
|
case !SameRepository(p.Repository, repository):
|
|
return fmt.Errorf("delivery: the module %s is published from %s, and the value names the repository %q", p.Module, repository, p.Repository)
|
|
case p.Dir == "":
|
|
return fmt.Errorf("delivery: the published module %s names no directory (%q for the repository's root)", p.Module, RootPath)
|
|
case !askCommitOK.MatchString(p.Commit):
|
|
return fmt.Errorf("delivery: the published module %s names the commit %q, which is no full commit", p.Module, p.Commit)
|
|
case p.Requires == "":
|
|
return fmt.Errorf("delivery: the published module %s names no digest of its requirements", p.Module)
|
|
case p.At.IsZero():
|
|
return fmt.Errorf("delivery: the published module %s names no time", p.Module)
|
|
}
|
|
for i, d := range p.Packages {
|
|
if d.Package != p.Module && !strings.HasPrefix(d.Package, p.Module+"/") {
|
|
return fmt.Errorf("delivery: the package %q is not in the module %s", d.Package, p.Module)
|
|
}
|
|
if d.Digest == "" {
|
|
return fmt.Errorf("delivery: the package %s names no digest", d.Package)
|
|
}
|
|
if i > 0 && p.Packages[i-1].Package >= d.Package {
|
|
return fmt.Errorf("delivery: the packages of %s are not sorted and each once (%s after %s)", p.Module, d.Package, p.Packages[i-1].Package)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EncodePublishedPackages is p as JSON at Version; it refuses what a reader could not take (check).
|
|
func EncodePublishedPackages(p PublishedPackages) ([]byte, error) {
|
|
if err := p.check(); err != nil {
|
|
return nil, err
|
|
}
|
|
p.V = Version
|
|
return json.Marshal(p)
|
|
}
|
|
|
|
// DecodePublishedPackages reads a packages value; it refuses an unknown version (ErrUnknownVersion) and what
|
|
// EncodePublishedPackages refuses.
|
|
func DecodePublishedPackages(data []byte) (PublishedPackages, error) {
|
|
var p PublishedPackages
|
|
if err := decode(data, &p); err != nil {
|
|
return PublishedPackages{}, err
|
|
}
|
|
if err := p.check(); err != nil {
|
|
return PublishedPackages{}, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// CheckImports refuses an import set (KeptInputs2.Imports, BuildAnswer.Imports, BuildLogEntry.Imports) that holds a
|
|
// path that is no mesh package's, or is not sorted and each once.
|
|
func CheckImports(imports []string) error {
|
|
for i, pkg := range imports {
|
|
if _, _, ok := PackageRepository(pkg); !ok || strings.ContainsAny(pkg, " \t\n@") {
|
|
return fmt.Errorf("delivery: the import %q is no mesh package's path (it starts with %s)", pkg, ForgeModulePrefix)
|
|
}
|
|
if i > 0 && imports[i-1] >= pkg {
|
|
return fmt.Errorf("delivery: the imports are not sorted and each once (%s after %s)", pkg, imports[i-1])
|
|
}
|
|
}
|
|
return nil
|
|
}
|