diff --git a/client/client_test.go b/client/client_test.go index ead07cd53ffd2..681d0d9241caf 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -185,6 +185,7 @@ func TestIntegration(t *testing.T) { testAttestationBundle, testSBOMScan, testSBOMScanSingleRef, + testMountStubsTimestamp, ) tests = append(tests, diffOpTestCases()...) integration.Run(t, tests, mirrors) @@ -7759,6 +7760,61 @@ EOF require.Equal(t, map[string]interface{}{"success": false}, attest.Predicate) } +// https://github.com/moby/buildkit/issues/3148 +func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) { + c, err := New(sb.Context(), sb.Address()) + require.NoError(t, err) + defer c.Close() + + const sourceDateEpoch = int64(1234567890) // Fri Feb 13 11:31:30 PM UTC 2009 + st := llb.Image("busybox:latest").Run( + llb.Args([]string{"/bin/touch", fmt.Sprintf("--date=@%d", sourceDateEpoch), "/bin", "/etc"}), + ) + def, err := st.Marshal(sb.Context()) + require.NoError(t, err) + + tmpDir := t.TempDir() + tarFile := filepath.Join(tmpDir, "out.tar") + tarFileW, err := os.Create(tarFile) + require.NoError(t, err) + defer tarFileW.Close() + + _, err = c.Solve(sb.Context(), def, SolveOpt{ + Exports: []ExportEntry{ + { + Type: ExporterTar, + Output: fixedWriteCloser(tarFileW), + }, + }, + }, nil) + require.NoError(t, err) + tarFileW.Close() + + tarFileR, err := os.Open(tarFile) + require.NoError(t, err) + defer tarFileR.Close() + tarR := tar.NewReader(tarFileR) + touched := map[string]*tar.Header{ + "bin/": nil, + "etc/": nil, + } + for { + hd, err := tarR.Next() + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + if x, ok := touched[hd.Name]; ok && x == nil { + touched[hd.Name] = hd + } + } + for name, hd := range touched { + t.Logf("Verifying %q (%+v)", name, hd) + require.NotNil(t, hd, name) + require.Equal(t, sourceDateEpoch, hd.ModTime.Unix(), name) + } +} + func makeSSHAgentSock(t *testing.T, agent agent.Agent) (p string, err error) { tmpDir, err := integration.Tmpdir(t) if err != nil { diff --git a/executor/stubs.go b/executor/stubs.go index 2c13b13053a4f..167db203f6570 100644 --- a/executor/stubs.go +++ b/executor/stubs.go @@ -7,6 +7,8 @@ import ( "syscall" "github.com/containerd/continuity/fs" + "github.com/moby/buildkit/util/system" + "github.com/sirupsen/logrus" ) func MountStubsCleaner(dir string, mounts []Mount) func() { @@ -43,7 +45,29 @@ func MountStubsCleaner(dir string, mounts []Mount) func() { if st.Size() != 0 { continue } - os.Remove(p) + // Back up the timestamps of the dir for reproducible builds + // https://github.com/moby/buildkit/issues/3148 + dir := filepath.Dir(p) + dirSt, err := os.Stat(dir) + if err != nil { + logrus.WithError(err).Warnf("Failed to stat %q (parent of mount stub %q)", dir, p) + continue + } + mtime := dirSt.ModTime() + atime, err := system.Atime(dirSt) + if err != nil { + logrus.WithError(err).Warnf("Failed to stat atime of %q (parent of mount stub %q)", dir, p) + atime = mtime + } + + if err := os.Remove(p); err != nil { + logrus.WithError(err).Warnf("Failed to remove mount stub %q", p) + } + + // Restore the timestamps of the dir + if err := os.Chtimes(dir, atime, mtime); err != nil { + logrus.WithError(err).Warnf("Failed to restore time time mount stub timestamp (os.Chtimes(%q, %v, %v))", dir, atime, mtime) + } } } } diff --git a/util/system/atime_unix.go b/util/system/atime_unix.go new file mode 100644 index 0000000000000..d3f44aa53a324 --- /dev/null +++ b/util/system/atime_unix.go @@ -0,0 +1,21 @@ +//go:build !windows +// +build !windows + +package system + +import ( + "fmt" + iofs "io/fs" + "syscall" + "time" + + "github.com/containerd/continuity/fs" +) + +func Atime(st iofs.FileInfo) (time.Time, error) { + stSys, ok := st.Sys().(*syscall.Stat_t) + if !ok { + return time.Time{}, fmt.Errorf("expected st.Sys() to be *syscall.Stat_t, got %T", st.Sys()) + } + return fs.StatATimeAsTime(stSys), nil +} diff --git a/util/system/atime_windows.go b/util/system/atime_windows.go new file mode 100644 index 0000000000000..808408b613cfb --- /dev/null +++ b/util/system/atime_windows.go @@ -0,0 +1,17 @@ +package system + +import ( + "fmt" + iofs "io/fs" + "syscall" + "time" +) + +func Atime(st iofs.FileInfo) (time.Time, error) { + stSys, ok := st.Sys().(*syscall.Win32FileAttributeData) + if !ok { + return time.Time{}, fmt.Errorf("expected st.Sys() to be *syscall.Win32FileAttributeData, got %T", st.Sys()) + } + // ref: https://github.com/golang/go/blob/go1.19.2/src/os/types_windows.go#L230 + return time.Unix(0, stSys.LastAccessTime.Nanoseconds()), nil +}