105

I have a Gradle build script (build.gradle), in which I created some tasks. These tasks consist mostly of method calls. The called methods are also in the build script.

Now, here's the situation:

I am creating a fair amount of build scripts, which contain different tasks, but utilise the same methods from the original script. Thus, I would like to extract these "common methods" in some way, so I can easily re-use them instead of copying them for each new script I create.

If Gradle were PHP, something like the following would be ideal:

//script content
...
require("common-methods.gradle");
...
//more script content

But of course, that isn't possible. Or is it?

Anyway, how can I achieve this result? What is the best possible method to do this? I've read through the Gradle documentation already, but I can't seem to determine which method will be the easiest and best suited for this.


UPDATE:

I've managed to extract the methods in another file

(using apply from: 'common-methods.gradle'),

so the structure is as follows:

parent/
      /build.gradle              // The original build script
      /common-methods.gradle     // The extracted methods
      /gradle.properties         // Properties used by the build script

After executing a task from build.gradle, I've bumped into a new problem: apparently, methods don't get recognized when they're in common-methods.gradle.

How can that be fixed?

starball
  • 20,030
  • 7
  • 43
  • 238
Pieter VDE
  • 2,195
  • 4
  • 25
  • 44
  • Are you sure you need to be writing the methods at all ? You would miss out on some of the Gradle goodies if you write your build scripts in terms of methods, most importantly it will take extra work to get the incremental build to work correctly. The intended abstraction is to use and re-use [Task](https://docs.gradle.org/current/dsl/org.gradle.api.Task.html)s. You can also create [custom tasks](https://docs.gradle.org/current/userguide/custom_tasks.html). Perhaps you should consider putting the implementations you now have in methods into tasks. – Alpar Oct 05 '16 at 06:25
  • 1
    @Alpar and _others_; what purpose is served making something like a `timestamp()` or `currentWorkingDirectory()` methods as _`task`_-s (for example). Utility functions and similar things are nominally scalar -- They wouldn't be tasks except that there are limitations on code-reuse in-built with Gradle and most build systems. I like the *DRY* world where I can make a thing ONE time and reuse it. In fact, extending @Pieter VDE's example I also use a "`root.gradle`" pattern for my parent project -- The build.gradle file usually defines some project specifics and then just `apply ${ROOT}` ... – will Dec 30 '17 at 13:59
  • If you need a centralized way to work with properties maybe this question can help you: https://stackoverflow.com/questions/60251228/how-to-import-a-helper-class-in-buildsrc-build-gradle-kts-settings-gradle-kts/60270896#60270896 – GarouDan Feb 17 '20 at 21:55

6 Answers6

193

Building on Peter's answer, this is how I export my methods:

Content of helpers/common-methods.gradle:

// Define methods as usual
def commonMethod1(param) {
    return true
}
def commonMethod2(param) {
    return true
}

// Export methods by turning them into closures
ext {
    commonMethod1 = this.&commonMethod1
    otherNameForMethod2 = this.&commonMethod2
}

And this is how I use those methods in another script:

// Use double-quotes, otherwise $ won't work
apply from: "$rootDir/helpers/common-methods.gradle"

// You can also use URLs
//apply from: "https://bitbucket.org/mb/build_scripts/raw/master/common-methods.gradle"

task myBuildTask {
    def myVar = commonMethod1("parameter1")
    otherNameForMethod2(myVar)
}

Here's more on converting methods to closures in Groovy.

Matthias Braun
  • 32,039
  • 22
  • 142
  • 171
  • is there any specific reason to use the closure name as ext? – Anoop Dec 28 '16 at 14:29
  • 1
    @AnoopSS We add the two closures to [Gradle's extra properties](https://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtraPropertiesExtension.html). These extra properties are bundled in an object called `ext`. – Matthias Braun Dec 28 '16 at 15:00
  • Can we, somehow, cast the value as a class of ours, which is defined in the included file? – GarouDan Feb 16 '20 at 20:01
  • It's probably a good idea to post a separate question with example code about this, @GarouDan. – Matthias Braun Feb 17 '20 at 10:15
81

It isn't possible to share methods, but you can share extra properties containing a closure, which boils down to the same thing. For example, declare ext.foo = { ... } in common-methods.gradle, use apply from: to apply the script, and then call the closure with foo().

Peter Niederwieser
  • 121,412
  • 21
  • 324
  • 259
  • 1
    It does the trick indeed! But I do have a question about this: How about methods returning something? F.e. `File foo(String f)` will become `ext.foo = { f -> ... }`, can I then just do something like: `File f = foo(...)`? – Pieter VDE Sep 10 '13 at 12:31
  • 2
    Apparently, the question in my previous comment is possible. So thank you Peter, for answering this question! – Pieter VDE Sep 10 '13 at 14:16
  • 1
    @PeterNiederwieser Why isn't it possible? Gradle.org thinks otherwise: https://docs.gradle.org/current/userguide/organizing_build_logic.html#sec:external_build – IgorGanapolsky Jul 28 '16 at 21:41
  • 1
    @IgorGanapolsky thanks for the link. I wonder how can I use a value generated in separate build file in the gradle.build - this way it would be very useful :) – kiedysktos May 10 '17 at 08:08
  • @IgorGanapolsky How should the link you shared help in the context of Peter VDEs question? – t0r0X Jun 09 '17 at 10:08
  • @t0r0X Look for word **shared** – IgorGanapolsky Jun 09 '17 at 12:56
  • @IgorGanapolsky hmmm, ok, looks like you haven't completely read the question details. I thought there was some specialty I overlooked in the docs you linked. "build logic" != "methods/functions", but "tasks". So tasks are shared, but methods/functions not. Therefore the workarounds described by Peter and Matthias are needed. Thanks anyway. – t0r0X Jun 12 '17 at 10:51
  • Any idea how i could achieve something similar for kotlin but typesafe way? like dont guess the type of a function and extract it from ext, but instead import a class or kotlin object which already has all type information for its functions... – vach Aug 29 '19 at 14:19
  • Note that the `ext.` prefix is not optional :) https://docs.gradle.org/current/dsl/org.gradle.api.plugins.ExtraPropertiesExtension.html – Sridhar Sarnobat Mar 24 '21 at 23:26
13

Using the Kotlin DSL it works like this:

build.gradle.kts:

apply {
  from("external.gradle.kts")
}

val foo = extra["foo"] as () -> Unit
foo()

external.gradle.kts:

extra["foo"] = fun() {
  println("Hello world!")
}
Mahozad
  • 18,032
  • 13
  • 118
  • 133
GitProphet
  • 870
  • 1
  • 12
  • 22
  • 2
    great is there a way to share the actuall type? you're basically loosing the type safety and compilers help... if you could share the class that contains your methods then you could make use of the compiler. – vach Aug 29 '19 at 09:44
  • Doesn't work at all, I'm afraid... although I'm trying to call the function from inside another block, not at the root level. Claims the property on `extra` doesn't exist. – Jorn Aug 18 '23 at 12:14
3

I would suggest a slight adjustment to Matthias Braun's answer, in that instead of writing the same method-name twice and still have it clear and consise, why not simply do the following:

ext.commonMethod1 = (param) -> {
    return true
} as Closure<boolean>

The usage of the as-operator simply tells one explicitly, that this function will return a value of boolean-type.

Because after all, this still is Groovy goodness. Neat huh?

1

Another approach for Kotlin DSL could be:

my-plugin.gradle.kts

extra["sum"] = { x: Int, y: Int -> x + y }

settings.gradle.kts

@Suppress("unchecked_cast", "nothing_to_inline")
inline fun <T> uncheckedCast(target: Any?): T = target as T
    
apply("my-plugin.gradle.kts")
    
val sum = uncheckedCast<(Int, Int) -> Int>(extra["sum"])
    
println(sum(1, 2))
Mahozad
  • 18,032
  • 13
  • 118
  • 133
GarouDan
  • 3,743
  • 9
  • 49
  • 75
1

You can create a plugin by using the conventions pattern.

By doing so, you can create a plugin that shares some functions. Here's an example:

Versions.kt

object Versions {

    inline fun Configuration.replace(replacement: Provider<MinimalExternalModuleDependency>, crossinline cause: () -> String? = { null }) {
        val dependency = replacement.get()
        resolutionStrategy.eachDependency {
            if(requested.group == dependency.group && requested.name == dependency.name) {
                useVersion(dependency.version!!)
                val because = cause.invoke()
                if (because != null) {
                    because(because)
                }
            }
        }
    }
}

And its usage: build.gradle.kts


import Versions.replace


configurations.all {
    replace(libs.jackson.core) { "Conflict resolution." }
    replace(libs.jackson.databind) { "The version of the compiler has a security issue associated with this dependency." }
    replace(libs.apache.commons.text) { "The version of the compiler has a security issue associated with this dependency." }
    replace(libs.apache.commons.lang) { "Conflict resolution." }
    replace(libs.slf4j.api) { "Conflict resolution." }
}
LeoFuso
  • 72
  • 4