This feature is a bucket for improvements to the Gradle application
plugin.
Most of these improvements are not strategic.
- GRADLE-2299 - Application plugin should allow custom templates for the start scripts
- GRADLE-2207 - Allow application plugin start scripts to be more flexible for different environments
- GRADLE-1783 - Gradle unix scripts does not work on platforms when bash is not available
- The user of the application plugin wants full flexibility and control over the scripts templates e.g. conditionals as part of the logic, using additional variables.
- Modifying the default scripts by proving additional logic.
- Generating additional start scripts for dedicated use cases in addition to the default batch and shell scripts.
Introduce a new public interface org.gradle.api.scripting.ScriptGenerator
. This interface will allow for having different implementations of a script generator e.g one for Unix and one for Windows. The interface supports a single method:
package org.gradle.api.scripting;
import java.io.Writer;
public interface ScriptGenerator<T extends ScriptGenerationDetails> {
void generateScript(T details, Writer destination);
}
The parameter with type org.gradle.api.scripting.ScriptGenerationDetails
provides the parameters needed for generating the script, for example the classpath or the JVM parameters. ScriptGenerationDetails
could start out as marker interface. The parameter destination
determines the target output of the generator. Using the type java.io.Writer
allows for more flexibility.
The existing org.gradle.api.internal.plugins.StartScriptGenerator
implementation has to be broken up into two different classes:
org.gradle.api.internal.plugins.UnixStartScriptGenerator
: Generates Unix start script for application plugin.org.gradle.api.internal.plugins.WindowsStartScriptGenerator
: Generates Windows start script for application plugin.
Both classes need to implement the new interface org.gradle.api.scripting.ScriptGenerator
. Furthermore, the class needs to be serializable so it can be used as input property for tasks. The implementation of ScriptGenerationDetails
, org.gradle.api.internal.plugins.DefaultScriptGenerationDetails
, should contain all fields from StartScriptGenerator
. Certain methods StartScriptGenerator
are reusable and should be extracted as utility class.
There are various places across the Gradle codebase that use the class StartScriptGenerator
. All of these places will need to use the new implementation.
Currently, the task org.gradle.api.tasks.application.CreateStartScripts
creates an instance of StartScriptGenerator
in its action. To allow for users to set the ScriptGenerator
from the outside, we will expose two properties: unixStartScriptGenerator
and windowsStartScriptGenerator
. Their default values are the respective default implementations. Both properties should be annotated with @Input
.
The existing functionality should work as usual. The main change to the user are the exposed properties of type ScriptGenerator
in the custom task CreateStartScripts
.
- Break up
StartScriptGeneratorTest
into two classes: one for the Unix-based, one for the Windows-based implementation. - Write an example implementation of
ScriptGenerator
and test it. CreateStartScriptsTest
should work with the existing assertions.- Add a test case that allows for setting custom implementations of
ScriptGenerator
to generate start scripts. ApplicationPluginTest
should work with the existing assertions.- Extend test cases of
ApplicationPluginTest
: - Verify that the task named
startScripts
can be reconfigured to use custom implementationsScriptGenerator
. - Allow for modifying the default generation of start scripts.
- The classes
UnixStartScriptGenerator
andWindowsStartScriptGenerator
should probably become part of the public API so users can create composite implementations that modify the start script generated by the default implementations. - In the proposed approach there's no way to add new start script generators.
- GRADLE-3084 - CreateStartScripts in Application plugin escapes $ whether you want it to or not
- GRADLE-2207 - Allow application plugin start scripts to be more flexible for different environments
The default Unix and Windows script generator implementations come with hard-coded escape logic for JVM options passed in from the build script. At the moment this logic is not easily changeable or extensible. An example that comes to mind is the use of an environment variable as part of the JVM options.
startScripts.defaultJvmOpts = ['-DserverPort=$PORT']
As part of this change, we'd introduce a new interface org.gradle.api.scripting.EscapeHandler
. The EscapeHandler
is responsible for escaping characters in a String based on the provided replacements.
The escape behavior can also be changed from the outside by either adding new replacements or by modifying existing replacement mappings.
package org.gradle.api.scripting;
public interface EscapeHandler {
void addReplacement(String target, String replacement);
void removeReplacement(String target);
Map<String, String> getReplacements();
String escape(String source);
}
Individual start script generators would have a dedicated implementation of their EscapeHandler
. The following example shows a potential implementation approach for the Unix start script
generator:
public class UnixJvmOptEscapeHandler implements EscapeHandler {
private final Map<String, String> replacements = new HashMap<String, String>();
public UnixJvmOptEscapeHandler() {
addReplacement("$", "\\$");
// add more replacements
}
public void addReplacement(String target, String replacement) {
replacements.put(target, replacement);
}
public void removeReplacement(String target) {
replacements.remove(target);
}
public Map<String, String> getReplacements() {
return replacements;
}
public String escape(String jvmOpt) {
for(Map.Entry<String, Object> entry : replacements.entrySet()) {
jvmOpt = jvmOpt.replace(entry.getKey(), entry.getValue());
}
return jvmOpt;
}
}
Every start script generator can only have a single escape handler. It should be possible to cover all escape requirements in a single class. For that purpose, we'll
introduce a new interface that needs to be implemented by UnixStartScriptGenerator
and WindowsStartScriptGenerator
.
package org.gradle.api.internal.plugins;
public interface EscapeHandlerAware<T extends EscapeHandler> {
T getEscapeHandler();
void setEscapeHandler(T escapeHandler);
}
- The default escape logic for JVM options should work as before.
- The default escape handler can be modified to add more replacements. The rule kicks in when escaping the JVM options.
- The default escape handler can be modified to change the replacement behavior for an existing escape rule. The rule kicks in when escaping the JVM options.
- A user can ask for the replacement rules and do something with it.
- The default escape handler can be replaced by a custom implementation. The escape logic works solely on the provided implementation and not the default implementation.
A user should not be able to set a custom escape handler or modify the default implementation through the task type CreateStartScripts
.
startScripts {
unixStartScriptGenerator.escapeHandler = new CustomUnixEscapeHandler()
windowsStartScriptGenerator.escapeHandler = new CustomWindowsEscapeHandler()
}
class CustomUnixEscapeHandler implements EscapeHandler { ... }
class CustomWindowsEscapeHandler implements EscapeHandler { ... }
startScripts {
unixStartScriptGenerator.escapeHandler.addReplacement('<', '<')
unixStartScriptGenerator.escapeHandler.replacements['$'] = '\\$'
}
- GRADLE-2333 - Ability to rename 'lib' and 'bin' directories
- GRADLE-2991 - Application plugin's CreateStartScripts assumes all classpath files are present in lib dir
- The user of the application plugin wants to change the default directories to better reflect a custom or legacy directory structure.
- The generated distribution needs to contain more than just the libraries under
lib
. There might be additional files (e.g. property files) that should be added to the application's classpath.
The referenced directories lib
and bin
are not exclusively used by the task responsible for generating the start scripts. These directories are also referenced by other tasks provided
by the application plugin e.g. installDist
. Therefore, the implementation needs to be elevated to the level of the plugin. A good way to let users configure the behavior of a plugin
is to expose an extension. At the moment the application plugin does not provide an extension so we'll need to introduce one with the name javaApplication
. We should probably turn
ApplicationPluginConvention
into an extension though that would be a breaking change.
The extension would implement the following interface:
package org.gradle.api.internal.plugins;
public interface ApplicationDistributionSpec {
StartScriptsSpec getStartScripts();
ApplicationClasspathSpec getClasspath();
}
StartScriptsSpec
defines the target directory for start scripts. Possibly, we could also expose the Unix and Windows start generator implementations so users don't have to go directly
through the task CreateStartScripts
anymore.
package org.gradle.api.internal.plugins;
import java.io.File;
import org.gradle.api.scripting.ScriptGenerator;
import org.gradle.api.scripting.JavaAppStartScriptGenerationDetails;
public interface StartScriptsSpec {
File getLaunchDir();
void setLaunchDir(File launchDir);
ScriptGenerator<JavaAppStartScriptGenerationDetails> getUnixStartScriptGenerator();
void setUnixStartScriptGenerator(ScriptGenerator<JavaAppStartScriptGenerationDetails> unixStartScriptGenerator);
ScriptGenerator<JavaAppStartScriptGenerationDetails> getWindowsStartScriptGenerator();
void setWindowsStartScriptGenerator(ScriptGenerator<JavaAppStartScriptGenerationDetails> windowsStartScriptGenerator);
}
ApplicationClasspathSpec
defines the target directory for libraries. Additionally, we'd like to expose a way to add more files to the classpath via a CopySpec
.
package org.gradle.api.internal.plugins;
import java.io.File;
import org.gradle.api.file.CopySpec;
public interface ApplicationClasspathSpec {
File getLibraryDir();
void setLibraryDir(File libraryDir);
CopySpec getAdditionalClasspath();
}
All methods provided by ApplicationDistributionSpec
have to be implemented in the extension. For the interfaces StartScriptsSpec
and ApplicationClasspathSpec
we have to add a
default implementation as part of the internal API. Where applicable, we'll need to assign default values e.g. the launch directory has to point to bin
.
- Change all existing tests to work with the extension instead of the convention.
- If the user doesn't configure the extension, the application distribution should use the default directories.
- The extension is used to configure a custom launch directory. Executing the task
CreateStartScripts
generates start script in the declared directory. - The extension is used to configure a custom libraries directory. The generated start scripts and creating the distribution reflects the declared directory.
- Custom start script generators can be declared through the extension.
- Using the extension folders and/or files can be added to the application classpath. The generated start scripts incorporate declared folders/files in the classpath. The relevant folders/files are copied to the distribution directory.
Users will mainly configure the application plugin through the extension. The following example build script snippet demonstrates its use:
javaApplication {
launchDir = file('launch')
libraryDir = file('libraries')
additionalClasspath {
from('conf') {
include '**/*.properties'
into('configuration')
}
}
}
- Should we deprecate the existing
ApplicationPluginConvention
and keep it around for a while to avoid a hard breaking change?