diff --git a/mkctr.go b/mkctr.go index 86a3882..b67b116 100644 --- a/mkctr.go +++ b/mkctr.go @@ -568,7 +568,10 @@ func layerFromFiles(logf logf, files map[string]string, layerMediaType types.Med dirs[dir] = true return nil } - for src, dst := range files { + // Visit sources in sorted order so the same inputs always produce the + // same layer. + for _, src := range slices.Sorted(maps.Keys(files)) { + dst := files[src] err := filepath.WalkDir(src, func(srcWalk string, d fs.DirEntry, err error) error { path := strings.TrimPrefix(srcWalk, src) dstWalk := filepath.Join(dst, path) diff --git a/mkctr_test.go b/mkctr_test.go new file mode 100644 index 0000000..f28015f --- /dev/null +++ b/mkctr_test.go @@ -0,0 +1,45 @@ +// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/types" +) + +func TestLayerFromFilesReproducible(t *testing.T) { + dir := t.TempDir() + files := map[string]string{} + for _, name := range []string{"a", "b", "c", "d", "e"} { + src := filepath.Join(dir, name) + if err := os.WriteFile(src, []byte(name), 0o644); err != nil { + t.Fatal(err) + } + files[src] = "/usr/local/bin/" + name + } + logf := func(string, ...any) {} + + var first string + for i := range 30 { + layer, err := layerFromFiles(logf, files, types.OCILayer) + if err != nil { + t.Fatal(err) + } + d, err := layer.DiffID() + if err != nil { + t.Fatal(err) + } + if i == 0 { + first = d.String() + continue + } + if d.String() != first { + t.Fatalf("build %d produced layer %s; build 0 produced %s; want identical layers for identical inputs", i, d, first) + } + } +}