A-71 (builder): toolchain-wait-minutes reaches the builder as a file the manifest declares, read at its start, never its environment

The manifest gains the file resource toolchain-wait (content ${config:toolchain-wait-minutes}) and names its path in
MESH_BUILDER_TOOLCHAIN_WAIT_FILE; main.go's toolchainWaitOf reads it.

Builds: design 47 G §672, A-71 (G-20)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 23:03:11 +01:00
parent 5906cecf19
commit 60b1479e8a
4 changed files with 95 additions and 10 deletions
@@ -7,8 +7,10 @@ package main
import (
"cmp"
"context"
"errors"
"fmt"
"io"
"io/fs"
"log"
"maps"
"os"
@@ -200,13 +202,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
b.Numbers = build.VersionNumbers{N: numberer}
// The builder's toolchain waits (design 47 G §672): its state waits, and wait-held and wait-ended on its stream waits; its
// bound is its setting toolchain-wait-minutes.
waitBound := defaultToolchainWait
if v := strings.TrimSpace(getenv("MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n < 1 {
return fmt.Errorf("MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES %q is not a number of minutes", v)
}
waitBound = time.Duration(n) * time.Minute
waitBound, err := toolchainWaitOf(getenv("MESH_BUILDER_TOOLCHAIN_WAIT_FILE"))
if err != nil {
return err
}
waitsBucket := process.AwaitBucket(ctx, js, conn, build.WaitsBucket, "the toolchain waits are kept in memory only, and builder_waits shows none", logger.Printf)
b.Waits = newWaitsKept(waitsBucket, func(ctx context.Context, event, id string, body any) error {
@@ -569,3 +567,28 @@ func (s *sourceHeads) head(_ context.Context, rev uint64, repository string) (st
}
return "", nil
}
// toolchainWaitOf reads the toolchain wait's setting, toolchain-wait-minutes, from the file the manifest declares with the
// content ${config:toolchain-wait-minutes} (design 47 G §672, A-71), at the builder's start: no file, or an empty one, is
// the default; a content that is no number of minutes is an error naming the file.
func toolchainWaitOf(path string) (time.Duration, error) {
if path == "" {
return defaultToolchainWait, nil
}
raw, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
return defaultToolchainWait, nil
}
if err != nil {
return 0, err
}
v := strings.TrimSpace(string(raw))
if v == "" {
return defaultToolchainWait, nil
}
n, err := strconv.Atoi(v)
if err != nil || n < 1 {
return 0, fmt.Errorf("%s holds %q, which is not a number of minutes", path, v)
}
return time.Duration(n) * time.Minute, nil
}
@@ -0,0 +1,39 @@
package main
import (
"os"
"path/filepath"
"testing"
"time"
)
// A-71: the toolchain wait's setting is read at the start from the file the manifest declares, never from the environment.
func TestToolchainWaitIsReadFromItsFile(t *testing.T) {
dir := t.TempDir()
n := 0
file := func(content string) string {
n++
p := filepath.Join(dir, "w"+string(rune('a'+n)))
if err := os.WriteFile(p, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
return p
}
for _, c := range []struct {
name, path string
want time.Duration
bad bool
}{
{"no file named", "", defaultToolchainWait, false},
{"file absent", filepath.Join(dir, "absent"), defaultToolchainWait, false},
{"empty", file(" \n"), defaultToolchainWait, false},
{"minutes", file("15\n"), 15 * time.Minute, false},
{"not a number", file("soon"), 0, true},
{"zero", file("0"), 0, true},
} {
got, err := toolchainWaitOf(c.path)
if (err != nil) != c.bad || got != c.want {
t.Errorf("%s: %v, %v; want %v, error %v", c.name, got, err, c.want, c.bad)
}
}
}
+17 -2
View File
@@ -3,6 +3,7 @@ package manifest
import (
"bytes"
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
@@ -221,8 +222,22 @@ func TestManifestProcess(t *testing.T) { // 1607/AC1
t.Errorf("process = %v", p)
}
env, _ := p["env"].(map[string]any)
if len(env) != 2 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES"] != "${config:toolchain-wait-minutes}" {
t.Errorf("env = %v, want the broker file and the toolchain wait's setting (design 47 G §672)", p["env"])
if len(env) != 2 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_BUILDER_TOOLCHAIN_WAIT_FILE"] != "${dir:state}/toolchain-wait-minutes" {
t.Errorf("env = %v, want the broker file and the path of the toolchain wait's file, never the setting itself (design 47 G §672, A-71)", p["env"])
}
for k, v := range env {
if strings.Contains(fmt.Sprint(v), "${config:") {
t.Errorf("env %s = %v: a setting reaches the process as a file, never its environment (A-71)", k, v)
}
}
found := false
for _, r := range m.Resources {
if r["type"] == "file" && r["path"] == "${dir:state}/toolchain-wait-minutes" && r["content"] == "${config:toolchain-wait-minutes}" {
found = true
}
}
if !found {
t.Errorf("no file resource with the content ${config:toolchain-wait-minutes}: A-71")
}
for _, r := range m.Resources {
if r["id"] == "settings" {
+9 -1
View File
@@ -165,6 +165,14 @@
"place": ".",
"unknown": "program"
},
{
"id": "toolchain-wait",
"type": "file",
"path": "${dir:state}/toolchain-wait-minutes",
"owner": "mesh-delivery-builder",
"mode": "0600",
"content": "${config:toolchain-wait-minutes}"
},
{
"id": "builder",
"type": "process",
@@ -175,7 +183,7 @@
],
"env": {
"MESH_BROKER_FILE": "${dir:state}/broker",
"MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES": "${config:toolchain-wait-minutes}"
"MESH_BUILDER_TOOLCHAIN_WAIT_FILE": "${dir:state}/toolchain-wait-minutes"
},
"user": "mesh-delivery-builder",
"health": {