The indexer writes '.' for a root module's path and the toolchains each fingerprint was computed with (issue 1615)

This commit is contained in:
2026-10-21 11:33:49 +02:00
parent 0b0cfb6e37
commit 3bd2a065f9
5 changed files with 77 additions and 15 deletions
@@ -63,7 +63,7 @@ func (ix *Indexer) applyMerged(ctx context.Context, f delivery.Forge) error {
continue
}
fp, none := fingerprintOf(k, f.Trees)
next := delivery.Module{Repository: f.Repository, Commit: f.Merge, FP: fp, None: none, Src: ix.srcOf(f.Repository, f.Merge), Manifest: digests[k.Path], Path: k.Path}
next := delivery.Module{Repository: f.Repository, Commit: f.Merge, FP: fp, None: none, Src: ix.srcOf(f.Repository, f.Merge), Manifest: digests[k.Path], Path: pathOf(k.Path)}
// The new form (issue 1444, R4c-2): no module has a fingerprint of it yet, so each has the reason it has none, which
// names what is missing, from its manifest and lock hashes (inputs.go).
raw, has := f.Manifests[k.Path]
@@ -109,4 +109,12 @@ func (ix *Indexer) baseFP(ctx context.Context) baseFP {
}
}
// pathOf is the module's directory as Module.Path holds it: "." for the repository's root, since empty means not known.
func pathOf(dir string) string {
if dir == "" {
return delivery.RootPath
}
return dir
}
func withV(m delivery.Module) delivery.Module { m.V = delivery.Version; return m }
@@ -234,7 +234,7 @@ func TestAMergeWritesTheModulesDirectoryAndKeepsItThroughSourceAndRecompute(t *t
if err := ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree", "": "roottree"})); err != nil {
t.Fatal(err)
}
for module, want := range map[string]string{"a": "modules/a", "r": ""} {
for module, want := range map[string]string{"a": "modules/a", "r": delivery.RootPath} {
if m, ok := moduleOf(t, mods, module); !ok || m.Path != want {
t.Errorf("after the merge %s = %+v (%v), want path %q", module, m, ok, want)
}
@@ -252,7 +252,14 @@ func TestAMergeWritesTheModulesDirectoryAndKeepsItThroughSourceAndRecompute(t *t
if !strings.Contains(raw, `"path":"modules/a"`) {
t.Errorf("the stored value %s has no path", raw)
}
if strings.Contains(string(mods.vals["r"]), `"path"`) {
t.Errorf("the root module's value %s names a path", mods.vals["r"])
}
func TestARootModuleHasTheDirectoryDot(t *testing.T) { // 1615/AC3: empty means not known, a root module is "."
ix, mods := modulesIndexer(t, rec(t, "build-1", "r", "novox/mesh-sdk", "", "main", "", "2026-10-19T09:00:00Z", strings.Replace(aInputs, `"Module":"a"`, `"Module":"r"`, 1)))
if err := ix.Apply(context.Background(), mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree"})); err != nil {
t.Fatal(err)
}
if m, ok := moduleOf(t, mods, "r"); !ok || m.Path != delivery.RootPath {
t.Errorf("module = %+v (%v), want path %q", m, ok, delivery.RootPath)
}
}
@@ -2,6 +2,7 @@ package index
import (
"context"
"reflect"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -32,7 +33,7 @@ func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing.
m, ok := moduleOf(t, mods, "a")
want := before
want.Src = 7
if !ok || m != want {
if !ok || !reflect.DeepEqual(m, want) {
t.Errorf("module a = %+v (%v), want %+v: the fingerprint and the commit as the merge wrote them, with Src 7", m, ok, want)
}
if c, ok := moduleOf(t, mods, "c"); ok {
@@ -179,7 +179,10 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
all[key] = &known{key: key, cur: cur, languages: languagesOf(m), bases: basesOf(m), contexts: hasContext(m)}
}
tools := ix.agreed()
type result struct{ fp, none string }
type result struct {
fp, none string
tcs map[string]delivery.ToolchainUsed // by language, when there is a fingerprint
}
done := map[string]result{}
visiting := map[string]bool{}
var of func(module string, chain []string) result
@@ -192,16 +195,16 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
cur, has := held[module]
switch {
case !has:
return result{"", "the indexer holds no record of it"}
return result{fp: "", none: "the indexer holds no record of it"}
case cur.FP2 != "":
return result{cur.FP2, ""}
return result{fp: cur.FP2, none: ""}
case cur.None2 != "":
return result{"", cur.None2}
return result{fp: "", none: cur.None2}
}
return result{"", "its record carries no fingerprint of the new form"}
return result{fp: "", none: "its record carries no fingerprint of the new form"}
}
if visiting[module] {
return result{"", "a cycle of bases: " + strings.Join(append(chain, module), " -> ")}
return result{fp: "", none: "a cycle of bases: " + strings.Join(append(chain, module), " -> ")}
}
visiting[module] = true
defer delete(visiting, module)
@@ -258,6 +261,9 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
r.none = noBuilder
default:
r.fp, r.none = delivery.SourceFingerprint2(kept.With(module, toolchains))
if r.fp != "" {
r.tcs = toolchainsUsed(toolchains)
}
}
done[module] = r
k.cur.Inputs2 = &kept
@@ -269,7 +275,7 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
r := of(key, nil)
if _, merged := overlay[key]; merged {
final := overlay[key]
final.FP2, final.None2, final.Inputs2 = r.fp, r.none, k.cur.Inputs2
final.FP2, final.None2, final.Inputs2, final.Toolchains = r.fp, r.none, k.cur.Inputs2, r.tcs
// What the bases and the builder gave is kept as it was while the value it gave does not change, so a merge
// that moves nothing for the module writes nothing (as Recompute does).
if value, _, found, err := ix.Modules.Get(ctx, key); err == nil && found {
@@ -282,7 +288,7 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
finals[key] = final
continue
}
if k.cur.FP2 == r.fp && k.cur.None2 == r.none {
if k.cur.FP2 == r.fp && k.cur.None2 == r.none && reflect.DeepEqual(k.cur.Toolchains, r.tcs) {
continue
}
digest, inputs := k.cur.Manifest, k.cur.Inputs2
@@ -294,10 +300,10 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
if err != nil || cur.Manifest != digest || cur.Inputs2 == nil {
return nil, false, nil // a merge changed it meanwhile; its own recompute follows
}
if cur.FP2 == r.fp && cur.None2 == r.none {
if cur.FP2 == r.fp && cur.None2 == r.none && reflect.DeepEqual(cur.Toolchains, r.tcs) {
return nil, false, nil
}
cur.FP2, cur.None2 = r.fp, r.none
cur.FP2, cur.None2, cur.Toolchains = r.fp, r.none, r.tcs
cur.Inputs2 = inputs
data, err := delivery.EncodeModule(cur)
return data, false, err
@@ -365,3 +371,15 @@ func hasContext(m catalogue.Manifest) bool {
}
return false
}
// toolchainsUsed are the toolchains a fingerprint was computed with, by language (an artifact's toolchain is its language's).
func toolchainsUsed(by map[string]delivery.Toolchain2) map[string]delivery.ToolchainUsed {
if len(by) == 0 {
return nil
}
out := map[string]delivery.ToolchainUsed{}
for _, t := range by {
out[t.Language] = delivery.ToolchainUsed{Version: t.Version, Recipe: t.Recipe}
}
return out
}
@@ -2,6 +2,7 @@ package index
import (
"context"
"reflect"
"strings"
"testing"
"time"
@@ -238,3 +239,30 @@ func TestAMergeKeepsAFingerprintTheModuleStillHas(t *testing.T) { // 1444 (revie
t.Errorf("fp2 = %q (was %q), writes %d more", after.FP2, before.FP2, mods.writes-w)
}
}
func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) { // 1615/AC3
ix, mods := newFormIndexer(t, builderModules(t)...)
if m, _ := moduleOf(t, mods, "avahi"); m.Toolchains != nil {
t.Fatalf("before a toolchain is reported: toolchains = %+v, want none", m.Toolchains)
}
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
m, _ := moduleOf(t, mods, "avahi")
want := map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}}
if m.FP2 == "" || !reflect.DeepEqual(m.Toolchains, want) {
t.Fatalf("avahi: fp2 %q toolchains %+v, want %+v", m.FP2, m.Toolchains, want)
}
if o, _ := moduleOf(t, mods, "other"); o.FP2 != "" || o.Toolchains != nil {
t.Errorf("a module with no fingerprint carries toolchains %+v", o.Toolchains)
}
// An upgrade moves the fingerprint and the toolchains together, with no merge.
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.2")
n, _ := moduleOf(t, mods, "avahi")
if n.FP2 == m.FP2 || n.Toolchains["go"].Version != "go1.27.2" {
t.Errorf("after the upgrade: fp2 %q (was %q) toolchains %+v, want the new version with the new fingerprint", n.FP2, m.FP2, n.Toolchains)
}
// Nodes that disagree leave no fingerprint and no toolchains.
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.26.0")
if d, _ := moduleOf(t, mods, "avahi"); d.FP2 != "" || d.Toolchains != nil {
t.Errorf("disagreeing nodes: fp2 %q toolchains %+v, want neither", d.FP2, d.Toolchains)
}
}