Each run step goes through mesh-sdk's buildstep.Isolated: no network, none of the account, the workspace read-write, the toolchain read-only, the npm cache beneath a throwaway layer unless the build step is told publish (a trunk build). The module declares the bubblewrap package and refuses a step, unhealthy, while bwrap is missing or older than 0.10. The proof test runs a probe script through the real test step.
111 lines
4.1 KiB
Go
111 lines
4.1 KiB
Go
package main
|
|
|
|
// The run steps shut off (novox/hq ADR 0448; issue 1309, lead decision L2 of R4e): the type-check, the package's test
|
|
// script, tsc and esbuild run the change's code, so each runs through mesh-sdk's buildstep.Isolated, in bubblewrap: no
|
|
// network, none of the account (its home, its ssh key, its registry secret, its bus login, the runtime directory), the
|
|
// workspace read-write, the toolchain read-only, the shared npm cache read-only beneath a throwaway writable layer, or
|
|
// read-write for a trunk build alone (publish). The fetch step (git, npm ci of the module's own packages) 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"
|
|
"time"
|
|
|
|
"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 cache unchanged by a candidate, came in 0.10.
|
|
const BubblewrapLeast = "0.10"
|
|
|
|
// npmCache is the shared npm cache a package's test script reads.
|
|
func (m *Module) npmCache() string { return filepath.Join(m.Toolchain.Paths.Cache, "npm") }
|
|
|
|
// isolation is how a run step in the workspace root is shut off.
|
|
func (m *Module) isolation(root string, write bool) buildstep.Isolation {
|
|
return buildstep.Isolation{
|
|
Workspace: root,
|
|
ReadOnly: []string{m.toolchainDir()},
|
|
Caches: []string{m.npmCache()},
|
|
WriteCaches: write,
|
|
Env: []string{"PATH=" + pathOrDefault(), "LC_ALL=C"},
|
|
}
|
|
}
|
|
|
|
// stepRunner is the runner of a run step in the workspace root: buildstep.Isolated over m.Run, every command shut off;
|
|
// write is a trunk build's alone.
|
|
func (m *Module) stepRunner(root string, write bool) (forgekey.Runner, error) {
|
|
// bubblewrap binds only a directory that is there.
|
|
if err := os.MkdirAll(m.npmCache(), 0o700); err != nil {
|
|
return nil, fmt.Errorf("the shared cache %s: %w", m.npmCache(), err)
|
|
}
|
|
return buildstep.Isolated(m.Run, m.isolation(root, 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, 15*time.Second)
|
|
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 "".
|
|
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 := range b {
|
|
var x 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.
|
|
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
|
|
}
|