diff --git a/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionExtension.kt b/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionExtension.kt index 41b3908..9934af3 100644 --- a/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionExtension.kt +++ b/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionExtension.kt @@ -1,10 +1,7 @@ package org.metaborg.convention import org.gradle.api.model.ObjectFactory -import org.gradle.api.plugins.JavaPlugin import org.gradle.api.provider.Property -import org.gradle.api.publish.plugins.PublishingPlugin -import org.gradle.language.base.plugins.LifecycleBasePlugin import javax.inject.Inject /** Configuration for the root project convention. */ @@ -12,32 +9,18 @@ open class RootProjectConventionExtension @Inject constructor( /** The Gradle object factory. */ objects: ObjectFactory, ) { - /** Suffix for all task names. */ - val taskNameSuffix: Property = objects.property(String::class.java) - .convention("") - /** The name of the `assemble` task. */ - val assembleTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${LifecycleBasePlugin.ASSEMBLE_TASK_NAME}$it"} ) - /** The name of the `build` task. */ - val buildTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${LifecycleBasePlugin.BUILD_TASK_NAME}$it"} ) - /** The name of the `clean` task. */ - val cleanTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${LifecycleBasePlugin.CLEAN_TASK_NAME}$it"} ) - /** The name of the `publish` task. */ - val publishTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}$it"} ) - /** The name of the `publishToMavenLocal` task. */ - val publishToMavenLocalTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}${it}ToMavenLocal"} ) - /** The name of the `check` task. */ - val checkTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${LifecycleBasePlugin.CHECK_TASK_NAME}$it"} ) - /** The name of the `test` task. */ - val testTaskName: Property = objects.property(String::class.java) - .convention(taskNameSuffix.map { "${JavaPlugin.TEST_TASK_NAME}$it"} ) - /** The name of the `tasks` task. */ - val tasksTaskName: Property = objects.property(String::class.java) - .convention("allTasks") + /** Whether to add the aggregate `assembleAll`, `buildAll`, `checkAll`, and `cleanAll` lifecycle tasks. */ + val addAggregateLifecycleTasks: Property = objects.property(Boolean::class.java) + .convention(true) + /** Whether to add the aggregate `publishAll`, `publishAllToMavenLocal` lifecycle tasks. */ + val addAggregatePublishTasks: Property = objects.property(Boolean::class.java) + .convention(false) + + /** Whether to add stub `assemble`, `build`, `check`, and `clean` lifecycle tasks that depend on their `*All` counterparts, if not already defined. */ + val addStubLifecycleTasks: Property = objects.property(Boolean::class.java) + .convention(true) + /** Whether to add stub `publish` and `publishToMavenLocal` tasks that depend on their `*All` counterparts, if not already defined. */ + val addStubPublishTasks: Property = objects.property(Boolean::class.java) + .convention(false) } \ No newline at end of file diff --git a/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionPlugin.kt b/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionPlugin.kt index 19e4302..f94e84c 100644 --- a/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionPlugin.kt +++ b/convention-plugin/src/main/kotlin/org/metaborg/convention/RootProjectConventionPlugin.kt @@ -2,23 +2,24 @@ package org.metaborg.convention import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.plugins.JavaPlugin +import org.gradle.api.Task import org.gradle.api.publish.plugins.PublishingPlugin +import org.gradle.api.tasks.TaskProvider import org.gradle.kotlin.dsl.create import org.gradle.language.base.plugins.LifecycleBasePlugin /** - * Configures a root project of a multi-project build, by adding explicit tasks + * Configures a root project of a multi-project/composite build, by adding explicit lifecycle tasks * that call tasks in the sub-builds and subprojects. * - * Normally, when you execute a task such as `test` in a multi-project build, you will execute - * all `:test` tasks in all projects. In contrast, when you specifically execute `:test` - * (prefixed with a colon), you execute the `:test` task only in the root project. + * Normally, when you execute a task such as `check` in a multi-project build, you will execute + * all `:check` tasks in all projects. In contrast, when you specifically execute `:check` + * (prefixed with a colon), you execute the `:check` task only in the root project. * Now, we would like to create a task in this composite build that executes basically - * the equivalent of `test` in each of the included multi-project builds, which would execute - * `:test` in each of the projects. However, this seems to be impossible to write down. Instead, - * we call the root `:test` task in the included build, and in each included build's multi-project - * root project we'll extend the `test` task to depend on the `:test` tasks of the subprojects. + * the equivalent of `check` in each of the included multi-project builds, which would execute + * `:check` in each of the projects. However, this seems to be impossible to write down. Instead, + * we call the root `:check` task in the included build, and in each included build's multi-project + * root project we'll extend the `check` task to depend on the `:check` tasks of the subprojects. */ class RootProjectConventionPlugin: Plugin { override fun apply(project: Project): Unit = with(project) { @@ -26,60 +27,113 @@ class RootProjectConventionPlugin: Plugin { val extension = extensions.create("rootProjectConvention") afterEvaluate { - // Build tasks - tasks.register(extension.assembleTaskName.get()) { - group = LifecycleBasePlugin.BUILD_GROUP - description = "Assembles the outputs of the subprojects and included builds." - dependsOn(gradle.includedBuilds.map { it.task(":${LifecycleBasePlugin.ASSEMBLE_TASK_NAME}") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(LifecycleBasePlugin.ASSEMBLE_TASK_NAME) }) - } - tasks.register(extension.buildTaskName.get()) { - group = LifecycleBasePlugin.BUILD_GROUP - description = "Assembles and tests the subprojects and included builds." - dependsOn(gradle.includedBuilds.map { it.task(":${LifecycleBasePlugin.BUILD_TASK_NAME}") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(LifecycleBasePlugin.BUILD_TASK_NAME) }) - } - tasks.register(extension.cleanTaskName.get()) { - group = LifecycleBasePlugin.BUILD_GROUP - description = "Cleans the outputs of the subprojects and included builds." - dependsOn(gradle.includedBuilds.map { it.task(":${LifecycleBasePlugin.CLEAN_TASK_NAME}") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(LifecycleBasePlugin.CLEAN_TASK_NAME) }) - } + // Lifecycle tasks + registerRootTasks( + "${LifecycleBasePlugin.ASSEMBLE_TASK_NAME}All", + LifecycleBasePlugin.ASSEMBLE_TASK_NAME, + LifecycleBasePlugin.BUILD_GROUP, + "Assembles the outputs of this project, subprojects, and included builds.", + extension.addAggregateLifecycleTasks.get(), + extension.addStubLifecycleTasks.get(), + ) + registerRootTasks( + "${LifecycleBasePlugin.BUILD_TASK_NAME}All", + LifecycleBasePlugin.BUILD_TASK_NAME, + LifecycleBasePlugin.BUILD_GROUP, + "Assembles and tests this project, subprojects, and included builds.", + extension.addAggregateLifecycleTasks.get(), + extension.addStubLifecycleTasks.get(), + ) + registerRootTasks( + "${LifecycleBasePlugin.CLEAN_TASK_NAME}All", + LifecycleBasePlugin.CLEAN_TASK_NAME, + LifecycleBasePlugin.BUILD_GROUP, + "Cleans the outputs of this project, subprojects, and included builds.", + extension.addAggregateLifecycleTasks.get(), + extension.addStubLifecycleTasks.get(), + ) + registerRootTasks( + "${LifecycleBasePlugin.CHECK_TASK_NAME}All", + LifecycleBasePlugin.CHECK_TASK_NAME, + LifecycleBasePlugin.VERIFICATION_GROUP, + "Runs all checks on this project, subprojects, and included builds.", + extension.addAggregateLifecycleTasks.get(), + extension.addStubLifecycleTasks.get(), + ) - // Publishing tasks - tasks.register(extension.publishTaskName.get()) { - group = PublishingPlugin.PUBLISH_TASK_GROUP - description = "Publishes all subprojects and included builds to a remote Maven repository." - dependsOn(gradle.includedBuilds.map { it.task(":${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME) }) - } - tasks.register(extension.publishToMavenLocalTaskName.get()) { - group = PublishingPlugin.PUBLISH_TASK_GROUP - description = "Publishes all subprojects and included builds to the local Maven repository." - dependsOn(gradle.includedBuilds.map { it.task(":${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}ToMavenLocal") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName("${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}ToMavenLocal") }) - } + // Publish tasks + registerRootTasks( + "${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}All", + PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME, + PublishingPlugin.PUBLISH_TASK_GROUP, + "Publishes all publications produced by this project, subprojects, and included builds to remote Maven repositories.", + extension.addAggregatePublishTasks.get(), + extension.addStubPublishTasks.get(), + ) + registerRootTasks( + "${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}AllToMavenLocal", + "${PublishingPlugin.PUBLISH_LIFECYCLE_TASK_NAME}ToMavenLocal", + PublishingPlugin.PUBLISH_TASK_GROUP, + "Publishes all Maven publications produced by this project, subprojects, and included builds to the local Maven cache.", + extension.addAggregatePublishTasks.get(), + extension.addStubPublishTasks.get(), + ) + } + } - // Verification tasks - tasks.register(extension.checkTaskName.get()) { - group = LifecycleBasePlugin.VERIFICATION_GROUP - description = "Runs all checks on the subprojects and included builds." - dependsOn(gradle.includedBuilds.map { it.task(":${LifecycleBasePlugin.CHECK_TASK_NAME}") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(LifecycleBasePlugin.CHECK_TASK_NAME) }) - } - tasks.register(extension.testTaskName.get()) { - group = LifecycleBasePlugin.VERIFICATION_GROUP - description = "Runs all unit tests on the subprojects and included builds." - dependsOn(gradle.includedBuilds.map { it.task(":${JavaPlugin.TEST_TASK_NAME}") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(JavaPlugin.TEST_TASK_NAME) }) + /** + * Registers the tasks in the root project. + * + * This registers a `buildAll` task (if [addAggregateLifecycleTask] is `true`) that depends on + * the existing `build` task in the root project, the `build` tasks in the subprojects, + * and the `build` tasks in the included builds. + * + * This also registers a `build` task (if [addStubLifecycleTask] is `true`) that depends on + * the `buildAll` task (if defined). + * + * @param allTaskName The name of the `*All` task, for example `buildAll`. + * @param taskName The name of the task, for example `build`. + * @param group The group of the tasks. + * @param description The description of the tasks. + * @param addAggregateLifecycleTask Whether to add the aggregate `*All` lifecycle task. + * @param addStubLifecycleTask Whether to add the stub `*` lifecycle task. + */ + private fun Project.registerRootTasks( + allTaskName: String, + taskName: String, + group: String, + description: String, + addAggregateLifecycleTask: Boolean, + addStubLifecycleTask: Boolean, + ) { + // NOTE: The order is important. + // First, we check if there is already a `build` lifecycle task. + // Then we add the `buildAll` lifecycle task, which may depend on the existing `build` task, if any. + // To prevent cycles, only after that do we add the `build` stub lifecycle task itself, + // which depends on the `buildAll` task (if defined). + + val existingTask = project.tasks.findByName(taskName) + + val allTask: TaskProvider? = if (addAggregateLifecycleTask) { + tasks.register(allTaskName) { + this.group = group + this.description = description + // Depend on the normal lifecycle task, if any + existingTask?.let { dependsOn(it) } + // Depend on the normal lifecycle tasks of the subprojects, if present + dependsOn(project.subprojects.mapNotNull { it.tasks.findByName(taskName) }) + // Depend on the normal lifecycle tasks in included builds + // NOTE: This may cause an error if the build doesn't define the specified task. + dependsOn(gradle.includedBuilds.map { it.task(":$taskName") }) } + } else null - // Help tasks - tasks.register(extension.tasksTaskName.get()) { - group = "Help" - description = "Displays all tasks of subprojects and included builds." - dependsOn(gradle.includedBuilds.map { it.task(":tasks") }) - dependsOn(project.subprojects.mapNotNull { it.tasks.findByName("tasks") }) + if (addStubLifecycleTask && existingTask == null) { + tasks.register(taskName) { + this.group = group + this.description = description + // Depend on the aggregate lifecycle task, if any + allTask?.let { dependsOn(it) } } } } diff --git a/docs/content/conventions/rootproject.md b/docs/content/conventions/rootproject.md index c993361..caf7bfb 100644 --- a/docs/content/conventions/rootproject.md +++ b/docs/content/conventions/rootproject.md @@ -2,7 +2,7 @@ title: "Root Project Convention" --- # Root Project Convention Plugin -The Root Project convention plugin adds tasks that invoke the corresponding tasks on the sub-builds and subprojects. +The Root Project convention plugin adds `buildAll`, `assembleAll`, `cleanAll`, and `checkAll` tasks that invoke the corresponding tasks on the sub-builds and subprojects. Optionally, it can add `publishAll` and `publishAllToMavenLocal` tasks as well, but this is generally not recommended as not all included builds may have the `maven-publish` plugin applied. ```kotlin title="build.gradle.kts" plugins { @@ -15,15 +15,10 @@ The plugin can be configured using the `rootProjectConvention` extension: ```kotlin title="build.gradle.kts" rootProjectConvention { - // The suffix for tasks that invoke tasks in subprojects and included builds - taskNameSuffix.set("All") - assembleTaskName.set("assembleAll") - buildTaskName.set("buildAll") - cleanTaskName.set("cleanAll") - publishTaskName.set("publishAll") - publishToMavenLocalTaskName.set("publishAllToMavenLocal") - checkTaskName.set("checkAll") - testTaskName.set("testAll") - tasksTaskName.set("allTasks") + addAggregateLifecycleTasks.set(true) + addAggregatePublishTasks.set(false) + + addStubLifecycleTasks.set(true) + addStubPublishTasks.set(false) } -``` \ No newline at end of file +```