Skip to content

Latest commit

 

History

History
275 lines (184 loc) · 13.8 KB

application-plugin-improvements.md

File metadata and controls

275 lines (184 loc) · 13.8 KB

Application plugin

This feature is a bucket for improvements to the Gradle application plugin. Most of these improvements are not strategic.

Allow custom templates for the start scripts (DONE)

  • 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

Use cases

  • 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.

Implementation plan

Introduce new interface

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.

Change StartScriptGenerator

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.

Modify existing usage of StartScriptGenerator

There are various places across the Gradle codebase that use the class StartScriptGenerator. All of these places will need to use the new implementation.

Allow for setting the ScriptGenerator for task type CreateStartScripts

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.

User visible changes

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.

Test coverage

  • 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 implementations ScriptGenerator.
  • Allow for modifying the default generation of start scripts.

Open issues

  • The classes UnixStartScriptGenerator and WindowsStartScriptGenerator 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.

Custom JVM options escape logic for 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

Use cases

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']

Implementation plan

Introducing a new interface for escape handlers

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);
}

Introducing default Unix/Windows escape handler implementations

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;
    }
}

Adding escape handler implementations to start script generator

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);
}

Test coverage

  • 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.

User visible changes

A user should not be able to set a custom escape handler or modify the default implementation through the task type CreateStartScripts.

Providing a custom implementation

startScripts {
    unixStartScriptGenerator.escapeHandler = new CustomUnixEscapeHandler()
    windowsStartScriptGenerator.escapeHandler = new CustomWindowsEscapeHandler()
}

class CustomUnixEscapeHandler implements EscapeHandler { ... }
class CustomWindowsEscapeHandler implements EscapeHandler { ... }

Changing the default implementation behavior

startScripts {
    unixStartScriptGenerator.escapeHandler.addReplacement('<', '&lt;')
    unixStartScriptGenerator.escapeHandler.replacements['$'] = '\\$'
}

Open issues

Custom directory structure for generated application files

  • GRADLE-2333 - Ability to rename 'lib' and 'bin' directories
  • GRADLE-2991 - Application plugin's CreateStartScripts assumes all classpath files are present in lib dir

Use cases

  • 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.

Implementation plan

Introducing the extension

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.

Introducing interface to be implemented by extension

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();
}

Implementing the interface methods

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.

Test coverage

  • 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.

User visible changes

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')
        }
    }
}

Open issues

  • Should we deprecate the existing ApplicationPluginConvention and keep it around for a while to avoid a hard breaking change?