Skip to content

Commit c0d4d98

Browse files
authored
nix: make internal/nix.cmd public (#2473)
Move `internal/nix.cmd` and friends to the top-level nix package and export it. The constructor function is now a `Nix.Command` method. New commands use `Nix.Logger` for debug logs and include default arguments from `Nix.ExtraArgs`. As a convenience, the package-level `Command` function calls `Default.Command` (which uses the default Nix installation found in $PATH).
1 parent 344dc6c commit c0d4d98

File tree

15 files changed

+427
-349
lines changed

15 files changed

+427
-349
lines changed

internal/nix/build.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func Build(ctx context.Context, args *BuildArgs, installables ...string) error {
2424
FixInstallableArgs(installables)
2525

2626
// --impure is required for allowUnfreeEnv/allowInsecureEnv to work.
27-
cmd := command("build", "--impure")
27+
cmd := Command("build", "--impure")
2828
cmd.Args = appendArgs(cmd.Args, args.Flags)
2929
cmd.Args = appendArgs(cmd.Args, installables)
3030
// Adding extra substituters only here to be conservative, but this could also

internal/nix/cache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func CopyInstallableToCache(
1818
env []string,
1919
) error {
2020
fmt.Fprintf(out, "Copying %s to %s\n", installable, to)
21-
cmd := command(
21+
cmd := Command(
2222
"copy", "--to", to,
2323
// --impure makes NIXPKGS_ALLOW_* environment variables work.
2424
"--impure",

internal/nix/command.go

Lines changed: 5 additions & 318 deletions
Original file line numberDiff line numberDiff line change
@@ -1,332 +1,19 @@
11
package nix
22

3-
import (
4-
"bytes"
5-
"context"
6-
"errors"
7-
"fmt"
8-
"io"
9-
"log/slog"
10-
"os"
11-
"os/exec"
12-
"slices"
13-
"strconv"
14-
"strings"
15-
"syscall"
16-
"time"
17-
)
18-
19-
type cmd struct {
20-
Args cmdArgs
21-
Env []string
22-
23-
Stdin io.Reader
24-
Stdout io.Writer
25-
Stderr io.Writer
26-
27-
execCmd *exec.Cmd
28-
err error
29-
dur time.Duration
30-
logger *slog.Logger
31-
}
32-
33-
func command(args ...any) *cmd {
34-
cmd := &cmd{
35-
Args: append(cmdArgs{
36-
"nix",
37-
"--extra-experimental-features", "ca-derivations",
38-
"--option", "experimental-features", "nix-command flakes fetch-closure",
39-
}, args...),
40-
logger: slog.Default(),
41-
}
42-
return cmd
43-
}
44-
45-
func (c *cmd) CombinedOutput(ctx context.Context) ([]byte, error) {
46-
cmd := c.initExecCommand(ctx)
47-
c.logger.DebugContext(ctx, "nix command starting", "cmd", c)
48-
49-
start := time.Now()
50-
out, err := cmd.CombinedOutput()
51-
c.dur = time.Since(start)
52-
53-
c.err = c.error(ctx, err)
54-
c.logger.DebugContext(ctx, "nix command exited", "cmd", c)
55-
return out, c.err
56-
}
57-
58-
func (c *cmd) Output(ctx context.Context) ([]byte, error) {
59-
cmd := c.initExecCommand(ctx)
60-
c.logger.DebugContext(ctx, "nix command starting", "cmd", c)
61-
62-
start := time.Now()
63-
out, err := cmd.Output()
64-
c.dur = time.Since(start)
65-
66-
c.err = c.error(ctx, err)
67-
c.logger.DebugContext(ctx, "nix command exited", "cmd", c)
68-
return out, c.err
69-
}
70-
71-
func (c *cmd) Run(ctx context.Context) error {
72-
cmd := c.initExecCommand(ctx)
73-
c.logger.DebugContext(ctx, "nix command starting", "cmd", c)
74-
75-
start := time.Now()
76-
err := cmd.Run()
77-
c.dur = time.Since(start)
78-
79-
c.err = c.error(ctx, err)
80-
c.logger.DebugContext(ctx, "nix command exited", "cmd", c)
81-
return c.err
82-
}
83-
84-
func (c *cmd) LogValue() slog.Value {
85-
attrs := []slog.Attr{
86-
slog.Any("args", c.Args),
87-
}
88-
if c.execCmd == nil {
89-
return slog.GroupValue(attrs...)
90-
}
91-
attrs = append(attrs, slog.String("path", c.execCmd.Path))
92-
93-
var exitErr *exec.ExitError
94-
if errors.As(c.err, &exitErr) {
95-
stderr := c.stderrExcerpt(exitErr.Stderr)
96-
if len(stderr) != 0 {
97-
attrs = append(attrs, slog.String("stderr", stderr))
98-
}
99-
}
100-
if proc := c.execCmd.Process; proc != nil {
101-
attrs = append(attrs, slog.Int("pid", proc.Pid))
102-
}
103-
if procState := c.execCmd.ProcessState; procState != nil {
104-
if procState.Exited() {
105-
attrs = append(attrs, slog.Int("code", procState.ExitCode()))
106-
}
107-
if status, ok := procState.Sys().(syscall.WaitStatus); ok && status.Signaled() {
108-
if status.Signaled() {
109-
attrs = append(attrs, slog.String("signal", status.Signal().String()))
110-
}
111-
}
112-
}
113-
if c.dur != 0 {
114-
attrs = append(attrs, slog.Duration("dur", c.dur))
115-
}
116-
return slog.GroupValue(attrs...)
117-
}
118-
119-
func (c *cmd) String() string {
120-
return c.Args.String()
121-
}
122-
123-
func (c *cmd) initExecCommand(ctx context.Context) *exec.Cmd {
124-
if c.execCmd != nil {
125-
return c.execCmd
126-
}
127-
128-
args := c.Args.StringSlice()
129-
c.execCmd = exec.CommandContext(ctx, args[0], args[1:]...)
130-
c.execCmd.Env = c.Env
131-
c.execCmd.Stdin = c.Stdin
132-
c.execCmd.Stdout = c.Stdout
133-
c.execCmd.Stderr = c.Stderr
134-
135-
c.execCmd.Cancel = func() error {
136-
// Try to let Nix exit gracefully by sending an interrupt
137-
// instead of the default behavior of killing it.
138-
c.logger.DebugContext(ctx, "sending interrupt to nix process", slog.Group("cmd",
139-
"args", c.Args,
140-
"path", c.execCmd.Path,
141-
"pid", c.execCmd.Process.Pid,
142-
))
143-
err := c.execCmd.Process.Signal(os.Interrupt)
144-
if errors.Is(err, os.ErrProcessDone) {
145-
// Nix already exited; execCmd.Wait will use the exit
146-
// code.
147-
return err
148-
}
149-
if err != nil {
150-
// We failed to send SIGINT, so kill the process
151-
// instead.
152-
//
153-
// - If Nix already exited, Kill will return
154-
// os.ErrProcessDone and execCmd.Wait will use
155-
// the exit code.
156-
// - Otherwise, execCmd.Wait will always return an
157-
// error.
158-
c.logger.ErrorContext(ctx, "error interrupting nix process, attempting to kill",
159-
"err", err, slog.Group("cmd",
160-
"args", c.Args,
161-
"path", c.execCmd.Path,
162-
"pid", c.execCmd.Process.Pid,
163-
))
164-
return c.execCmd.Process.Kill()
165-
}
166-
167-
// We sent the SIGINT successfully. It's still possible for Nix
168-
// to exit successfully, so return os.ErrProcessDone so that
169-
// execCmd.Wait uses the exit code instead of ctx.Err.
170-
return os.ErrProcessDone
171-
}
172-
// Kill Nix if it doesn't exit within 15 seconds of Devbox sending an
173-
// interrupt.
174-
c.execCmd.WaitDelay = 15 * time.Second
175-
return c.execCmd
176-
}
177-
178-
func (c *cmd) error(ctx context.Context, err error) error {
179-
if err == nil {
180-
return nil
181-
}
182-
183-
cmdErr := &cmdError{err: err}
184-
if errors.Is(err, exec.ErrNotFound) {
185-
cmdErr.msg = fmt.Sprintf("nix: %s not found in $PATH", c.Args[0])
3+
func init() {
4+
Default.ExtraArgs = Args{
5+
"--extra-experimental-features", "ca-derivations",
6+
"--option", "experimental-features", "nix-command flakes fetch-closure",
1867
}
187-
188-
switch {
189-
case errors.Is(ctx.Err(), context.Canceled):
190-
cmdErr.msg = "nix: command canceled"
191-
case errors.Is(ctx.Err(), context.DeadlineExceeded):
192-
cmdErr.msg = "nix: command timed out"
193-
default:
194-
cmdErr.msg = "nix: command error"
195-
}
196-
cmdErr.msg += ": " + c.String()
197-
198-
var exitErr *exec.ExitError
199-
if errors.As(err, &exitErr) {
200-
if stderr := c.stderrExcerpt(exitErr.Stderr); len(stderr) != 0 {
201-
cmdErr.msg += ": " + stderr
202-
}
203-
if exitErr.Exited() {
204-
cmdErr.msg += fmt.Sprintf(": exit code %d", exitErr.ExitCode())
205-
return cmdErr
206-
}
207-
if stat, ok := exitErr.Sys().(syscall.WaitStatus); ok && stat.Signaled() {
208-
cmdErr.msg += fmt.Sprintf(": exit due to signal %d (%[1]s)", stat.Signal())
209-
return cmdErr
210-
}
211-
}
212-
213-
if !errors.Is(err, ctx.Err()) {
214-
cmdErr.msg += ": " + err.Error()
215-
}
216-
return cmdErr
217-
}
218-
219-
func (*cmd) stderrExcerpt(stderr []byte) string {
220-
stderr = bytes.TrimSpace(stderr)
221-
if len(stderr) == 0 {
222-
return ""
223-
}
224-
225-
lines := bytes.Split(stderr, []byte("\n"))
226-
slices.Reverse(lines)
227-
for _, line := range lines {
228-
line = bytes.TrimSpace(line)
229-
after, found := bytes.CutPrefix(line, []byte("error: "))
230-
if !found {
231-
continue
232-
}
233-
after = bytes.TrimSpace(after)
234-
if len(after) == 0 {
235-
continue
236-
}
237-
stderr = after
238-
break
239-
240-
}
241-
242-
excerpt := string(stderr)
243-
if !strconv.CanBackquote(excerpt) {
244-
quoted := strconv.Quote(excerpt)
245-
excerpt = quoted[1 : len(quoted)-1]
246-
}
247-
return excerpt
2488
}
2499

250-
type cmdArgs []any
251-
252-
func appendArgs[E any](args cmdArgs, new []E) cmdArgs {
10+
func appendArgs[E any](args Args, new []E) Args {
25311
for _, elem := range new {
25412
args = append(args, elem)
25513
}
25614
return args
25715
}
25816

259-
func (c cmdArgs) StringSlice() []string {
260-
s := make([]string, len(c))
261-
for i := range c {
262-
s[i] = fmt.Sprint(c[i])
263-
}
264-
return s
265-
}
266-
267-
func (c cmdArgs) String() string {
268-
if len(c) == 0 {
269-
return ""
270-
}
271-
272-
sb := &strings.Builder{}
273-
c.writeQuoted(sb, fmt.Sprint(c[0]))
274-
if len(c) == 1 {
275-
return sb.String()
276-
}
277-
278-
for _, arg := range c[1:] {
279-
sb.WriteByte(' ')
280-
c.writeQuoted(sb, fmt.Sprint(arg))
281-
}
282-
return sb.String()
283-
}
284-
285-
func (cmdArgs) writeQuoted(dst *strings.Builder, str string) {
286-
needsQuote := strings.ContainsAny(str, ";\"'()$|&><` \t\r\n\\#{~*?[=")
287-
if !needsQuote {
288-
dst.WriteString(str)
289-
return
290-
}
291-
292-
canSingleQuote := !strings.Contains(str, "'")
293-
if canSingleQuote {
294-
dst.WriteByte('\'')
295-
dst.WriteString(str)
296-
dst.WriteByte('\'')
297-
return
298-
}
299-
300-
dst.WriteByte('"')
301-
for _, r := range str {
302-
switch r {
303-
// Special characters inside double quotes:
304-
// https://pubs.opengroup.org/onlinepubs/009604499/utilities/xcu_chap02.html#tag_02_02_03
305-
case '$', '`', '"', '\\':
306-
dst.WriteRune('\\')
307-
}
308-
dst.WriteRune(r)
309-
}
310-
dst.WriteByte('"')
311-
}
312-
313-
type cmdError struct {
314-
msg string
315-
err error
316-
}
317-
318-
func (c *cmdError) Redact() string {
319-
return c.Error()
320-
}
321-
322-
func (c *cmdError) Error() string {
323-
return c.msg
324-
}
325-
326-
func (c *cmdError) Unwrap() error {
327-
return c.err
328-
}
329-
33017
func allowUnfreeEnv(curEnv []string) []string {
33118
return append(curEnv, "NIXPKGS_ALLOW_UNFREE=1")
33219
}

internal/nix/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type ConfigField[T any] struct {
3838
func CurrentConfig(ctx context.Context) (Config, error) {
3939
// `nix show-config` is deprecated in favor of `nix config show`, but we
4040
// want to remain compatible with older Nix versions.
41-
cmd := command("show-config", "--json")
41+
cmd := Command("show-config", "--json")
4242
out, err := cmd.Output(ctx)
4343
var exitErr *exec.ExitError
4444
if errors.As(err, &exitErr) && len(exitErr.Stderr) != 0 {

0 commit comments

Comments
 (0)