From 3bd2a065f90c9e62b4b8c6c7fd43f55d0f435e63 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 21 Oct 2026 11:33:49 +0200 Subject: [PATCH] The indexer writes '.' for a root module's path and the toolchains each fingerprint was computed with (issue 1615) --- .../internal/index/modules.go | 10 ++++- .../internal/index/modules_test.go | 13 +++++-- .../internal/index/source_test.go | 3 +- .../internal/index/toolchains.go | 38 ++++++++++++++----- .../internal/index/toolchains_test.go | 28 ++++++++++++++ 5 files changed, 77 insertions(+), 15 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/modules.go b/modules/mesh-delivery-indexer/internal/index/modules.go index 23cf31a..7e55e43 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules.go +++ b/modules/mesh-delivery-indexer/internal/index/modules.go @@ -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 } diff --git a/modules/mesh-delivery-indexer/internal/index/modules_test.go b/modules/mesh-delivery-indexer/internal/index/modules_test.go index db2d58f..36eee4f 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules_test.go +++ b/modules/mesh-delivery-indexer/internal/index/modules_test.go @@ -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) } } diff --git a/modules/mesh-delivery-indexer/internal/index/source_test.go b/modules/mesh-delivery-indexer/internal/index/source_test.go index 8f8fd7a..634e216 100644 --- a/modules/mesh-delivery-indexer/internal/index/source_test.go +++ b/modules/mesh-delivery-indexer/internal/index/source_test.go @@ -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 { diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains.go b/modules/mesh-delivery-indexer/internal/index/toolchains.go index fd5ce2c..8fbb0de 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains.go @@ -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 +} diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index d9fd7f4..3cd1184 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -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) + } +}