Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
b298c5f572 | |||
4d9d4bcef2 | |||
b6961508e8 | |||
e0278a6d7d | |||
e448541464 |
@ -85,7 +85,7 @@ func TestBuildBadCommand(t *testing.T) {
|
||||
wantErr := os.ErrNotExist
|
||||
breakNixCommand(t)
|
||||
if err := nix.Build(
|
||||
nix.New(t.Context(), nil, nil, nil),
|
||||
nix.New(t.Context(), nil, nil, nil, nil),
|
||||
nil,
|
||||
); !errors.Is(err, wantErr) {
|
||||
t.Errorf("Build: error = %v, want %v", err, wantErr)
|
||||
|
@ -45,11 +45,14 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
var extraArgs []string
|
||||
var (
|
||||
store nix.Store
|
||||
extraArgs []string
|
||||
)
|
||||
flagStore = strings.TrimSpace(flagStore)
|
||||
if flagStore != string(os.PathSeparator) {
|
||||
store = nix.Local(flagStore)
|
||||
extraArgs = append(extraArgs,
|
||||
"--store", flagStore,
|
||||
// do not use any binary cache
|
||||
nix.FlagOption, nix.OptionBuildUseSubstitutes, nix.ValueFalse,
|
||||
nix.FlagOption, nix.OptionSubstituters, "",
|
||||
@ -62,7 +65,7 @@ func main() {
|
||||
if flagVerbose {
|
||||
stderr = os.Stderr
|
||||
}
|
||||
ctx = nix.New(nixCtx, extraArgs, os.Stdout, stderr)
|
||||
ctx = nix.New(nixCtx, store, extraArgs, os.Stdout, stderr)
|
||||
|
||||
return nil
|
||||
}).
|
||||
@ -111,7 +114,7 @@ func main() {
|
||||
}
|
||||
|
||||
log.Println("copying to binary cache...")
|
||||
if err := nix.Copy(ctx, flagCacheKeyPath, &nix.BinaryCache{
|
||||
if err := nix.Copy(ctx, &nix.BinaryCache{
|
||||
Compression: flagCacheComp,
|
||||
ParallelCompression: flagCachePComp,
|
||||
Bucket: flagCacheBucket,
|
||||
@ -119,6 +122,7 @@ func main() {
|
||||
Region: flagCacheRegion,
|
||||
Scheme: flagCacheScheme,
|
||||
CredentialsPath: flagCacheCredPath,
|
||||
KeyPath: flagCacheKeyPath,
|
||||
}, slices.Values(collective)); err != nil {
|
||||
return commandHandlerError(fmt.Sprintf("cannot copy: %v", err))
|
||||
}
|
||||
|
39
copy.go
39
copy.go
@ -2,43 +2,12 @@ package nix
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"iter"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
EnvAwsSharedCredentialsFile = "AWS_SHARED_CREDENTIALS_FILE"
|
||||
)
|
||||
|
||||
// A BinaryCache holds credentials and parameters to a s3 binary cache.
|
||||
type BinaryCache struct {
|
||||
// Compression is the name of the compression algorithm to use. Example: "zstd".
|
||||
Compression string `json:"compression"`
|
||||
// ParallelCompression determines whether parallel compression is enabled.
|
||||
ParallelCompression bool `json:"parallel_compression,omitempty"`
|
||||
|
||||
// Bucket is the s3 bucket name.
|
||||
Bucket string `json:"bucket"`
|
||||
// Endpoint is the s3 endpoint. Example: "s3.example.org".
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
// Region is the s3 region. Example: "ap-northeast-1".
|
||||
Region string `json:"region"`
|
||||
// Scheme is the s3 protocol. Example: "https".
|
||||
Scheme string `json:"scheme"`
|
||||
// CredentialsPath is the path to the s3 shared credentials file.
|
||||
CredentialsPath string `json:"credentials_path"`
|
||||
}
|
||||
|
||||
func (store *BinaryCache) String() string {
|
||||
return fmt.Sprintf(
|
||||
"s3://%s?compression=%s¶llel-compression=%t®ion=%s&scheme=%s&endpoint=%s",
|
||||
store.Bucket, store.Compression, store.ParallelCompression, store.Region, store.Scheme, store.Endpoint,
|
||||
)
|
||||
}
|
||||
|
||||
// Copy copies installables to the binary cache store, signing all paths using the key at keyPath.
|
||||
func Copy(ctx Context, keyPath string, store *BinaryCache, installables iter.Seq[string]) error {
|
||||
// Copy copies installables to the binary cache store.
|
||||
func Copy(ctx Context, store Store, installables iter.Seq[string]) error {
|
||||
if store == nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
@ -47,9 +16,9 @@ func Copy(ctx Context, keyPath string, store *BinaryCache, installables iter.Seq
|
||||
defer cancel()
|
||||
|
||||
cmd := ctx.Nix(c, CommandCopy,
|
||||
FlagTo, store.String()+"&secret-key="+keyPath,
|
||||
FlagTo, store.String(),
|
||||
FlagStdin)
|
||||
cmd.Env = append(os.Environ(), EnvAwsSharedCredentialsFile+"="+store.CredentialsPath)
|
||||
cmd.Env = append(os.Environ(), store.Environ()...)
|
||||
|
||||
cmd.Stdout, cmd.Stderr = ctx.Streams()
|
||||
_, err := ctx.WriteStdin(cmd, installables, nil)
|
||||
|
38
copy_test.go
38
copy_test.go
@ -36,46 +36,10 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func TestBinaryCache(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
store *nix.BinaryCache
|
||||
want string
|
||||
}{
|
||||
{"example", &nix.BinaryCache{
|
||||
Compression: "none",
|
||||
ParallelCompression: false,
|
||||
Bucket: "example",
|
||||
Endpoint: "s3.example.org",
|
||||
Region: "us-east-1",
|
||||
Scheme: "http",
|
||||
CredentialsPath: "/dev/null",
|
||||
}, "s3://example?compression=none¶llel-compression=false®ion=us-east-1&scheme=http&endpoint=s3.example.org"},
|
||||
|
||||
{"gensokyo", &nix.BinaryCache{
|
||||
Compression: "zstd",
|
||||
ParallelCompression: true,
|
||||
Bucket: "nix-cache",
|
||||
Endpoint: "s3.gensokyo.uk",
|
||||
Region: "ap-northeast-1",
|
||||
Scheme: "https",
|
||||
CredentialsPath: "/var/lib/persist/cache/s3",
|
||||
}, "s3://nix-cache?compression=zstd¶llel-compression=true®ion=ap-northeast-1&scheme=https&endpoint=s3.gensokyo.uk"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.store.String(); got != tc.want {
|
||||
t.Errorf("String: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
stubNixCommand(t)
|
||||
if err := nix.Copy(
|
||||
newStubContext(t.Context(), nil, os.Stdout, os.Stderr),
|
||||
nonexistent,
|
||||
&nix.BinaryCache{
|
||||
Compression: "none",
|
||||
ParallelCompression: false,
|
||||
@ -84,6 +48,7 @@ func TestCopy(t *testing.T) {
|
||||
Region: "us-east-1",
|
||||
Scheme: "http",
|
||||
CredentialsPath: "/dev/null",
|
||||
KeyPath: nonexistent,
|
||||
},
|
||||
slices.Values(instWant["pluiedev pappardelle"]),
|
||||
); err != nil {
|
||||
@ -93,7 +58,6 @@ func TestCopy(t *testing.T) {
|
||||
t.Run("nil store", func(t *testing.T) {
|
||||
if err := nix.Copy(
|
||||
newStubContext(t.Context(), nil, os.Stdout, os.Stderr),
|
||||
nonexistent,
|
||||
nil,
|
||||
nil,
|
||||
); !errors.Is(err, os.ErrInvalid) {
|
||||
|
1
default.nix
Normal file
1
default.nix
Normal file
@ -0,0 +1 @@
|
||||
with import <nixpkgs> { }; pkgsStatic.callPackage ./package.nix { }
|
19
exec.go
19
exec.go
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultWaitDelay = 15 * time.Second
|
||||
defaultWaitDelay = 30 * time.Second
|
||||
)
|
||||
|
||||
// Nix is the name of the nix program.
|
||||
@ -19,6 +19,7 @@ var Nix = "nix"
|
||||
|
||||
type nix struct {
|
||||
name string
|
||||
store Store
|
||||
ctx context.Context
|
||||
extra []string
|
||||
|
||||
@ -40,12 +41,17 @@ A non-nil stderr implies verbose.
|
||||
|
||||
Streams will not be connected for commands outputting JSON.
|
||||
*/
|
||||
func New(ctx context.Context, extraArgs []string, stdout, stderr io.Writer) Context {
|
||||
func New(ctx context.Context, store Store, extraArgs []string, stdout, stderr io.Writer) Context {
|
||||
extra := []string{ExtraExperimentalFeatures, ExperimentalFeaturesFlakes}
|
||||
if store != nil {
|
||||
extra = append(extraArgs, FlagStore, store.String())
|
||||
}
|
||||
return &nix{
|
||||
name: Nix,
|
||||
ctx: ctx,
|
||||
name: Nix,
|
||||
store: store,
|
||||
ctx: ctx,
|
||||
// since flakes are supposedly experimental
|
||||
extra: append(extraArgs, ExtraExperimentalFeatures, ExperimentalFeaturesFlakes),
|
||||
extra: append(extraArgs, extra...),
|
||||
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
@ -56,6 +62,9 @@ func (n *nix) Nix(ctx context.Context, arg ...string) *exec.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
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@ func stubNixCommand(t *testing.T) {
|
||||
|
||||
// newStubContext creates a context for use with the nix command stub.
|
||||
func newStubContext(ctx context.Context, extraArgs []string, stdout, stderr io.Writer) nix.Context {
|
||||
return nix.New(ctx, append(stubExtraArgs, extraArgs...), stdout, stderr)
|
||||
return nix.New(ctx, nil, append(stubExtraArgs, extraArgs...), stdout, stderr)
|
||||
}
|
||||
|
||||
type stubContextCommand struct {
|
||||
@ -107,7 +107,7 @@ func TestNixStub(t *testing.T) {
|
||||
Flag(&flagExtraExperimentalFeatures, trimFlagName(nix.ExtraExperimentalFeatures), command.StringFlag(""),
|
||||
fmt.Sprintf("expects exactly %q", nix.ExperimentalFeaturesFlakes))
|
||||
|
||||
c.Command("true", command.UsageInternal, func([]string) error { return nil })
|
||||
c.Command(nix.ValueTrue, command.UsageInternal, func([]string) error { return nil })
|
||||
|
||||
for _, f := range stubCommandInit {
|
||||
f(c)
|
50
exec_test.go
50
exec_test.go
@ -1,6 +1,7 @@
|
||||
package nix_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
@ -12,8 +13,36 @@ import (
|
||||
)
|
||||
|
||||
func TestNixWriteStdin(t *testing.T) {
|
||||
t.Run("store", func(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,
|
||||
}, nil, nil, nil)
|
||||
cmd := ctx.Nix(t.Context(), nix.FlagVersion)
|
||||
|
||||
wantArgs := []string{
|
||||
nix.Nix,
|
||||
nix.FlagStore,
|
||||
"s3://example?compression=none¶llel-compression=false®ion=us-east-1&scheme=http&endpoint=s3.example.org&secret-key=/proc/nonexistent",
|
||||
nix.FlagVersion}
|
||||
if !slices.Equal(cmd.Args, wantArgs) {
|
||||
t.Errorf("Args = %#v, want %#v", cmd.Args, wantArgs)
|
||||
}
|
||||
|
||||
wantEnv := []string{nix.EnvAwsSharedCredentialsFile + "=/dev/null"}
|
||||
if !slices.Equal(cmd.Env, wantEnv) {
|
||||
t.Errorf("Env = %#v, want %#v", cmd.Env, wantEnv)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("already set", func(t *testing.T) {
|
||||
ctx := nix.New(t.Context(), nil, os.Stdout, os.Stderr)
|
||||
ctx := nix.New(t.Context(), nil, nil, os.Stdout, os.Stderr)
|
||||
cmd := exec.CommandContext(t.Context(), nonexistent)
|
||||
cmd.Stdin = os.Stdin
|
||||
if _, err := ctx.WriteStdin(cmd, nil, nil); err == nil {
|
||||
@ -33,4 +62,23 @@ func TestNixWriteStdin(t *testing.T) {
|
||||
t.Fatalf("WriteStdinCommand: error = %v, want %v", err, syscall.ENOSYS)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("exit before cancel", func(t *testing.T) {
|
||||
stubNixCommand(t)
|
||||
ctx := newStubContext(t.Context(), nil, os.Stdout, os.Stderr)
|
||||
c, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
cmd := ctx.Nix(c, "true")
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("Start: error = %v", err)
|
||||
}
|
||||
// Cancel is skipped after exec.Cmd.Wait completes
|
||||
if _, err := cmd.Process.Wait(); err != nil {
|
||||
t.Fatalf("Wait: error = %v", err)
|
||||
}
|
||||
cancel()
|
||||
if cmd.Err != nil {
|
||||
t.Fatalf("Err = %v", cmd.Err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -80,6 +80,9 @@ const (
|
||||
// FlagVersion show version information.
|
||||
FlagVersion = "--version"
|
||||
|
||||
// FlagStore is a loosely documented flag for specifying the store url to operate on.
|
||||
FlagStore = "--store"
|
||||
|
||||
// FlagKeepGoing keep going in case of failed builds, to the greatest extent possible.
|
||||
// That is, if building an input of some derivation fails, Nix will still build the other inputs,
|
||||
// but not the derivation itself.
|
||||
|
@ -107,7 +107,7 @@ func TestInstantiatedEvaluatorBadCommand(t *testing.T) {
|
||||
breakNixCommand(t)
|
||||
|
||||
if _, err := nix.EvalInstantiated(
|
||||
nix.New(t.Context(), nil, os.Stdout, os.Stderr),
|
||||
nix.New(t.Context(), nil, nil, os.Stdout, os.Stderr),
|
||||
"",
|
||||
); !errors.Is(err, wantErr) {
|
||||
t.Errorf("EvalInstantiated: error = %v, want %v", err, wantErr)
|
||||
|
24
package.nix
Normal file
24
package.nix
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
lib,
|
||||
stdenv,
|
||||
buildGoModule,
|
||||
pkg-config,
|
||||
}:
|
||||
buildGoModule {
|
||||
pname = "nix-tool";
|
||||
version = "0.1.4";
|
||||
src = ./.;
|
||||
|
||||
vendorHash = "sha256-SVSrY9SZnS6NLin+apdN4efqCzHgBBY8LIjXcUCXbUo=";
|
||||
|
||||
ldflags =
|
||||
[ "-s -w" ]
|
||||
++ lib.optionals stdenv.hostPlatform.isStatic [
|
||||
"-linkmode external"
|
||||
"-extldflags \"-static\""
|
||||
];
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
];
|
||||
}
|
56
store.go
Normal file
56
store.go
Normal file
@ -0,0 +1,56 @@
|
||||
package nix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
// Environ returns extra environment variables specified by Store.
|
||||
Environ() []string
|
||||
|
||||
fmt.Stringer
|
||||
}
|
||||
|
||||
// Local points to a local filesystem path containing a nix store.
|
||||
type Local string
|
||||
|
||||
func (Local) Environ() []string { return nil }
|
||||
func (store Local) String() string { return string(store) }
|
||||
|
||||
const (
|
||||
EnvAwsSharedCredentialsFile = "AWS_SHARED_CREDENTIALS_FILE"
|
||||
)
|
||||
|
||||
// A BinaryCache holds credentials and parameters to a s3 binary cache.
|
||||
type BinaryCache struct {
|
||||
// Compression is the name of the compression algorithm to use. Example: "zstd".
|
||||
Compression string `json:"compression"`
|
||||
// ParallelCompression determines whether parallel compression is enabled.
|
||||
ParallelCompression bool `json:"parallel_compression,omitempty"`
|
||||
|
||||
// Bucket is the s3 bucket name.
|
||||
Bucket string `json:"bucket"`
|
||||
// Endpoint is the s3 endpoint. Example: "s3.example.org".
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
// Region is the s3 region. Example: "ap-northeast-1".
|
||||
Region string `json:"region"`
|
||||
// Scheme is the s3 protocol. Example: "https".
|
||||
Scheme string `json:"scheme"`
|
||||
// CredentialsPath is the path to the s3 shared credentials file.
|
||||
CredentialsPath string `json:"credentials_path"`
|
||||
|
||||
// KeyPath is the path to the nix secret key for signing all newly copied paths.
|
||||
KeyPath string `json:"key_path"`
|
||||
}
|
||||
|
||||
func (store *BinaryCache) Environ() []string {
|
||||
return []string{EnvAwsSharedCredentialsFile + "=" + strings.TrimSpace(store.CredentialsPath)}
|
||||
}
|
||||
|
||||
func (store *BinaryCache) String() string {
|
||||
return fmt.Sprintf(
|
||||
"s3://%s?compression=%s¶llel-compression=%t®ion=%s&scheme=%s&endpoint=%s&secret-key=%s",
|
||||
store.Bucket, store.Compression, store.ParallelCompression, store.Region, store.Scheme, store.Endpoint, store.KeyPath,
|
||||
)
|
||||
}
|
57
store_test.go
Normal file
57
store_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package nix_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gensokyo.uk/nix"
|
||||
)
|
||||
|
||||
func TestLocal(t *testing.T) {
|
||||
if got := nix.Local("/").String(); got != "/" {
|
||||
t.Errorf("String: %v, want %v", got, "/")
|
||||
}
|
||||
|
||||
if got := nix.Local("").Environ(); got != nil {
|
||||
t.Errorf("Environ: %v, want %v", got, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBinaryCache(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
store *nix.BinaryCache
|
||||
want string
|
||||
wantEnv []string
|
||||
}{
|
||||
{"example", &nix.BinaryCache{
|
||||
Compression: "none",
|
||||
ParallelCompression: false,
|
||||
Bucket: "example",
|
||||
Endpoint: "s3.example.org",
|
||||
Region: "us-east-1",
|
||||
Scheme: "http",
|
||||
CredentialsPath: "/dev/null",
|
||||
KeyPath: nonexistent,
|
||||
}, "s3://example?compression=none¶llel-compression=false®ion=us-east-1&scheme=http&endpoint=s3.example.org&secret-key=/proc/nonexistent",
|
||||
[]string{nix.EnvAwsSharedCredentialsFile + "=/dev/null"}},
|
||||
|
||||
{"gensokyo", &nix.BinaryCache{
|
||||
Compression: "zstd",
|
||||
ParallelCompression: true,
|
||||
Bucket: "nix-cache",
|
||||
Endpoint: "s3.gensokyo.uk",
|
||||
Region: "ap-northeast-1",
|
||||
Scheme: "https",
|
||||
CredentialsPath: "/var/lib/persist/cache/s3",
|
||||
KeyPath: "/var/lib/persist/cache/key",
|
||||
}, "s3://nix-cache?compression=zstd¶llel-compression=true®ion=ap-northeast-1&scheme=https&endpoint=s3.gensokyo.uk&secret-key=/var/lib/persist/cache/key",
|
||||
[]string{nix.EnvAwsSharedCredentialsFile + "=/var/lib/persist/cache/s3"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.store.String(); got != tc.want {
|
||||
t.Errorf("String: %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user