BG5, BN5, BG6, BN6 (toolchain part): toolchain-reported on the seat's subject, with the toolchain program's digest
The guard's toolchains value carries the sha256 of the go or node program found on the path (read again when the version changes); toolchain-reported is said on the build seat's subject of the node at start and on each change of version, recipe or digest; a build's ToolchainUsed names the digest. The legacy toolchain.reported emit and its stream go: nobody consumes them. Builds: design 47 G §492, §472, §466 (lead answer A-152) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -87,14 +87,6 @@
|
||||
"build_go_status",
|
||||
"build_go_health"
|
||||
],
|
||||
"streams": [
|
||||
{
|
||||
"name": "toolchains-go",
|
||||
"events": [
|
||||
"toolchain.**"
|
||||
]
|
||||
}
|
||||
],
|
||||
"requires": [
|
||||
{
|
||||
"name": "detected-steps",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user