Can Gradle fail the build if the jar is too big? - gradle

What is a good way for gradle to warn the developer that the code/dependency change they’re making makes the built jar too big?
We’re making updates to a repo that builds a jar that’s deployed in an AWS Lambda function. We are limited to 50MB for the executable. We’d ideally like the devs to get this feedback much before our CI/CD tries to upload to a lambda function and fails.

You could define and use a custom task to implement the file size check, and create a dependency between this task and the task that produces the Jar.
Assuming you are using Sprinboot, you could create a finalizer task that will be executed just after the bootJar task and make the build fail when jar size exceeds a certain limit.
Simple implementation as example:
task jarSizeChecker() {
def maxSize = 15_000_000 // jar size limit in bytes
group "verification"
description "Checks that the produced jar size does not exceed the defined limit"
doLast {
println " checking jar size"
def jarFileSize = bootJar.archiveFile.get().getAsFile().length()
println " fileSize is ${jarFileSize}"
if (jarFileSize > maxSize) {
throw new GradleException("Jar size exceed the defined limit")
}
}
}
bootJar.finalizedBy(jarSizeChecker)
Result:
$ ./gradlew bootJar
[...]
> Task :bootJar
> Task :jarSizeChecker FAILED
checking jar size
fileSize is 17333895
7 actionable tasks: 5 executed, 2 from cache
FAILURE: Build failed with an exception.
* Where:
Build file 'C:\dev\workspaces\...\build.gradle'
* What went wrong:
Execution failed for task ':jarSizeChecker'.
> Jar size exceed the defined limit

Related

How can I debug Gradle does not know how file '...' was created?

I have created a task in Gradle as follows:
task findTaskCreatingSpecificOutput() {
dependsOn testClasses
doLast {
tasks.findAll { task ->
task.outputs.getFiles().getFiles().each { output ->
if (output != null && output.getAbsolutePath().contains('generated')) {
println task
println output
}
}
}
}
}
to try and find which two tasks are writing to the same generated location. However the output is a list of tasks and directories which do not overlap at all so it should be easy for Gradle to know exactly which task created the location.
Example output of ./gradlew clean findTaskCreatingSpecificOutput --info
/Users/mylocation/shared/build/generated/sources/annotationProcessor/java/main
task ':shared:compileJava'
/Users/mylocation/shared/build/generated/sources/headers/java/main
task ':shared:compileTestFixturesJava'
/Users/mylocation/shared/build/generated/sources/annotationProcessor/java/testFixtures
task ':shared:compileTestFixturesJava'
/Users/mylocation/shared/build/generated/sources/headers/java/testFixtures
task ':shared:compileTestJava'
/Users/mylocation/shared/build/generated/sources/annotationProcessor/java/test
task ':shared:compileTestJava'
/Users/mylocation/shared/build/generated/sources/headers/java/test
task ':shared:delombok'
/Users/mylocation/shared/build/generated/sources/delombok/java/main
task ':shared:delombokTest'
/Users/mylocation/shared/build/generated/sources/delombok/java/test
task ':shared:delombokTestFixtures'
/Users/mylocation/shared/build/generated/sources/delombok/java/testFixtures
However I still get the warning Gradle does not know how file 'build/classes/java/main/generated' was created (output property 'destinationDirectory'). Task output caching requires exclusive access to output paths to guarantee correctness (i.e. multiple tasks are not allowed to produce output in the same location).
I don't think there's any way to find out which task actually wrote any particular file. The code I used only lists the declared outputs.
In my case the problem was that I was running a bytecode weaving task in doLast for compileJava.
I've changed compileJava to write to a new directory using destinationDirectory and then the weaving task writes the updated classes to sourceSets.main.output.classesDirs.singleFile

Configure a Gradle Task at Runtime

Is It possible to configure inputs of a gradle task at runtime after other tasks have run?
For example I am calculating a SHA of a zip in one step, and then uploading the zip with a path consisting of the SHA from a previous step. But when I got get get the value of the SHA which is contained in a file via: def sha = shaFile.text I get an error: (No such file or directory).
I had always assumed tasks were closures which were run at runtime but I guess that is just the doFirst & doLast, but the inputs need to be configured already before that.
Is It possible to configure inputs of a gradle task at runtime after other tasks have run?
Think of it this way:
In order for task B to run, task A must run first, that is to say, task B has a dependency on task A.
Refer to Adding dependencies to a task for more details on task dependencies.
Ok so now we're at the point where we need the output of task A (SHA value) as an input for task B. Since we have a dependency on task A, Gradle well make sure that task A is executed prior to B's execution.
Here's quick dirty example in Kotlin DSL (should be easily translated to Groovy):
tasks {
val taskA = register("taskA") {
val shaText = File("sha.txt")
if (shaText.exists()) {
shaText.delete()
}
File("sha.txt").writeText("abc");
}
register("taskB") {
dependsOn(taskA)
println(File("sha.txt").readText())
}
}
Ideally, you should create a custom task type specifying the input file and also specifying the output file so that Gradle can cache tasks inputs/outputs. Refer to Incremental tasks
for more details.

Create a second installDist task?

During development I am using a standard-function installDist (from the application plugin) in build.gradle:
installDist{}
... but I now want to have another task which installs/distributes/deploys a "production" version to the production location, which also incorporates the version into the directory structure. I tried this:
task deployOperativeVersion( type: installDist ) {
destinationDir = file( "$productionDir/$version" )
}
Build failure output:
Build file '/home/mike/IdeaProjects/JavaFXExp2/Organiser/build.gradle' line: 98
* What went wrong:
A problem occurred evaluating root project 'Organiser'.
> class org.gradle.api.tasks.Sync_Decorated cannot be cast to class java.lang.Class
(org.gradle.api.tasks.Sync_Decorated is in unnamed module of loader org.gradle.
internal.classloader.VisitableURLClassLoader #aec6354; java.lang.Class is in module
java.base of loader 'bootstrap')
It appears that installDist is not a "type" as in Test.
How can I achieve this? Incidentally I would be really keen on having two separate tasks: to get installDist to run I've found that you only have to type ./gradlew inst ... with a task called deployXXX it would be sufficient to type ./gradlew depl.
I also tried this:
task deployOperativeVersion{
installDist{
destinationDir = file( "$operativeDir/$version" )
}
}
... which doesn't seem to have done anything. Nor this:
task deployOperativeVersion{
doFirst {
installDist {
destinationDir = file("$operativeDir/$version")
}
}
}
A bit later I thought I had indeed found the answer:
task deployOperativeVersion{
dependsOn installDist{ destinationDir=file("$productionDir/$version")
}
... but to my amazement (will I ever get to a reasonable understanding of Gradle before Hell freezes over?), including this actually appears to influence the "routine" installDist task: specifically, it stops the latter from operating normally, and means that even when I run installDist the deployment/distribution/installation still goes to productionDir/version, rather than the default location.
So then I wondered about two tasks both of which are dependent on installDist:
task deployOperativeVersion{
dependsOn installDist{ destinationDir=file("$productionDir/$version") }
}
task stdInstall{
dependsOn installDist{ destinationDir=file("build/install") }
}
... haha, no joy: I run one and it deploys OK. I then run the other... and get an error:
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':installDist'.
> The specified installation directory '/home/mike/IdeaProjects/JavaFXExp2/Organiser/build/install' is neither empty nor does it contain an installation for 'Organiser'.
If you really want to install to this directory, delete it and run the install task again.
Alternatively, choose a different installation directory.
... needless to say, this is NOT the case: under ...Organiser/build/install there is one directory only, Organiser, with /bin and /lib directories under it.
Your task should be declared as a Sync task, which is the actual type of the installDist task. The application plugin is using the distribution plugin. You can grab the content configuration from the main distribution, which is the source, or from the installDist task.
task deployOperativeVersion(type: Sync) {
destinationDir = file("${productionDir}/${version}")
with distributions.main.content
}
or
task deployOperativeVersion(type: Sync) {
destinationDir = file("${productionDir}/${version}")
with installDist
}

Showing a message whether the mapstruct annotation processor creates files or not - preferable for my custom 'generateMappers' task

Courtesy of M.Ricciuti who answered my initial 'Migrating from Gradle 4 to 5. How to get mapstruct 1.20.final working with it' question I've an addition question. Created a new new thread because 'add comment' does not allow me enough characters/text.
I would like to have a Gradle 'generateMappers' task to generate the mapstruct source files AND display a message when the sources are generated and preferable one which tells me that nothing is generated because it wasn't needed.
In the old situation we had a generateMappers task which used mapstruct to generate source files AND display a message when the files were created. Now its basically empty.
task generateMappers (type: JavaCompile, dependsOn: compileJava) {
doFirst {
println "\tGenerating mapper classes"
}
doLast {
println "\tMapping classes generated"
}
}
Gradlew -i generateMappers reveals the following.
> Task :eu.myfirm.rest:generateMappers NO-SOURCE
Skipping task ':eu.myfirm.rest:generateMappers' as it has no source files and no previous output files.
:eu.myfirm.rest:generateMappers (Thread[Execution worker for ':' Thread 4,5,main]) completed. Took 0.0 secs.[enter link description here][1]
NOTE: > Task :eu.myfirm.rest:compileJava FROM-CACHE does trigger correctly ONLY when the sources are not there.
By changing the task into 'task generateMappers {...}' I ALWAY get a message - even when no new files are generated because nothing has changed in the mappers. Gradlew --info reveals
> Task :eu.myfirm.rest:generateMappers
Custom actions are attached to task ':eu.myfirm.rest:generateMappers'.
Caching disabled for task ':eu.myfirm.rest:generateMappers' because:
Caching has not been enabled for the task
Task ':eu.myfirm.rest:generateMappers' is not up-to-date because:
Task has not declared any outputs despite executing actions.
Generating mapper classes
Mapping classes generated
Its wrong because caching is disabled and I know this is only cosmetic because whether the sources are there or not I will always get this message.
I've this feeling I'm on the WRONG track and I somehow have to hook into the annotationProcessor to get the required two messages BUT I also would like to have a seperate 'generateMappers' task to call. Any further hints/pointers is greatly appreciated.

How do I make Gradle rerun a task when its dependencies are run?

Let's say that I have a task "main" that depends on another task "dependency." I would like "main" to be rerun any time its dependency (or its dependency's dependencies) is rebuilt because "main" relies on the artifacts produced by "dependency" (or the dependencies of "dependency").
A build.gradle file containing an example of what I'm dealing with is the following:
defaultTasks 'main'
task baseDependency {
outputs.file 'deps.out'
outputs.upToDateWhen { false }
doLast {
exec {
commandLine 'bash', '-c', 'echo hello world > deps.out'
}
}
}
task dependency(dependsOn: baseDependency)
task main(dependsOn: dependency) {
outputs.file 'main.out'
doLast {
exec {
commandLine 'bash', '-c', 'echo hello world > main.out'
}
}
}
Executing gradle the first time:
:baseDependency
:dependency
:main
BUILD SUCCESSFUL
Total time: 0.623 secs
Executing it a second time:
:baseDependency
:dependency
:main UP-TO-DATE
BUILD SUCCESSFUL
Total time: 0.709 secs
I would really love if "main" were not marked "UP-TO-DATE" if its dependencies had to be rebuilt. That seems essential. How do you make sure that's the case?
The standard way to specify dependency between tasks is via task's inputs and outputs. This can be any file, fileset or directory.
In your case you should modify main task and add inputs.file 'deps.out' to its definition.
Note that gradle has an optimization that may lead to unexpected behavior in a simplistic example that you provided.
Before a task is executed for the first time, Gradle takes a snapshot
of the inputs. This snapshot contains the set of input files and a
hash of the contents of each file. Gradle then executes the task. If
the task completes successfully, Gradle takes a snapshot of the
outputs. This snapshot contains the set of output files and a hash of
the contents of each file. Gradle persists both snapshots for the next
time the task is executed.
Each time after that, before the task is executed, Gradle takes a new
snapshot of the inputs and outputs. If the new snapshots are the same
as the previous snapshots, Gradle assumes that the outputs are up to
date and skips the task. If they are not the same, Gradle executes the
task. Gradle persists both snapshots for the next time the task is
executed.
So even if you specify correct inputs in a simple example where the same file is generated the dependent task will be marked as up to date on the second and subsequent runs.
If you do not want or cannot hardcode dependency on the file you can override upToDateWhen for dependent task and calculate the condition if the task is up to date based on dependencies of this task and their state like this:
outputs.upToDateWhen { task ->
task.taskDependencies.inject(true) { r, dep ->
r && dep.values.inject(true) { res, v ->
res && (!(v instanceof Task) || v?.state.getSkipped())
}
}
}
upToDateWhen should return true if this task should not be run at all (because its output are already up-to-date). And this is the case when no dependent task was run (the gradle documentation is a bit vague about this I must admit but getSkipped seems work as expected).

Resources