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, cmd := ctx.Nix(c, CommandBuild,
FlagKeepGoing, FlagNoLink, FlagStdin) FlagKeepGoing, FlagNoLink, FlagStdin)
if stdout != nil { if stdout != nil {
cmd.Stdout = stdout cmd.Stdout(stdout)
cmd.Args = append(cmd.Args, FlagPrintBuildLogs) cmd.AppendArgs(FlagPrintBuildLogs)
} else { } else {
cmd.Args = append(cmd.Args, FlagQuiet) cmd.AppendArgs(FlagQuiet)
} }
if stderr != nil { if stderr != nil {
cmd.Stderr = stderr cmd.Stderr(stderr)
cmd.Args = append(cmd.Args, FlagVerbose) cmd.AppendArgs(FlagVerbose)
} }
_, err := ctx.WriteStdin(cmd, installables, nil) _, err := ctx.WriteStdin(cmd, installables, nil)

View File

@ -4,19 +4,40 @@ import (
"context" "context"
"io" "io"
"iter" "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. // Context holds configuration and environment information for interacting with nix.
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)
// Nix returns the [exec.Cmd] struct to execute a nix command. // Nix returns an implementation of [Cmd] for running a nix command.
Nix(ctx context.Context, arg ...string) *exec.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 [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 returns the stored [context.Context]
Unwrap() 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" "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 { func Copy(ctx Context, store Store, installables iter.Seq[string]) error {
if store == nil { if store == nil {
return os.ErrInvalid return os.ErrInvalid
@ -18,13 +18,13 @@ func Copy(ctx Context, store Store, installables iter.Seq[string]) error {
cmd := ctx.Nix(c, CommandCopy, cmd := ctx.Nix(c, CommandCopy,
FlagTo, store.String(), FlagTo, store.String(),
FlagStdin) FlagStdin)
cmd.Env = append(os.Environ(), store.Environ()...) cmd.ReplaceEnv(append(os.Environ(), store.Environ()...))
if _, ok := store.(Local); ok { if _, ok := store.(Local); ok {
// this is required for chroot stores, but does not seem to have any effect on binary cache // 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) _, err := ctx.WriteStdin(cmd, installables, nil)
return err return err
} }

View File

@ -60,7 +60,7 @@ func DerivationShow(ctx Context, installables iter.Seq[string]) (DerivationMap,
decoder = json.NewDecoder(r) decoder = json.NewDecoder(r)
} }
if stderr != nil { if stderr != nil {
cmd.Stderr = stderr cmd.Stderr(stderr)
} }
var v DerivationMap 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 := 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()...)
} }
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() w, err := cmd.StdinPipe()
if err != nil { if err != nil {
return 0, err return 0, err

View File

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

View File

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

View File

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

View File

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

View File

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