package update import ( "context" "errors" "fmt" "os" "syscall" "time" "unicode/utf8" ) // Verification is "does this tree build and does it pass its own tests", run // before a single byte is written to the install dir. // // It is `make build` and `make test`, not `go build`: the CGO daemons need the // vendored toolchain and the whisper/piper include and library paths wired // through the Makefile, and a bare `go build` on them fails in a way that has // nothing to do with the change being deployed. `make test` is the -race suite // with the CGO env set, and it is the only evidence available on a single box // that the new code does what the old code did. // // This is not a substitute for a second environment. A test suite that passes // says the code is self-consistent; it does not say the new build will start // against this machine's actual models, sockets and encrypted store. That is // what the post-restart health check is for, and it is why the install is // reversible rather than merely careful. // Step — one verification or orchestration step and how it went. Kept so the CLI // can print a truthful account of what was done, including on the failure path. type Step struct { Name string Argv []string Took time.Duration Err error Output string // combined output, only retained for failures } // Verify runs the build and the test suite in SourceDir. func (u *Updater) Verify(ctx context.Context) ([]Step, error) { if err := u.refuseRootBuild(); err != nil { return nil, err } ctx, cancel := context.WithTimeout(ctx, u.cfg.verifyTimeout()) defer cancel() var steps []Step for _, argv := range [][]string{{"make", "build"}, {"make", "test"}} { u.log("verify: %v (this takes a while)", argv) start := u.now() out, err := u.run(ctx, u.cfg.SourceDir, argv) st := Step{Name: argv[len(argv)-1], Argv: argv, Took: u.now().Sub(start), Err: err} if err != nil { st.Output = tail(out, 4000) } steps = append(steps, st) if err != nil { u.log("verify: %v FAILED after %s", argv, st.Took.Round(time.Second)) return steps, fmt.Errorf("%w: %v: %v", ErrVerifyFailed, argv, err) } u.log("verify: %v ok in %s", argv, st.Took.Round(time.Second)) } return steps, nil } // refuseRootBuild stops a sudo'd apply from building in a tree it does not own. // // The seam is injected so the tests can drive both sides without a second uid. func (u *Updater) refuseRootBuild() error { uid, owner, err := u.ids(u.cfg.SourceDir) if err != nil || uid != 0 || owner == 0 { return nil // not root, or root's own tree, or we cannot tell } return fmt.Errorf("%w: %s is owned by uid %d", ErrRootOnHisTree, u.cfg.SourceDir, owner) } // realIDs — the running uid and the owner of dir. Split out for the tests. func realIDs(dir string) (uid int, owner uint32, err error) { fi, err := os.Stat(dir) if err != nil { return 0, 0, err } st, ok := fi.Sys().(*syscall.Stat_t) if !ok { return 0, 0, errors.New("update: cannot read directory ownership") } return os.Geteuid(), st.Uid, nil } // tail keeps the last n bytes — a failing `make test` prints far more than is // useful, and the failure is always at the end. // // The cut is nudged forward to a rune boundary. Russian test names and fixture // strings are the common case in this tree, and a slice landing mid-rune starts // the log with a replacement character. func tail(s string, n int) string { if len(s) <= n { return s } cut := len(s) - n for cut < len(s) && !utf8.RuneStart(s[cut]) { cut++ } return "…" + s[cut:] }