diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt index e622aa058d..b5af9870d1 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompileTask.kt @@ -25,6 +25,7 @@ import okio.ByteString.Companion.decodeHex import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputDirectory @@ -62,6 +63,10 @@ abstract class ZiplineCompileTask : DefaultTask() { @get:Input abstract val version: Property + @get:Optional + @get:Input + abstract val metadata: MapProperty + internal fun configure( jsProductionTask: JsProductionTask, extension: ZiplineExtension, @@ -79,6 +84,7 @@ abstract class ZiplineCompileTask : DefaultTask() { mainModuleId.set(extension.mainModuleId) mainFunction.set(extension.mainFunction) version.set(extension.version) + metadata.set(extension.metadata) signingKeys.set( project.provider { @@ -111,6 +117,7 @@ abstract class ZiplineCompileTask : DefaultTask() { else -> null } val version = version.orNull + val metadata = metadata.orNull ?: mapOf() if (inputChanges.isIncremental) { fun filterByChangeType(filter: (ChangeType) -> Boolean): List { @@ -128,6 +135,7 @@ abstract class ZiplineCompileTask : DefaultTask() { mainModuleId = mainModuleId, manifestSigner = manifestSigner, version = version, + metadata = metadata, ) } else { ZiplineCompiler.compile( @@ -137,6 +145,7 @@ abstract class ZiplineCompileTask : DefaultTask() { mainModuleId = mainModuleId, manifestSigner = manifestSigner, version = version, + metadata = metadata, ) } } diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt index 879b269aa6..7d721db301 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineCompiler.kt @@ -46,6 +46,7 @@ internal object ZiplineCompiler { mainModuleId: String?, manifestSigner: ManifestSigner?, version: String?, + metadata: Map, ) { val jsFiles = getJsFiles(inputDir.listFiles()!!.asList()) val modules = compileFilesInParallel(jsFiles, outputDir) @@ -56,6 +57,7 @@ internal object ZiplineCompiler { manifestSigner = manifestSigner, modules = modules, version = version, + metadata = metadata, ) } @@ -68,6 +70,7 @@ internal object ZiplineCompiler { mainModuleId: String?, manifestSigner: ManifestSigner?, version: String?, + metadata: Map, ) { val modifiedFileNames = getJsFiles(modifiedFiles).map { it.name }.toSet() val removedFileNames = getJsFiles(removedFiles).map { it.name }.toSet() @@ -95,6 +98,7 @@ internal object ZiplineCompiler { manifestSigner = manifestSigner, modules = unchangedModules + compiledModules, version = version, + metadata = metadata, ) } @@ -150,17 +154,19 @@ internal object ZiplineCompiler { private fun writeManifest( outputDir: File, - mainFunction: String? = null, - mainModuleId: String? = null, - manifestSigner: ManifestSigner? = null, + mainFunction: String?, + mainModuleId: String?, + manifestSigner: ManifestSigner?, modules: Map, - version: String? = null, + version: String?, + metadata: Map, ) { val unsignedManifest = ZiplineManifest.create( modules = modules, mainFunction = mainFunction, mainModuleId = mainModuleId, version = version, + metadata = metadata, ) val manifest = manifestSigner?.sign(unsignedManifest) ?: unsignedManifest diff --git a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineExtension.kt b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineExtension.kt index 8708d7bda4..5598a5cf4b 100644 --- a/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineExtension.kt +++ b/zipline-gradle-plugin/src/main/kotlin/app/cash/zipline/gradle/ZiplineExtension.kt @@ -17,6 +17,7 @@ package app.cash.zipline.gradle import app.cash.zipline.loader.SignatureAlgorithmId import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property abstract class ZiplineExtension { @@ -25,6 +26,7 @@ abstract class ZiplineExtension { abstract val version: Property abstract val signingKeys: NamedDomainObjectContainer abstract val httpServerPort: Property + abstract val metadata: MapProperty abstract class SigningKey(val name: String) { abstract val algorithmId: Property diff --git a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt index 2c0e8995c2..b8e99f75a1 100644 --- a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt +++ b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplineCompilerTest.kt @@ -151,6 +151,7 @@ class ZiplineCompilerTest { mainModuleId = mainModuleId, manifestSigner = null, version = null, + metadata = mapOf(), ) val expectedNumberFiles = if (dirHasSourceMaps) inputDir.listFiles()!!.size / 2 else inputDir.listFiles()!!.size @@ -182,6 +183,7 @@ class ZiplineCompilerTest { removedFiles = removedFiles, manifestSigner = null, version = null, + metadata = mapOf(), ) val expectedNumberFiles = inputDir.listFiles()!!.size + addedFiles.size - removedFiles.size diff --git a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt index 014ff433b8..5781b8dc72 100644 --- a/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt +++ b/zipline-gradle-plugin/src/test/kotlin/app/cash/zipline/gradle/ZiplinePluginTest.kt @@ -139,6 +139,26 @@ class ZiplinePluginTest { assertThat(ziplineOut.resolve("multipleJsTargets-lib-blue.zipline").exists()).isTrue() } + @Test + fun versionAndMetadata() { + val projectDir = File("src/test/projects/basic") + + val taskName = ":lib:compileDevelopmentExecutableKotlinJsZipline" + val result = createRunner(projectDir, "clean", taskName).build() + assertThat(SUCCESS_OUTCOMES) + .contains(result.task(taskName)!!.outcome) + + val ziplineOut = projectDir.resolve( + "lib/build/compileSync/js/main/developmentExecutable/kotlinZipline", + ) + val manifest = ziplineOut.resolve(manifestFileName) + val manifestText = manifest.readText() + assertThat(manifestText) + .contains(""""version":"1.2.3"""") + assertThat(manifestText) + .contains(""""metadata":{"build_timestamp":"2023-10-25T12:00:00T"}""") + } + @Test fun manifestSigning() { val projectDir = File("src/test/projects/signing") diff --git a/zipline-gradle-plugin/src/test/projects/basic/lib/build.gradle.kts b/zipline-gradle-plugin/src/test/projects/basic/lib/build.gradle.kts index ae15ad69b9..7a2ac22eb6 100644 --- a/zipline-gradle-plugin/src/test/projects/basic/lib/build.gradle.kts +++ b/zipline-gradle-plugin/src/test/projects/basic/lib/build.gradle.kts @@ -42,6 +42,8 @@ val launchGreetService by tasks.creating(JavaExec::class) { zipline { mainFunction.set("app.cash.zipline.tests.launchGreetService") + version.set("1.2.3") + metadata.put("build_timestamp", "2023-10-25T12:00:00T") } plugins.withType { diff --git a/zipline-gradle-plugin/src/test/projects/signing/lib/build.gradle.kts b/zipline-gradle-plugin/src/test/projects/signing/lib/build.gradle.kts index 5daf4e37e8..aac836e4d8 100644 --- a/zipline-gradle-plugin/src/test/projects/signing/lib/build.gradle.kts +++ b/zipline-gradle-plugin/src/test/projects/signing/lib/build.gradle.kts @@ -8,7 +8,7 @@ plugins { } kotlin { - js() { + js { browser() binaries.executable() } diff --git a/zipline/api/android/zipline.api b/zipline/api/android/zipline.api index c0ef7ed019..e9dcdbaa44 100644 --- a/zipline/api/android/zipline.api +++ b/zipline/api/android/zipline.api @@ -198,23 +198,29 @@ public abstract interface class app/cash/zipline/ZiplineFunction { public final class app/cash/zipline/ZiplineManifest { public static final field Companion Lapp/cash/zipline/ZiplineManifest$Companion; - public synthetic fun (ILapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public synthetic fun (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public synthetic fun (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lapp/cash/zipline/ZiplineManifest$Unsigned; public final fun component2 ()Ljava/util/Map; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; - public final fun copy (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; - public final fun copy (Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun component6 ()Ljava/util/Map; + public final synthetic fun copy (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun copy (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lapp/cash/zipline/ZiplineManifest; + public final synthetic fun copy (Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun copy (Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lapp/cash/zipline/ZiplineManifest; public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; + public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; + public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; public final fun encodeJson ()Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public final fun getBaseUrl ()Ljava/lang/String; public final fun getFreshAtEpochMs ()Ljava/lang/Long; public final fun getMainFunction ()Ljava/lang/String; public final fun getMainModuleId ()Ljava/lang/String; + public final fun getMetadata ()Ljava/util/Map; public final fun getModules ()Ljava/util/Map; public final fun getSignaturePayload ()Lokio/ByteString; public final fun getSignatures ()Ljava/util/Map; @@ -237,8 +243,10 @@ public final class app/cash/zipline/ZiplineManifest$$serializer : kotlinx/serial } public final class app/cash/zipline/ZiplineManifest$Companion { - public final fun create (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final synthetic fun create (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun create (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;)Lapp/cash/zipline/ZiplineManifest; public static synthetic fun create$default (Lapp/cash/zipline/ZiplineManifest$Companion;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; + public static synthetic fun create$default (Lapp/cash/zipline/ZiplineManifest$Companion;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; public final fun decodeJson (Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; public final fun serializer ()Lkotlinx/serialization/KSerializer; } diff --git a/zipline/api/jvm/zipline.api b/zipline/api/jvm/zipline.api index c0ef7ed019..e9dcdbaa44 100644 --- a/zipline/api/jvm/zipline.api +++ b/zipline/api/jvm/zipline.api @@ -198,23 +198,29 @@ public abstract interface class app/cash/zipline/ZiplineFunction { public final class app/cash/zipline/ZiplineManifest { public static final field Companion Lapp/cash/zipline/ZiplineManifest$Companion; - public synthetic fun (ILapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public synthetic fun (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public synthetic fun (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Lapp/cash/zipline/ZiplineManifest$Unsigned; public final fun component2 ()Ljava/util/Map; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/lang/String; public final fun component5 ()Ljava/lang/String; - public final fun copy (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; - public final fun copy (Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun component6 ()Ljava/util/Map; + public final synthetic fun copy (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun copy (Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lapp/cash/zipline/ZiplineManifest; + public final synthetic fun copy (Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun copy (Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Lapp/cash/zipline/ZiplineManifest; public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; + public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Lapp/cash/zipline/ZiplineManifest$Unsigned;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; + public static synthetic fun copy$default (Lapp/cash/zipline/ZiplineManifest;Ljava/util/Map;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; public final fun encodeJson ()Ljava/lang/String; public fun equals (Ljava/lang/Object;)Z public final fun getBaseUrl ()Ljava/lang/String; public final fun getFreshAtEpochMs ()Ljava/lang/Long; public final fun getMainFunction ()Ljava/lang/String; public final fun getMainModuleId ()Ljava/lang/String; + public final fun getMetadata ()Ljava/util/Map; public final fun getModules ()Ljava/util/Map; public final fun getSignaturePayload ()Lokio/ByteString; public final fun getSignatures ()Ljava/util/Map; @@ -237,8 +243,10 @@ public final class app/cash/zipline/ZiplineManifest$$serializer : kotlinx/serial } public final class app/cash/zipline/ZiplineManifest$Companion { - public final fun create (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final synthetic fun create (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; + public final fun create (Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;)Lapp/cash/zipline/ZiplineManifest; public static synthetic fun create$default (Lapp/cash/zipline/ZiplineManifest$Companion;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; + public static synthetic fun create$default (Lapp/cash/zipline/ZiplineManifest$Companion;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lapp/cash/zipline/ZiplineManifest; public final fun decodeJson (Ljava/lang/String;)Lapp/cash/zipline/ZiplineManifest; public final fun serializer ()Lkotlinx/serialization/KSerializer; } diff --git a/zipline/src/hostMain/kotlin/app/cash/zipline/ZiplineManifest.kt b/zipline/src/hostMain/kotlin/app/cash/zipline/ZiplineManifest.kt index e725bcafc5..4e3a450f52 100644 --- a/zipline/src/hostMain/kotlin/app/cash/zipline/ZiplineManifest.kt +++ b/zipline/src/hostMain/kotlin/app/cash/zipline/ZiplineManifest.kt @@ -22,7 +22,6 @@ import app.cash.zipline.internal.jsonForManifest import app.cash.zipline.internal.signaturePayload import app.cash.zipline.internal.topologicalSort import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import okio.ByteString import okio.ByteString.Companion.encodeUtf8 @@ -51,6 +50,12 @@ data class ZiplineManifest private constructor( /** Version to represent the code as defined in this manifest, by default it will be Git commit SHA. */ val version: String? = null, + + /** + * Arbitrary application-layer metadata about this release. Use this to embed build or diagnostic + * metadata, such as the timestamp that this application was built. + */ + val metadata: Map = mapOf(), ) { init { require(modules.keys.toList().isTopologicallySorted { id -> modules[id]!!.dependsOnIds }) { @@ -108,6 +113,50 @@ data class ZiplineManifest private constructor( val baseUrl: String? get() = unsigned.baseUrl + @Deprecated( + message = "This is here for binary-compatibility only", + level = DeprecationLevel.HIDDEN, + ) + fun copy( + signatures: Map = this.signatures, + freshAtEpochMs: Long? = this.freshAtEpochMs, + baseUrl: String? = this.baseUrl, + modules: Map = this.modules, + mainModuleId: String = this.mainModuleId, + mainFunction: String? = this.mainFunction, + version: String? = this.version, + ): ZiplineManifest = copy( + unsigned = unsigned.copy( + freshAtEpochMs = freshAtEpochMs, + signatures = signatures, + baseUrl = baseUrl, + ), + modules = modules, + mainModuleId = mainModuleId, + mainFunction = mainFunction, + version = version, + metadata = this.metadata, + ) + + @Deprecated( + message = "This is here for binary-compatibility only", + level = DeprecationLevel.HIDDEN, + ) + fun copy( + unsigned: Unsigned = this.unsigned, + modules: Map = this.modules, + mainModuleId: String = this.mainModuleId, + mainFunction: String? = this.mainFunction, + version: String? = this.version, + ): ZiplineManifest = copy( + unsigned = unsigned, + modules = modules, + mainModuleId = mainModuleId, + mainFunction = mainFunction, + version = version, + metadata = this.metadata, + ) + fun copy( signatures: Map = this.signatures, freshAtEpochMs: Long? = this.freshAtEpochMs, @@ -116,7 +165,8 @@ data class ZiplineManifest private constructor( mainModuleId: String = this.mainModuleId, mainFunction: String? = this.mainFunction, version: String? = this.version, - ) = copy( + metadata: Map = this.metadata, + ): ZiplineManifest = copy( unsigned = unsigned.copy( freshAtEpochMs = freshAtEpochMs, signatures = signatures, @@ -126,6 +176,8 @@ data class ZiplineManifest private constructor( mainModuleId = mainModuleId, mainFunction = mainFunction, version = version, + // Defensive copy! + metadata = metadata.toMap(), ) /** @@ -151,6 +203,29 @@ data class ZiplineManifest private constructor( } companion object { + @Deprecated( + message = "This is here for binary-compatibility only", + level = DeprecationLevel.HIDDEN, + ) + fun create( + modules: Map, + mainFunction: String? = null, + mainModuleId: String? = null, + version: String? = null, + builtAtEpochMs: Long? = null, + baseUrl: String? = null, + ): ZiplineManifest { + return create( + modules = modules, + mainFunction = mainFunction, + mainModuleId = mainModuleId, + version = version, + builtAtEpochMs = builtAtEpochMs, + baseUrl = baseUrl, + metadata = mapOf(), + ) + } + fun create( modules: Map, mainFunction: String? = null, @@ -158,6 +233,7 @@ data class ZiplineManifest private constructor( version: String? = null, builtAtEpochMs: Long? = null, baseUrl: String? = null, + metadata: Map = mapOf(), ): ZiplineManifest { val sortedModuleIds = modules.keys .toList() @@ -178,6 +254,8 @@ data class ZiplineManifest private constructor( mainModuleId = mainModuleId ?: sortedModuleIds.last(), mainFunction = mainFunction, version = version, + // Defensive copy! + metadata = metadata.toMap(), ) } diff --git a/zipline/src/hostTest/kotlin/app/cash/zipline/ZiplineManifestTest.kt b/zipline/src/hostTest/kotlin/app/cash/zipline/ZiplineManifestTest.kt index f5cb76e35c..0943660855 100644 --- a/zipline/src/hostTest/kotlin/app/cash/zipline/ZiplineManifestTest.kt +++ b/zipline/src/hostTest/kotlin/app/cash/zipline/ZiplineManifestTest.kt @@ -19,6 +19,8 @@ package app.cash.zipline import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotSame +import kotlin.test.assertSame import okio.ByteString.Companion.encodeUtf8 class ZiplineManifestTest { @@ -168,6 +170,7 @@ class ZiplineManifestTest { ), ), mainFunction = "zipline.ziplineMain", + metadata = mapOf("build_timestamp" to "2023-10-25T12:00:00T"), ) val serialized = original.encodeJson() @@ -197,7 +200,10 @@ class ZiplineManifestTest { | }, | "mainModuleId": "bravo", | "mainFunction": "zipline.ziplineMain", - | "version": null + | "version": null, + | "metadata": { + | "build_timestamp": "2023-10-25T12:00:00T" + | } |} """.trimMargin(), prettyPrint(serialized), @@ -270,4 +276,50 @@ class ZiplineManifestTest { ZiplineManifest.decodeJson(serialized), ) } + + /** + * Confirm we attempt to make defensive copies. Note that this isn't perfect and we observe + * mutable instances with [copy]. + */ + @Test + fun metadataIsDefensivelyCopiedByCreate() { + val mutableMetadata = mutableMapOf() + val manifest = ZiplineManifest.create( + modules = mapOf( + "alpha" to ZiplineManifest.Module( + url = "/alpha.zipline", + sha256 = "abc123".encodeUtf8(), + dependsOnIds = listOf(), + ), + ), + mainFunction = "zipline.ziplineMain", + metadata = mutableMetadata, + ) + + assertNotSame(mutableMetadata, manifest.metadata) + + mutableMetadata["build_timestamp"] = "2023-10-25T12:00:00T" + assertEquals(mapOf(), manifest.metadata) + } + + @Test + fun metadataIsNotDefensivelyCopiedByDataClassCopy() { + val mutableMetadata = mutableMapOf() + val manifest = ZiplineManifest.create( + modules = mapOf( + "alpha" to ZiplineManifest.Module( + url = "/alpha.zipline", + sha256 = "abc123".encodeUtf8(), + dependsOnIds = listOf(), + ), + ), + mainFunction = "zipline.ziplineMain", + ).copy( + metadata = mutableMetadata, + ) + + // This isn't the behavior we want, but it's the behavior we get. There's no good way to do + // defensive copies with data classes, and ZiplineManifest should have used POKO. Sigh. + assertSame(mutableMetadata, manifest.metadata) + } } diff --git a/zipline/src/hostTest/kotlin/app/cash/zipline/internal/SignaturePayloadTest.kt b/zipline/src/hostTest/kotlin/app/cash/zipline/internal/SignaturePayloadTest.kt index 21a86d18bc..39e267c5f2 100644 --- a/zipline/src/hostTest/kotlin/app/cash/zipline/internal/SignaturePayloadTest.kt +++ b/zipline/src/hostTest/kotlin/app/cash/zipline/internal/SignaturePayloadTest.kt @@ -172,6 +172,29 @@ class SignaturePayloadTest { ) } + @Test + fun metadataIsSignificant() { + val manifestA = ZiplineManifest.create( + modules = mapOf( + "./kotlin_kotlin.js" to ZiplineManifest.Module( + url = "kotlin_kotlin.zipline", + sha256 = "6bd4baa9f46afa62477fec8c9e95528de7539f036d26fc13885177b32fc0d6ab".decodeHex(), + dependsOnIds = listOf(), + ), + ), + metadata = mapOf("build_timestamp" to "2023-10-25T12:00:00T"), + ) + + val manifestB = manifestA.copy( + metadata = mapOf(), + ) + + assertNotEquals( + manifestA.signaturePayload, + manifestB.signaturePayload, + ) + } + @Test fun signaturePayloadStripsBaseUrlsAndSignatures() { val manifestJson = """