Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@

**Deprecated**

- Deprecate `JavaJarExec`, it will be removed in the next major release. ([#1956](https://github.com/GradleUp/shadow/pull/1956))
- Deprecate `KnowsTask`, it will be removed in the next major release. ([#1957](https://github.com/GradleUp/shadow/pull/1957))

**Fixed**

- Fix compatibility with Isolated Projects. ([#1947](https://github.com/GradleUp/shadow/pull/1947))
- Fix interaction with Gradle artifact transforms. ([#1949](https://github.com/GradleUp/shadow/pull/1949))
- Fix `Log4j2PluginsCacheFileTransformer` not working for merging `Log4j2Plugins.dat` files. ([#1955](https://github.com/GradleUp/shadow/pull/1955))
- Fix various issues about `runShadow` and polish `ShadowApplicationPlugin`. ([#1956](https://github.com/GradleUp/shadow/pull/1956))

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,137 +1,154 @@
package com.github.jengelman.gradle.plugins.shadow

import com.github.jengelman.gradle.plugins.shadow.internal.JavaJarExec
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.gradle.api.GradleException
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.distribution.Distribution
import org.gradle.api.distribution.DistributionContainer
import org.gradle.api.file.CopySpec
import org.gradle.api.plugins.ApplicationPlugin
import org.gradle.api.plugins.JavaApplication
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.application.CreateStartScripts
import org.gradle.jvm.toolchain.JavaLauncher
import org.gradle.jvm.toolchain.JavaToolchainService

class ShadowApplicationPlugin implements Plugin<Project> {
/**
* A {@link Plugin} which packages and runs a project as a Java Application using the shadowed jar.
*
* Modified from
* <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>.
*
* @see ApplicationPlugin
*/
abstract class ShadowApplicationPlugin implements Plugin<Project> {

public static final String SHADOW_RUN_TASK_NAME = 'runShadow'
public static final String SHADOW_SCRIPTS_TASK_NAME = 'startShadowScripts'
public static final String SHADOW_INSTALL_TASK_NAME = 'installShadowDist'

private Project project
private JavaApplication javaApplication
private static final String DISTRIBUTION_NAME = ShadowBasePlugin.EXTENSION_NAME

@Override
void apply(Project project) {
this.project = project
this.javaApplication = project.extensions.getByType(JavaApplication)

DistributionContainer distributions = project.extensions.getByName("distributions") as DistributionContainer
Distribution distribution = distributions.create("shadow")

addRunTask(project)
addCreateScriptsTask(project)

configureDistSpec(project, distribution.contents)

configureDistribution(project)
configureJarMainClass(project)
configureInstallTask(project)
}

protected void configureJarMainClass(Project project) {
def classNameProvider = javaApplication.mainClass
jar.configure { jar ->
jar.inputs.property('mainClassName', classNameProvider)
jar.doFirst {
jar.manifest.attributes 'Main-Class': classNameProvider.get()
}
}
}

protected void addRunTask(Project project) {
project.tasks.register(SHADOW_RUN_TASK_NAME, JavaExec) { task ->
task.description = "Runs this project as a JVM application using the shadow jar"
task.group = ApplicationPlugin.APPLICATION_GROUP

project.tasks.register(SHADOW_RUN_TASK_NAME, JavaJarExec) { run ->
def install = project.tasks.named(SHADOW_INSTALL_TASK_NAME, Sync)
run.dependsOn SHADOW_INSTALL_TASK_NAME
run.mainClass.set('-jar')
run.description = 'Runs this project as a JVM application using the shadow jar'
run.group = ApplicationPlugin.APPLICATION_GROUP
run.conventionMapping.jvmArgs = { javaApplication.applicationDefaultJvmArgs }
run.conventionMapping.jarFile = {
project.file("${install.get().destinationDir.path}/lib/${jar.get().archiveFile.get().asFile.name}")
}
configureJavaLauncher(run)
}
}
task.classpath = project.files(project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME))

def applicationExtension = project.extensions.getByType(JavaApplication)
def javaPluginExtension = project.extensions.getByType(JavaPluginExtension)
def javaToolchainService = project.extensions.getByType(JavaToolchainService)

task.mainModule.convention(applicationExtension.mainModule)
task.mainClass.convention(applicationExtension.mainClass)
task.jvmArguments.convention(project.provider { applicationExtension.applicationDefaultJvmArgs })

private void configureJavaLauncher(JavaJarExec run) {
def toolchain = project.getExtensions().getByType(JavaPluginExtension.class).toolchain
JavaToolchainService service = project.getExtensions().getByType(JavaToolchainService.class)
Provider<JavaLauncher> defaultLauncher = service.launcherFor(toolchain)
run.getJavaLauncher().set(defaultLauncher)
task.modularity.inferModulePath.convention(javaPluginExtension.modularity.inferModulePath)
task.javaLauncher.convention(javaToolchainService.launcherFor(javaPluginExtension.toolchain))
}
}

protected void addCreateScriptsTask(Project project) {
project.tasks.register(SHADOW_SCRIPTS_TASK_NAME, CreateStartScripts) { startScripts ->
startScripts.description = 'Creates OS specific scripts to run the project as a JVM application using the shadow jar'
startScripts.group = ApplicationPlugin.APPLICATION_GROUP
startScripts.classpath = project.files(jar)
startScripts.mainClass.set(javaApplication.mainClass)
startScripts.conventionMapping.applicationName = { javaApplication.applicationName }
startScripts.conventionMapping.outputDir = { new File(project.layout.buildDirectory.asFile.get(), 'scriptsShadow') }
startScripts.conventionMapping.defaultJvmOpts = { javaApplication.applicationDefaultJvmArgs }
startScripts.inputs.files project.objects.fileCollection().from { -> jar }
project.tasks.register(SHADOW_SCRIPTS_TASK_NAME, CreateStartScripts) { task ->
task.description = "Creates OS specific scripts to run the project as a JVM application using the shadow jar"

task.classpath = project.files(project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME))

def applicationExtension = project.extensions.getByType(JavaApplication)
def javaPluginExtension = project.extensions.getByType(JavaPluginExtension)

// TODO: replace usages of conventionMapping.
task.mainModule.convention(applicationExtension.mainModule)
task.mainClass.convention(applicationExtension.mainClass)
task.conventionMapping.map("applicationName") { applicationExtension.applicationName }
task.conventionMapping.map("outputDir") {
project.layout.buildDirectory.dir("scriptsShadow").get().asFile
}
task.conventionMapping.map("executableDir") { applicationExtension.executableDir }
task.conventionMapping.map("defaultJvmOpts") { applicationExtension.applicationDefaultJvmArgs }

task.modularity.inferModulePath.convention(javaPluginExtension.modularity.inferModulePath)
}
}

protected void configureInstallTask(Project project) {
project.tasks.named(SHADOW_INSTALL_TASK_NAME, Sync).configure { task ->
task.doFirst {
if (task.destinationDir.directory) {
if (task.destinationDir.listFiles().size() != 0 && (!new File(task.destinationDir, 'lib').directory || !new File(task.destinationDir, 'bin').directory)) {
throw new GradleException("The specified installation directory '${task.destinationDir}' is neither empty nor does it contain an installation for '${javaApplication.applicationName}'.\n" +
def applicationExtension = project.extensions.getByType(JavaApplication)
def applicationName = project.provider { applicationExtension.applicationName }
def executableDir = project.provider { applicationExtension.executableDir }

task.doFirst("Check installation directory") {
def destinationDir = task.destinationDir
def children = destinationDir.list()
if (children == null) {
throw new IOException("Could not list directory ${destinationDir}")
}
if (children.length == 0) return
if (!new File(destinationDir, "lib").isDirectory() ||
!new File(destinationDir, executableDir.get()).isDirectory()) {
throw new GradleException(
"The specified installation directory '${destinationDir}' is neither empty nor does it contain an installation for '${applicationName.get()}'.\n" +
"If you really want to install to this directory, delete it and run the install task again.\n" +
"Alternatively, choose a different installation directory."
)
}
)
}
}
task.doLast {
task.eachFile {
if (it.path == "bin/${javaApplication.applicationName}") {
it.mode = 0x755
}
}
}

protected void configureDistribution(Project project) {
def distributions = project.extensions.getByType(DistributionContainer)
distributions.register(DISTRIBUTION_NAME) { dist ->
def applicationExtension = project.extensions.getByType(JavaApplication)
dist.distributionBaseName.convention(
project.provider {
// distributionBaseName defaults to `$project.name-$distribution.name`, applicationName
// defaults to project.name
// so we append the suffix to match the default distributionBaseName. Modified from
// `ApplicationPlugin.configureDistribution()`.
"${applicationExtension.applicationName}-${DISTRIBUTION_NAME}"
}
)
dist.contents { distSpec ->
distSpec.from(project.file("src/dist"))
distSpec.into("lib") { lib ->
lib.from(project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME))
// Reflects the value of the `Class-Path` attribute in the JAR manifest.
lib.from(project.configurations.named(ShadowBasePlugin.CONFIGURATION_NAME))
}
// Defaults to bin dir.
distSpec.into(project.provider { applicationExtension.executableDir }) { bin ->
bin.from(project.tasks.named(SHADOW_SCRIPTS_TASK_NAME))
bin.filePermissions { permissions -> permissions.unix('rwxr-xr-x') }
}
distSpec.with(applicationExtension.applicationDistribution)
configureDistSpec(project, distSpec)
}
}
}

protected CopySpec configureDistSpec(Project project, CopySpec distSpec) {
def startScripts = project.tasks.named(SHADOW_SCRIPTS_TASK_NAME)

distSpec.with {
from(project.file("src/dist"))
return distSpec
}

into("lib") {
from(jar)
from(project.configurations.shadow)
}
into("bin") {
from(startScripts)
filePermissions { it.unix(493) }
protected void configureJarMainClass(Project project) {
def applicationExtension = project.extensions.getByType(JavaApplication)
project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME, ShadowJar).configure { task ->
task.inputs.property('mainClassName', applicationExtension.mainClass)
task.doFirst {
task.manifest.attributes 'Main-Class': applicationExtension.mainClass.get()
}
}

distSpec
}

private TaskProvider<ShadowJar> getJar() {
project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME, ShadowJar)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.JavaExec
import org.gradle.api.tasks.TaskAction

/**
* @deprecated This is unused for now, it will be removed in the next major release.
*/
@Deprecated
abstract class JavaJarExec extends JavaExec {

@InputFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class ApplicationSpec extends PluginSpecification {
settingsFile << "rootProject.name = 'myapp'"

when:
BuildResult result = runWithSuccess('runShadow')
BuildResult result = runWithSuccess('installShadowDist', 'runShadow')

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

when:
BuildResult result = runWithSuccess('runShadow')
BuildResult result = runWithSuccess('installShadowDist', 'runShadow')

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