Tool Executions Tasks and Execution Specifications
Gradle script authors are quite aware of Exec and JavaExec tasks as well as the projects extensions exec and javaexec. Implementing tasks or extensions to support specific tools can involve a lot of work. This is where this set of abstract classes come in to simplify the work to a minimum and allowing plugin authors to think about what kind of tool functionality to wrap rather than implementing heaps of boilerplate code.
Wrapping an external tool within a gradle plugin usually have three components:
-
Execution specification
-
Project extension
-
Task type
How to implement these components are described in the following sections.
As from Grolifant 2.0, the original classes in `org.ysb33r.grolifant.api.v4.runnable from Grolifant 1.1 has been deprecated and replaced with a much simpler, but more declarative solution.
The classes in org.ysb33r.grolifant.api.v4.exec that were deprecated since Grolifant 1.1 have been removed.
Please see the upgrading guide for converting to the new classes.
|
Execution specifications
Execution specifications are used for configuring the necessary details for running an external process. The latter will then be used by a task type of a project extension.
The interfaces provide the configuration DSL, whereas the abstract classes from the basis of implementing your own wrappers for external execution. In all cases you need to implement the appropriate execution specification and then implement a task class that uses that specification. Under the hood the classes will take care of putting of configuring an ExecSpec and then executing it. In most cases a AbstractExecSpec is the minimum what you’ll need to set up.
If you want to implement tasks around an executable that uses commands then you need AbstractCommandExecSpec.
AN example of this is something like terraform
or `packer.
If you want to implement tasks around an executable that executes scripts then you need AbstractScriptExecSpec.
Configuring an executable
All tasks (and execution specifications) are configured the same way. Let’s assume that you have something called gitExec
with which you want to run git
commands.
Your Groovy-DSL will look something like this.
gitExec {
entrypoint { (1)
executable = 'git'
addEnvironmentProvider( project.provider { -> [ GIT_COMMITER_NAME : 'ysb33r@domain.example' ]}) (2)
}
runnerSpec { (3)
args 'log', '--oneline'
}
process { (4)
}
}
1 | Configures anything that can be set in ProcessForkOptions and well as addEnvironmentProvider. |
2 | addEnvironmentProvider allows extertnal prvoiders to add additional environment variables into the execution environment.
These are added after the normal environment settings have been added. |
3 | Configures everything related to arguments. |
4 | Configures what is needed during and after execution of the process. It is essentially the same as hat can be configured in BaseExecSpec. |
Configuring a command-based executable
The command and its parameters are configured via an addition cmd
block.
For instance you might have implemented Git as a command-based executable.
You can then configure it as follows.
gitExec {
entrypoint {
executable = 'git'
addEnvironmentProvider( project.provider { -> [ GIT_COMMITER_NAME : 'ysb33r@domain.example' ]})
}
runnerSpec {
args '-C', '/my/project'
}
cmd {
command = 'log' (1)
args '--oneline' (2)
}
}
1 | Configures the command. |
2 | Configures arguments related to the command. |
The above example will result in a command line of git -C /my/project log --oneline
.
Configuring a script-based executable.
In a similar fashion it is possible to wrap scripts. For instance, you might be wrapping CRuby. You can then configure it as follows.
cruby {
entrypoint {
executable = 'ruby'
}
script { (1)
name = 'rb.env' (2)
path = '/path/to/rb.env' (3)
}
}
1 | Everything script-related is configured in the script block. |
2 | Use name to simply provide the script name.
In this case the implementation that you provide mut resolve the script in a suitable fashion.
For instance, a Ruby implementation might add -S to the command-line in this case |
3 | Provide the path to the script. In this case the path must exist and it will be passed as-is or resolved for a canonical location. |
Configuring the post-process
process {
ignoreExitValue = true (1)
output { (2)
capture() (3)
captureAndForward() (4)
captureTo( project.provider { -> file('build/this-output.txt') }) (5)
forward() (6)
noOutput() (7)
}
errorOutput { (8)
capture()
captureAndForward()
captureTo( project.provider { -> file('build/this-output.txt') })
forward()
noOutput()
}
afterExecute { (9)
}
}
1 | Whether to ignore the exit value. |
2 | Configure the standard output. |
3 | Capture the output. |
4 | Capture the output and forward the output to console. |
5 | Write the output to the file. |
6 | Forward the output to console. |
7 | Do not produce any output. |
8 | Configure the error output. |
9 | Add one of more closures/actions to process output.
This is called when the output or error output is captured.
It is passed a ExecOutput instance. |
Wrapping a tool with AbstractExecWrapperTask
The AbstractExecWrapperTask is a simplified way of abstract tools into gradlesque tasks. Unlike the other abstraction execution task types mentioned above, it does not expose the full command-line options to the build script author, but rather allows a plugin author to provide suitable functional abstractions. For instance the Terraform plugin provides a hierarchy of tasks that wrap around the Terraform executable, deals with initialisation and simplifies integration of a very popular tool into a build pipeline in a very gradlesque way.
This abstract task also relies on the creation of suitable extension derived from AbstractToolExtension. The result is a very flexible DSL. This can be illustrated by the following example which is also a good starting point for any plugin author wanting to abstract a tool in such a way.
Step 1 - Create an execution specification
The details for creating execution specifications has been described earlier. You can use the one which is best suited to your application.
In this example we are using AbstractCommandExecSpec as the base execution specification.
@CompileStatic
class MyCmdExecSpec extends AbstractCommandExecSpec<MyCmdExecSpec> {
MyCmdExecSpec(ConfigCacheSafeOperations po) {
super(ConfigCacheSafeOperations.from(po))
}
}
Step 2 - Create the task
@CompileStatic
@CompileStatic
class MySimpleWrapperTask extends AbstractExecWrapperTask<MyCmdExecSpec> { (1)
MySimpleWrapperTask() {
super()
this.execSpec = new MyCmdExecSpec(this)
}
@Override
protected MyCmdExecSpec getExecSpec() { (2)
this.execSpec
}
@Override
protected Provider<File> getExecutableLocation() { (3)
}
private final MyCmdExecSpec execSpec
}
1 | Extend AbstractExecWrapperTask. |
2 | Implement a method that will rreturn an instance of your execution specification - in this example, MyCmdExecSpec . |
3 | Implement a method that will return the location of the executable. It can be a name or a full path. |
Wrapping a tool with AbstractToolExtension and AbstractExecWrapperWithExtensionTask
Step 1 - Create an execution specification
Firstly create an execution specification as described in the previous section
Step 2 - Create an extension
We start with an extension class that will only be used as a project extension.
@CompileStatic
class MyToolExtension extends AbstractToolExtension<MyToolExtension> { (1)
static final String NAME = 'toolConfig'
MyToolExtension(ProjectOperations po) { (2)
super(po)
}
MyToolExtension(Task task, ProjectOperations po, MyToolExtension projectExt) {
super(task, po, projectExt) (3)
}
@Override
protected String runExecutableAndReturnVersion() throws ConfigurationException { (4)
try {
projectOperations.execTools.parseVersionFromOutput( (5)
['--version'], (6)
executablePathOrNull(), (7)
{ String output -> '1.0.0' } (8)
)
} catch (RuntimeException e) {
throw new ConfigurationException('Cannot determine version', e)
}
}
@Override
protected ExecutableDownloader getDownloader() { (9)
}
}
1 | Derive from AbstractToolExtension .
This will provide methods for setting the executable. |
2 | Create a constructor for attaching the extension to a project. |
3 | You will also need a constructor for attaching to a task. In this case you will also need to specify the name of the project extension. By convention, always have the task and project extension as the same name. For simplicity, we’ll ignore this constructor and return to it a bit later. |
4 | When a user specified a tool by path or search path, we ned a different way of obtaining the version. Implement this method to perform that task. |
5 | Using parseVersionFromOutput is the easiest way to implement this. |
6 | Pass the appropriate parameters that will return output that declares the version. |
7 | executablePathOrNull is the easiest way to obtain the path to the executable that was set earlier. |
8 | Provide a parser to extract the version from output. The parser is supplied the output from running the executable with the given command-line parameters. |
9 | Returns an instance that will return the locaiton of an executable buy possibly downloading it if not available locally. See for more details on implementing your own. |
Step 3 - Create the task class
@CompileStatic
class MyExtensionWrapperTask extends AbstractExecWrapperWithExtensionTask<MyToolExtension, MyCmdExecSpec> { (1)
MyExtensionWrapperTask() {
super()
this.execSpec = new MyCmdExecSpec(this) (2)
this.executableLocation = toolExtension.executable (3)
}
@Override
protected MyCmdExecSpec getExecSpec() { (4)
this.execSpec
}
@Override
protected MyToolExtension getToolExtension() { (5)
project.extensions.getByType(MyToolExtension)
}
@Override
protected Provider<File> getExecutableLocation() { (6)
this.executableLocation
}
private final Provider<File> executableLocation
private final MyCmdExecSpec execSpec
}
1 | Your task class must extend AbstractExecWrapperWithExtensionTask and specify the type of the associated execution specification as well as the type of the extension. |
2 | Cache the lazy-evaluated location of the executable. |
3 | Create a execution specification that is associated with the task. |
4 | In most cases the implements of getExecSpec is to return an instance that was created in the constructor. |
5 | Return the location of the extension. This is just a shortcut method and cannot be called from a task action. |
6 | A method to return the location of an executable. In most cases this will simply be the value of a private field. |