Skip to content

Commit

Permalink
Merge pull request #835 from SpineEventEngine/update-proto-extensions
Browse files Browse the repository at this point in the history
Update `ProtoTaskExtensions.kt`
  • Loading branch information
alexander-yevsyukov authored Nov 19, 2024
2 parents 6b87c76 + aa75a8e commit 076cc71
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 77 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

# Spine Base

This repository contains common data types and utilities used by the Spine SDK subprojects (e.g.
[core-java][core-java]).
This repository contains common data types and utilities used by
the Spine SDK subprojects (e.g. [core-java][core-java]).

## Adding to a Gradle project

Spine Base is not supposed to be used directly in end user project. But if you need to,
here's how you add it to your Gradle project:
Spine Base is not supposed to be used directly in an end-user project.
But if you need to, here's how you add it to your Gradle project:

```kotlin
dependencies {
Expand Down
212 changes: 144 additions & 68 deletions buildSrc/src/main/kotlin/io/spine/gradle/protobuf/ProtoTaskExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,29 @@
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
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.
Expand All @@ -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.
Expand All @@ -78,10 +82,8 @@ private fun GenerateProtoTask.generatedDir(language: String = ""): File {
* The usage of this extension in a <em>module build file</em> would be:
* ```
* protobuf {
* generateProtoTasks {
* for (task in all()) {
* task.setup()
* }
* generateProtoTasks.all().configureEach {
* setup()
* }
* }
* ```
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand All @@ -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(
Expand Down Expand Up @@ -220,14 +223,16 @@ 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

// Filter out directories belonging to `build/generated/source/proto`.
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<String>())
java.srcDirs(newSourceDirectories)

Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down Expand Up @@ -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<File>, excludeDir: File): Set<File> =
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<IdeaModel>()
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:
*
* `<source-set-name>/<builtIn-or-plugin-name>`
*/
private fun Project.protocTargets(): List<Path> {
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<Path>): List<Path> =
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<Path>) {
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")
}
4 changes: 2 additions & 2 deletions dependencies.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.217`
# Dependencies of `io.spine:spine-base:2.0.0-SNAPSHOT.218`

## Runtime
1. **Group** : com.google.code.findbugs. **Name** : jsr305. **Version** : 3.0.2.
Expand Down Expand Up @@ -845,4 +845,4 @@

The dependencies distributed under several licenses, are used according their commercial-use-friendly license.

This report was generated on **Fri Nov 01 15:49:52 WET 2024** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
This report was generated on **Tue Nov 19 15:59:03 WET 2024** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE).
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ all modules and does not describe the project structure per-subproject.
-->
<groupId>io.spine</groupId>
<artifactId>base</artifactId>
<version>2.0.0-SNAPSHOT.217</version>
<version>2.0.0-SNAPSHOT.218</version>

<inceptionYear>2015</inceptionYear>

Expand Down
2 changes: 1 addition & 1 deletion version.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

val versionToPublish: String by extra("2.0.0-SNAPSHOT.217")
val versionToPublish: String by extra("2.0.0-SNAPSHOT.218")

0 comments on commit 076cc71

Please sign in to comment.