From da37d51707c9f17e1726df6aa7d86162591fd7a5 Mon Sep 17 00:00:00 2001 From: "sebastian.sellmair" Date: Wed, 22 Jun 2022 14:00:20 +0200 Subject: [PATCH] [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 --- .../gradle/MPPBuildReproducibilityIT.kt | 80 +++++++++++++++++++ .../build.gradle.kts | 27 +++++++ .../src/commonMain/kotlin/CommonMain.kt | 9 +++ .../src/jvmMain/kotlin/JvmMain.kt | 1 + .../linuxArm64Main/kotlin/LinuxArm64Main.kt | 1 + .../src/linuxX64Main/kotlin/LinuxX64Main.kt | 1 + .../src/nativeMain/kotlin/NativeMain.kt | 3 + .../gradle/plugin/KotlinTargetConfigurator.kt | 4 +- .../plugin/mpp/KotlinMultiplatformPlugin.kt | 2 + .../KotlinMetadataTargetConfigurator.kt | 2 + .../gradle/ArchiveReproducibilityTest.kt | 56 +++++++++++++ 11 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MPPBuildReproducibilityIT.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/commonMain/kotlin/CommonMain.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/jvmMain/kotlin/JvmMain.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxArm64Main/kotlin/LinuxArm64Main.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxX64Main/kotlin/LinuxX64Main.kt create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/nativeMain/kotlin/NativeMain.kt create mode 100644 libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ArchiveReproducibilityTest.kt diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MPPBuildReproducibilityIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MPPBuildReproducibilityIT.kt new file mode 100644 index 00000000000..299ebe8e63c --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/MPPBuildReproducibilityIT.kt @@ -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("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) +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/build.gradle.kts new file mode 100644 index 00000000000..714aaf43e60 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/build.gradle.kts @@ -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) +} diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/commonMain/kotlin/CommonMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/commonMain/kotlin/CommonMain.kt new file mode 100644 index 00000000000..73efcd1e0e2 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/commonMain/kotlin/CommonMain.kt @@ -0,0 +1,9 @@ +fun main() { + CommonMain.run() +} + +object CommonMain { + fun run() = println(commonMainExpect()) +} + +expect fun commonMainExpect(): String diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/jvmMain/kotlin/JvmMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/jvmMain/kotlin/JvmMain.kt new file mode 100644 index 00000000000..83fe4d6a357 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/jvmMain/kotlin/JvmMain.kt @@ -0,0 +1 @@ +actual fun commonMainExpect() = "Hello from JVM" diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxArm64Main/kotlin/LinuxArm64Main.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxArm64Main/kotlin/LinuxArm64Main.kt new file mode 100644 index 00000000000..36279ba8f7f --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxArm64Main/kotlin/LinuxArm64Main.kt @@ -0,0 +1 @@ +actual fun nativeMainExpect() = "LinuxArm64" diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxX64Main/kotlin/LinuxX64Main.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxX64Main/kotlin/LinuxX64Main.kt new file mode 100644 index 00000000000..27fd30636ab --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/linuxX64Main/kotlin/LinuxX64Main.kt @@ -0,0 +1 @@ +actual fun nativeMainExpect() = "LinuxX64" diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/nativeMain/kotlin/NativeMain.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/nativeMain/kotlin/NativeMain.kt new file mode 100644 index 00000000000..80f4830e02b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/MPPBuildReproducibility/simpleMultiplatformProject/src/nativeMain/kotlin/NativeMain.kt @@ -0,0 +1,3 @@ +actual fun commonMainExpect(): String = "Hello from Kotlin/Native: ${nativeMainExpect()}" + +expect fun nativeMainExpect(): String diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt index 94eb0949dfb..e12dcbfe067 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinTargetConfigurator.kt @@ -413,6 +413,8 @@ abstract class KotlinOnlyTargetConfigurator.pluginConfigurationName - get() = lowerCamelCaseName(PLUGIN_CLASSPATH_CONFIGURATION_NAME, target.disambiguationClassifier, compilationName) \ No newline at end of file + get() = lowerCamelCaseName(PLUGIN_CLASSPATH_CONFIGURATION_NAME, target.disambiguationClassifier, compilationName) diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt index 9efa4a1e994..f8d1607001c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinMultiplatformPlugin.kt @@ -324,6 +324,8 @@ internal fun sourcesJarTaskNamed( val result = project.registerTask(taskName) { sourcesJar -> sourcesJar.archiveAppendix.set(artifactNameAppendix) sourcesJar.archiveClassifier.set("sources") + sourcesJar.isPreserveFileTimestamps = false + sourcesJar.isReproducibleFileOrder = true } project.whenEvaluated { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt index a87366e4520..e20b819e38f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt @@ -134,6 +134,8 @@ class KotlinMetadataTargetConfigurator : val result = target.project.registerTask(target.artifactsTaskName) { it.group = BasePlugin.BUILD_GROUP + it.isReproducibleFileOrder = true + it.isPreserveFileTimestamps = false /** The content is added to this JAR in [KotlinMetadataTargetConfigurator.configureTarget]. */ } diff --git a/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ArchiveReproducibilityTest.kt b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ArchiveReproducibilityTest.kt new file mode 100644 index 00000000000..9c08616963b --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/functionalTest/kotlin/org/jetbrains/kotlin/gradle/ArchiveReproducibilityTest.kt @@ -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'") + } + } +}