[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.description = "Assembles an archive containing the main classes."
|
||||||
it.group = BasePlugin.BUILD_GROUP
|
it.group = BasePlugin.BUILD_GROUP
|
||||||
it.from(target.compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME).output.allOutputs)
|
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 Project.commonKotlinPluginClasspath get() = configurations.getByName(PLUGIN_CLASSPATH_CONFIGURATION_NAME)
|
||||||
internal val KotlinCompilation<*>.pluginConfigurationName
|
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 ->
|
val result = project.registerTask<Jar>(taskName) { sourcesJar ->
|
||||||
sourcesJar.archiveAppendix.set(artifactNameAppendix)
|
sourcesJar.archiveAppendix.set(artifactNameAppendix)
|
||||||
sourcesJar.archiveClassifier.set("sources")
|
sourcesJar.archiveClassifier.set("sources")
|
||||||
|
sourcesJar.isPreserveFileTimestamps = false
|
||||||
|
sourcesJar.isReproducibleFileOrder = true
|
||||||
}
|
}
|
||||||
|
|
||||||
project.whenEvaluated {
|
project.whenEvaluated {
|
||||||
|
|||||||
+2
@@ -134,6 +134,8 @@ class KotlinMetadataTargetConfigurator :
|
|||||||
|
|
||||||
val result = target.project.registerTask<Jar>(target.artifactsTaskName) {
|
val result = target.project.registerTask<Jar>(target.artifactsTaskName) {
|
||||||
it.group = BasePlugin.BUILD_GROUP
|
it.group = BasePlugin.BUILD_GROUP
|
||||||
|
it.isReproducibleFileOrder = true
|
||||||
|
it.isPreserveFileTimestamps = false
|
||||||
/** The content is added to this JAR in [KotlinMetadataTargetConfigurator.configureTarget]. */
|
/** 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