Skip to content

Commit

Permalink
Merge pull request sashimono-dev#13 from stuartwdouglas/reproducability
Browse files Browse the repository at this point in the history
Make sure builds are reproducible
  • Loading branch information
stuartwdouglas authored Apr 22, 2024
2 parents 5ecfaf8 + 83d712f commit 48044c3
Show file tree
Hide file tree
Showing 9 changed files with 304 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashSet;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;

import javax.tools.Diagnostic;
Expand Down Expand Up @@ -54,7 +54,7 @@ public Path compile() {
StandardJavaFileManager fileManager = compiler.getStandardFileManager((DiagnosticListener) null, (Locale) null,
StandardCharsets.UTF_8);

Set<File> sourceFiles = new HashSet<>();
List<File> sourceFiles = new ArrayList<>();
sourceDirectories.forEach(s -> {
try {
Files.walkFileTree(s, new SimpleFileVisitor<>() {
Expand All @@ -71,6 +71,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
throw new RuntimeException(e);
}
});
//we sort the source files to help with reproducibility
sourceFiles.sort(Comparator.comparing(Object::toString));
try {
var output = Files.createTempDirectory("output");
fileManager.setLocation(StandardLocation.CLASS_PATH,
Expand Down
38 changes: 27 additions & 11 deletions builder/src/main/java/dev/sashimono/builder/jar/JarTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
Expand Down Expand Up @@ -43,22 +47,34 @@ public JarResult apply(TaskMap taskMap) {
}
parentDir = parentDir.resolve(gav.artifact());
parentDir = parentDir.resolve(gav.version());
List<Path> toJar = new ArrayList<>();
try {
Files.walkFileTree(deps.classesDirectory(), new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
toJar.add(file);
return FileVisitResult.CONTINUE;
}
});
toJar.sort(Comparator.comparing(Object::toString));
Files.createDirectories(parentDir);
Path target = parentDir.resolve(gav.artifact() + "-" + gav.version() + ".jar");
try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Files.walkFileTree(deps.classesDirectory(), new SimpleFileVisitor<>() {
toJar.forEach(new Consumer<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String entryName = deps.classesDirectory().relativize(file).toString();
ZipEntry entry = new ZipEntry(entryName);
entry.setCreationTime(FileTime.fromMillis(0));
entry.setSize(Files.size(file));
entry.setLastAccessTime(FileTime.fromMillis(0));
entry.setLastModifiedTime(FileTime.fromMillis(0));
out.putNextEntry(entry);
out.write(Files.readAllBytes(file));
return FileVisitResult.CONTINUE;
public void accept(Path file) {
try {
String entryName = deps.classesDirectory().relativize(file).toString();
ZipEntry entry = new ZipEntry(entryName);
entry.setCreationTime(FileTime.fromMillis(0));
entry.setSize(Files.size(file));
entry.setLastAccessTime(FileTime.fromMillis(0));
entry.setLastModifiedTime(FileTime.fromMillis(0));
out.putNextEntry(entry);
out.write(Files.readAllBytes(file));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
});
}
Expand Down
21 changes: 21 additions & 0 deletions builder/src/main/java/dev/sashimono/builder/util/FileUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package dev.sashimono.builder.util;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

public class FileUtil {
public static void deleteRecursive(final java.nio.file.Path file) {
try {
if (Files.isDirectory(file)) {
try (Stream<Path> files = Files.list(file)) {
files.forEach(FileUtil::deleteRecursive);
}
}
Files.delete(file);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,19 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.jar.JarFile;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import dev.sashimono.builder.Sashimono;
import dev.sashimono.builder.test.BuildResult;
import dev.sashimono.builder.test.BuildTest;

public class BuildProjectTestCase {

@Test
public void testBuildingSimpleProject(@TempDir Path output) throws IOException {
Path project = Paths.get("src/test/resources/simple-project");
Sashimono.builder().setProjectRoot(project).setOutputDir(output).build().buildProject();
Path jar = output.resolve("com").resolve("foo").resolve("test").resolve("1.1.0.Final").resolve("test-1.1.0.Final.jar");
@BuildTest("src/test/resources/simple-project")
public void testBuildingSimpleProject(BuildResult result) throws IOException {
Path jar = result.output().resolve("com").resolve("foo").resolve("test").resolve("1.1.0.Final")
.resolve("test-1.1.0.Final.jar");
Assertions.assertTrue(Files.exists(jar));

try (JarFile jarFile = new JarFile(jar.toFile())) {
Expand All @@ -29,11 +26,9 @@ public void testBuildingSimpleProject(@TempDir Path output) throws IOException {
}
}

@Test
public void testBuildingMultiModuleProject(@TempDir Path output) throws IOException {
Path project = Paths.get("src/test/resources/multi-module-project");
Sashimono.builder().setProjectRoot(project).setOutputDir(output).build().buildProject();
Path jar = output.resolve("com").resolve("acme").resolve("foo").resolve("1.0").resolve("foo-1.0.jar");
@BuildTest("src/test/resources/multi-module-project")
public void testBuildingMultiModuleProject(BuildResult result) throws IOException {
Path jar = result.output().resolve("com").resolve("acme").resolve("foo").resolve("1.0").resolve("foo-1.0.jar");
Assertions.assertTrue(Files.exists(jar));

try (JarFile jarFile = new JarFile(jar.toFile())) {
Expand All @@ -43,7 +38,7 @@ public void testBuildingMultiModuleProject(@TempDir Path output) throws IOExcept
Assertions.assertEquals(0, main.getLastModifiedTime().toMillis());
}

jar = output.resolve("com").resolve("acme").resolve("bar").resolve("1.0").resolve("bar-1.0.jar");
jar = result.output().resolve("com").resolve("acme").resolve("bar").resolve("1.0").resolve("bar-1.0.jar");
Assertions.assertTrue(Files.exists(jar));

try (JarFile jarFile = new JarFile(jar.toFile())) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package dev.sashimono.builder.test;

import java.nio.file.Path;
import java.nio.file.Paths;

import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.io.TempDirFactory;

import dev.sashimono.builder.Sashimono;

class BuildExtension implements BeforeEachCallback, ParameterResolver {

Path tempDir;

@Override
public void beforeEach(ExtensionContext context) throws Exception {
tempDir = TempDirFactory.Standard.INSTANCE.createTempDirectory(null, context);
var ann = context.getRequiredTestMethod().getAnnotation(BuildTest.class);
Path project = Paths.get(ann.value());
Sashimono.builder().setProjectRoot(project).setOutputDir(tempDir).build().buildProject();
}

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
Class<?> type = parameterContext.getParameter().getType();
return type == BuildResult.class;
}

@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return new BuildResult(tempDir);
}
}
12 changes: 12 additions & 0 deletions builder/src/test/java/dev/sashimono/builder/test/BuildResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package dev.sashimono.builder.test;

import java.nio.file.Path;

/**
* The results of a build, currently just the output directory.
*
* @param output
*/
public record BuildResult(Path output) {

}
24 changes: 24 additions & 0 deletions builder/src/test/java/dev/sashimono/builder/test/BuildTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package dev.sashimono.builder.test;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;

/**
* Runs a Sashimono build against the specified directory.
*
* The test is actually run twice and the results are compared to make sure the build is reproducible.
*
* The results can be injected via {@link BuildResult}
*/
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(BuildTestExtension.class)
@Target(ElementType.METHOD)
@TestTemplate
public @interface BuildTest {
String value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package dev.sashimono.builder.test;

import static org.junit.platform.commons.util.AnnotationUtils.isAnnotated;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;

/**
* Extension that builds a project multiple times and checks the result is reproducible.
*/
public class BuildTestExtension implements TestTemplateInvocationContextProvider {

@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return isAnnotated(context.getTestMethod(), BuildTest.class);
}

@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
Method testMethod = context.getRequiredTestMethod();
String displayName = context.getDisplayName();
BuildExtension first = new BuildExtension();
BuildExtension second = new BuildExtension();
List<TestTemplateInvocationContext> tests = new ArrayList<>();
tests.add(new BuildTestContext(displayName + " [First]", List.of(first)));
tests.add(new BuildTestContext(displayName + " [Second]", List.of(second)));
tests.add(new BuildTestContext(displayName + " [Reproducibility Check]",
List.of(new ProjectCompareExtension(first, second))));
return tests.stream();
}

class BuildTestContext implements TestTemplateInvocationContext {
final String name;
final List<Extension> extensions;

BuildTestContext(String name, List<Extension> extensions) {
this.name = name;
this.extensions = extensions;
}

@Override
public String getDisplayName(int invocationIndex) {
return name;
}

@Override
public List<Extension> getAdditionalExtensions() {
return extensions;
}
}
}
Loading

0 comments on commit 48044c3

Please sign in to comment.