Skip to content

Commit cba4a74

Browse files
committed
Fix various issues about runShadow and polish ShadowApplicationPlugin
1 parent a2d82f7 commit cba4a74

4 files changed

Lines changed: 105 additions & 88 deletions

File tree

src/docs/changes/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
- Fix compatibility with Isolated Projects. ([#1947](https://github.com/GradleUp/shadow/pull/1947))
99
- Fix interaction with Gradle artifact transforms. ([#1949](https://github.com/GradleUp/shadow/pull/1949))
10+
- Fix various issues about `runShadow` and polish `ShadowApplicationPlugin`. ([#1956](https://github.com/GradleUp/shadow/pull/1956))
1011

1112
## [v8.3.10] (2026-02-26)
1213

Lines changed: 98 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,149 @@
11
package com.github.jengelman.gradle.plugins.shadow
22

3-
import com.github.jengelman.gradle.plugins.shadow.internal.JavaJarExec
43
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
54
import org.gradle.api.GradleException
65
import org.gradle.api.Plugin
76
import org.gradle.api.Project
8-
import org.gradle.api.distribution.Distribution
97
import org.gradle.api.distribution.DistributionContainer
10-
import org.gradle.api.file.CopySpec
118
import org.gradle.api.plugins.ApplicationPlugin
129
import org.gradle.api.plugins.JavaApplication
1310
import org.gradle.api.plugins.JavaPluginExtension
14-
import org.gradle.api.provider.Provider
11+
import org.gradle.api.tasks.JavaExec
1512
import org.gradle.api.tasks.Sync
16-
import org.gradle.api.tasks.TaskProvider
1713
import org.gradle.api.tasks.application.CreateStartScripts
18-
import org.gradle.jvm.toolchain.JavaLauncher
1914
import org.gradle.jvm.toolchain.JavaToolchainService
2015

21-
class ShadowApplicationPlugin implements Plugin<Project> {
16+
/**
17+
* A {@link Plugin} which packages and runs a project as a Java Application using the shadowed jar.
18+
*
19+
* Modified from
20+
* <a href="https://github.com/gradle/gradle/blob/fdecc3c95828bb9a1c1bb6114483fe5b16f9159d/platforms/jvm/plugins-application/src/main/java/org/gradle/api/plugins/ApplicationPlugin.java">org.gradle.api.plugins.ApplicationPlugin.java</a>.
21+
*
22+
* @see ApplicationPlugin
23+
*/
24+
abstract class ShadowApplicationPlugin implements Plugin<Project> {
2225

2326
public static final String SHADOW_RUN_TASK_NAME = 'runShadow'
2427
public static final String SHADOW_SCRIPTS_TASK_NAME = 'startShadowScripts'
2528
public static final String SHADOW_INSTALL_TASK_NAME = 'installShadowDist'
2629

27-
private Project project
28-
private JavaApplication javaApplication
30+
private static final String DISTRIBUTION_NAME = ShadowBasePlugin.EXTENSION_NAME
2931

3032
@Override
3133
void apply(Project project) {
32-
this.project = project
33-
this.javaApplication = project.extensions.getByType(JavaApplication)
34-
35-
DistributionContainer distributions = project.extensions.getByName("distributions") as DistributionContainer
36-
Distribution distribution = distributions.create("shadow")
37-
3834
addRunTask(project)
3935
addCreateScriptsTask(project)
40-
41-
configureDistSpec(project, distribution.contents)
42-
43-
configureJarMainClass(project)
36+
configureDistribution(project)
37+
configureShadowJarMainClass(project)
4438
configureInstallTask(project)
4539
}
4640

47-
protected void configureJarMainClass(Project project) {
48-
def classNameProvider = javaApplication.mainClass
49-
jar.configure { jar ->
50-
jar.inputs.property('mainClassName', classNameProvider)
51-
jar.doFirst {
52-
jar.manifest.attributes 'Main-Class': classNameProvider.get()
53-
}
54-
}
55-
}
56-
5741
protected void addRunTask(Project project) {
42+
project.tasks.register(SHADOW_RUN_TASK_NAME, JavaExec) { task ->
43+
task.description = "Runs this project as a JVM application using the shadow jar"
44+
task.group = ApplicationPlugin.APPLICATION_GROUP
5845

59-
project.tasks.register(SHADOW_RUN_TASK_NAME, JavaJarExec) { run ->
60-
def install = project.tasks.named(SHADOW_INSTALL_TASK_NAME, Sync)
61-
run.dependsOn SHADOW_INSTALL_TASK_NAME
62-
run.mainClass.set('-jar')
63-
run.description = 'Runs this project as a JVM application using the shadow jar'
64-
run.group = ApplicationPlugin.APPLICATION_GROUP
65-
run.conventionMapping.jvmArgs = { javaApplication.applicationDefaultJvmArgs }
66-
run.conventionMapping.jarFile = {
67-
project.file("${install.get().destinationDir.path}/lib/${jar.get().archiveFile.get().asFile.name}")
68-
}
69-
configureJavaLauncher(run)
70-
}
71-
}
46+
task.classpath = project.files(project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME))
47+
48+
def applicationExtension = project.extensions.getByType(JavaApplication)
49+
def javaPluginExtension = project.extensions.getByType(JavaPluginExtension)
50+
def javaToolchainService = project.extensions.getByType(JavaToolchainService)
51+
52+
task.mainModule.convention(applicationExtension.mainModule)
53+
task.mainClass.convention(applicationExtension.mainClass)
54+
task.jvmArguments.convention(project.provider { applicationExtension.applicationDefaultJvmArgs })
7255

73-
private void configureJavaLauncher(JavaJarExec run) {
74-
def toolchain = project.getExtensions().getByType(JavaPluginExtension.class).toolchain
75-
JavaToolchainService service = project.getExtensions().getByType(JavaToolchainService.class)
76-
Provider<JavaLauncher> defaultLauncher = service.launcherFor(toolchain)
77-
run.getJavaLauncher().set(defaultLauncher)
56+
task.modularity.inferModulePath.convention(javaPluginExtension.modularity.inferModulePath)
57+
task.javaLauncher.convention(javaToolchainService.launcherFor(javaPluginExtension.toolchain))
58+
}
7859
}
7960

8061
protected void addCreateScriptsTask(Project project) {
81-
project.tasks.register(SHADOW_SCRIPTS_TASK_NAME, CreateStartScripts) { startScripts ->
82-
startScripts.description = 'Creates OS specific scripts to run the project as a JVM application using the shadow jar'
83-
startScripts.group = ApplicationPlugin.APPLICATION_GROUP
84-
startScripts.classpath = project.files(jar)
85-
startScripts.mainClass.set(javaApplication.mainClass)
86-
startScripts.conventionMapping.applicationName = { javaApplication.applicationName }
87-
startScripts.conventionMapping.outputDir = { new File(project.layout.buildDirectory.asFile.get(), 'scriptsShadow') }
88-
startScripts.conventionMapping.defaultJvmOpts = { javaApplication.applicationDefaultJvmArgs }
89-
startScripts.inputs.files project.objects.fileCollection().from { -> jar }
62+
project.tasks.register(SHADOW_SCRIPTS_TASK_NAME, CreateStartScripts) { task ->
63+
task.description = "Creates OS specific scripts to run the project as a JVM application using the shadow jar"
64+
65+
task.classpath = project.files(project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME))
66+
67+
def applicationExtension = project.extensions.getByType(JavaApplication)
68+
def javaPluginExtension = project.extensions.getByType(JavaPluginExtension)
69+
70+
// TODO: replace usages of conventionMapping.
71+
task.mainModule.convention(applicationExtension.mainModule)
72+
task.mainClass.convention(applicationExtension.mainClass)
73+
task.conventionMapping.map("applicationName") { applicationExtension.applicationName }
74+
task.conventionMapping.map("outputDir") {
75+
project.layout.buildDirectory.dir("scriptsShadow").get().asFile
76+
}
77+
task.conventionMapping.map("executableDir") { applicationExtension.executableDir }
78+
task.conventionMapping.map("defaultJvmOpts") { applicationExtension.applicationDefaultJvmArgs }
79+
80+
task.modularity.inferModulePath.convention(javaPluginExtension.modularity.inferModulePath)
9081
}
9182
}
9283

9384
protected void configureInstallTask(Project project) {
9485
project.tasks.named(SHADOW_INSTALL_TASK_NAME, Sync).configure { task ->
95-
task.doFirst {
96-
if (task.destinationDir.directory) {
97-
if (task.destinationDir.listFiles().size() != 0 && (!new File(task.destinationDir, 'lib').directory || !new File(task.destinationDir, 'bin').directory)) {
98-
throw new GradleException("The specified installation directory '${task.destinationDir}' is neither empty nor does it contain an installation for '${javaApplication.applicationName}'.\n" +
86+
def applicationExtension = project.extensions.getByType(JavaApplication)
87+
def applicationName = project.provider { applicationExtension.applicationName }
88+
def executableDir = project.provider { applicationExtension.executableDir }
89+
90+
task.doFirst("Check installation directory") {
91+
def destinationDir = task.destinationDir
92+
def children = destinationDir.list()
93+
if (children == null) {
94+
throw new IOException("Could not list directory ${destinationDir}")
95+
}
96+
if (children.length == 0) return
97+
if (!new File(destinationDir, "lib").isDirectory() ||
98+
!new File(destinationDir, "bin").isDirectory() ||
99+
!new File(destinationDir, executableDir.get()).isDirectory()) {
100+
throw new GradleException(
101+
"The specified installation directory '${destinationDir}' is neither empty nor does it contain an installation for '${applicationName.get()}'.\n" +
99102
"If you really want to install to this directory, delete it and run the install task again.\n" +
100103
"Alternatively, choose a different installation directory."
101-
)
102-
}
103-
}
104-
}
105-
task.doLast {
106-
task.eachFile {
107-
if (it.path == "bin/${javaApplication.applicationName}") {
108-
it.mode = 0x755
109-
}
104+
)
110105
}
111106
}
112107
}
113108
}
114109

115-
protected CopySpec configureDistSpec(Project project, CopySpec distSpec) {
116-
def startScripts = project.tasks.named(SHADOW_SCRIPTS_TASK_NAME)
117-
118-
distSpec.with {
119-
from(project.file("src/dist"))
120-
121-
into("lib") {
122-
from(jar)
123-
from(project.configurations.shadow)
124-
}
125-
into("bin") {
126-
from(startScripts)
127-
filePermissions { it.unix(493) }
110+
protected void configureDistribution(Project project) {
111+
def distributions = project.extensions.getByType(DistributionContainer)
112+
distributions.register(DISTRIBUTION_NAME) { dist ->
113+
def applicationExtension = project.extensions.getByType(JavaApplication)
114+
dist.distributionBaseName.convention(
115+
project.provider {
116+
// distributionBaseName defaults to `$project.name-$distribution.name`, applicationName
117+
// defaults to project.name
118+
// so we append the suffix to match the default distributionBaseName. Modified from
119+
// `ApplicationPlugin.configureDistribution()`.
120+
"${applicationExtension.applicationName}-${DISTRIBUTION_NAME}"
121+
}
122+
)
123+
dist.contents { distSpec ->
124+
distSpec.from(project.file("src/dist"))
125+
distSpec.into("lib") { lib ->
126+
lib.from(project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME))
127+
// Reflects the value of the `Class-Path` attribute in the JAR manifest.
128+
lib.from(project.configurations.named(ShadowBasePlugin.CONFIGURATION_NAME))
129+
}
130+
// Defaults to bin dir.
131+
distSpec.into(project.provider { applicationExtension.executableDir }) { bin ->
132+
bin.from(project.tasks.named(SHADOW_SCRIPTS_TASK_NAME))
133+
bin.filePermissions { permissions -> permissions.unix('rwxr-xr-x') }
134+
}
135+
distSpec.with(applicationExtension.applicationDistribution)
128136
}
129137
}
130-
131-
distSpec
132138
}
133139

134-
private TaskProvider<ShadowJar> getJar() {
135-
project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME, ShadowJar)
140+
protected void configureShadowJarMainClass(Project project) {
141+
project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME, ShadowJar).configure { task ->
142+
def applicationExtension = project.extensions.getByType(JavaApplication)
143+
task.inputs.property('mainClassName', applicationExtension.mainClass)
144+
task.doFirst {
145+
task.manifest.attributes 'Main-Class': applicationExtension.mainClass.get()
146+
}
147+
}
136148
}
137149
}

src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import org.gradle.api.tasks.InputFile
44
import org.gradle.api.tasks.JavaExec
55
import org.gradle.api.tasks.TaskAction
66

7+
/**
8+
* @deprecated This is unused for now, it will be removed in the next major release.
9+
*/
10+
@Deprecated
711
abstract class JavaJarExec extends JavaExec {
812

913
@InputFile

src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class ApplicationSpec extends PluginSpecification {
4545
settingsFile << "rootProject.name = 'myapp'"
4646

4747
when:
48-
BuildResult result = runWithSuccess('runShadow')
48+
BuildResult result = runWithSuccess('installShadowDist', 'runShadow')
4949

5050
then: 'tests that runShadow executed and exited'
5151
assert result.output.contains('TestApp: Hello World! (foo)')
@@ -123,7 +123,7 @@ class ApplicationSpec extends PluginSpecification {
123123
""".stripIndent()
124124

125125
when:
126-
BuildResult result = runWithSuccess('runShadow')
126+
BuildResult result = runWithSuccess('installShadowDist', 'runShadow')
127127

128128
then: 'tests that runShadow executed and exited'
129129
assert result.output.contains('Running application with JDK 17')

0 commit comments

Comments
 (0)