Search code examples
gradlegradle-kotlin-dsl

Is there a way to define property to be used in both settings.gradle.kts and projects/subprojects build.gradle.kts with gradle 6.0?


We have multi-module android app with build logic written in gradle kotlin dsl. We use buildSrc to extract common logic like dependencies versions. We have something like:

buildSrc/src/main/kotlin/Dependencies.kt:

object Versions {
    const val fooVersion = "1.2.3"
    const val barVersion = "4.5.6"
}

object Libraries {
    val foo = "com.example.foo:foo:$fooVersion"
    val bar = "com.example.bar:bar:$barVersion"
}

object Modules {
    const val app = ":app"
    const val base = ":base"
    const val baz = ":baz"
}

Then we can use these in modules' dependencies block to avoid hardcoded/duplicated values:

app/build.gradle.kts:

dependencies {
    implementation(Libs.foo)
    implementation(Libs.bar)

    implementation(project(Modules.base))
    implementation(project(Modules.baz))
}

And we also use it in settings.gradle.kts:

settings.gradle.kts:

include(
    Modules.app,
    Modules.base,
    Modules.baz
)

This works ok with gradle 5.6. When I upgrade to 6.0, I get Unresolved reference: Modules in settings.gradle.kts file. I found it mentioned in migration guide:

Previously, the buildSrc project was built before applying the project’s settings script and its classes were visible within the script. Now, buildSrc is built after the settings script and its classes are not visible to it. The buildSrc classes remain visible to project build scripts and script plugins.

Custom logic can be used from a settings script by declaring external dependencies.

So I know what broke the build and I can fix the build by using hardcoded values in settings.gradle.kts:

include(
    ":app",
    ":base",
    ":baz"
)

Is it possible to avoid this duplication with gradle 6.0?


Solution

  • Please make sure you read the updates down below.

    Original answer

    See ticket #11090 "Definitions from buildSrc/ not found in settings.gradle.kts using gradle 6.0-rc-1". As you already noticed this changed recently:

    This has changed in 6.0, and was deprecated in 5.6. Please see: https://docs.gradle.org/current/userguide/upgrading_version_5.html#buildsrc_usage_in_gradle_settings

    -- https://github.com/gradle/gradle/issues/11090#issuecomment-544473179

    One of the maintainers describes the reasons behind the decision:

    Unfortunately, there are pros and cons to both arrangements (settings-then-buildSrc and buildSrc-then-settings), and we opted for the former after considering.

    (...)

    The pros that compelled us to make the change:

    1. Settings plugins can influence buildSrc and main build (i.e. apply a build plugin to both)
    2. Build cache configuration is applied to buildSrc
    3. buildSrc behaves more like a regular included build

    -- https://github.com/gradle/gradle/issues/11090#issuecomment-545697268

    And finally some bad news:

    We won't be changing the behaviour back to the pre Gradle 6 arrangement. Please let us know if you would like more detail on how to use one of the alternative mechanisms for using complex logic in a settings script.

    -- https://github.com/gradle/gradle/issues/11090#issuecomment-545697268

    Workarounds

    In the aforementioned post the author proposes some workarounds:

    The con of this is exactly what you have hit. It's now less convenient to use complex logic in your settings script. Now, you have to either:

    1. Inline the logic into the settings file
    2. Move the logic to a shared script that can be used where it needs to
    3. Move the logic to a pre-built binary that you load in the settings file (i.e. a settings plugin)

    -- https://github.com/gradle/gradle/issues/11090#issuecomment-545697268

    #1 is pretty straightforward, but I can only assume what #2 and #3 mean. I come from the Groovy world and only recently started making friends with Kotlin DSL. Having said that let's give it a try.

    In #3 the author might be talking about developing an external plugin and applying it in both scripts. I'm not really sure if this is something that would make sense to implement (it gives you strong typing though).

    "#2 Move the logic to a shared script that can be used where it needs to"

    I think it's about having a common script plugin and including it in both settings.gradle and build.gradle files. The plugin would put the static information in the ExtraPropertiesExtension of the ExtensionAware in scope (Settings in case of a settings.gradle script plugin and Project in case of build.gradle). This is described in this answer to "Include scripts with Gradle Kotlin DSL":

    How can I put all common constants (such as dependency versions) to the separate file to include them just by using something like springBootVersion or Constants.springBootVersion with compile-time checks?

    There is no good way to do it currently. You can use extra properties, but it won't guarantee compile time checks. Something like that:

    // $rootDir/dependencies.gradle.kts
    
    // this will try to take configuration from existing ones
    val compile by configurations
    val api by configurations
    dependencies {
      compile("commons-io:commons-io:1.2.3")
      api("some.dep")
    }
    
    // This will put your version into extra extension
    extra["springBootVersion"] = "1.2.3"
    

    And you can use it like this:

    // $rootDir/build.gradle.kts
    subprojects {
      apply {
        plugin<JavaLibraryPlugin>()
        from("$rootDir/dependencies.gradle.kts")
      }
    

    And in your module:

    // $rootDir/module/build.gradle.kts
    // This will take existing dependency from extra
    val springBootVersion: String by extra
    dependencies {
      compile("org.spring:boot:$springBootVersion")
    }
    

    -- Include scripts with Gradle Kotlin DSL

    UPDATE 1

    The issue has become popular and gained attraction from Gradle maintainers:

    Since there are so many comments still on this issue, let me clarify a few things about where we are and where we are actively moving to right now.

    For the general topic "I want buildSrc to be done before anything else" there will be a solution soon with included builds and setting plugins. Most likely in the next release. Then you will be able to include a build, using a new DSL method, that is available earlier. This build can then contain a settings plugin which you can apply in settings. This makes the Jar containing the plugin available on the settings classpath. (Although ideally you would define a plugin extension and not use classes/static methods directly.)

    settings.gradle.kts

    pluginManagement {
        includeBuildEarly("my-build-logic") //<- WIP - new API under development
    }
    plugins {
       apply("my.settings.plugin) // this is defined in the "my-build-logic" build
    }
    

    For the topic of sharing dependencies and versions, we are also working on general improvements that might make some "custom solutions" unnecessary in the future.

    Generally, you should try to avoid using buildscript {} or using resolutionStrategy {} in settings. Instead you can define plugins in included builds. You can then manage all dependencies (also to plugins) in the build files of these builds.

    If you want to share constants between all these builds, you can have one build only for these constants that you include in all others. Like the libraries build in this example..

    See also the sample about sharing convention plugins with build logic build.

    Hope these are some helpful pointers.

    -- https://github.com/gradle/gradle/issues/11090#issuecomment-734795353

    UPDATE 2

    Nowadays (Gradle 7+) you could use Version Catalogs to share dependency versions across buildSrc and the regular build. Please refer to the official documentation and this answer.