From e1c831c0eab5e15f91135a108786225b1e7c68a1 Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 19 Nov 2024 16:38:38 +0000 Subject: [PATCH 1/2] Apply update from Validation --- .../gradle/protobuf/ProtoTaskExtensions.kt | 212 ++++++++++++------ 1 file changed, 144 insertions(+), 68 deletions(-) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt index d9928807..f5fa9326 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt @@ -27,14 +27,18 @@ package io.spine.gradle.protobuf import com.google.protobuf.gradle.GenerateProtoTask +import com.google.protobuf.gradle.ProtobufExtension import io.spine.gradle.sourceSets import java.io.File import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths import java.nio.file.StandardOpenOption.TRUNCATE_EXISTING import org.gradle.api.Project import org.gradle.api.file.SourceDirectorySet import org.gradle.api.tasks.SourceSet import org.gradle.kotlin.dsl.get +import org.gradle.kotlin.dsl.getByType import org.gradle.plugins.ide.idea.GenerateIdeaModule import org.gradle.plugins.ide.idea.model.IdeaModel import org.gradle.plugins.ide.idea.model.IdeaModule @@ -42,10 +46,10 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinCompile import titleCaseFirstChar /** - * Obtains the name of the `generated` directory under the project root directory. + * Obtains the path of the `generated` directory under the project root directory. */ -private val Project.generatedDir: String - get() = "${projectDir}/generated" +private val Project.generatedDir: Path + get() = projectDir.resolve("generated").toPath() /** * Obtains the `generated` directory for the source set of the task. @@ -58,14 +62,14 @@ private fun GenerateProtoTask.generatedDir(language: String = ""): File { } /** - * Configures protobuf code generation task for the code which cannot use Spine Model Compiler - * (e.g. the `base` project). + * Configures a [GenerateProtoTask] for the code which cannot use Spine Model Compiler + * (e.g., Spine Base or Spine Validation modules). * * The task configuration consists of the following steps: * * 1. Adding `"kotlin"` to the list of involved `protoc` builtins. * - * 2. Generation of descriptor set file is turned on for each source set. + * 2. Turning on the generation of a descriptor set file for each source set. * These files are placed under the `build/descriptors` directory. * * 3. Removing source code generated for `com.google` package for both Java and Kotlin. @@ -78,10 +82,8 @@ private fun GenerateProtoTask.generatedDir(language: String = ""): File { * The usage of this extension in a module build file would be: * ``` * protobuf { - * generateProtoTasks { - * for (task in all()) { - * task.setup() - * } + * generateProtoTasks.all().configureEach { + * setup() * } * } * ``` @@ -109,29 +111,30 @@ fun GenerateProtoTask.setup() { excludeProtocOutput() setupKotlinCompile() dependOnProcessResourcesTask() - configureIdeaDirs() + makeDirsForIdeaModule() } /** * Tell `protoc` to generate descriptor set files under the project build dir. * * The name of the descriptor set file to be generated - * is made to be unique per project's Maven coordinates. + * is made to be unique via the project's Maven coordinates. * * As the last step of this task, writes a `desc.ref` file * for the contextual source set, pointing to the generated descriptor set file. - * This is needed in order to allow other Spine libraries - * to locate and load the generated descriptor set files properly. + * This is needed to allow other Spine libraries to locate and load the generated + * descriptor set files properly. * - * Such a job is usually performed by Spine McJava plugin, + * Such a job is usually performed by Spine McJava plugin; * however, it is not possible to use this plugin (or its code) * in this repository due to cyclic dependencies. */ @Suppress( - "TooGenericExceptionCaught" /* Handling all file-writing failures in the same way.*/) -private fun GenerateProtoTask.setupDescriptorSetFileCreation() { - // Tell `protoc` generate descriptor set file. - // The name of the generated file reflects project's Maven coordinates. + "TooGenericExceptionCaught" /* Handling all file-writing failures in the same way.*/ +) +fun GenerateProtoTask.setupDescriptorSetFileCreation() { + // Tell `protoc` generate a descriptor set file. + // The name of the generated file reflects the Maven coordinates of the project. val ssn = sourceSet.name generateDescriptorSet = true val buildDir = project.layout.buildDirectory.asFile.get().path @@ -143,7 +146,7 @@ private fun GenerateProtoTask.setupDescriptorSetFileCreation() { includeSourceInfo = true } - // Make the descriptor set file included into the resources. + // Add the descriptor set file into the resources. project.sourceSets.named(ssn) { resources.srcDirs(descriptorsDir) } @@ -167,7 +170,7 @@ private fun GenerateProtoTask.setupDescriptorSetFileCreation() { * reflecting the Maven coordinates of Gradle artifact, and the source set * for which the descriptor set name is to be generated. * - * The returned value is just a file name, and does not contain a file path. + * The returned value is just a file name and does not contain a file path. */ private fun Project.descriptorSetName(sourceSet: SourceSet) = arrayOf( @@ -220,7 +223,7 @@ private fun GenerateProtoTask.deleteComGoogle(language: String) { * Exclude [GenerateProtoTask.outputBaseDir] from Java source set directories to avoid * duplicated source code files. */ -private fun GenerateProtoTask.excludeProtocOutput() { +fun GenerateProtoTask.excludeProtocOutput() { val protocOutputDir = File(outputBaseDir).parentFile val java: SourceDirectorySet = sourceSet.java @@ -228,6 +231,8 @@ private fun GenerateProtoTask.excludeProtocOutput() { val newSourceDirectories = java.sourceDirectories .filter { !it.residesIn(protocOutputDir) } .toSet() + // Make sure we start from scratch. + // Not doing this failed the following, real, assignment sometimes. java.setSrcDirs(listOf()) java.srcDirs(newSourceDirectories) @@ -239,7 +244,7 @@ private fun GenerateProtoTask.excludeProtocOutput() { /** * Make sure Kotlin compilation explicitly depends on this `GenerateProtoTask` to avoid racing. */ -private fun GenerateProtoTask.setupKotlinCompile() { +fun GenerateProtoTask.setupKotlinCompile() { val kotlinCompile = project.kotlinCompileFor(sourceSet) kotlinCompile?.dependsOn(this) } @@ -253,7 +258,7 @@ private fun GenerateProtoTask.setupKotlinCompile() { * by Gradle during the build because Protobuf Gradle Plugin does not set * dependencies between `generateProto` and `processResources` tasks. */ -private fun GenerateProtoTask.dependOnProcessResourcesTask() { +fun GenerateProtoTask.dependOnProcessResourcesTask() { val processResources = processResourceTaskName(sourceSet.name) project.tasks[processResources].dependsOn(this) } @@ -283,63 +288,134 @@ private fun Project.kotlinCompileFor(sourceSet: SourceSet): KotlinCompile<*>? { private fun File.residesIn(directory: File): Boolean = canonicalFile.startsWith(directory.absolutePath) -private fun GenerateProtoTask.configureIdeaDirs() = project.plugins.withId("idea") { - val module = project.extensions.findByType(IdeaModel::class.java)!!.module - - // Make IDEA forget about sources under `outputBaseDir`. - val protocOutputDir = File(outputBaseDir).parentFile - module.generatedSourceDirs.removeIf { dir -> - dir.residesIn(protocOutputDir) +/** + * Ensures that generated directories for Java and Kotlin are created before [GenerateIdeaModule]. + * + * This works as advised by `Utils.groovy` from Protobuf Gradle plugin: + * ``` + * This is required because the IntelliJ IDEA plugin does not allow adding source directories + * that do not exist. The IntelliJ IDEA config files should be valid from the start even if + * a user runs './gradlew idea' before running './gradlew generateProto'. + * ``` + */ +fun GenerateProtoTask.makeDirsForIdeaModule() { + project.plugins.withId("idea") { + val javaDir = generatedDir("java") + val kotlinDir = generatedDir("kotlin") + project.tasks.withType(GenerateIdeaModule::class.java).forEach { + it.doFirst { + javaDir.mkdirs() + kotlinDir.mkdirs() + } + } } +} - module.sourceDirs.removeIf { dir -> - dir.residesIn(protocOutputDir) - } +/** + * Prints diagnostic output of `sourceDirs` and `generatedSourceDirs` of an [IdeaModule]. + * + * To get a handle on [IdeaModule] please use the following code: + * + * ```kotlin + * val module = project.extensions.findByType(IdeaModel::class.java)!!.module + * ``` + */ +@Suppress("unused") // To be used when debugging build scripts. +fun IdeaModule.printSourceDirectories() { + println("**** [IDEA] Source directories:") + sourceDirs.forEach { println(it) } + println() + println("**** [IDEA] Generated source directories:") + generatedSourceDirs.forEach { println(it) } + println() + println("**** [IDEA] Excluded directories:") + excludeDirs.forEach { println(it) } +} + +/** + * Obtains the directory where the Protobuf Gradle Plugin should place the generated code. + * + * The directory is fixed to be `$buildDir/generated/source/proto` and cannot be + * changed by the settings of the plugin. Even though [ProtobufExtension] has a property + * [generatedFilesBaseDir][ProtobufExtension.getGeneratedFilesBaseDir], which is supposed + * to be used for this purpose, it is declared with `@PackageScope` and thus cannot be + * accessed from outside the plugin. The Protobuf Gradle Plugin (at v0.9.2) does not + * modify the value of the property either. + */ +val Project.generatedSourceProtoDir: Path + get() = layout.buildDirectory.dir("generated/source/proto").get().asFile.toPath() - val javaDir = generatedDir("java") - val kotlinDir = generatedDir("kotlin") +/** + * Ensures that the sources generated by Protobuf Gradle Plugin + * are not included in the IDEA project. + * + * IDEA should only see the sources generated by ProtoData as + * we define in [GenerateProtoTask.excludeProtocOutput]. + */ +fun Project.configureIdea() { + + fun filterSources(sources: Set, excludeDir: File): Set = + sources.filter { !it.residesIn(excludeDir) }.toSet() - // As advised by `Utils.groovy` from Protobuf Gradle plugin: - // This is required because the IntelliJ IDEA plugin does not allow adding source directories - // that do not exist. The IntelliJ IDEA config files should be valid from the start even if - // a user runs './gradlew idea' before running './gradlew generateProto'. - project.tasks.withType(GenerateIdeaModule::class.java).forEach { - it.doFirst { - javaDir.mkdirs() - kotlinDir.mkdirs() + pluginManager.withPlugin("idea") { + val idea = extensions.getByType() + with(idea.module) { + val protocOutput = file(generatedSourceProtoDir) + val protocTargets = protocTargets() + excludeWithNested(protocOutput.toPath(), protocTargets) + sourceDirs = filterSources(sourceDirs, protocOutput) + testSources.filter { !it.residesIn(protocOutput) } + generatedSourceDirs = generatedDir.resolve(protocTargets) + .map { it.toFile() } + .toSet() } } +} - if (isTest) { - module.testSources.run { - from(javaDir) - from(kotlinDir) - } - } else { - module.sourceDirs.run { - add(javaDir) - add(kotlinDir) +/** + * Lists target directories for Protobuf code generation. + * + * The directory names are in the following format: + * + * `/` + */ +private fun Project.protocTargets(): List { + val protobufTasks = tasks.withType(GenerateProtoTask::class.java) + val codegenTargets = sequence { + protobufTasks.forEach { task -> + val sourceSet = task.sourceSet.name + val builtins = task.builtins.map { builtin -> builtin.name } + val plugins = task.plugins.map { plugin -> plugin.name } + val combined = builtins + plugins + combined.forEach { subdir -> + yield(Paths.get(sourceSet, subdir)) + } } } + return codegenTargets.toList() +} - module.generatedSourceDirs.run { - add(javaDir) - add(kotlinDir) +private fun Path.resolve(subdirs: Iterable): List = + subdirs.map { + resolve(it) } -} /** - * Prints diagnostic output of `sourceDirs` and `generatedSourceDirs` of an [IdeaModule]. + * Excludes the given directory and its subdirectories from + * being seen as ones with the source code. * - * The warning `"unused"` is suppressed because this function is not used in - * the production mode. + * The primary use of this extension is to exclude `build/generated/source/proto` and its + * subdirectories to avoid duplication of types in the generated code with those in + * produced by ProtoData under the `$projectDir/generated/` directory. */ -@Suppress("unused") -private fun IdeaModule.printSourceDirectories() { - println("**** [IDEA] Source directories:") - sourceDirs.forEach { println(it) } - println() - println("**** [IDEA] Generated source directories:") - generatedSourceDirs.forEach { println(it) } - println() +private fun IdeaModule.excludeWithNested(directory: Path, subdirs: Iterable) { + excludeDirs.add(directory.toFile()) + directory.resolve(subdirs).forEach { + excludeDirs.add(it.toFile()) + } +} + +@Suppress("unused") // To be used when debugging build scripts. +private fun printExcluded(dir: Any) { + println(" [IDEA] Excluding directory: $dir") } From a1c1cbff9ba969ddf8ad26d9ae83fc510a905e7e Mon Sep 17 00:00:00 2001 From: alexander-yevsyukov Date: Tue, 19 Nov 2024 16:38:57 +0000 Subject: [PATCH 2/2] Suppress `unused` --- .../main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt b/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt index f5fa9326..8d75d8f6 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt @@ -352,6 +352,7 @@ val Project.generatedSourceProtoDir: Path * IDEA should only see the sources generated by ProtoData as * we define in [GenerateProtoTask.excludeProtocOutput]. */ +@Suppress("unused") fun Project.configureIdea() { fun filterSources(sources: Set, excludeDir: File): Set =