Skip to content

Commit

Permalink
reproducible builds working as expected
Browse files Browse the repository at this point in the history
explicitly setting tar archive entry modified dates & gzip modified dates. 

https://buildpacks.io/docs/features/reproducibility/
buildpacks/lifecycle#823
buildpacks/lifecycle#108
  • Loading branch information
joeybrown committed May 6, 2022
1 parent cc13d85 commit 49cd4fe
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 11 deletions.
4 changes: 2 additions & 2 deletions src/Dotknet/Commands/PublishCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public void Execute()

using var tarArchive = TarArchive.Create();
tarArchive.AddAllFromDirectory(_options.Output!, "dotknet-app");

using var layer = new TarArchiveLayer(tarArchive);
_logger.LogInformation("Layer Created: {Layer}", layer);

_logger.LogInformation("Output will create layer {Layer}", layer);
}
}
10 changes: 7 additions & 3 deletions src/Dotknet/Extensions/IWritableArchiveExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@

public static class IWritableArchiveExtensions
{
public static void AddAllFromDirectory(this IWritableArchive writableArchive, string filePath, string rootDir, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories)
// timestamp necessary for reproducability
public static void AddAllFromDirectory(this IWritableArchive writableArchive, string filePath, string rootDir, string searchPattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories, DateTime? timestamp = null)
{
if (!timestamp.HasValue) {
timestamp = new DateTime(1980, 01, 01, 00, 00, 01);
}
using (writableArchive.PauseEntryRebuilding())
{
foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
{
var fileInfo = new FileInfo(path);
var key = rootDir + "/" + path.Substring(filePath.Length);
writableArchive.AddEntry(key, fileInfo.OpenRead(), true, fileInfo.Length, fileInfo.LastWriteTime);
var key = rootDir + path.Substring(filePath.Length);
writableArchive.AddEntry(key, fileInfo.OpenRead(), true, fileInfo.Length, timestamp);
}
}
}
Expand Down
29 changes: 23 additions & 6 deletions src/Dotknet/Models/Layer.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
using System.IO.Compression;
using Dotknet.Extensions;
using SharpCompress.Archives.Tar;

namespace Dotknet.Models;

/// ref: https://github.com/google/go-containerregistry/blob/main/pkg/v1/layer.go
public interface ILayer: IDisposable
public interface ILayer : IDisposable
{
// Digest returns the Hash of the compressed layer.
public Hash Digest();
Expand All @@ -28,19 +29,34 @@ public interface ILayer: IDisposable
//todo: consider perf/caching
public class TarArchiveLayer : ILayer
{
public TarArchiveLayer(TarArchive tarArchive)
// timestamp necessary for reproducibility
public TarArchiveLayer(TarArchive tarArchive, DateTime? modified = null)
{
_tarArchive = tarArchive;
if (!modified.HasValue)
{
modified = new DateTime(1980, 01, 01, 00, 00, 01, DateTimeKind.Utc);
}
_modified = modified;
}

private DateTime EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

private bool disposedValue;
private readonly TarArchive _tarArchive;
private readonly DateTime? _modified;

public Stream Compressed()
{
var memoryStream = new MemoryStream();
_tarArchive.SaveTo(memoryStream, new SharpCompress.Writers.WriterOptions(SharpCompress.Common.CompressionType.GZip));
return memoryStream;
using var originalStream = new MemoryStream();
_tarArchive.SaveTo(originalStream, new SharpCompress.Writers.WriterOptions(SharpCompress.Common.CompressionType.GZip));
var buffer = originalStream.ToArray();
var seconds = (int)(_modified! - EPOCH).Value.TotalSeconds;
buffer[4] = (byte)seconds;
buffer[5] = (byte)(seconds >> 8);
buffer[6] = (byte)(seconds >> 16);
buffer[7] = (byte)(seconds >> 24);
return new MemoryStream(buffer);
}

public Hash DiffId()
Expand Down Expand Up @@ -89,7 +105,8 @@ public void Dispose()
GC.SuppressFinalize(this);
}

public override string ToString(){
public override string ToString()
{
return $"Layer DiffId: {DiffId().Hex.Substring(0, 6)} Digest: {Digest().Hex.Substring(0, 6)}";
}
}
44 changes: 44 additions & 0 deletions test/Dotknet.Test/Models/LayerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Linq;
using Dotknet.Models;
using FluentAssertions;
using SharpCompress.Archives.Tar;
using Xunit;

namespace Dotknet.Test.Models;

public class LayerTests
{
[Fact]
public void LayerModifiedTests()
{
var archive = TarArchive.Create();
var timestamp = new DateTime(1990, 05, 13, 0, 0, 0);
using var text = GenerateStreamFromString("foo foo foo foo foo foo foo");
archive.AddEntry("bar", text, text.Length, timestamp);
archive.AddEntry("baz", text, text.Length, timestamp);
using var layer = new TarArchiveLayer(archive);
using var compressed = layer.Compressed();
compressed.Seek(0, SeekOrigin.Begin);
using var ms2 = new MemoryStream();
compressed.CopyTo(ms2);
ms2.Seek(0, SeekOrigin.Begin);
var bytes = ms2.ToArray();
var timestampBytes = bytes.Skip(4).Take(4).ToArray();
var seconds = BitConverter.ToUInt32(timestampBytes, 0);
var EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var datetime = EPOCH.AddSeconds(seconds);
datetime.Should().Be(new DateTime(1980, 01, 01, 00, 00, 01, DateTimeKind.Utc));
}

public static Stream GenerateStreamFromString(string s)
{
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
writer.Write(s);
writer.Flush();
stream.Position = 0;
return stream;
}
}

0 comments on commit 49cd4fe

Please sign in to comment.