The operator's desktop slows while it builds (shanks at 92%, the Go build runtime at 1650% of 24 processors). The build modules declare cpu-cap (default shanks=50,g14=50,ace=75), which the composer turns into the runtime's quota at idle priority; each step's go, node and buildah commands get GOMAXPROCS and UV_THREADPOOL_SIZE for their part of it, and the status tools show the limits and what the kernel holds the runtime to.
124 lines
4.7 KiB
Go
124 lines
4.7 KiB
Go
package main
|
|
|
|
// The run steps shut off (novox/hq ADR 0448; issue 1309, lead decision L2 of R4e): every command that runs a change's
|
|
// code (go test, go build, go list) runs through mesh-sdk's buildstep.Isolated, in bubblewrap: no network, none of the
|
|
// account (its home, its ssh key, its bus login, the runtime directory), the workspace read-write, the shared Go caches
|
|
// read-only under a throwaway writable layer, or read-write for a trunk build alone (publish). The fetch step (git, go
|
|
// mod download) holds the key and the network and runs none of the change's code, so it runs as before.
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
// BubblewrapLeast is the oldest bubblewrap a run step is shut off with: --overlay-src and --tmp-overlay, which keep the
|
|
// shared caches unchanged by a candidate, came in 0.10.
|
|
const BubblewrapLeast = "0.10"
|
|
|
|
// caches are the shared warm caches of the module: Go's module cache and its build cache.
|
|
func (m *Module) caches() []string {
|
|
return []string{filepath.Join(m.Cache, "gomod"), filepath.Join(m.Cache, "gobuild")}
|
|
}
|
|
|
|
// isolation is how a run step of the workspace is shut off: the workspace (its tree, its siblings, its run directories,
|
|
// its packed artifacts) read-write, the caches beneath a throwaway layer unless write, and an environment of a PATH and
|
|
// a locale alone (the step's HOME is its own, inside its /tmp).
|
|
func (m *Module) isolation(workspace string, write bool) buildstep.Isolation {
|
|
path := os.Getenv("PATH")
|
|
if path == "" {
|
|
path = defaultPath
|
|
}
|
|
return buildstep.Isolation{
|
|
Workspace: filepath.Join(m.Work, workspace),
|
|
Caches: m.caches(),
|
|
WriteCaches: write,
|
|
Env: append([]string{"PATH=" + path, "LC_ALL=C"}, m.threadEnv()...),
|
|
}
|
|
}
|
|
|
|
// stepRunner is the runner of a run step of the workspace: buildstep.Isolated over m.Run, so every command it runs is
|
|
// shut off; write is true for a trunk build alone (the shared caches are written by trunk builds only). Inside is the
|
|
// integration level's own process, already running shut off (level.go): it runs its commands as they are.
|
|
func (m *Module) stepRunner(workspace string, write bool) (forgekey.Runner, error) {
|
|
if m.Inside {
|
|
return m.Run, nil
|
|
}
|
|
for _, c := range m.caches() {
|
|
// bubblewrap binds only a directory that is there; the fetch makes them, a step on a fresh node may come first.
|
|
if err := os.MkdirAll(c, 0o700); err != nil {
|
|
return nil, fmt.Errorf("the shared cache %s: %w", c, err)
|
|
}
|
|
}
|
|
return buildstep.Isolated(m.Run, m.isolation(workspace, write)), nil
|
|
}
|
|
|
|
// ProbeBubblewrap reads the installed bubblewrap's version (`bwrap --version`: "bubblewrap 0.13.0").
|
|
func ProbeBubblewrap(ctx context.Context) (string, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, ProbeTimeout)
|
|
defer cancel()
|
|
r := forgekey.ExecRunnerClean(0)(ctx, []string{"PATH=" + defaultPath}, buildstep.Bubblewrap, "--version")
|
|
if r.Status != 0 || r.Err != "" {
|
|
return "", fmt.Errorf("`%s --version` failed: %s", buildstep.Bubblewrap, firstLine(r.Err+"\n"+r.Stderr))
|
|
}
|
|
return parseBubblewrapVersion(r.Stdout)
|
|
}
|
|
|
|
var bubblewrapVersion = regexp.MustCompile(`^bubblewrap (\d+)\.(\d+)(\.\d+)?\s*$`)
|
|
|
|
func parseBubblewrapVersion(out string) (string, error) {
|
|
f := bubblewrapVersion.FindStringSubmatch(strings.TrimSpace(out))
|
|
if f == nil {
|
|
return "", fmt.Errorf("`%s --version` answered %q", buildstep.Bubblewrap, strings.TrimSpace(out))
|
|
}
|
|
return f[1] + "." + f[2] + f[3], nil
|
|
}
|
|
|
|
// bubblewrapProblem says why the installed bubblewrap cannot shut a run step off, or "" when it can: it is missing, or
|
|
// older than BubblewrapLeast.
|
|
func bubblewrapProblem(version string, err error) string {
|
|
if err != nil {
|
|
return "bubblewrap cannot be run, and every build and test step runs in it (ADR 0448): " + err.Error()
|
|
}
|
|
if !atLeast(version, BubblewrapLeast) {
|
|
return fmt.Sprintf("bubblewrap %s installed, and a build or test step needs %s or later (ADR 0448)", version, BubblewrapLeast)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// atLeast says whether the dotted version v is least or later.
|
|
func atLeast(v, least string) bool {
|
|
a, b := strings.Split(v, "."), strings.Split(least, ".")
|
|
for i := 0; i < len(b); i++ {
|
|
var x, y int
|
|
if i < len(a) {
|
|
x, _ = strconv.Atoi(a[i])
|
|
}
|
|
y, _ = strconv.Atoi(b[i])
|
|
if x != y {
|
|
return x > y
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkBubblewrap refuses a step when bubblewrap cannot shut it off; nil Bubblewrap probes nothing (a module that only
|
|
// guards).
|
|
func (m *Module) checkBubblewrap(ctx context.Context) error {
|
|
if m.Bubblewrap == nil {
|
|
return nil
|
|
}
|
|
v, err := m.Bubblewrap(ctx)
|
|
if why := bubblewrapProblem(v, err); why != "" {
|
|
return &buildstep.Refusal{Text: why, Want: BubblewrapLeast + " or later", Have: v}
|
|
}
|
|
return nil
|
|
}
|