diff --git a/modules/mesh-build-go/cmd/mesh-build-go/asks.go b/modules/mesh-build-go/cmd/mesh-build-go/asks.go index b49bd7d1b..30d9e74a2 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/asks.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/asks.go @@ -40,7 +40,7 @@ func (m *Module) askSteps(steps []StepTool) buildstep.AskSteps { if err != nil { return sdkstep.ToolchainUsed{}, err } - return sdkstep.ToolchainUsed{Version: installed, Recipe: m.Recipe.Version()}, nil + return sdkstep.ToolchainUsed{Version: installed, Recipe: m.Recipe.Version(), Digest: m.programDigest(installed)}, nil }, // The kept results reach a test step in its ask (design 47 *Carried, not run*, line 593 at b229f5c4; issue 1966 // K1): this module reads no other part's kept state. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/emit_test.go b/modules/mesh-build-go/cmd/mesh-build-go/emit_test.go index 46112afd6..fe43817da 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/emit_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/emit_test.go @@ -5,51 +5,69 @@ import ( "encoding/json" "os" "path/filepath" - "slices" "testing" ) -// Issues 1483 and 1488 (for the indexer, issue 1444): the guard emits toolchain.reported after each write of the -// toolchains key, with the node and the language. -func TestTheGuardEmitsToolchainReportedAfterEachWrite(t *testing.T) { - m := moduleWith(newStates(), "go1.27.1") +// Design 47 G §492, §472, §466 (BG5, BG6): the guard says toolchain-reported on the Go build seat's subject of its node, +// with the version, the recipe and the digest of the go program, at the first write and again only when one of them +// changed; the toolchains key and a build's toolchain carry the digest. +func TestTheGuardSaysToolchainReportedOnAChangeWithTheProgramDigest(t *testing.T) { + states := newStates() + m := moduleWith(states, "go1.27.1") + prog := filepath.Join(t.TempDir(), "go") + os.WriteFile(prog, []byte("go program"), 0o755) + m.ToolchainProgram = func() (string, error) { return prog, nil } type event struct { - typ string - body map[string]string + subject string + body map[string]string + id string } var got []event - m.Emit = func(typ string, body any) error { + m.SayToolchain = func(subject string, body any, id string) error { raw, _ := json.Marshal(body) var b map[string]string json.Unmarshal(raw, &b) - got = append(got, event{typ, b}) + got = append(got, event{subject, b, id}) return nil } if err := m.Check(context.Background()); err != nil { t.Fatal(err) } - if len(got) != 1 || got[0].typ != "toolchain.reported" || got[0].body["node"] != "ace" || got[0].body["language"] != "go" || len(got[0].body) != 2 { - t.Errorf("events = %+v, want one toolchain.reported for ace, go", got) + if len(got) != 1 || got[0].subject != "mesh.seat.node-build-go.event.toolchain-reported.ace" || got[0].body["node"] != "ace" || got[0].body["language"] != "go" || + got[0].body["version"] != "go1.27.1" || got[0].body["recipe"] != m.Recipe.Version() || len(got[0].body["digest"]) != len("sha256:")+64 { + t.Fatalf("events = %+v, want one toolchain-reported for ace, go with the program's digest", got) + } + if tc, _ := states.toolchain("ace.go"); tc.Digest != got[0].body["digest"] { + t.Errorf("the toolchains key's digest is %q, the event's %q", tc.Digest, got[0].body["digest"]) } m.Check(context.Background()) - if len(got) != 2 { - t.Errorf("a second guard pass emitted %d events in all, want 2", len(got)) + if len(got) != 1 { + t.Errorf("a second pass that changed nothing said %d events in all, want 1", len(got)) + } + // A build's toolchain names the same digest (§466). + if used, err := m.askSteps(nil).Toolchain(context.Background()); err != nil || used.Digest != got[0].body["digest"] || used.Version != "go1.27.1" { + t.Errorf("a build's toolchain = %+v (%v), want the digest %s", used, err, got[0].body["digest"]) + } + os.WriteFile(prog, []byte("another go program"), 0o755) + m.Probe = func(context.Context) (string, error) { return "go1.27.2", nil } + m.Check(context.Background()) + if len(got) != 2 || got[1].body["version"] != "go1.27.2" || got[1].body["digest"] == got[0].body["digest"] { + t.Errorf("a new version said %+v, want a second event with another digest", got) } } -// The event is carried by the module's stream, not named in emits (ADR 0363: one place); the validator checks that. -func TestTheManifestDeclaresTheToolchainsStream(t *testing.T) { +// The seat's events are its definition's: the module declares no stream of its own for the toolchain (§492). +func TestTheManifestDeclaresNoToolchainStream(t *testing.T) { raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) if err != nil { t.Fatal(err) } var m struct { Streams []struct { - Name string `json:"name"` - Events []string `json:"events"` + Name string `json:"name"` } `json:"streams"` } - if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) < 1 || m.Streams[0].Name != "toolchains-go" || !slices.Equal(m.Streams[0].Events, []string{"toolchain.**"}) { + if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 0 { t.Errorf("streams = %+v (%v)", m.Streams, err) } } 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 fa75df4c1..f5727ac0e 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/main.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/main.go @@ -40,7 +40,7 @@ func main() { Node: strings.TrimSpace(os.Getenv("MESH_NODE")), Recipe: Current(), Toolchains: stdio.State(seat.StateToolchains).Put, - Emit: func(t string, b any) error { return stdio.Emit(t, b) }, + SayToolchain: stdio.SeatPublish, Steps: stdio.State(seat.StateSteps).Put, Detected: stdio.State(seat.StateDetectedSteps).Put, ReadDetected: readOf(stdio.State(seat.StateDetectedSteps)).Get, 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 e6e00fd9a..a300f93be 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/module.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/module.go @@ -6,6 +6,7 @@ import ( "fmt" stdio "git.novox.be/novox/mesh-sdk/go" "net/http" + "os/exec" "sync" "time" @@ -48,9 +49,6 @@ type Module struct { ForgetPause func(key string) error // WatchPauses follows the drops other holders write in the shared pauses state (stdio.State(...).Watch). WatchPauses func(key string, handle func(stdio.StateChange) error) error - // Emit tells the bus an event (stdio.Emit); nil emits nothing. The guard emits toolchain.reported after each write of - // the toolchains key, so the indexer, which holds no timer, learns that the bucket exists (issue 1444). - Emit func(eventType string, body any) error // Ctx is the process's context, which a step runs under (the tool call's is cut at 30 s, the step's is not). Ctx context.Context Say func(format string, a ...any) @@ -123,8 +121,17 @@ type Module struct { KeptBuilds KeptObjects Self string - spacesOnce sync.Once - workspaces *buildstep.Workspaces + // SayToolchain says an event on a seat's subject (stdio.SeatPublish; nil says none, a test's); ToolchainProgram finds the go + // program whose digest the toolchain's is (exec.LookPath when nil). + SayToolchain func(subject string, body any, id string) error + ToolchainProgram func() (string, error) + + reporterOnce sync.Once + reporter *toolchainReporter + digestMu sync.Mutex + digestFor, progDigest string + spacesOnce sync.Once + workspaces *buildstep.Workspaces } // guard is the SDK guard for the version the probe just read: the module names no Go version (ADR 0477 §2), so the @@ -243,20 +250,38 @@ func (m *Module) Status(ctx context.Context) map[string]any { return out } -// ToolchainReported is the event the guard emits after it writes the toolchains key. -const ToolchainReported = "toolchain.reported" - -// reportingPut is the toolchains Put that then emits toolchain.reported with the node and the language. A write that -// failed emits nothing; an emit that failed is said and does not refuse the step. +// reportingPut is the toolchains Put that then says toolchain-reported on the Go build seat's subject of this node when +// the toolchain changed (design 47 G §492): at the module's start and on each change of the key. A write that failed +// says nothing; an event that failed is said and does not refuse the step. func (m *Module) reportingPut() buildstep.Put { - return func(key string, value any) (uint64, error) { - rev, err := m.Toolchains(key, value) - if err != nil || m.Emit == nil { - return rev, err + m.reporterOnce.Do(func() { + m.reporter = &toolchainReporter{AskLanguage: sdkstep.AskGo, Node: m.Node, Publish: m.SayToolchain} + }) + return putReporting(m.Toolchains, m.reporter, m.programDigest, m.Say) +} + +// programDigest is the digest of the Go program this node's go-toolchain provides, the `go` found on the path: read again +// only when the installed version changed (design 47 G §472; the lead's answer A-152). +func (m *Module) programDigest(version string) string { + m.digestMu.Lock() + defer m.digestMu.Unlock() + if m.digestFor == version && m.progDigest != "" { + return m.progDigest + } + find := m.ToolchainProgram + if find == nil { + find = func() (string, error) { return exec.LookPath("go") } + } + path, err := find() + if err == nil { + var d string + if d, err = programFileDigest(path); err == nil { + m.digestFor, m.progDigest = version, d + return d } - if eerr := m.Emit(ToolchainReported, map[string]string{"node": m.Node, "language": "go"}); eerr != nil && m.Say != nil { - m.Say("the event %s could not be emitted: %v", ToolchainReported, eerr) - } - return rev, nil + } + if m.Say != nil { + m.Say("the digest of the go toolchain program could not be read: %v", err) } + return "" } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/toolchainreport.go b/modules/mesh-build-go/cmd/mesh-build-go/toolchainreport.go new file mode 100644 index 000000000..d0c609a74 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/toolchainreport.go @@ -0,0 +1,94 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + "io" + "os" + "sync" + + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" +) + +// toolchainReporter says `toolchain-reported` on the build seat's subject of its node (design 47 G §492): once at the +// module's start and each time its toolchains key of the language changes (a write that changes nothing says nothing). +// Language is the seat's ask language (go, node, image). +type toolchainReporter struct { + AskLanguage, Node string + // Publish is stdio.SeatPublish: the event is de-duplicated by its id. + Publish func(subject string, body any, id string) error + + mu sync.Mutex + last map[string]sdkstep.Toolchain +} + +// Report says the toolchain written under key at revision rev when it differs from the last one said for the key. +func (r *toolchainReporter) Report(key string, rev uint64, t sdkstep.Toolchain) error { + r.mu.Lock() + defer r.mu.Unlock() + if prev, ok := r.last[key]; ok && prev.Version == t.Version && prev.Recipe == t.Recipe && prev.Digest == t.Digest && prev.Language == t.Language { + return nil + } + seat, err := sdkstep.BuildSeat(r.AskLanguage) + if err != nil { + return err + } + if _, err := sdkstep.ToolchainKey(r.Node, t.Language); err != nil { + return err + } + body := map[string]string{"node": r.Node, "language": t.Language, "version": t.Version, "recipe": t.Recipe, "digest": t.Digest} + subject := "mesh.seat." + seat + ".event.toolchain-reported." + r.Node + if err := r.Publish(subject, body, fmt.Sprintf("toolchain-reported.%s.%d", key, rev)); err != nil { + return err + } + if r.last == nil { + r.last = map[string]sdkstep.Toolchain{} + } + r.last[key] = t + return nil +} + +// putReporting wraps a toolchains Put so each write that changes the toolchain is then said by r. A write that failed +// says nothing; an event that failed is passed to say and does not refuse the write. +func putReporting(put buildstep.Put, r *toolchainReporter, digest func(version string) string, say func(format string, a ...any)) buildstep.Put { + return func(key string, value any) (uint64, error) { + raw, merr := json.Marshal(value) + var t sdkstep.Toolchain + if merr != nil || json.Unmarshal(raw, &t) != nil { + return put(key, value) // not a toolchain value: written as it is + } + // The value carries the digest of the toolchain program beside the version and the recipe (design 47 G §472). + if t.Digest == "" && digest != nil { + t.Digest = digest(t.Version) + } + enc, eerr := sdkstep.EncodeToolchain(t) + if eerr != nil { + return put(key, value) + } + rev, err := put(key, json.RawMessage(enc)) + if err != nil || r == nil || r.Publish == nil { + return rev, err + } + if eerr := r.Report(key, rev, t); eerr != nil && say != nil { + say("toolchain-reported could not be said: %v", eerr) + } + return rev, nil + } +} + +// ProgramDigest is the sha256: digest of the file at path, a toolchain program (design 47 G §472). +func programFileDigest(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index b4a1eb975..f70fc1b91 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -87,14 +87,6 @@ "build_go_status", "build_go_health" ], - "streams": [ - { - "name": "toolchains-go", - "events": [ - "toolchain.**" - ] - } - ], "requires": [ { "name": "detected-steps", diff --git a/modules/mesh-build-node/cmd/mesh-build-node/asks.go b/modules/mesh-build-node/cmd/mesh-build-node/asks.go index da739e18a..d0381878d 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/asks.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/asks.go @@ -37,7 +37,7 @@ func (m *Module) askSteps(steps []StepTool) buildstep.AskSteps { if err != nil { return sdkstep.ToolchainUsed{}, err } - return sdkstep.ToolchainUsed{Version: installed, Recipe: m.recipe().Version()}, nil + return sdkstep.ToolchainUsed{Version: installed, Recipe: m.recipe().Version(), Digest: m.programDigest(installed)}, nil }, Release: func(ws string) error { return m.spaces().Release(ws) }, Logf: m.Say, diff --git a/modules/mesh-build-node/cmd/mesh-build-node/emit_test.go b/modules/mesh-build-node/cmd/mesh-build-node/emit_test.go index 5d98c2395..4be2ed41e 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/emit_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/emit_test.go @@ -5,51 +5,69 @@ import ( "encoding/json" "os" "path/filepath" - "slices" "testing" ) -// Issues 1483 and 1488 (for the indexer, issue 1444): the guard emits toolchain.reported after each write of the -// toolchains key, with the node and the language. -func TestTheGuardEmitsToolchainReportedAfterEachWrite(t *testing.T) { - m := moduleWith(newStates(), "v26.10.0") +// Design 47 G §492, §472, §466 (BG5, BG6): the guard says toolchain-reported on the Node build seat's subject of its node, +// with the version, the recipe and the digest of the node program, at the first write and again only when one of them +// changed; the toolchains key and a build's toolchain carry the digest. +func TestTheGuardSaysToolchainReportedOnAChangeWithTheProgramDigest(t *testing.T) { + states := newStates() + m := moduleWith(states, "v26.10.0") + prog := filepath.Join(t.TempDir(), "node") + os.WriteFile(prog, []byte("node program"), 0o755) + m.ToolchainProgram = func() (string, error) { return prog, nil } type event struct { - typ string - body map[string]string + subject string + body map[string]string + id string } var got []event - m.Emit = func(typ string, body any) error { + m.SayToolchain = func(subject string, body any, id string) error { raw, _ := json.Marshal(body) var b map[string]string json.Unmarshal(raw, &b) - got = append(got, event{typ, b}) + got = append(got, event{subject, b, id}) return nil } if err := m.Check(context.Background()); err != nil { t.Fatal(err) } - if len(got) != 1 || got[0].typ != "toolchain.reported" || got[0].body["node"] != "ace" || got[0].body["language"] != "typescript" || len(got[0].body) != 2 { - t.Errorf("events = %+v, want one toolchain.reported for ace, typescript", got) + if len(got) != 1 || got[0].subject != "mesh.seat.node-build-node.event.toolchain-reported.ace" || got[0].body["node"] != "ace" || got[0].body["language"] != "typescript" || + got[0].body["version"] != "v26.10.0" || got[0].body["recipe"] != m.Recipe.Version() || len(got[0].body["digest"]) != len("sha256:")+64 { + t.Fatalf("events = %+v, want one toolchain-reported for ace, go with the program's digest", got) + } + if tc, _ := states.toolchain("ace.typescript"); tc.Digest != got[0].body["digest"] { + t.Errorf("the toolchains key's digest is %q, the event's %q", tc.Digest, got[0].body["digest"]) } m.Check(context.Background()) - if len(got) != 2 { - t.Errorf("a second guard pass emitted %d events in all, want 2", len(got)) + if len(got) != 1 { + t.Errorf("a second pass that changed nothing said %d events in all, want 1", len(got)) + } + // A build's toolchain names the same digest (§466). + if used, err := m.askSteps(nil).Toolchain(context.Background()); err != nil || used.Digest != got[0].body["digest"] || used.Version != "v26.10.0" { + t.Errorf("a build's toolchain = %+v (%v), want the digest %s", used, err, got[0].body["digest"]) + } + os.WriteFile(prog, []byte("another node program"), 0o755) + m.Probe = func(context.Context) (string, error) { return "v26.11.0", nil } + m.Check(context.Background()) + if len(got) != 2 || got[1].body["version"] != "v26.11.0" || got[1].body["digest"] == got[0].body["digest"] { + t.Errorf("a new version said %+v, want a second event with another digest", got) } } -// The event is carried by the module's stream, not named in emits (ADR 0363: one place); the validator checks that. -func TestTheManifestDeclaresTheToolchainsStream(t *testing.T) { +// The seat's events are its definition's: the module declares no stream of its own for the toolchain (§492). +func TestTheManifestDeclaresNoToolchainStream(t *testing.T) { raw, err := os.ReadFile(filepath.Join("..", "..", "module.json")) if err != nil { t.Fatal(err) } var m struct { Streams []struct { - Name string `json:"name"` - Events []string `json:"events"` + Name string `json:"name"` } `json:"streams"` } - if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 1 || m.Streams[0].Name != "toolchains-node" || !slices.Equal(m.Streams[0].Events, []string{"toolchain.**"}) { + if err := json.Unmarshal(raw, &m); err != nil || len(m.Streams) != 0 { t.Errorf("streams = %+v (%v)", m.Streams, err) } } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/main.go b/modules/mesh-build-node/cmd/mesh-build-node/main.go index eb037cd9e..e8ff97ff1 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/main.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/main.go @@ -45,7 +45,7 @@ func main() { Probe: ProbeNode, Toolchain: &Toolchain{Paths: paths, Run: dforgekey.ExecRunnerClean(0), Registry: registry}, Toolchains: stdio.State(seat.StateToolchains).Put, - Emit: func(t string, b any) error { return stdio.Emit(t, b) }, + SayToolchain: stdio.SeatPublish, Steps: stdio.State(seat.StateSteps).Put, ReadSteps: readOf(stdio.State(seat.StateSteps)), Detected: stdio.State(seat.StateDetectedSteps).Put, diff --git a/modules/mesh-build-node/cmd/mesh-build-node/module.go b/modules/mesh-build-node/cmd/mesh-build-node/module.go index f51485aff..1d93108f7 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/module.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/module.go @@ -7,6 +7,7 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "net/http" "os" + "os/exec" "sync" "time" @@ -64,9 +65,15 @@ type Module struct { ForgetPause func(key string) error // WatchPauses follows the drops other holders write in the shared pauses state (stdio.State(...).Watch). WatchPauses func(key string, handle func(stdio.StateChange) error) error - // Emit tells the bus an event (stdio.Emit); nil emits nothing. The guard emits toolchain.reported after each write of - // the toolchains key, so the indexer, which holds no timer, learns that the bucket exists (issue 1444). - Emit func(eventType string, body any) error + // SayToolchain says an event on a seat's subject (stdio.SeatPublish; nil says none, a test's); ToolchainProgram finds the + // node program whose digest the toolchain's is (exec.LookPath when nil). + SayToolchain func(subject string, body any, id string) error + ToolchainProgram func() (string, error) + reporterOnce sync.Once + reporter *toolchainReporter + digestMu sync.Mutex + digestFor string + progDigest string // Ctx is the process's context, which a step runs under (the tool call's is cut at 30 s, the step's is not). Ctx context.Context Say func(format string, a ...any) @@ -266,20 +273,38 @@ func pathOrDefault() string { return defaultPath } -// ToolchainReported is the event the guard emits after it writes the toolchains key. -const ToolchainReported = "toolchain.reported" - -// reportingPut is the toolchains Put that then emits toolchain.reported with the node and the language. A write that -// failed emits nothing; an emit that failed is said and does not refuse the step. +// reportingPut is the toolchains Put that then says toolchain-reported on the Node build seat's subject of this node when +// the toolchain changed (design 47 G §492): at the module's start and on each change of the key. A write that failed +// says nothing; an event that failed is said and does not refuse the step. func (m *Module) reportingPut() buildstep.Put { - return func(key string, value any) (uint64, error) { - rev, err := m.Toolchains(key, value) - if err != nil || m.Emit == nil { - return rev, err + m.reporterOnce.Do(func() { + m.reporter = &toolchainReporter{AskLanguage: sdkstep.AskNode, Node: m.Node, Publish: m.SayToolchain} + }) + return putReporting(m.Toolchains, m.reporter, m.programDigest, m.Say) +} + +// programDigest is the digest of the node program this node's node-toolchain provides, the `node` found on the path: read +// again only when the installed version changed (design 47 G §472; the lead's answer A-152). +func (m *Module) programDigest(version string) string { + m.digestMu.Lock() + defer m.digestMu.Unlock() + if m.digestFor == version && m.progDigest != "" { + return m.progDigest + } + find := m.ToolchainProgram + if find == nil { + find = func() (string, error) { return exec.LookPath("node") } + } + path, err := find() + if err == nil { + var d string + if d, err = programFileDigest(path); err == nil { + m.digestFor, m.progDigest = version, d + return d } - if eerr := m.Emit(ToolchainReported, map[string]string{"node": m.Node, "language": "typescript"}); eerr != nil && m.Say != nil { - m.Say("the event %s could not be emitted: %v", ToolchainReported, eerr) - } - return rev, nil + } + if m.Say != nil { + m.Say("the digest of the node toolchain program could not be read: %v", err) } + return "" } diff --git a/modules/mesh-build-node/cmd/mesh-build-node/toolchainreport.go b/modules/mesh-build-node/cmd/mesh-build-node/toolchainreport.go new file mode 100644 index 000000000..d0c609a74 --- /dev/null +++ b/modules/mesh-build-node/cmd/mesh-build-node/toolchainreport.go @@ -0,0 +1,94 @@ +package main + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + "io" + "os" + "sync" + + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" +) + +// toolchainReporter says `toolchain-reported` on the build seat's subject of its node (design 47 G §492): once at the +// module's start and each time its toolchains key of the language changes (a write that changes nothing says nothing). +// Language is the seat's ask language (go, node, image). +type toolchainReporter struct { + AskLanguage, Node string + // Publish is stdio.SeatPublish: the event is de-duplicated by its id. + Publish func(subject string, body any, id string) error + + mu sync.Mutex + last map[string]sdkstep.Toolchain +} + +// Report says the toolchain written under key at revision rev when it differs from the last one said for the key. +func (r *toolchainReporter) Report(key string, rev uint64, t sdkstep.Toolchain) error { + r.mu.Lock() + defer r.mu.Unlock() + if prev, ok := r.last[key]; ok && prev.Version == t.Version && prev.Recipe == t.Recipe && prev.Digest == t.Digest && prev.Language == t.Language { + return nil + } + seat, err := sdkstep.BuildSeat(r.AskLanguage) + if err != nil { + return err + } + if _, err := sdkstep.ToolchainKey(r.Node, t.Language); err != nil { + return err + } + body := map[string]string{"node": r.Node, "language": t.Language, "version": t.Version, "recipe": t.Recipe, "digest": t.Digest} + subject := "mesh.seat." + seat + ".event.toolchain-reported." + r.Node + if err := r.Publish(subject, body, fmt.Sprintf("toolchain-reported.%s.%d", key, rev)); err != nil { + return err + } + if r.last == nil { + r.last = map[string]sdkstep.Toolchain{} + } + r.last[key] = t + return nil +} + +// putReporting wraps a toolchains Put so each write that changes the toolchain is then said by r. A write that failed +// says nothing; an event that failed is passed to say and does not refuse the write. +func putReporting(put buildstep.Put, r *toolchainReporter, digest func(version string) string, say func(format string, a ...any)) buildstep.Put { + return func(key string, value any) (uint64, error) { + raw, merr := json.Marshal(value) + var t sdkstep.Toolchain + if merr != nil || json.Unmarshal(raw, &t) != nil { + return put(key, value) // not a toolchain value: written as it is + } + // The value carries the digest of the toolchain program beside the version and the recipe (design 47 G §472). + if t.Digest == "" && digest != nil { + t.Digest = digest(t.Version) + } + enc, eerr := sdkstep.EncodeToolchain(t) + if eerr != nil { + return put(key, value) + } + rev, err := put(key, json.RawMessage(enc)) + if err != nil || r == nil || r.Publish == nil { + return rev, err + } + if eerr := r.Report(key, rev, t); eerr != nil && say != nil { + say("toolchain-reported could not be said: %v", eerr) + } + return rev, nil + } +} + +// ProgramDigest is the sha256: digest of the file at path, a toolchain program (design 47 G §472). +func programFileDigest(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + return "sha256:" + hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index 4678e794e..99da70e35 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -56,14 +56,6 @@ "build_node_status", "build_node_health" ], - "streams": [ - { - "name": "toolchains-node", - "events": [ - "toolchain.**" - ] - } - ], "requires": [ {"name": "detected-steps", "storage": "key-value", "template": "state", "options": {"history": 1}}, "artifact-store",