diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 3c6e4c007..7c530ffd8 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -5,6 +5,7 @@ **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** @@ -12,6 +13,7 @@ - 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) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy index 9fdc54bc8..e2bfc928f 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy @@ -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 { +/** + * A {@link Plugin} which packages and runs a project as a Java Application using the shadowed jar. + * + * Modified from + * org.gradle.api.plugins.ApplicationPlugin.java. + * + * @see ApplicationPlugin + */ +abstract class ShadowApplicationPlugin implements Plugin { 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 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 getJar() { - project.tasks.named(ShadowJavaPlugin.SHADOW_JAR_TASK_NAME, ShadowJar) } } diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy index 28ed02308..8e5f4bc03 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/JavaJarExec.groovy @@ -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 diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy index 13d35e39f..2b52b4cf7 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy @@ -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)') @@ -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')