Repository: GradleUp/shadow (kotlin)

Issue:
Can't include a directory outside the dist directory into the application distribution
Hey @johnrengelman 

I've been trying to include a directory outside the dist directory in to the application distribution. When doing this with the installApp task the directory is properly brought into the install build directory but when using the installShadowApp it isn't copied into the installShadow build directory. I've used the following code to include the directory

``` groovy
applicationDistribution.from('config') {
    into "config"
}
```

Is there something I'm missing?

Pull request:
- Closes #118.
- Closes #133.
- Follow up #1216.

Syncs with https://github.com/gradle/gradle/blob/45a20d82b623786d19b50185e595adf3d7b196b2/platforms/jvm/plugins-application/src/main/java/org/gradle/api/plugins/ApplicationPlugin.java.

---

- [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 4e38af587..4ff35dff6 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -8,9 +8,9 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowApplicati
 	protected fun addRunTask (Lorg/gradle/api/Project;)V
 	public synthetic fun apply (Ljava/lang/Object;)V
 	public fun apply (Lorg/gradle/api/Project;)V
-	protected fun configureDistSpec (Lorg/gradle/api/Project;)V
+	protected fun configureDistribution (Lorg/gradle/api/Project;)V
 	protected fun configureInstallTask (Lorg/gradle/api/Project;)V
-	protected fun configureJarMainClass (Lorg/gradle/api/Project;)V
+	protected fun configureShadowJarMainClass (Lorg/gradle/api/Project;)V
 }
 
 public final class com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin$Companion {
diff --git a/src/docs/application-plugin/README.md b/src/docs/application-plugin/README.md
index 113bfe118..3d2765d19 100644
--- a/src/docs/application-plugin/README.md
+++ b/src/docs/application-plugin/README.md
@@ -38,6 +38,8 @@ plugins {
 
 application {
   mainClass = 'myapp.Main'
+  // Optionally, you can add default JVM arguments to the start scripts like this:
+  applicationDefaultJvmArgs = ['--add-opens=java.base/java.lang=ALL-UNNAMED']
 }
 
 tasks.named('runShadow', JavaExec) {
@@ -68,12 +70,15 @@ plugins {
 
 application {
   mainClass = 'myapp.Main'
-  // Optionally, you can add default JVM arguments to the start scripts like this:
-  applicationDefaultJvmArgs = ['--add-opens=java.base/java.lang=ALL-UNNAMED']
+  // Optionally, you can include `some/dir` files in the distribution like this:
+  applicationDistribution.from('some/dir') {
+    include '*.txt'
+  }
 }
 
 // `shadow` is the name of the distribution created by Shadow plugin
 distributions.named('shadow') {
+  // Optionally, you can add more files into extra directory in the distribution like this:
   contents.into('extra') {
     from project.file('extra/echo.sh')
   }
diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md
index 56a3ac36b..cfe199e41 100644
--- a/src/docs/changes/README.md
+++ b/src/docs/changes/README.md
@@ -3,6 +3,10 @@
 
 ## [Unreleased]
 
+**Added**
+
+- Sync `ShadowApplicationPlugin` with `ApplicationPlugin`. ([#1224](https://github.com/GradleUp/shadow/pull/1224))
+
 
 ## [v9.0.0-beta7] (2025-02-02)
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt
index c4bb1bcac..74034b7b4 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt
@@ -18,28 +18,20 @@ import org.gradle.api.tasks.TaskProvider
 import org.gradle.api.tasks.application.CreateStartScripts
 import org.gradle.jvm.application.scripts.TemplateBasedScriptGenerator
 
+/**
+ * A [Plugin] which packages and runs a project as a Java Application using the shadowed jar.
+ *
+ * Modified from [org.gradle.api.plugins.ApplicationPlugin.java](https://github.com/gradle/gradle/blob/45a20d82b623786d19b50185e595adf3d7b196b2/platforms/jvm/plugins-application/src/main/java/org/gradle/api/plugins/ApplicationPlugin.java).
+ */
 public abstract class ShadowApplicationPlugin : Plugin<Project> {
   override fun apply(project: Project) {
     project.addRunTask()
     project.addCreateScriptsTask()
-    project.configureDistSpec()
-    project.configureJarMainClass()
+    project.configureDistribution()
+    project.configureShadowJarMainClass()
     project.configureInstallTask()
   }
 
-  protected open fun Project.configureJarMainClass() {
-    val mainClassName = applicationExtension.mainClass
-    tasks.shadowJar.configure { task ->
-      task.inputs.property("mainClassName", mainClassName)
-      task.doFirst {
-        // Inject the Main-Class attribute if it is not already present.
-        if (!task.manifest.attributes.contains("Main-Class")) {
-          task.manifest.attributes["Main-Class"] = mainClassName.get()
-        }
-      }
-    }
-  }
-
   protected open fun Project.addRunTask() {
     val extension = applicationExtension
     tasks.register(SHADOW_RUN_TASK_NAME, JavaExec::class.java) { task ->
@@ -50,15 +42,15 @@ public abstract class ShadowApplicationPlugin : Plugin<Project> {
         i.destinationDir.resolve("lib/${s.archiveFile.get().asFile.name}")
       }
       task.classpath(jarFile)
+      task.mainModule.set(extension.mainModule)
       task.mainClass.set(extension.mainClass)
-      task.conventionMapping.map("jvmArgs", extension::getApplicationDefaultJvmArgs)
-      task.javaLauncher.set(javaToolchainService.launcherFor(javaPluginExtension.toolchain))
+      task.jvmArguments.convention(provider { extension.applicationDefaultJvmArgs })
+
+      task.modularity.inferModulePath.convention(javaPluginExtension.modularity.inferModulePath)
+      task.javaLauncher.convention(javaToolchainService.launcherFor(javaPluginExtension.toolchain))
     }
   }
 
-  /**
-   * Syncs with [ApplicationPlugin.addCreateScriptsTask](https://github.com/gradle/gradle/blob/bcecbb416f19438c7532e309456e3c3ed287f8f5/platforms/jvm/plugins-application/src/main/java/org/gradle/api/plugins/ApplicationPlugin.java#L184-L203).
-   */
   protected open fun Project.addCreateScriptsTask() {
     val extension = applicationExtension
     tasks.register(SHADOW_SCRIPTS_TASK_NAME, CreateStartScripts::class.java) { task ->
@@ -104,30 +96,49 @@ public abstract class ShadowApplicationPlugin : Plugin<Project> {
       task.doLast {
         task.eachFile {
           if (it.path == "bin/${applicationName.get()}") {
-            it.permissions { permissions -> permissions.unix(755) }
+            it.permissions { permissions -> permissions.unix(UNIX_SCRIPT_PERMISSIONS) }
           }
         }
       }
     }
   }
 
-  protected open fun Project.configureDistSpec() {
+  protected open fun Project.configureDistribution() {
     distributions.register(ShadowBasePlugin.DISTRIBUTION_NAME) { distributions ->
-      distributions.contents { contents ->
-        contents.from(file("src/dist"))
-        contents.into("lib") { lib ->
+      distributions.contents { distSpec ->
+        distSpec.from(file("src/dist"))
+        distSpec.into("lib") { lib ->
           lib.from(tasks.shadowJar)
           lib.from(configurations.shadow)
         }
-        contents.into("bin") { bin ->
+        distSpec.into("bin") { bin ->
           bin.from(tasks.startShadowScripts)
-          bin.filePermissions { it.unix(493) }
+          bin.filePermissions { it.unix(UNIX_SCRIPT_PERMISSIONS) }
+        }
+        distSpec.with(applicationExtension.applicationDistribution)
+      }
+    }
+  }
+
+  protected open fun Project.configureShadowJarMainClass() {
+    val mainClassName = applicationExtension.mainClass
+    tasks.shadowJar.configure { task ->
+      task.inputs.property("mainClassName", mainClassName)
+      task.doFirst {
+        // Inject the Main-Class attribute if it is not already present.
+        if (!task.manifest.attributes.contains("Main-Class")) {
+          task.manifest.attributes["Main-Class"] = mainClassName.get()
         }
       }
     }
   }
 
   public companion object {
+    /**
+     * Reflects the number of 755.
+     */
+    private const val UNIX_SCRIPT_PERMISSIONS = "rwxr-xr-x"
+
     public const val SHADOW_RUN_TASK_NAME: String = "runShadow"
     public const val SHADOW_SCRIPTS_TASK_NAME: String = "startShadowScripts"
     public const val SHADOW_INSTALL_TASK_NAME: String = "installShadowDist"


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt
index c0b7f8586..a4e00e9b3 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/ApplicationPluginTest.kt
@@ -20,9 +20,7 @@ import java.nio.file.Path
 import java.util.zip.ZipFile
 import kotlin.io.path.ExperimentalPathApi
 import kotlin.io.path.appendText
-import kotlin.io.path.createDirectories
 import kotlin.io.path.isRegularFile
-import kotlin.io.path.outputStream
 import kotlin.io.path.readText
 import kotlin.io.path.relativeTo
 import kotlin.io.path.walk
@@ -100,73 +98,6 @@ class ApplicationPluginTest : BasePluginTest() {
     )
   }
 
-  @Test
-  fun shadowApplicationDistributionsShouldUseShadowJar() {
-    prepare(
-      mainClassWithImports = true,
-      applicationBlock = """
-        applicationDefaultJvmArgs = ['--add-opens=java.base/java.lang=ALL-UNNAMED']
-      """.trimIndent(),
-      dependenciesBlock = "shadow 'junit:junit:3.8.2'",
-    )
-
-    run(shadowDistZipTask)
-
-    val zipPath = path("build/distributions/myapp-shadow-1.0.zip")
-    val extractedPath = path("build/distributions/extracted/")
-
-    // Unzip the whole file.
-    ZipFile(zipPath.toFile()).use { zip ->
-      zip.entries().toList().forEach { entry ->
-        val entryDest = extractedPath.resolve(entry.name)
-        if (entry.isDirectory) {
-          entryDest.createDirectories()
-        } else {
-          zip.getInputStream(entry).use { it.copyTo(entryDest.outputStream()) }
-        }
-      }
-    }
-
-    assertThat(extractedPath.walkEntries()).containsOnly(
-      "myapp-shadow-1.0/bin/myapp",
-      "myapp-shadow-1.0/bin/myapp.bat",
-      "myapp-shadow-1.0/lib/myapp-1.0-all.jar",
-      "myapp-shadow-1.0/lib/junit-3.8.2.jar",
-    )
-
-    commonAssertions(
-      jarPath("myapp-shadow-1.0/lib/myapp-1.0-all.jar", extractedPath),
-      entriesContained = arrayOf("shadow/Main.class"),
-      classPathAttr = "junit-3.8.2.jar",
-    )
-
-    val unixScript = path("myapp-shadow-1.0/bin/myapp", extractedPath)
-    val winScript = path("myapp-shadow-1.0/bin/myapp.bat", extractedPath)
-
-    assertThat(unixScript.readText()).contains(
-      "CLASSPATH=\$APP_HOME/lib/myapp-1.0-all.jar",
-      "exec \"\$JAVACMD\" \"\$@\"",
-      "DEFAULT_JVM_OPTS='\"--add-opens=java.base/java.lang=ALL-UNNAMED\"'",
-    )
-    assertThat(winScript.readText()).contains(
-      "set CLASSPATH=%APP_HOME%\\lib\\myapp-1.0-all.jar",
-      "set DEFAULT_JVM_OPTS=\"--add-opens=java.base/java.lang=ALL-UNNAMED\"",
-    )
-
-    val runningOutput = if (isWindows) {
-      // Use `cmd` and `/c` explicitly to prevent `The process cannot access the file because it is being used by another process` errors.
-      runProcess("cmd", "/c", winScript.toString(), "bar")
-    } else {
-      // Use `sh` explicitly since the extracted script is non-executable.
-      // Avoid `chmod +x` to prevent `Text file busy` errors on Linux.
-      runProcess("sh", unixScript.toString(), "bar")
-    }
-    assertThat(runningOutput).contains(
-      "Hello, World! (bar) from Main",
-      "Refs: junit.framework.Test",
-    )
-  }
-
   @Test
   fun installShadowDoesNotExecuteDependentShadowTask() {
     prepare()
@@ -219,6 +150,7 @@ class ApplicationPluginTest : BasePluginTest() {
   @Test
   fun canAddExtraFilesIntoDistribution() {
     path("extra/echo.sh").writeText("echo 'Hello, World!'")
+    path("some/dir/hello.txt").writeText("'Hello, World!'")
     prepare(
       projectBlock = """
         distributions.named('$DISTRIBUTION_NAME') {
@@ -227,6 +159,11 @@ class ApplicationPluginTest : BasePluginTest() {
           }
         }
       """.trimIndent(),
+      applicationBlock = """
+        applicationDistribution.from('some/dir') {
+          include '*.txt'
+        }
+      """.trimIndent(),
     )
 
     run(shadowDistZipTask)
@@ -239,9 +176,12 @@ class ApplicationPluginTest : BasePluginTest() {
         "myapp-shadow-1.0/bin/myapp.bat",
         "myapp-shadow-1.0/lib/myapp-1.0-all.jar",
         "myapp-shadow-1.0/extra/echo.sh",
+        "myapp-shadow-1.0/hello.txt",
       )
       assertThat(zip.getContent("myapp-shadow-1.0/extra/echo.sh"))
         .isEqualTo("echo 'Hello, World!'")
+      assertThat(zip.getContent("myapp-shadow-1.0/hello.txt"))
+        .isEqualTo("'Hello, World!'")
     }
   }