How can I call a function from Gradle's `pluginManagement {}` block?

Hi all, this is my first post on Gradle forums. I have a problem with Gradle and would be very grateful for any help. I originally posted this on StackOverflow, but didn’t receive satisfying responses, so I thought that the official forum might be a good place to ask:)

Introduction

I’m trying to deduplicate the code found in my app’s settings.gradle. Here’s what my file looks like currently:

pluginManagement {
    def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
    def properties = new Properties()
    assert localPropertiesFile.exists()
    localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
    def flutterSdkPath = properties.getProperty("flutter.sdk")
    assert flutterSdkPath != null, "flutter.sdk not set in local.properties"

    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

    plugins {
        id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
    }
}

include ':app'

def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

(You can ignore the technology-specific things and why it has to be done like this – let’s just assume it has to be done like this. If you’re interested in more details, please see this PR to Flutter)

It’s easy to see that the code responsible for loading flutter.sdk property from the local.properties file is duplicated twice. So I set out to remove this duplication.

Attempt 1 (variable)

At first, I tried the below code, only to be quickly reminded by Gradle that the “the pluginManagement {} block must appear before any other statements in the script.”

def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"

pluginManagement {
    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

    plugins {
        id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
    }
}

include ':app'

apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

Attempt 2 (function)

The error message in the previous attempt says that no statements are allowed before the pluginManagement {} block. I thought about moving the code to a separate function, since, in my (modest) understanding, function isn’t a statement in Groovy:

String flutterSdkPath() {
    def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
    def properties = new Properties()
    assert localPropertiesFile.exists()
    localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
    def flutterSdkPath = properties.getProperty("flutter.sdk")
    assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
    return flutterSdkPath
}

pluginManagement {
    includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

    plugins {
        id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
    }
}

include ':app'

apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

Unfortunately, it still doesn’t work, and the error only got less helpful:

FAILURE: Build failed with an exception.

* Where:
Settings file '/Users/bartek/fvm/versions/master/examples/hello_world/android/settings.gradle' line: 20

* What went wrong:
A problem occurred evaluating settings 'android'.
> Could not get unknown property 'flutterSdkPath' for object of type org.gradle.plugin.management.internal.DefaultPluginManagementSpec.

I’m pretty sure it has to do something with how to pluginManagement {} block is treated by Gradle in a special way (that is, it’s evaluated first, before the rest of code in settings.gradle is evaluated)

Attempt 3 (static method in a class)

I thought that maybe all code that’s before the pluginManagement {} block is somehow stripped (?), so I tried creating a class with a static method in it:

pluginManagement {
    includeBuild("${Flutter.flutterSdkPath}/packages/flutter_tools/gradle")

    plugins {
        id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false
    }
}

include ':app'

apply from: "${Flutter.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle"

class Flutter {
    String flutterSdkPath() {
        def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
        def properties = new Properties()
        assert localPropertiesFile.exists()
        localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
        def flutterSdkPath = properties.getProperty("flutter.sdk")
        assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
        return flutterSdkPath
    }
}

This time the error message was even less helpful:

* Where:
Settings file '/Users/bartek/fvm/versions/master/examples/hello_world/android/settings.gradle' line: 11

* What went wrong:
A problem occurred evaluating settings 'android'.
> Flutter

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

After running Gradle with the --stracktrace flag, I noticed this line:

... (<very long stack trace>)
Caused by: java.lang.NoClassDefFoundError: Flutter
...

Looks like the same problem that I encountered in attempt 2).

I have no more ideas on how to move the properties-loading code to a single place and call it from the inside of the pluginManagement {} block and from its outside. So, I’m turning to you :slight_smile:

I think a Gradle Settings Plugin could work here, but I’d prefer not to do it unless necessary.

PS I’d also happily accept an explanation as to why I got an error early in attempt 1), but not in attempt (2).

Unfortunately, it still doesn’t work, and the error only got less helpful:

It says the property you try to reference is not found, as there is no such property anymore as you made it a method.

If you would instead try to call the method, it would tell you that the method is not found.

I’m pretty sure it has to do something with how to pluginManagement {} block is treated by Gradle in a special way (that is, it’s evaluated first, before the rest of code in settings.gradle is evaluated)

Exact, it is extracted and evaluated separately.
The pluginManagement block can define plugin repositories or include builds with settings plugins, so to compile the rest of the script, it needs to be evaluated first separately.
Which is also the reason you cannot use properties or methods from outside the block.
It has to be self-contained.

I thought that maybe all code that’s before the pluginManagement {} block is somehow stripped (?), so I tried creating a class with a static method in it:

As I said, all is “stripped”, otherwise you could also simply define the method after the block and call it.

I think a Gradle Settings Plugin could work here, but I’d prefer not to do it unless necessary.

No, it wouldn’t help due to hen-and-egg problem.
settings plugins are evaluated after the pluginManagement block as that block can define included builds with settings plugins or repositories where to find settings plugins, so a settings plugin is too late to configure that block.

PS I’d also happily accept an explanation as to why I got an error early in attempt 1), but not in attempt (2).

Not sure what you mean by “early”, both errors come more or less immediately.
Just in attempt 1 you had statements before the block which is disallowed in Groovy DSL (it is allowed in Kotlin DSL but wouldn’t work either to be used inside the block) and thus get an error while parsing the script to extract the block, while in attempt 2 you get an error when actually evaluating the block as it uses a property that does not exist.


You can do what you want to achieve by using “extra” properties.
Using them is usually a code-smell and a just a work-around for not doing something properly, but this might be one of the rare cases where its use might be appropriate.

While the pluginManagment block is evaluated separately standalone, it can still modify the model, so you can add a closure as extra property that is then also available to the remaining script:

pluginManagement {
    settings.ext.foo = { "Foo" }
    println("1: ${foo()}")
    includeBuild("${foo()}")
}

println("2: ${foo()}")
1 Like