1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Package drv is the small standard library of runGoCommand builders: a Go
// program that *is* a Nix derivation's builder. See runGoCommand.7.
//
// Everything here fails loudly: a builder has no error recovery, so every
// helper that can fail calls Fatal instead of returning an error. That keeps
// builders as short as the shell they replace, without `set -e` surprises.
package drv

import (
	"fmt"
	"io"
	"io/fs"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

// Out is $out, the derivation's (default) output path.
func Out() string { return Env("out") }

// Env returns a required environment variable, i.e. an attribute of
// derivationArgs. Nix passes every attribute as a string; store paths are
// their path.
func Env(name string) string {
	v, ok := os.LookupEnv(name)
	if !ok {
		Fatalf("environment variable %s not set; pass it in derivationArgs", name)
	}
	return v
}

// EnvList splits a list attribute, which Nix passes space-separated.
func EnvList(name string) []string { return strings.Fields(Env(name)) }

// Fatalf reports and exits with failure.
func Fatalf(format string, a ...any) {
	fmt.Fprintf(os.Stderr, "builder: "+format+"\n", a...)
	os.Exit(1)
}

// Check is Fatalf for an error: no-op on nil.
func Check(err error, context ...any) {
	if err != nil {
		if len(context) > 0 {
			Fatalf("%s: %v", fmt.Sprint(context...), err)
		}
		Fatalf("%v", err)
	}
}

// Run runs a program with its output going to the build log. The program is
// a path, usually an attribute holding a store path; there is no PATH in a
// builder unless derivationArgs sets one.
func Run(prog string, args ...string) {
	c := exec.Command(prog, args...)
	c.Stdout, c.Stderr = os.Stderr, os.Stderr
	Check(c.Run(), prog, " ", strings.Join(args, " "))
}

// RunIn is Run with a working directory.
func RunIn(dir, prog string, args ...string) {
	c := exec.Command(prog, args...)
	c.Dir = dir
	c.Stdout, c.Stderr = os.Stderr, os.Stderr
	Check(c.Run(), prog, " ", strings.Join(args, " "))
}

// Output runs a program and returns its stdout.
func Output(prog string, args ...string) string {
	c := exec.Command(prog, args...)
	c.Stderr = os.Stderr
	b, err := c.Output()
	Check(err, prog, " ", strings.Join(args, " "))
	return string(b)
}

// Pipe runs a program with stdin from r and stdout to w.
func Pipe(r io.Reader, w io.Writer, prog string, args ...string) {
	c := exec.Command(prog, args...)
	c.Stdin, c.Stdout, c.Stderr = r, w, os.Stderr
	Check(c.Run(), prog, " ", strings.Join(args, " "))
}

// Mkdir creates directories with parents, mode 0755.
func Mkdir(paths ...string) {
	for _, p := range paths {
		Check(os.MkdirAll(p, 0o755), "mkdir ", p)
	}
}

// Write writes a file, creating parent directories.
func Write(path string, content string, mode os.FileMode) {
	Mkdir(filepath.Dir(path))
	Check(os.WriteFile(path, []byte(content), mode), "write ", path)
}

// WriteShebang writes an executable script: the #! line naming interpreter
// (with any arguments, e.g. "/bin/execlineb -P"), then each line, each
// newline-terminated. A line may itself span several lines.
//
//	drv.WriteShebang(out+"/run", "/bin/execlineb -P", "/bin/foo bar")
func WriteShebang(path, interpreter string, lines ...string) {
	var b strings.Builder
	b.WriteString("#!" + interpreter + "\n")
	for _, l := range lines {
		b.WriteString(l)
		if !strings.HasSuffix(l, "\n") {
			b.WriteString("\n")
		}
	}
	Write(path, b.String(), 0o755)
}

// Touch creates an empty file.
func Touch(path string) { Write(path, "", 0o644) }

// Symlink creates a symlink at link pointing to target.
func Symlink(target, link string) {
	Mkdir(filepath.Dir(link))
	Check(os.Symlink(target, link), "symlink ", link)
}

// CopyFile copies a file's contents (following symlinks) and sets mode;
// a mode of 0 keeps the source's permission bits, made owner-writable.
func CopyFile(src, dst string, mode os.FileMode) {
	in, err := os.Open(src)
	Check(err, "open ", src)
	defer in.Close()
	if mode == 0 {
		st, err := in.Stat()
		Check(err, "stat ", src)
		mode = st.Mode().Perm() | 0o200
	}
	Mkdir(filepath.Dir(dst))
	out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode)
	Check(err, "create ", dst)
	_, err = io.Copy(out, in)
	Check(err, "copy ", src, " -> ", dst)
	Check(out.Close(), "close ", dst)
}

// CopyTree copies a directory tree (dereferencing nothing: symlinks stay
// symlinks), making everything owner-writable, as the store is read-only.
func CopyTree(src, dst string) {
	Check(filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		rel, _ := filepath.Rel(src, p)
		target := filepath.Join(dst, rel)
		info, err := d.Info()
		if err != nil {
			return err
		}
		switch {
		case info.Mode()&fs.ModeSymlink != 0:
			l, err := os.Readlink(p)
			if err != nil {
				return err
			}
			Symlink(l, target)
		case d.IsDir():
			Check(os.MkdirAll(target, info.Mode().Perm()|0o700), "mkdir ", target)
		default:
			CopyFile(p, target, info.Mode().Perm()|0o200)
		}
		return nil
	}), "copy tree ", src)
}

// Glob is filepath.Glob that fails on a bad pattern.
func Glob(pattern string) []string {
	m, err := filepath.Glob(pattern)
	Check(err, "glob ", pattern)
	return m
}