Repository: GradleUp/shadow (kotlin)

Issue:
Support for merging YAML files
It would be nice to have a support for dumb merging YAML files:

YAML files can consist of multiple parts separated by `---` so what we need is just a [AppendingTransformer](https://github.com/johnrengelman/shadow/blob/master/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy) which would append new line **and** `---`

see https://github.com/johnrengelman/shadow/blob/master/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.groovy#L53

Pull request:
- Closes #545.
- Closes #778.
- Closes #1168.

---

- [x] [CHANGELOG](https://github.com/GradleUp/shadow/blob/main/src/docs/changes/README.md)'s "Unreleased" section has been updated, if applicable.

Patch:
diff --git a/api/shadow.api b/api/shadow.api
index f25735c69..ae77e4ad5 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -297,6 +297,8 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar
 	public fun <init> ()V
 	public fun append (Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar;
 	public synthetic fun append (Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec;
+	public fun append (Ljava/lang/String;Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar;
+	public synthetic fun append (Ljava/lang/String;Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec;
 	protected fun copy ()V
 	protected fun createCopyAction ()Lorg/gradle/api/internal/file/copy/CopyAction;
 	public fun dependencies (Lorg/gradle/api/Action;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar;
@@ -349,6 +351,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar
 
 public abstract interface class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec : org/gradle/api/file/CopySpec {
 	public abstract fun append (Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec;
+	public abstract fun append (Ljava/lang/String;Ljava/lang/String;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec;
 	public abstract fun dependencies (Lorg/gradle/api/Action;)Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec;
 	public abstract fun getStats ()Lcom/github/jengelman/gradle/plugins/shadow/ShadowStats;
 	public abstract fun mergeGroovyExtensionModules ()Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec;
@@ -397,15 +400,21 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNotic
 }
 
 public class com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/Transformer {
+	public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer$Companion;
+	public static final field DEFAULT_SEPARATOR Ljava/lang/String;
 	public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
 	public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z
 	public final fun getObjectFactory ()Lorg/gradle/api/model/ObjectFactory;
 	public fun getResource ()Lorg/gradle/api/provider/Property;
+	public fun getSeparator ()Lorg/gradle/api/provider/Property;
 	public fun hasTransformedResource ()Z
 	public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V
 	public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V
 }
 
+public final class com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer$Companion {
+}
+
 public abstract interface annotation class com/github/jengelman/gradle/plugins/shadow/transformers/CacheableTransformer : java/lang/annotation/Annotation {
 }
 
diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md
index bf38070dd..2ecd59b23 100644
--- a/src/docs/changes/README.md
+++ b/src/docs/changes/README.md
@@ -12,6 +12,8 @@
 - **BREAKING CHANGE:** Migrate all `ListProperty` usages to `SetProperty`. ([#1103](https://github.com/GradleUp/shadow/pull/1103))  
   Some public `List` parameters are also changed to `Set`.
 - Replace deprecated `SelfResolvingDependency` with `FileCollectionDependency`. ([#1114](https://github.com/GradleUp/shadow/pull/1114))
+- Support configuring `separator` in `AppendingTransformer`. ([#1169](https://github.com/GradleUp/shadow/pull/1169))  
+  This is useful for handling files like `resources/application.yml`.  
 
 **Fixed**
 
diff --git a/src/docs/configuration/merging/README.md b/src/docs/configuration/merging/README.md
index 9c9fbcc42..1e2c7d8ba 100644
--- a/src/docs/configuration/merging/README.md
+++ b/src/docs/configuration/merging/README.md
@@ -180,7 +180,7 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
 
 Generic text files can be appended together using the
 [`AppendingTransformer`](https://gradleup.com/shadow/api/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.html).
-Each file is appended using new lines to separate content.
+Each file is appended using separators (defaults to `\n`) to separate content.
 The [`ShadowJar`](https://gradleup.com/shadow/api/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.html) task provides a short syntax 
 method of
 [`append(String)`](https://gradleup.com/shadow/api/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.html#append(java.lang.String)) to 
@@ -193,6 +193,20 @@ tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.Shadow
 }
 ```
 
+```groovy
+// Appending application.yml files
+tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+  // short syntax
+  append('resources/application.yml', '\n---\n')
+  // full syntax
+  transform(com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer.class) {
+    resource = 'resources/custom-config/application.yml'
+    separator = '\n---\n'
+  }
+}
+```
+
+
 ## Appending XML Files
 
 XML files require a special transformer for merging.
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
index 2ef97d17a..0c00f4206 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt
@@ -209,9 +209,23 @@ public abstract class ShadowJar :
   }
 
   override fun append(resourcePath: String): ShadowJar {
+    return append(resourcePath, AppendingTransformer.DEFAULT_SEPARATOR)
+  }
+
+  /**
+   * Append contents to a resource in the jar.
+   *
+   * e.g. `append("resources/application.yml", "\n---\n")` for merging `resources/application.yml` files.
+   *
+   * @param resourcePath The path to the resource in the jar.
+   * @param separator The separator to use between the original content and the appended content,
+   * defaults to `\n` ([AppendingTransformer.DEFAULT_SEPARATOR]).
+   */
+  override fun append(resourcePath: String, separator: String): ShadowJar {
     return runCatching {
       transform(AppendingTransformer::class.java) {
         it.resource.set(resourcePath)
+        it.separator.set(separator)
       }
     }.getOrDefault(this)
   }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.kt
index f51bf1716..8f360f642 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowSpec.kt
@@ -45,6 +45,8 @@ public interface ShadowSpec : CopySpec {
 
   public fun append(resourcePath: String): ShadowSpec
 
+  public fun append(resourcePath: String, separator: String): ShadowSpec
+
   public fun relocate(pattern: String, destination: String): ShadowSpec
 
   public fun relocate(pattern: String, destination: String, action: Action<SimpleRelocator>?): ShadowSpec
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt
index 182d12f33..7b3dd768b 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt
@@ -27,20 +27,26 @@ public open class AppendingTransformer @Inject constructor(
    * Defer initialization, see [issue 763](https://github.com/GradleUp/shadow/issues/763).
    */
   private var _data: ByteArrayOutputStream? = null
-  private inline val data get() = if (_data == null) ByteArrayOutputStream().also { _data = it } else _data!!
+  private inline val data get() = _data ?: ByteArrayOutputStream().also { _data = it }
 
   @get:Optional
   @get:Input
   public open val resource: Property<String> = objectFactory.property()
 
+  @get:Input
+  public open val separator: Property<String> = objectFactory.property(DEFAULT_SEPARATOR)
+
   override fun canTransformResource(element: FileTreeElement): Boolean {
     return resource.orNull.equals(element.relativePath.pathString, ignoreCase = true)
   }
 
   override fun transform(context: TransformerContext) {
     context.inputStream.use {
+      if (data.size() > 0) {
+        // Append the separator before the new content to ensure the separator is not at the end of the file.
+        data.write(separator.get().toByteArray())
+      }
       it.copyTo(data)
-      data.write('\n'.code)
     }
   }
 
@@ -57,4 +63,8 @@ public open class AppendingTransformer @Inject constructor(
     data.toByteArray().inputStream().copyTo(os)
     data.reset()
   }
+
+  public companion object {
+    public const val DEFAULT_SEPARATOR: String = "\n"
+  }
 }


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.kt
index a32a8aa9b..a883faf8a 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformerTest.kt
@@ -3,12 +3,13 @@ package com.github.jengelman.gradle.plugins.shadow.transformers
 import assertk.assertThat
 import assertk.assertions.isEqualTo
 import com.github.jengelman.gradle.plugins.shadow.util.getContent
+import java.nio.file.Path
 import kotlin.io.path.appendText
 import org.junit.jupiter.api.Test
 
 class AppendingTransformerTest : BaseTransformerTest() {
   @Test
-  fun appendingTransformer() {
+  fun appendTestProperties() {
     val one = buildJarOne {
       insert(ENTRY_TEST_PROPERTIES, CONTENT_ONE)
     }
@@ -26,12 +27,12 @@ class AppendingTransformerTest : BaseTransformerTest() {
 
     run(shadowJarTask)
 
-    val content = outputShadowJar.use { it.getContent(ENTRY_TEST_PROPERTIES) }.trimIndent()
+    val content = outputShadowJar.use { it.getContent(ENTRY_TEST_PROPERTIES) }
     assertThat(content).isEqualTo(CONTENT_ONE_TWO)
   }
 
   @Test
-  fun appendingTransformerShortSyntax() {
+  fun appendTestPropertiesShortSyntax() {
     val one = buildJarOne {
       insert(ENTRY_TEST_PROPERTIES, CONTENT_ONE)
     }
@@ -49,7 +50,84 @@ class AppendingTransformerTest : BaseTransformerTest() {
 
     run(shadowJarTask)
 
-    val content = outputShadowJar.use { it.getContent(ENTRY_TEST_PROPERTIES) }.trimIndent()
+    val content = outputShadowJar.use { it.getContent(ENTRY_TEST_PROPERTIES) }
     assertThat(content).isEqualTo(CONTENT_ONE_TWO)
   }
+
+  @Test
+  fun appendApplicationYaml() {
+    val (one, two) = writeApplicationYamlJars()
+
+    projectScriptPath.appendText(
+      transform<AppendingTransformer>(
+        shadowJarBlock = fromJar(one, two),
+        transformerBlock = """
+          resource = 'resources/$APPLICATION_YML_FILE'
+          separator = '$APPLICATION_YML_SEPARATOR'
+        """.trimIndent(),
+      ),
+    )
+
+    run(shadowJarTask)
+
+    val content = outputShadowJar.use { it.getContent("resources/$APPLICATION_YML_FILE") }
+    assertThat(content).isEqualTo(
+      """
+      $CONTENT_ONE
+      ---
+      $CONTENT_TWO
+      """.trimIndent(),
+    )
+  }
+
+  @Test
+  fun appendApplicationYamlShortSyntax() {
+    val (one, two) = writeApplicationYamlJars()
+
+    projectScriptPath.appendText(
+      """
+        $shadowJar {
+          ${fromJar(one, two)}
+          append('resources/$APPLICATION_YML_FILE', '$APPLICATION_YML_SEPARATOR')
+          append('resources/config/$APPLICATION_YML_FILE', '$APPLICATION_YML_SEPARATOR')
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarTask)
+
+    val content1 = outputShadowJar.use { it.getContent("resources/$APPLICATION_YML_FILE") }
+    assertThat(content1).isEqualTo(
+      """
+      $CONTENT_ONE
+      ---
+      $CONTENT_TWO
+      """.trimIndent(),
+    )
+    val content2 = outputShadowJar.use { it.getContent("resources/config/$APPLICATION_YML_FILE") }
+    assertThat(content2).isEqualTo(
+      """
+      $CONTENT_TWO
+      ---
+      $CONTENT_THREE
+      """.trimIndent(),
+    )
+  }
+
+  private fun writeApplicationYamlJars(): Pair<Path, Path> {
+    val one = buildJarOne {
+      insert("resources/$APPLICATION_YML_FILE", CONTENT_ONE)
+      insert("resources/config/$APPLICATION_YML_FILE", CONTENT_TWO)
+    }
+    val two = buildJarTwo {
+      insert("resources/$APPLICATION_YML_FILE", CONTENT_TWO)
+      insert("resources/config/$APPLICATION_YML_FILE", CONTENT_THREE)
+    }
+    return one to two
+  }
+
+  private companion object {
+    const val APPLICATION_YML_FILE = "application.yml"
+    const val APPLICATION_YML_SEPARATOR = "\\n---\\n"
+  }
 }