From 89dd57bed05da32881bf1122781989619e57c05d Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Wed, 21 Oct 2026 09:37:14 +0000 Subject: [PATCH] Give delivery.Module the toolchains its fingerprint was computed with, and '.' as a root module's path, so the builder builds a key only with its own toolchain (issue 1615) --- go/delivery/source.go | 18 ++++++++++++++++-- go/delivery/source_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/go/delivery/source.go b/go/delivery/source.go index a21bf9d..e82ce1d 100644 --- a/go/delivery/source.go +++ b/go/delivery/source.go @@ -389,11 +389,20 @@ type Module struct { Src uint64 `json:"src,omitempty"` Repository string `json:"repository"` Commit string `json:"commit"` - // Path is the module's directory in its repository, trimmed of slashes, empty when its manifest is the repository's - // root (novox/hq ADR 0478 clause 8): the build tools need it. A value written before has none. + // Path is the module's directory in its repository, trimmed of slashes, and RootPath (".") when its manifest is the + // repository's root (novox/hq ADR 0478 clause 8, issue 1615): the build tools need it. Empty means not known: a value + // written before the indexer learned paths has none. Path string `json:"path,omitempty"` + // Toolchains are the toolchains FP2 was computed with, by language (go, typescript): the version installed and the + // build module's recipe version the build nodes agreed on then (novox/hq ADR 0477 clause 2, ADR 0478 clause 3). A builder + // builds the fingerprint only with these. Empty when FP2 is empty or the module has no toolchain; a value written + // before has none (issue 1615). + Toolchains map[string]ToolchainUsed `json:"toolchains,omitempty"` } +// RootPath is Module.Path of a module whose manifest is its repository's root. +const RootPath = "." + // KeptInputs2 are SourceInputs2 without the module's name (the key has it) and without toolchains (those come from // the build modules' toolchain keys when the fingerprint is computed): what a Module keeps of a merge. type KeptInputs2 struct { @@ -421,6 +430,11 @@ func (m Module) check() error { case m.FP2 != "" && m.None2 != "": return fmt.Errorf("delivery: a module has both fp2 and none2") } + for language, t := range m.Toolchains { + if t.Version == "" || t.Recipe == "" { + return fmt.Errorf("delivery: the module's toolchain of %q needs its version and its recipe version", language) + } + } if m.Inputs2 != nil && m.Manifest == "" { return fmt.Errorf("delivery: a module keeps new-form inputs but has no manifest digest") } diff --git a/go/delivery/source_test.go b/go/delivery/source_test.go index cdfd10a..3eae4bc 100644 --- a/go/delivery/source_test.go +++ b/go/delivery/source_test.go @@ -397,3 +397,31 @@ func TestModulePath(t *testing.T) { // 1588/AC1 t.Errorf("a module without a path encodes %s", again) } } + +func TestModuleRootPathAndToolchains(t *testing.T) { // 1615/AC1, AC2 + tc := map[string]ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "2"}} + data, err := EncodeModule(Module{FP: "src1:x", Repository: "a/b", Commit: "c", Path: RootPath, Toolchains: tc}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(data), `"path":"."`) || !strings.Contains(string(data), `"toolchains":{"go":{"version":"go1.27.1","recipe":"1"}`) { + t.Errorf("encoded %s", data) + } + m, err := DecodeModule(data) + if err != nil || m.Path != RootPath || !reflect.DeepEqual(m.Toolchains, tc) { + t.Errorf("round trip = %+v, %v", m, err) + } + // A value written before has none: it decodes and encodes without the key. + old, err := DecodeModule([]byte(`{"v":1,"fp":"src1:x","repository":"a/b","commit":"c","path":"modules/a"}`)) + if err != nil || old.Toolchains != nil { + t.Fatalf("old shape = %+v, %v", old, err) + } + if again, _ := EncodeModule(old); strings.Contains(string(again), "toolchains") { + t.Errorf("a module without toolchains encodes %s", again) + } + for _, bad := range []map[string]ToolchainUsed{{"go": {Recipe: "1"}}, {"go": {Version: "v"}}} { + if _, err := EncodeModule(Module{FP: "src1:x", Repository: "a/b", Commit: "c", Toolchains: bad}); err == nil { + t.Errorf("toolchains %v accepted", bad) + } + } +}