context: expose more internals

This makes overriding the Nix method easier.
This commit is contained in:
Ophestra 2025-09-13 18:32:12 +09:00
parent a3427ce7dd
commit 8d502f1574
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
3 changed files with 64 additions and 4 deletions

View File

@ -25,10 +25,14 @@ type Cmd interface {
type Context interface { type Context interface {
// Streams returns the stdout and stderr writers held by this [Context]. // Streams returns the stdout and stderr writers held by this [Context].
Streams() (stdout, stderr io.Writer) Streams() (stdout, stderr io.Writer)
// Extra returns a copy of extra arguments held by this [Context].
Extra() []string
// StoreEnv returns extra [Store] environment variables.
StoreEnv() []string
// Nix returns an implementation of [Cmd] for running a nix command. // Nix returns an implementation of [Cmd] for running a nix command.
Nix(ctx context.Context, arg ...string) Cmd Nix(ctx context.Context, arg ...string) Cmd
// WriteStdin calls [WriteStdin] for [exec.Cmd]. The function f points to is called if [WriteStdin] succeeds. // WriteStdin calls [WriteStdin] for [Cmd]. The function f points to is called if [WriteStdin] succeeds.
WriteStdin(cmd Cmd, installables iter.Seq[string], f func() error) (int, error) WriteStdin(cmd Cmd, installables iter.Seq[string], f func() error) (int, error)
// Unwrap returns the stored [context.Context] // Unwrap returns the stored [context.Context]

13
exec.go
View File

@ -11,7 +11,7 @@ import (
) )
const ( const (
defaultWaitDelay = 30 * time.Second DefaultWaitDelay = 30 * time.Second
) )
// Nix is the name of the nix program. // Nix is the name of the nix program.
@ -28,6 +28,13 @@ type nix struct {
func (n *nix) Unwrap() context.Context { return n.ctx } func (n *nix) Unwrap() context.Context { return n.ctx }
func (n *nix) Streams() (stdout, stderr io.Writer) { return n.stdout, n.stderr } func (n *nix) Streams() (stdout, stderr io.Writer) { return n.stdout, n.stderr }
func (n *nix) Extra() (v []string) { v = make([]string, len(n.extra)); copy(v, n.extra); return }
func (n *nix) StoreEnv() (v []string) {
if n.store == nil {
return nil
}
return n.store.Environ()
}
const ( const (
ExtraExperimentalFeatures = "--extra-experimental-features" ExtraExperimentalFeatures = "--extra-experimental-features"
@ -42,6 +49,7 @@ A non-nil stderr implies verbose.
Streams will not be connected for commands outputting JSON. Streams will not be connected for commands outputting JSON.
*/ */
func New(ctx context.Context, store Store, extraArgs []string, stdout, stderr io.Writer) Context { func New(ctx context.Context, store Store, extraArgs []string, stdout, stderr io.Writer) Context {
// since flakes are supposedly experimental
extra := []string{ExtraExperimentalFeatures, ExperimentalFeaturesFlakes} extra := []string{ExtraExperimentalFeatures, ExperimentalFeaturesFlakes}
if store != nil { if store != nil {
extra = append(extraArgs, FlagStore, store.String()) extra = append(extraArgs, FlagStore, store.String())
@ -50,7 +58,6 @@ func New(ctx context.Context, store Store, extraArgs []string, stdout, stderr io
name: Nix, name: Nix,
store: store, store: store,
ctx: ctx, ctx: ctx,
// since flakes are supposedly experimental
extra: append(extraArgs, extra...), extra: append(extraArgs, extra...),
stdout: stdout, stdout: stdout,
@ -70,7 +77,7 @@ func (cmd ExecCmd) ReplaceEnv(env []string) { cmd.Cmd.Env = env }
func (n *nix) Nix(ctx context.Context, arg ...string) Cmd { func (n *nix) Nix(ctx context.Context, arg ...string) Cmd {
cmd := exec.CommandContext(ctx, n.name, append(n.extra, arg...)...) cmd := exec.CommandContext(ctx, n.name, append(n.extra, arg...)...)
cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) } cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) }
cmd.WaitDelay = defaultWaitDelay cmd.WaitDelay = DefaultWaitDelay
if n.store != nil { if n.store != nil {
cmd.Env = append(cmd.Env, n.store.Environ()...) cmd.Env = append(cmd.Env, n.store.Environ()...)
} }

View File

@ -5,13 +5,62 @@ import (
"errors" "errors"
"os" "os"
"os/exec" "os/exec"
"reflect"
"slices" "slices"
"syscall" "syscall"
"testing" "testing"
"unsafe"
"gensokyo.uk/nix" "gensokyo.uk/nix"
) )
func TestContext(t *testing.T) {
ctx := nix.New(t.Context(), &nix.BinaryCache{
Compression: "none",
ParallelCompression: false,
Bucket: "example",
Endpoint: "s3.example.org",
Region: "us-east-1",
Scheme: "http",
CredentialsPath: "/dev/null",
KeyPath: nonexistent,
}, []string{
nix.FlagOption, nix.OptionBuildUseSubstitutes, nix.ValueFalse,
nix.FlagOption, nix.OptionSubstituters, "",
nix.FlagOption, nix.OptionTrustedSubstituters, "",
nix.FlagOption, nix.OptionTrustedPublicKeys, "",
}, nil, nil)
extraVal := reflect.ValueOf(ctx).Elem().FieldByName("extra")
wantExtra := reflect.NewAt(extraVal.Type(), unsafe.Pointer(extraVal.UnsafeAddr())).Elem().Interface().([]string)
t.Run("extra", func(t *testing.T) {
got := ctx.Extra()
if !slices.Equal(got, wantExtra) {
t.Errorf("Extra: %#v, want %#v", got, wantExtra)
}
got[0] = "\x00"
if slices.Equal(got, wantExtra) {
t.Errorf("Extra did not return a copy")
}
})
t.Run("store env", func(t *testing.T) {
t.Run("nil", func(t *testing.T) {
want := nix.New(t.Context(), nil, nil, nil, nil).StoreEnv()
if want != nil {
t.Errorf("StoreEnv: %#v", want)
}
})
want := []string{"AWS_SHARED_CREDENTIALS_FILE=/dev/null"}
got := ctx.StoreEnv()
if !slices.Equal(got, want) {
t.Errorf("StoreEnv: %#v, want %#v", got, want)
}
})
}
func TestNixWriteStdin(t *testing.T) { func TestNixWriteStdin(t *testing.T) {
t.Run("store", func(t *testing.T) { t.Run("store", func(t *testing.T) {
ctx := nix.New(t.Context(), &nix.BinaryCache{ ctx := nix.New(t.Context(), &nix.BinaryCache{