[Gradle][MPP] Ensure that built archives are reproducible

This is done by setting up the archive tasks to

a) isReproducibleFileOrder = true
Which is kind of self-explanatory: The zip entries will be ordered
therefore the same binary will be produced

b) isPreserveFileTimestamps = false
This one ensures, that all timestamps in the zip file
will be equal no matter the build host, or the actual time of build
(or timestamps of files)

^KT-52740 Verification Pending
This commit is contained in:
sebastian.sellmair
2022-06-22 14:00:20 +02:00
committed by Space
parent 53bbe805ce
commit da37d51707
11 changed files with 185 additions and 1 deletions
@@ -0,0 +1,80 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.gradle
import org.gradle.testkit.runner.TaskOutcome
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.testbase.*
import org.junit.jupiter.api.DisplayName
import kotlin.io.path.appendText
import kotlin.test.assertTrue
@MppGradlePluginTests
@DisplayName("Multiplatform Build Reproducibility")
class MPPBuildReproducibilityIT : KGPBaseTest() {
@GradleTest
@GradleTestVersions(minVersion = TestVersions.Gradle.G_7_0)
@DisplayName("Check if consecutive builds produce same binaries: Simple Multiplatform project")
fun `test simpleMultiplatformProject`(gradleVersion: GradleVersion) {
project("MPPBuildReproducibility/simpleMultiplatformProject", gradleVersion) {
assertConsecutiveBuildsProduceSameBinaries()
}
}
}
/**
* This will test if building a project consecutively will result in the same binaries.
* This method will inject code into the build.gradle.kts file to perform the following:
*
* - publish the project into a local repository (repo1)
* - publish the project into a local repository (repo2) (whilst --rerun-tasks guarantees everything is re-built)
* - Run the 'diff' tool to ensure that the published artifacts are equal in repo1 & repo2
*/
private fun TestProject.assertConsecutiveBuildsProduceSameBinaries() {
buildGradleKts.appendText(
"""
val repo1 = buildDir.resolve("repo1")
val repo2 = buildDir.resolve("repo2")
publishing {
repositories {
maven(repo1) { name = "repo1" }
maven(repo2) { name = "repo2" }
}
}
tasks.register<Exec>("checkBuildConsistency") {
doFirst {
if (!repo1.exists()) {
throw IllegalStateException("Missing repo1")
}
if (!repo2.exists()) {
throw IllegalStateException("Missing repo2")
}
}
commandLine(
"diff", repo1.absolutePath, repo2.absolutePath, "-r", "-x", "maven-metadata**"
)
}
""".trimIndent()
)
build("publishAllPublicationsToRepo1Repository")
build("publishAllPublicationsToRepo2Repository", "--rerun-tasks") {
tasks.forEach { task ->
assertTrue(
task.outcome in setOf(TaskOutcome.SUCCESS, TaskOutcome.NO_SOURCE),
"Expected all tasks to be re-executed. Task ${task.path} was ${task.outcome}"
)
}
}
build("checkBuildConsistency", forceOutput = true)
}
@@ -0,0 +1,27 @@
plugins {
kotlin("multiplatform")
`maven-publish`
}
version = "1.0"
group = "org.jetbrains"
repositories {
mavenLocal()
mavenCentral()
}
kotlin {
jvm()
linuxX64()
linuxArm64()
val commonMain by sourceSets.getting
val linuxX64Main by sourceSets.getting
val linuxArm64Main by sourceSets.getting
val nativeMain by sourceSets.creating
nativeMain.dependsOn(commonMain)
linuxX64Main.dependsOn(nativeMain)
linuxArm64Main.dependsOn(nativeMain)
}
@@ -0,0 +1,9 @@
fun main() {
CommonMain.run()
}
object CommonMain {
fun run() = println(commonMainExpect())
}
expect fun commonMainExpect(): String
@@ -0,0 +1,3 @@
actual fun commonMainExpect(): String = "Hello from Kotlin/Native: ${nativeMainExpect()}"
expect fun nativeMainExpect(): String
@@ -413,6 +413,8 @@ abstract class KotlinOnlyTargetConfigurator<KotlinCompilationType : KotlinCompil
it.description = "Assembles an archive containing the main classes."
it.group = BasePlugin.BUILD_GROUP
it.from(target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME).output.allOutputs)
it.isPreserveFileTimestamps = false
it.isReproducibleFileOrder = true
}
}
@@ -556,4 +558,4 @@ private fun Configuration.setJavaTargetEnvironmentAttributeIfSupported(project:
internal val Project.commonKotlinPluginClasspath get() = configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
internal val KotlinCompilation<*>.pluginConfigurationName
get() = lowerCamelCaseName(PLUGIN_CLASSPATH_CONFIGURATION_NAME, target.disambiguationClassifier, compilationName)
get() = lowerCamelCaseName(PLUGIN_CLASSPATH_CONFIGURATION_NAME, target.disambiguationClassifier, compilationName)
@@ -324,6 +324,8 @@ internal fun sourcesJarTaskNamed(
val result = project.registerTask<Jar>(taskName) { sourcesJar ->
sourcesJar.archiveAppendix.set(artifactNameAppendix)
sourcesJar.archiveClassifier.set("sources")
sourcesJar.isPreserveFileTimestamps = false
sourcesJar.isReproducibleFileOrder = true
}
project.whenEvaluated {
@@ -134,6 +134,8 @@ class KotlinMetadataTargetConfigurator :
val result = target.project.registerTask<Jar>(target.artifactsTaskName) {
it.group = BasePlugin.BUILD_GROUP
it.isReproducibleFileOrder = true
it.isPreserveFileTimestamps = false
/** The content is added to this JAR in [KotlinMetadataTargetConfigurator.configureTarget]. */
}
@@ -0,0 +1,56 @@
/*
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("FunctionName")
package org.jetbrains.kotlin.gradle
import org.gradle.api.Project
import org.gradle.api.tasks.bundling.AbstractArchiveTask
import org.jetbrains.kotlin.gradle.mpp.buildProjectWithMPP
import org.jetbrains.kotlin.gradle.mpp.kotlin
import org.junit.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
* Test intended to check if all relevant [AbstractArchiveTask]'s are configured correctly, to ensure
* reproducible binary artifacts between builds.
*/
class ArchiveReproducibilityTest {
@Test
fun `test simple multiplatform project`() {
val project = buildProjectWithMPP {
kotlin {
jvm()
linuxX64()
linuxArm64()
val commonMain = sourceSets.getByName("commonMain")
val linuxX64Main = sourceSets.getByName("linuxX64Main")
val linuxArm64Main = sourceSets.getByName("linuxArm64Main")
val nativeMain = sourceSets.create("nativeMain")
nativeMain.dependsOn(commonMain)
linuxX64Main.dependsOn(nativeMain)
linuxArm64Main.dependsOn(nativeMain)
}
}
project.evaluate()
project.assertAllArchiveTasksAreReproducible()
}
private fun Project.assertAllArchiveTasksAreReproducible() {
tasks.withType(AbstractArchiveTask::class.java).forEach { task ->
/* Check that we keep file order in zip files, to get the same binary */
assertTrue(task.isReproducibleFileOrder, "Expected ${task.path} to set 'isReproducibleFileOrder'")
/* Check that zip files do not include the actual timestamps (everything will be set to the same date) */
assertFalse(task.isPreserveFileTimestamps, "Expected ${task.path} to *not* set 'isPreserveFileTimestamps'")
}
}
}