[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:
committed by
Space
parent
53bbe805ce
commit
da37d51707
+80
@@ -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)
|
||||
}
|
||||
+27
@@ -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)
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
fun main() {
|
||||
CommonMain.run()
|
||||
}
|
||||
|
||||
object CommonMain {
|
||||
fun run() = println(commonMainExpect())
|
||||
}
|
||||
|
||||
expect fun commonMainExpect(): String
|
||||
+1
@@ -0,0 +1 @@
|
||||
actual fun commonMainExpect() = "Hello from JVM"
|
||||
+1
@@ -0,0 +1 @@
|
||||
actual fun nativeMainExpect() = "LinuxArm64"
|
||||
+1
@@ -0,0 +1 @@
|
||||
actual fun nativeMainExpect() = "LinuxX64"
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
actual fun commonMainExpect(): String = "Hello from Kotlin/Native: ${nativeMainExpect()}"
|
||||
|
||||
expect fun nativeMainExpect(): String
|
||||
+3
-1
@@ -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)
|
||||
|
||||
+2
@@ -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 {
|
||||
|
||||
+2
@@ -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]. */
|
||||
}
|
||||
|
||||
|
||||
+56
@@ -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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user