mirror of
https://github.com/juanfont/headscale.git
synced 2026-09-26 18:24:54 +09:00
c3d771e23e
Keeps the interlocked pins current and reports what it could not move. Areas apply, gate and commit one at a time, so a dependency that breaks the build costs one commit rather than the whole pull request.
150 lines
4.1 KiB
Go
150 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// repo is a checkout the bump operates on. Every helper runs from the
|
|
// repository root so relative paths in the Makefile recipes and the
|
|
// //go:generate directives resolve the way they do for a developer.
|
|
type repo struct {
|
|
Root string
|
|
// System is the Nix system double (e.g. x86_64-linux) used to address
|
|
// flake checks.
|
|
System string
|
|
}
|
|
|
|
// tailLines is how much of a failing command's output is carried into the
|
|
// report. Enough to see a compiler error, short enough to paste into a pull
|
|
// request body.
|
|
const tailLines = 40
|
|
|
|
// cmdError carries the tail of a failed command's output so the reason an
|
|
// area was dropped survives all the way into the pull request body.
|
|
type cmdError struct {
|
|
Argv []string
|
|
Tail string
|
|
Err error
|
|
}
|
|
|
|
func (e *cmdError) Error() string {
|
|
return fmt.Sprintf("%s: %v\n%s", strings.Join(e.Argv, " "), e.Err, e.Tail)
|
|
}
|
|
|
|
func (e *cmdError) Unwrap() error { return e.Err }
|
|
|
|
func openRepo(ctx context.Context) (*repo, error) {
|
|
out, err := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("locating repository root: %w", err)
|
|
}
|
|
|
|
r := &repo{Root: strings.TrimSpace(string(out))}
|
|
|
|
sys, err := r.run(ctx, "nix", "eval", "--impure", "--raw", "--expr", "builtins.currentSystem")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r.System = strings.TrimSpace(sys)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// run executes argv in the repository root and returns its combined output.
|
|
func (r *repo) run(ctx context.Context, argv ...string) (string, error) {
|
|
return r.runIn(ctx, r.Root, argv...)
|
|
}
|
|
|
|
// runIn executes argv in dir, which may be relative to the repository root.
|
|
func (r *repo) runIn(ctx context.Context, dir string, argv ...string) (string, error) {
|
|
if !filepath.IsAbs(dir) {
|
|
dir = filepath.Join(r.Root, dir)
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, argv[0], argv[1:]...) //nolint:gosec // argv is built from repo state, not user input
|
|
cmd.Dir = dir
|
|
|
|
// Streams stay separate: nix writes progress and a dirty-tree warning to
|
|
// stderr, and folding that into stdout corrupts every value parsed out of
|
|
// a command run through the devShell.
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
cmd.Env = append(os.Environ(), "GOWORK=off", "NIX_CONFIG=warn-dirty = false")
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return stdout.String(), &cmdError{
|
|
Argv: argv,
|
|
Tail: tail(stderr.String() + stdout.String()),
|
|
Err: err,
|
|
}
|
|
}
|
|
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// nixRun executes argv inside `nix develop`. It re-enters the shell on every
|
|
// call on purpose: a single long-lived shell would pin every later command to
|
|
// the toolchain that was locked before the flake area ran.
|
|
func (r *repo) nixRun(ctx context.Context, argv ...string) (string, error) {
|
|
return r.nixRunIn(ctx, r.Root, argv...)
|
|
}
|
|
|
|
func (r *repo) nixRunIn(ctx context.Context, dir string, argv ...string) (string, error) {
|
|
full := append([]string{"nix", "develop", "--fallback", "--command"}, argv...)
|
|
|
|
return r.runIn(ctx, dir, full...)
|
|
}
|
|
|
|
// path joins a repository-relative path onto the root.
|
|
func (r *repo) path(rel ...string) string {
|
|
return filepath.Join(append([]string{r.Root}, rel...)...)
|
|
}
|
|
|
|
// readFile reads a repository-relative file.
|
|
func (r *repo) readFile(rel string) (string, error) {
|
|
b, err := os.ReadFile(r.path(rel))
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading %s: %w", rel, err)
|
|
}
|
|
|
|
return string(b), nil
|
|
}
|
|
|
|
// writeFile writes a repository-relative file, preserving its current mode.
|
|
func (r *repo) writeFile(rel, content string) error {
|
|
name := r.path(rel)
|
|
|
|
mode := os.FileMode(0o644)
|
|
if fi, err := os.Stat(name); err == nil { //nolint:noinlineerr
|
|
mode = fi.Mode().Perm()
|
|
}
|
|
|
|
err := os.WriteFile(name, []byte(content), mode)
|
|
if err != nil {
|
|
return fmt.Errorf("writing %s: %w", rel, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// tail returns the last tailLines lines of s.
|
|
func tail(s string) string {
|
|
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
|
if len(lines) > tailLines {
|
|
lines = lines[len(lines)-tailLines:]
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|