From 049202d3733033b940d0bcef935c63f7d1b82cc2 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Wed, 21 Oct 2026 08:02:54 +0000 Subject: [PATCH] Build Go with the installed go and name no Go version (issue 1572, ADR 0477) --- .../mesh-build-go/cmd/mesh-build-go/build.go | 6 +-- .../mesh-build-go/cmd/mesh-build-go/digest.go | 4 +- .../mesh-build-go/cmd/mesh-build-go/fetch.go | 7 ++-- .../mesh-build-go/cmd/mesh-build-go/main.go | 8 ++-- .../mesh-build-go/cmd/mesh-build-go/module.go | 35 +++++++++-------- .../mesh-build-go/cmd/mesh-build-go/probe.go | 38 +++++-------------- .../mesh-build-go/cmd/mesh-build-go/recipe.go | 23 +++++------ 7 files changed, 51 insertions(+), 70 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build.go b/modules/mesh-build-go/cmd/mesh-build-go/build.go index 1adb0b5c..4dbf22d2 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build.go @@ -145,8 +145,8 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { return BuildResult{Artifact: art.Name, Binary: binary, Digest: digest, Size: len(archive), Path: path}, nil } -// buildEnv is the environment of the compile: the build agent's (CGO_ENABLED=0, GOFLAGS) with the recipe's Go release -// alone, the modules the fetch put in the module's cache and no way to fetch another (GOPROXY=off), so a build never +// buildEnv is the environment of the compile: the build agent's (CGO_ENABLED=0, GOFLAGS) with the installed go +// alone (GOTOOLCHAIN=local), the modules the fetch put in the module's cache and no way to fetch another (GOPROXY=off), so a build never // reaches the network. A directory that vendors is built from its vendor directory. func (m *Module) buildEnv(dir string) []string { mod := "-mod=mod" @@ -156,7 +156,7 @@ func (m *Module) buildEnv(dir string) []string { return []string{ "CGO_ENABLED=0", "GOFLAGS=" + recipeFlags + " " + mod, - "GOTOOLCHAIN=" + RecipeGo, + "GOTOOLCHAIN=local", "GOMODCACHE=" + filepath.Join(m.Cache, "gomod"), "GOCACHE=" + filepath.Join(m.Cache, "gobuild"), "GOPROXY=off", diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest.go b/modules/mesh-build-go/cmd/mesh-build-go/digest.go index 2158df98..54f640a8 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest.go @@ -20,7 +20,7 @@ import ( ) // DigestVersion is the form of the digest's text: changed whenever what the digest hashes changes in kind. -const DigestVersion = "1" +const DigestVersion = "2" // 2: no Go version in the text; the toolchain enters a fingerprint as the agreed version (ADR 0477 §3) // goPackage is the part of `go list -json` a digest reads. type goPackage struct { @@ -106,7 +106,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { return nil, fmt.Errorf("no workspace %s: it was never fetched here, or it is older than a day and was removed", workspace) } var tags []string - header := []string{"digest " + DigestVersion, "level " + level, "go " + m.Recipe.Go} + header := []string{"digest " + DigestVersion, "level " + level} switch level { case LevelUnit: case LevelIntegration: diff --git a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go index 59d18db8..bddf11ac 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/fetch.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/fetch.go @@ -112,13 +112,13 @@ func (m *Module) goModDirs(ctx context.Context, tree string) ([]string, error) { return dirs, nil } -// goEnv is what a go command of a step is given beyond the base environment: the recipe's Go release (ADR 0476: the probe put it in the module's cache; a step never fetches it), +// goEnv is what a go command of a step is given beyond the base environment: the installed go alone (GOTOOLCHAIN=local, ADR 0477 §1: no toolchain is ever downloaded), // modules resolved by their go.mod and go.sum, the module's own caches, the mesh's own modules fetched directly (by // git, through the account's rewrite of the forge's address to ssh), and nothing that asks a question. For a directory // that vendors, see goEnvFor. func (m *Module) goEnv() []string { return []string{ - "GOTOOLCHAIN=" + RecipeGo, + "GOTOOLCHAIN=local", "GOFLAGS=-mod=mod", "GOMODCACHE=" + filepath.Join(m.Cache, "gomod"), "GOCACHE=" + filepath.Join(m.Cache, "gobuild"), @@ -165,8 +165,7 @@ func (m *Module) clock() time.Time { func (m *Module) now() time.Time { return m.clock() } // causeLine is the first line of a go command's output that says something other than Go's own progress: `go: -// downloading ...` comes first when Go fetches a toolchain or a module, and the cause of a failure on a later line -// (issue 1560: a refusal read only "go: downloading go1.27.1 (linux/amd64)"). +// downloading ...` comes first when Go fetches a module, and the cause of a failure on a later line. func causeLine(s string) string { for _, l := range strings.Split(s, "\n") { if l = strings.TrimSpace(l); l != "" && !strings.HasPrefix(l, "go: downloading ") { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/main.go b/modules/mesh-build-go/cmd/mesh-build-go/main.go index 5778776b..723b5d45 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/main.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/main.go @@ -1,8 +1,8 @@ // mesh-build-go: the Go build module (novox/hq ADR 0405, ADR 0466 §1, ADR 0469, ADR 0470; design 47 R4c-3, issue -// 1483). A Go tools bundle the node's runtime launches as the module's own account (runs-as). It names the one Go -// toolchain version it builds with, writes the version it finds to .go of its `toolchains` state at start and -// before every step, and refuses every step on a node that has another: its health is then unhealthy, so the -// controller raises the node's condition. What it says, it says on stderr; stdout is the MCP channel. +// 1483). A Go tools bundle the node's runtime launches as the module's own account (runs-as). It builds with the +// go its node has installed (GOTOOLCHAIN=local, ADR 0477 §1; issue 1572), names no Go version, writes the version it +// finds to .go of its `toolchains` state at start and before every step, and refuses a step only when go +// cannot be run: its health is then unhealthy, so the controller raises the node's condition. What it says, it says on stderr; stdout is the MCP channel. package main import ( diff --git a/modules/mesh-build-go/cmd/mesh-build-go/module.go b/modules/mesh-build-go/cmd/mesh-build-go/module.go index 5c0fbc79..728ba4ee 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/module.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/module.go @@ -45,14 +45,21 @@ type Module struct { StartBus func(ctx context.Context, dir string) (url string, stop func(), err error) } -func (m *Module) guard() buildstep.Guard { - return buildstep.Guard{Language: "go", Want: m.Recipe.Go, Recipe: m.Recipe.Version(), Probe: m.Probe, Node: m.Node, Put: m.reportingPut()} +// guard is the SDK guard for the version the probe just read: the module names no Go version (ADR 0477 §2), so the +// version wanted is the one installed and the guard refuses only when go cannot be run at all, writing the installed +// version to .go of the toolchains state every time. +func (m *Module) guard(installed string, probeErr error) buildstep.Guard { + return buildstep.Guard{Language: "go", Want: installed, Recipe: m.Recipe.Version(), Node: m.Node, Put: m.reportingPut(), + Probe: func(context.Context) (string, error) { return installed, probeErr }} } -// Check reads the installed toolchain, writes .go of the toolchains state and answers nil, or the refusal -// (buildstep.Guard.Check). It runs at start, before every step, at every health look and every CheckRound. +// Check reads the installed toolchain, writes .go of the toolchains state and answers nil, or the refusal when +// go cannot be run (buildstep.Guard.Check). A version other than another node's is not a refusal: the indexer waits +// while build nodes disagree (ADR 0477 §2). It runs at start, before every step, at every health look and every +// CheckRound. func (m *Module) Check(ctx context.Context) error { - return m.guard().Check(ctx) + installed, err := m.Probe(ctx) + return m.guard(installed, err).Check(ctx) } // Start checks once, as the module starts, and then every CheckRound until ctx ends. @@ -70,22 +77,20 @@ func (m *Module) Start(ctx context.Context) { } } -// Health is the module's health (the runtime-health tool, ADR 0366): unhealthy while the toolchain installed is not -// the recipe's, saying both versions, so the controller raises the node's condition. It looks again each time it is +// Health is the module's health (the runtime-health tool, ADR 0366): unhealthy only while go cannot be run, saying why, so the +// controller raises the node's condition; healthy it names the installed version (ADR 0477 §2). It looks again each time it is // asked. func (m *Module) Health(ctx context.Context) health.Answer { - err := m.Check(ctx) - switch { - case err == nil: - return health.Healthy(fmt.Sprintf("%s installed, as the recipe %s builds with", m.Recipe.Go, m.Recipe.Version())) - default: + installed, perr := m.Probe(ctx) + if err := m.guard(installed, perr).Check(ctx); err != nil { return health.Unhealthy(err.Error()) } + return health.Healthy(fmt.Sprintf("%s installed, the version the module builds with (recipe %s)", installed, m.Recipe.Version())) } // Long is a tool that runs a long step (ADR 0469 §2): it answers at once with the step's id, and the step ends on -// its key in the steps state. The guard runs inside the step, before run, so a node that refuses ends every step -// refused with both versions named. +// its key in the steps state. The guard runs inside the step, before run, so a node whose go cannot be run ends every step +// refused with the cause named. func (m *Module) Long(tool string, run func(ctx context.Context, args map[string]any) (any, error)) func(map[string]any) (any, error) { return func(args map[string]any) (any, error) { id, err := buildstep.Start(m.Ctx, m.Steps, m.Node, tool, func(ctx context.Context) (any, error) { @@ -118,7 +123,7 @@ func (m *Module) Status(ctx context.Context) map[string]any { err := m.Check(ctx) installed, _ := m.Probe(ctx) out := map[string]any{ - "node": m.Node, "recipe": m.Recipe.Version(), "go": m.Recipe.Go, "installed": installed, + "node": m.Node, "recipe": m.Recipe.Version(), "installed": installed, "postgres-major": m.Recipe.PostgresMajor, "nats-server": m.Recipe.NatsServer, "refusing": err != nil, } if err != nil { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/probe.go b/modules/mesh-build-go/cmd/mesh-build-go/probe.go index 1d96435b..f1a48126 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/probe.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/probe.go @@ -10,52 +10,34 @@ import ( "git.novox.be/novox/mesh-sdk/go/forgekey" ) -// ProbeTimeout bounds `go version` when the release is in the cache; FetchTimeout bounds it while the release is -// missing and the installed Go downloads it (about seventy megabytes). -const ( - ProbeTimeout = 15 * time.Second - FetchTimeout = 5 * time.Minute -) +// ProbeTimeout bounds `go version`: it reads the installed go and fetches nothing (ADR 0477 §1). +const ProbeTimeout = 15 * time.Second -// ProbeGo is the probe of the guard (novox/hq ADR 0476 §2, §3; issue 1560): it runs `go version` with -// GOTOOLCHAIN=RecipeGo, so it reads the version of Go's own release of the recipe's version, which is what the -// module builds with, and the third word of its answer is that version. It runs with network and the module's own -// Go module cache (cache/gomod): while the release is missing from it, the installed Go downloads it there, verified -// against Go's checksum database. No step fetches it; a step runs behind this probe and refuses when it is missing. +// ProbeGo is the probe of the guard (novox/hq ADR 0477 §1, §2; issue 1572): it runs `go version` with +// GOTOOLCHAIN=local, so it reads the version of the go the node's package manager installed, which is what the +// module builds with, and the third word of its answer is that version. It downloads nothing and needs no network. // The environment is built from scratch like a step's (StepRunner), so the module's bus credentials are not in it. func ProbeGo(home, cache string) func(ctx context.Context) (string, error) { run := forgekey.ExecRunnerClean(0) return func(ctx context.Context) (string, error) { return probeGo(ctx, run, home, cache) } } -// probeEnv is what the probe gives `go version` beyond the base environment: the recipe's release as the toolchain, -// the module's cache for it to land in (and the build cache the go command wants), and Go's own proxy and checksum -// database, which this module's steps never leave at their default except here and in fetch. +// probeEnv is what the probe gives `go version` beyond the base environment: the installed go alone, and the +// module's caches, so nothing is written to the account's home. func probeEnv(cache string) []string { return []string{ - "GOTOOLCHAIN=" + RecipeGo, + "GOTOOLCHAIN=local", "GOMODCACHE=" + filepath.Join(cache, "gomod"), "GOCACHE=" + filepath.Join(cache, "gobuild"), } } -// releaseCached says whether the release is in the module cache: golang.org/toolchain@v0.0.1-.-. -func releaseCached(cache string) bool { - dirs, _ := filepath.Glob(filepath.Join(cache, "gomod", "golang.org", "toolchain@v0.0.1-"+RecipeGo+".*")) - return len(dirs) > 0 -} - func probeGo(ctx context.Context, run forgekey.Runner, home, cache string) (string, error) { - bound := ProbeTimeout - if !releaseCached(cache) { - bound = FetchTimeout - } - ctx, cancel := context.WithTimeout(ctx, bound) + ctx, cancel := context.WithTimeout(ctx, ProbeTimeout) defer cancel() r := run(ctx, append(baseEnv(home), probeEnv(cache)...), "go", "version") if r.Status != 0 || r.Err != "" { - return "", fmt.Errorf("`go version` with GOTOOLCHAIN=%s failed (the recipe builds with %s): %s", RecipeGo, RecipeGo, - causeLine(r.Err+"\n"+r.Stderr)) + return "", fmt.Errorf("`go version` with GOTOOLCHAIN=local failed: %s", causeLine(r.Err+"\n"+r.Stderr)) } return parseGoVersion(r.Stdout) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/recipe.go b/modules/mesh-build-go/cmd/mesh-build-go/recipe.go index 0303045e..7a5c76f6 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/recipe.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/recipe.go @@ -1,9 +1,9 @@ package main // The recipe of the Go build module (novox/hq ADR 0405 §1 and §7, ADR 0469 §1, ADR 0470 §4; issue 1483): the one -// Go toolchain version it builds with, the one postgres major version its integration level tests with, and the -// recipe's own version. A node whose installed Go differs from the recipe's refuses every step (guard.go), so all -// builders run this version or none builds. +// postgres major version its integration level tests with, and the recipe's own version. The module names no Go +// version (ADR 0477 §1, §2; issue 1572): it builds with the go its node has installed (GOTOOLCHAIN=local), reports +// that version to the toolchains state, and the indexer takes the version the build nodes agree on. import ( "crypto/sha256" @@ -15,15 +15,11 @@ import ( "github.com/nats-io/nats-server/v2/server" ) -// RecipeGo is the Go version the recipe builds with: the text `go version` prints for it, which is what the guard -// compares and what the indexer reads from .go of the toolchains state. -const RecipeGo = "go1.27.1" - // PostgresMajor is the postgres major version the integration level tests with (ADR 0470 §4). const PostgresMajor = 18 // RecipeSteps is the recipe's own version: changed whenever the steps of fetch, test or build change in a way -// that can change what they produce (ADR 0405 §7). It is one of the four values the recipe version hashes. +// that can change what they produce (ADR 0405 §7). It is one of the three values the recipe version hashes. const RecipeSteps = "1" // natsModule is the library the integration level's bus is started from (ADR 0470 §2). @@ -33,8 +29,6 @@ const natsModule = "github.com/nats-io/nats-server/v2" type Recipe struct { // Steps is RecipeSteps. Steps string - // Go is the toolchain's version as `go version` prints it. - Go string // PostgresMajor is the integration level's postgres major version. PostgresMajor int // NatsServer is the nats-server library's version, from the program's build information. @@ -43,16 +37,17 @@ type Recipe struct { // Current is the recipe of this build of the module. func Current() Recipe { - return Recipe{Steps: RecipeSteps, Go: RecipeGo, PostgresMajor: PostgresMajor, NatsServer: natsServerVersion()} + return Recipe{Steps: RecipeSteps, PostgresMajor: PostgresMajor, NatsServer: natsServerVersion()} } -// Version is the recipe version: a hash of the recipe's own version, the Go version, the postgres major version and -// the nats-server version, so a change of any of the four moves it and with it every fingerprint that holds it +// Version is the recipe version: a hash of the recipe's own version, the postgres major version and the +// nats-server version, never a Go version (ADR 0477 §3): the toolchain enters each fingerprint once, as the version +// the build nodes agree on (ADR 0465). A change of any of the three moves it and with it every fingerprint that holds it // (ADR 0405 §7, ADR 0465 §2). The fields are named in the hashed text, so no two recipes share a text. func (r Recipe) Version() string { h := sha256.New() for _, f := range [][2]string{ - {"steps", r.Steps}, {"go", r.Go}, {"postgres", strconv.Itoa(r.PostgresMajor)}, {"nats-server", r.NatsServer}, + {"steps", r.Steps}, {"postgres", strconv.Itoa(r.PostgresMajor)}, {"nats-server", r.NatsServer}, } { h.Write([]byte(f[0] + "=" + f[1] + "\n")) }