Creating custom gradle task inside Grails plugin - gradle

I am trying to create a Grails plugin that creates a custom Gradle Task which can be depended on by bootRun. I would like to do something like this:
#CompileStatic
static void configureProcessConfig(Project project) {
TaskContainer taskContainer = project.tasks
if(taskContainer.findByName('processConfig') == null) {
taskContainer.create("processConfig") {
List<File> testResources = [project.file("src/test/resources")]
for (t in testResources) {
if (t.name.contains('.properties') || t.name.contains('.groovy')) {
Path originFile = t.toPath()
Path destFile = Paths.get('build/classes/main/' + t.name)
Files.copy(originFile, destFile)
}
}
}
def processConfigTask = taskContainer.findByName('processConfig')
taskContainer.findByName("bootRun")?.dependsOn(processConfigTask)
}
}
However, I can't seem to get it to work in my xxxGrailsPlugin.groovy file. I don't know where to get the Project file to call this. It doesn't create the task. I am happy to do something different, but I can't figure out how to do it. I would prefer not to write to every build.gradle file where this plugin is used, but if that's the best option, I guess I will.
Any help is appreciated. Thanks!

Related

Applying Gradle plugin from local file

I have the following gradle plugin that does the job of starting up a java process. The code for this lives under a file named startAppServerPlugin.gradle under the project's buildSrc directory.
The code of the plugin looks like this:
repositories.jcenter()
dependencies {
localGroovy()
gradleApi()
}
}
public class StartAppServer implements Plugin<Project> {
#Override
void apply(Project project) {
project.task('startServer', type: StartServerTask)
}
}
public class StartServerTask extends DefaultTask {
String command
String ready
String directory = '.'
StartServerTask(){
description = "Spawn a new server process in the background."
}
#TaskAction
void spawn(){
if(!(command && ready)) {
throw new GradleException("Ensure that mandatory fields command and ready are set.")
}
Process process = buildProcess(directory, command)
waitFor(process)
}
private waitFor(Process process) {
def line
def reader = new BufferedReader(new InputStreamReader(process.getInputStream()))
while ((line = reader.readLine()) != null) {
logger.quiet line
if (line.contains(ready)) {
logger.quiet "$command is ready."
break
}
}
}
private static Process buildProcess(String directory, String command) {
def builder = new ProcessBuilder(command.split(' '))
builder.redirectErrorStream(true)
builder.directory(new File(directory))
def process = builder.start()
process
}
}
I'm trying to figure out a way of having this imported into my main build.gradle file due everything I tried so far has been unsuccessful.
So far I have tried this:
apply from: 'startAppServerPlugin.gradle'
apply plugin: 'fts.gradle.plugins'
But it has been failing. I've tried searching online for examples of doing what I need to do but so far I've been unsuccessful. Can anyone please provide a hint as to how I'm supposed to do so?
The buildSrc folder is treated as an included build, where the code is compiled and put on the classpath of the surrounding project. The actual build.gradle file in buildSrc is only used for compiling that project, and the things you put in it will not be available elsewhere.
You are supposed to create your classes as a normal Java/Groovy/Kotlin project under buildSrc. I don't know if you can use the default package, but it is generally best practice to have a package name anyway.
For example, your StartAppServer plugin should be in buildSrc/src/main/groovy/my/package/StartAppServer.groovy. Then you can apply it in your build scripts with apply plugin: my.package.StartAppServer.
There are a lot of good examples in the user guide.
You are on the right path. The first order of business is to import the external gradle build using:
apply from: 'startAppServerPlugin.gradle'
Then you can apply the plugin with:
apply plugin: StartAppServer
See Script Plugins and Applying Binary Plugins

How to access variant.outputFileName in Kotlin

We've been using a snippet like this one to rename the APK file generated by our Gradle build:
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "${variant.name}-${variant.versionName}.apk"
}
}
Source: https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration#variant_output
I am now in the process of converting my build.gradle to build.gradle.kts, i. e. to the Gradle Kotlin DSL. This is one of the last missing pieces: I can't figure out how to access outputFileName.
According to the API docs it does not even seem to exist:
BaseVariant.getOutputs() returns a DomainObjectCollection<BaseVariantOutput> which provides the all method used in the snippet.
BaseVariantOutput extends OutputFile which extends VariantOutput but none of these has an outputFileName or any getters or setters of a matching name.
So, I suspect there is some advanced Groovy magic at work to make this work - but how do I get there in Kotlin?
A little simplified version of #david.mihola answer:
android {
/**
* Notes Impl: Use DomainObjectCollection#all
*/
applicationVariants.all {
val variant = this
variant.outputs
.map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl }
.forEach { output ->
val outputFileName = "YourAppName - ${variant.baseName} - ${variant.versionName} ${variant.versionCode}.apk"
println("OutputFileName: $outputFileName")
output.outputFileName = outputFileName
}
}
}
Browsing through the source code of the Android Gradle plugin, I think I found the answer - here we go:
We are actually dealing with objects of type BaseVariantOutputImpl and this class does have both these methods:
public String getOutputFileName() {
return apkData.getOutputFileName();
}
public void setOutputFileName(String outputFileName) {
if (new File(outputFileName).isAbsolute()) {
throw new GradleException("Absolute path are not supported when setting " +
"an output file name");
}
apkData.setOutputFileName(outputFileName);
}
Using this knowledge we can now:
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
and then cast our target objects like so:
applicationVariants.all(object : Action<ApplicationVariant> {
override fun execute(variant: ApplicationVariant) {
println("variant: ${variant}")
variant.outputs.all(object : Action<BaseVariantOutput> {
override fun execute(output: BaseVariantOutput) {
val outputImpl = output as BaseVariantOutputImpl
val fileName = output.outputFileName
.replace("-release", "-release-v${defaultConfig.versionName}-vc${defaultConfig.versionCode}-$gitHash")
.replace("-debug", "-debug-v${defaultConfig.versionName}-vc${defaultConfig.versionCode}-$gitHash")
println("output file name: ${fileName}")
outputImpl.outputFileName = fileName
}
})
}
})
So, I guess: Yes, there is some Groovy magic at work, namely that Groovy's dynamic type system allows you to just access getOutputFileName and setOutputFileName (by way of the abbreviated outputImpl.outputFileName syntax, as in Kotlin) from your code, hoping they will be there at runtime, even if the compile time interfaces that you know about don't have them.
Shorter version using lambdas:
applicationVariants.all{
outputs.all {
if(name.contains("release"))
(this as BaseVariantOutputImpl).outputFileName = "../../apk/$name-$versionName.apk"
}
}
This will place APK into app/apk folder with name made of variant name and version code.
You can change the format of filename as you wish.
Important: it must be done only on release builds, because ".." in path corrupts debug build process with strange errors.
For libraryVariants it is possible to change output file name without accessing internal api:
libraryVariants.all {
outputs.all {
packageLibraryProvider {
archiveFileName.set("yourlibrary-${buildType.name}.aar")
}
}
}
For Kotlin KTS.
NOTE: This is considered a temporal soluciĆ³n, until a proper way to do it in KTS is released by Android team.
Working in AGP v7.1.2 it might work also in lower versions of AGP.
:app build.gradle
android {
// ...
this.buildOutputs.all {
val variantOutputImpl = this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
val variantName: String = variantOutputImpl.name
val outputFileName = "custom-name-${variantName}.apk"
variantOutputImpl.outputFileName = outputFileName
}
}

Gradle: Delete files from directory, based on Filelist

I'm currently trying to migrate my old ant system to gradle. I'm pretty new to gradle and still learning, so maybe this is something trivial I just overlooked.
I got a file, called delete.list which contains a list of files I want to delete.
This is my code so far:
task deleteLib(type:DeleteFiles) {
deleteList = file("${buildDir}/delete.list")
}
class DeleteFiles extends DefaultTask {
#SkipWhenEmpty
#InputFile
File deleteList
DeleteFiles()
{
description = 'Deletes Libs from Integris zip'
}
#TaskAction
void delete(){
def lines = deleteList.readLines()
lines.each {
delete fileTree(dir: "${project.buildDir}", include: "${it}")
}
}
}
delete.list:
lib/java/activation.jar
lib/java/pdfbox*.jar
lib/java/fontbox*.jar
lib/java/xmpbox*.jar
lib/java/jempbox*.jar
lib/java/iText*.jar
lib/java/itext*.jar
lib/java/jakarta-poi.jar
lib/java/commons-net*.jar
lib/java/jfreechart*.jar
lib/java/jcommon*.jar
lib/java/dom4j*.jar
lib/java/xmlbeans*.jar
lib/java/jaxen*.jar
lib/java/avalon-framework*.jar
lib/java/batik-all*.jar
After googling a bit I found this solution, as my _delete.list may not exist during building phase.
My current problem is that gradle seems to have a problem with the fileTree method:
* What went wrong:
Execution failed for task ':deleteLib'.
> Could not find method fileTree() for arguments [{dir=C:\entwicklung\Testumgebung\testProjectGradle\build, include=lib/java/activation.jar}] on task ':deleteLib' of type DeleteFiles.
Have somebody an idea what I'm missing here?
Since both methods are defined on Project and groovy looks for the method defined in task you need to explicitly invoke the methods on project instance:
project.with {
delete fileTree(dir: "${project.buildDir}", include: "${it}")
}

Gradle distribution - create an empty directory

Is there a way to add empty directories (e.g, "logs") when creating a distribution with the gradle distribution plugin?
I saw this JIRA, describing the exact same thing. It is still open https://issues.gradle.org/browse/GRADLE-1671
I wonder if there are any workarounds I can use. I don't quite understand the workarounds described in the jira.
Thank you.
So I managed to work around this by following the suggestion in the mentioned JIRA to create a dummy empty directory and then copy it to the distribution location.
It's ugly but works. I'm sure it can be written more efficiently though. This is the Copy block from inside distributions/main/contents:
into('') {
//create an empty 'logs' directory in distribution root
def logDirBase = new File('/tmp/app-dummy-dir')
logDirBase.mkdirs()
def logDir = new File(logDirBase.absolutePath + '/logs')
logDir.mkdirs()
from {logDirBase}
}
Based on Logato's own answer I've come up with the following code, which is more elegant and also closes the file pointer correctly (using the with context):
distributions {
main {
baseName = 'app'
contents {
into('') {
File.createTempDir().with {
def tmpLog = new File(absolutePath, 'logs')
println tmpLog.absolutePath
tmpLog.mkdirs()
from (absolutePath) {
includeEmptyDirs = true
}
}
// ...
}
// ...
}
}
}
This seems odd answering this so late. But, there are 2 issues here.
We should really avoid creating empty directories. But, if we must...we must.
The previous examples attempt to create empty directories outside of the current project, which seems to go against the goal of most builds. We can avoid this and work more naturally within gradle by adding a custom task.
plugins {
id 'java'
id 'distribution'
}
group 'org.example'
version '1.0-SNAPSHOT'
task createEmptyLogDir() {
ext {
buildDir = layout.buildDirectory.dir('empty_dirs')
}
doFirst {
File.createTempDir().with {
def dir = new File(buildDir.get().asFile, 'logs')
dir.mkdirs()
}
}
outputs.dir(buildDir)
}
tasks.assembleDist.configure {
dependsOn('createEmptyLogDir')
}
distributions {
main {
distributionBaseName = 'app'
contents {
into('lib') {
from tasks.jar
}
from tasks.createEmptyLogDir {
includeEmptyDirs = true
}
}
}
}
This has the advantage of building within the build directory, using task inputs/outputs for up-to-date checks, and cleaning up.
An important note is that we cannot just create the distribution with empty directories, alone. This will be seen as no source and up-to-date. So, I added the jar in this example. Tested with gradle 7.1.

How to use gradle extension correctly in plugins using GradleBuild task?

EDIT : I rephrased my question in taken the propositon of David M. Karr into account.
I am writing a gradle plugin. This plugin is launching a task extending GradleBuild. The external gradle build file needs some info as parameters. These parameters are given in project extension.
Plugin code
class MyPlugin implements Plugin<Project> {
def mExt
void apply(Project project) {
mExt = project.extensions.create('myext',MyExt)
project.task('myTask', type:GradleBuild){
def param = new StartParameter()
param.setProjectProperties([target:getTarget()])
// Problem here
startParameter = param
buildFile = getMyBuildPath()
tasks = [
'build',
'generateDebugJavadocJar'
]
}
}
def getMyBuildPath(){
...
}
// Problem here
def getTarget(){
return {mExt.target}
}
}
class MyExt {
def String target = "uninitialised"
}
Gradle build file :
apply plugin : 'com.example.myplugin'
ext{
target = "myTarget"
}
External Gradle build file :
task build(){
println project.target
}
If I put a closure in getTarget(), println project.target shows the closure and not the string.
If I don't put the closure :
// Problem here
def getTarget(){
return mExt.target
}
Then I got "uninitialised" instead of "myTarget".
How can I get the value of myext.target here ?
I am using gradle 2.3
Try this:
Define an instance variable called "myext", of type "MyExt".
In the "apply" method, do this:
myext = project.extensions.create('myext',MyExt)
In the "getTarget" method, return "myext.target".
I have succeeded in getting what I wanted to in using project.afterEvaluate method. Thanks to this question
1) In gradle build task, startParameter.projectProperties is waiting for a map, not a closure. So the idea to put a closure for a lazy definition cannot work.
2) If I put directly in my plugin a reference to mExt.target or project.myext.target, then the initial value is set. The value put in my build.gradle file is not used because the plugin is already evaluated.
3) project.afterEvaluate() solve my problem. The closure ends configuring myTask in using the project's extension.
void apply(Project project) {
project.extensions.create('myext',MyExt)
project.task('myTask', type:GradleBuild){
buildFile = getMyBuildPath()
tasks = [
'build',
'generateDebugJavadocJar'
]
}
project.afterEvaluate { proj ->
proj.myTask.startParameter.projectProperties = [target:proj.myext.target]
}
}

Resources