Search code examples
gradle

Gradle copy/clone/inherit from existing task?


The following Gradle config lets me run gradle bootBuildImage inside of bitbucket-pipelines. But the same config settings also break the same command gradle bootBuildImage when run outside of bitbucket-pipelines.

I want one Gradle task to run locally (outside of bitbucket) and a second Gradle task to run within bitbucket. How do I do this? Can I copy the Gradle task? Or inherit it?

tasks.named<BootBuildImage>("bootBuildImage") {
    docker {
        host.set("tcp://172.17.0.1:2375")
        bindHostToBuilder.set(true)
        securityOptions.set(listOf())
        buildWorkspace {
            bind {
                source.set("/opt/atlassian/bitbucketci/agent/build/cache-${project.name}.work")
            }
        }
        buildCache {
            bind {
                source.set("/opt/atlassian/bitbucketci/agent/build/cache-${project.name}.build")
            }
        }
        launchCache {
            bind {
                source.set("/opt/atlassian/bitbucketci/agent/build/cache-${project.name}.launch")
            }
        }
    }
}

I tried the following, registering the same BootBuildImage class with a new task name:

tasks.register<BootBuildImage>("localBuildImage") {
}

However this fails because it lacks the config settings that the spring plugins give to bootBuildImage. I was hoping to be able to copy bootBuildImage or make a derived version that inherits from bootBuildImage. Are either of those possible?


Solution

  • Copying and inheriting existing tasks are not really Gradle-esque approaches, probably because the lazy APIs mean it wouldn't be clear what you were copying.

    Ideally the Spring Boot plugin would provide a function or other mechanism for defining such tasks with common configuration. As it doesn't, I think the best way is just to look at the plugin and copy over the key properties (as it turns out, there is really only one, the archiveFile – see code below).

    Moreover, the proper Gradle way to deal with such repeated configuration is to use a set of domain objects, so let's do it that way as it's the weekend. This gives you flexibility if you want to repeat such configuration further and would be ideal if you ever decide to extend what you've done to your own Gradle plugin.

    To follow this approach, first create domain objects to represent your different environments:

    class Environment(
        val name: String, val buildDir: File, val bootBuildTaskName: String
    ) {
        /**
         * A useful method for the task definitions later
         */
        fun cachePath(baseName: String): String {
            return buildDir.resolve(baseName).canonicalPath
        }
    }
    
    val environments = objects.domainObjectSet(Environment::class.java)
    
    environments.add(
        Environment(
            name = "BitBucket",
            buildDir = File("/opt/atlassian/bitbucketci/agent/build",
            bootBuildTaskName = "bootBuildImage"
        )
    )
    environments.add(
        Environment(
            name = "Local",
            buildDir = file("local"),
            bootBuildTaskName = "bootBuildImageLocal"
        )
    )
    

    Then you can use the power of the methods on the DomainObjectSet to pick up all Environment objects, while allowing new ones to be added at any time in the build (very useful for plugins):

    tasks {
        val bootBuildImage = named<BootBuildImage>("bootBuildImage")
        environments.all { // Inside here each Environment object is the "this" context
            if (!names.contains(bootBuildTaskName))
                /*
                 * Here we copy the task properties by hand as defined by the Spring Boot plugin.
                 * See code at [GitHub](https://github.com/spring-projects/spring-boot/blob/28dc9cbb598edc29bcd8770ca2f34972340ad6af/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java#L84)
                 */
                register(bootBuildTaskName) {
                    group = BasePlugin.BUILD_GROUP
                    archiveFile.value(bootBuildImage.flatMap { archiveFile })
                }
            /**
             * Now we can do common configuration in a DRY fashion
             *   on the task which correspond to each `Environment` object
             */
            named<BootBuildImage>(bootBuildTaskName) {
                description = "Builds an OCI image of the application using the output of the bootJar task in the $name environment"
                host.set("tcp://172.17.0.1:2375")
                bindHostToBuilder.set(true)
                securityOptions.set(listOf())
                buildWorkspace {
                    bind {
                        source.set(cachePath("cache-${project.name}.work"))
                    }
                }
                buildCache {
                    bind {
                        source.set(cachePath("cache-${project.name}.build"))
                    }
                }
                launchCache {
                    bind {
                        source.set(cachePath("cache-${project.name}.launch"))
                    }
                }
            }
        }
    }