diff --git a/examples/package-example/Main.scala b/examples/package-example/Main.scala new file mode 100644 index 00000000..61e6b631 --- /dev/null +++ b/examples/package-example/Main.scala @@ -0,0 +1,6 @@ +package package_example +object Main{ + def main( args: Array[String] ): Unit = { + println( Console.GREEN ++ "Hello World" ++ Console.RESET ) + } +} diff --git a/examples/package-example/build/build.scala b/examples/package-example/build/build.scala new file mode 100644 index 00000000..3874507a --- /dev/null +++ b/examples/package-example/build/build.scala @@ -0,0 +1,42 @@ +package package_example_build +import java.nio.file._ +import java.security.MessageDigest +import cbt._ +class Build(val context: Context) extends BaseBuild with PackageJars { + override def groupId = "org.example" + override def version = "1.0.0" + + def hash(file: Path): String = { + val digest = MessageDigest.getInstance("SHA-256"); + val in = Files.newInputStream(file) + try { + val chunk = new Array[Byte](4096) + while(in.read(chunk) > 0) { + digest.update(chunk) + } + } finally { + in.close + } + val bytes = digest.digest() + val builder = new StringBuilder(bytes.length * 2); + for(b <- bytes) { + builder.append(f"$b%02x") + } + builder.toString + } + + override def run = { + val pass1 = super.`package`.map(f => hash(f.toPath)) + lib.clean(cleanFiles, true, false, false, false) + transientCache.clear() + val pass2 = super.`package`.map(f => hash(f.toPath)) + + assert( + pass1 == pass2, + "The checksums of jars generated during two separate builds did not match. " + + "The build is not reproducible." + ) + ExitCode.Success + } + +} diff --git a/stage2/Lib.scala b/stage2/Lib.scala index 8801b33a..a237409e 100644 --- a/stage2/Lib.scala +++ b/stage2/Lib.scala @@ -257,6 +257,13 @@ final class Lib(val logger: Logger) extends m } + /** Create a jar from the given files and optionally make it + * executable by providing a main class. + * + * Note that all build-time related information is stripped from + * the jar, in order to make it reproducible on an independent + * system. See https://reproducible-builds.org/ for more + * information on reproducible builds. */ def createJar( jarFile: File, files: Seq[File], mainClass: Option[String] = None ): Option[File] = { deleteIfExists(jarFile.toPath) if( files.isEmpty ){ @@ -264,13 +271,19 @@ final class Lib(val logger: Logger) extends } else { jarFile.getParentFile.mkdirs logger.lib("Start packaging "++jarFile.string) - val jar = new JarOutputStream(new FileOutputStream(jarFile), createManifest(mainClass)) + val jar = new JarOutputStream(new FileOutputStream(jarFile)) try{ + val manifestEntry = new JarEntry( "META-INF/MANIFEST.MF" ) + manifestEntry.setTime(0l) + jar.putNextEntry(manifestEntry) + createManifest(mainClass).write(jar) + jar.closeEntry + assert( files.forall(_.exists) ) autoRelative(files).collect{ case (file, relative) if file.isFile => val entry = new JarEntry( relative ) - entry.setTime(file.lastModified) + entry.setTime(0l) jar.putNextEntry(entry) jar.write( readAllBytes( file.toPath ) ) jar.closeEntry diff --git a/test/test.scala b/test/test.scala index 9cb7fd9c..63bcabcf 100644 --- a/test/test.scala +++ b/test/test.scala @@ -506,6 +506,11 @@ object Main{ assert(res.exit0) } + { + val res = runCbt("../examples/package-example", Seq("run")) + assert(res.exit0) + } + /* // currently fails with // java.lang.UnsupportedOperationException: scalafix.rewrite.ScalafixMirror.fromMirror $anon#typeSignature requires the semantic api