context: interface command type

This should allow caller to override the method to run in a container.
This commit is contained in:
Ophestra 2025-09-13 13:06:39 +09:00
parent 6911583918
commit a3427ce7dd
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
10 changed files with 72 additions and 35 deletions

View File

@ -14,14 +14,14 @@ func Build(ctx Context, installables iter.Seq[string]) error {
cmd := ctx.Nix(c, CommandBuild,
FlagKeepGoing, FlagNoLink, FlagStdin)
if stdout != nil {
cmd.Stdout = stdout
cmd.Args = append(cmd.Args, FlagPrintBuildLogs)
cmd.Stdout(stdout)
cmd.AppendArgs(FlagPrintBuildLogs)
} else {
cmd.Args = append(cmd.Args, FlagQuiet)
cmd.AppendArgs(FlagQuiet)
}
if stderr != nil {
cmd.Stderr = stderr
cmd.Args = append(cmd.Args, FlagVerbose)
cmd.Stderr(stderr)
cmd.AppendArgs(FlagVerbose)
}
_, err := ctx.WriteStdin(cmd, installables, nil)

View File

@ -4,19 +4,40 @@ import (
"context"
"io"
"iter"
"os/exec"
)
// Cmd represents an external command or container being prepared to run.
type Cmd interface {
Start() error
Wait() error
StdinPipe() (io.WriteCloser, error)
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
Stdin(io.Reader)
Stdout(io.Writer)
Stderr(io.Writer)
AppendArgs(a ...string)
ReplaceEnv(env []string)
}
// Context holds configuration and environment information for interacting with nix.
type Context interface {
// Streams returns the stdout and stderr writers held by this [Context].
Streams() (stdout, stderr io.Writer)
// Nix returns the [exec.Cmd] struct to execute a nix command.
Nix(ctx context.Context, arg ...string) *exec.Cmd
// Nix returns an implementation of [Cmd] for running a nix command.
Nix(ctx context.Context, arg ...string) Cmd
// WriteStdin calls [WriteStdin] for [exec.Cmd]. The function f points to is called if [WriteStdin] succeeds.
WriteStdin(cmd *exec.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() context.Context
}
// setStreams sets the stdout and stderr streams of [Cmd] according to [Context].
func setStreams(ctx Context, cmd Cmd) {
stdout, stderr := ctx.Streams()
cmd.Stdout(stdout)
cmd.Stderr(stderr)
}

View File

@ -6,7 +6,7 @@ import (
"os"
)
// Copy copies installables to the binary cache store.
// Copy copies installables to [Store].
func Copy(ctx Context, store Store, installables iter.Seq[string]) error {
if store == nil {
return os.ErrInvalid
@ -18,13 +18,13 @@ func Copy(ctx Context, store Store, installables iter.Seq[string]) error {
cmd := ctx.Nix(c, CommandCopy,
FlagTo, store.String(),
FlagStdin)
cmd.Env = append(os.Environ(), store.Environ()...)
cmd.ReplaceEnv(append(os.Environ(), store.Environ()...))
if _, ok := store.(Local); ok {
// this is required for chroot stores, but does not seem to have any effect on binary cache
cmd.Args = append(cmd.Args, FlagNoCheckSigs)
cmd.AppendArgs(FlagNoCheckSigs)
}
cmd.Stdout, cmd.Stderr = ctx.Streams()
setStreams(ctx, cmd)
_, err := ctx.WriteStdin(cmd, installables, nil)
return err
}

View File

@ -60,7 +60,7 @@ func DerivationShow(ctx Context, installables iter.Seq[string]) (DerivationMap,
decoder = json.NewDecoder(r)
}
if stderr != nil {
cmd.Stderr = stderr
cmd.Stderr(stderr)
}
var v DerivationMap

15
exec.go
View File

@ -58,17 +58,26 @@ func New(ctx context.Context, store Store, extraArgs []string, stdout, stderr io
}
}
func (n *nix) Nix(ctx context.Context, arg ...string) *exec.Cmd {
// ExecCmd wraps [exec.Cmd] and implements extra [Cmd] methods.
type ExecCmd struct{ *exec.Cmd }
func (cmd ExecCmd) Stdin(r io.Reader) { cmd.Cmd.Stdin = r }
func (cmd ExecCmd) Stdout(w io.Writer) { cmd.Cmd.Stdout = w }
func (cmd ExecCmd) Stderr(w io.Writer) { cmd.Cmd.Stderr = w }
func (cmd ExecCmd) AppendArgs(a ...string) { cmd.Cmd.Args = append(cmd.Cmd.Args, a...) }
func (cmd ExecCmd) ReplaceEnv(env []string) { cmd.Cmd.Env = env }
func (n *nix) Nix(ctx context.Context, arg ...string) Cmd {
cmd := exec.CommandContext(ctx, n.name, append(n.extra, arg...)...)
cmd.Cancel = func() error { return cmd.Process.Signal(os.Interrupt) }
cmd.WaitDelay = defaultWaitDelay
if n.store != nil {
cmd.Env = append(cmd.Env, n.store.Environ()...)
}
return cmd
return ExecCmd{cmd}
}
func (n *nix) WriteStdin(cmd *exec.Cmd, installables iter.Seq[string], f func() error) (int, error) {
func (n *nix) WriteStdin(cmd Cmd, installables iter.Seq[string], f func() error) (int, error) {
w, err := cmd.StdinPipe()
if err != nil {
return 0, err

View File

@ -63,10 +63,10 @@ type stubContextCommand struct {
nix.Context
}
func (s *stubContextCommand) Nix(ctx context.Context, arg ...string) *exec.Cmd {
cmd := s.Context.Nix(ctx, arg...)
func (s *stubContextCommand) Nix(ctx context.Context, arg ...string) nix.Cmd {
cmd := s.Context.Nix(ctx, arg...).(nix.ExecCmd)
if s.f != nil {
s.f(cmd)
s.f(cmd.Cmd)
}
return cmd
}

View File

@ -24,7 +24,7 @@ func TestNixWriteStdin(t *testing.T) {
CredentialsPath: "/dev/null",
KeyPath: nonexistent,
}, nil, nil, nil)
cmd := ctx.Nix(t.Context(), nix.FlagVersion)
cmd := ctx.Nix(t.Context(), nix.FlagVersion).(nix.ExecCmd)
wantArgs := []string{
nix.Nix,
@ -43,8 +43,8 @@ func TestNixWriteStdin(t *testing.T) {
t.Run("already set", func(t *testing.T) {
ctx := nix.New(t.Context(), nil, nil, os.Stdout, os.Stderr)
cmd := exec.CommandContext(t.Context(), nonexistent)
cmd.Stdin = os.Stdin
cmd := nix.ExecCmd{Cmd: exec.CommandContext(t.Context(), nonexistent)}
cmd.Stdin(os.Stdin)
if _, err := ctx.WriteStdin(cmd, nil, nil); err == nil {
t.Fatal("WriteStdinCommand unexpectedly succeeded")
}
@ -68,7 +68,7 @@ func TestNixWriteStdin(t *testing.T) {
ctx := newStubContext(t.Context(), nil, os.Stdout, os.Stderr)
c, cancel := context.WithCancel(t.Context())
defer cancel()
cmd := ctx.Nix(c, "true")
cmd := ctx.Nix(c, "true").(nix.ExecCmd)
if err := cmd.Start(); err != nil {
t.Fatalf("Start: error = %v", err)
}

View File

@ -6,7 +6,6 @@ import (
"errors"
"io"
"iter"
"os/exec"
"path"
"slices"
"strings"
@ -153,7 +152,7 @@ func (d *InstantiatedDecoder) Decode() ([]string, error) {
// are significantly more complete than `nix-store -qR` and there does not appear to be a better way.
type InstantiatedEvaluator struct {
// underlying nix program
cmd *exec.Cmd
cmd Cmd
// populated by Close
waitErr error
// synchronises access to waitErr
@ -184,7 +183,7 @@ func NewInstantiatedEvaluator(ctx Context, installable string) (*InstantiatedEva
}
stdout, stderr := ctx.Streams()
e.cmd.Stdout = stdout
e.cmd.Stdout(stdout)
// verbose output ends up on stderr in the current nix implementation
if r, err := e.cmd.StderrPipe(); err != nil {

View File

@ -3,20 +3,28 @@
stdenv,
buildGoModule,
pkg-config,
libffi,
libseccomp,
}:
buildGoModule {
pname = "nix-tool";
version = "0.1.4";
src = ./.;
vendorHash = "sha256-lK9+fI8/GR2GY6X899HnoP28FuQnklYbzaiGqIkus8c=";
vendorHash = "sha256-AUSqbsXvJvkOE0BjO6XnPTDD2NkOHY2XUbo7jZtYmd4=";
ldflags =
[ "-s -w" ]
++ lib.optionals stdenv.hostPlatform.isStatic [
"-linkmode external"
"-extldflags \"-static\""
];
ldflags = [
"-s -w"
]
++ lib.optionals stdenv.hostPlatform.isStatic [
"-linkmode external"
"-extldflags \"-static\""
];
buildInputs = [
libffi
libseccomp
];
nativeBuildInputs = [
pkg-config

View File

@ -17,7 +17,7 @@ func Sign(ctx Context, keyPath string, installables iter.Seq[string]) error {
FlagKeyFile, keyPath,
FlagStdin)
cmd.Stdout, cmd.Stderr = ctx.Streams()
setStreams(ctx, cmd)
_, err := ctx.WriteStdin(cmd, installables, nil)
return err
}