Skip to content

Commit b2a161d

Browse files
committed
Add Spotless configuration to plugins
1 parent 3f3f370 commit b2a161d

File tree

8 files changed

+338
-1
lines changed

8 files changed

+338
-1
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
agp = "8.11.1"
33
kotlin = "2.0.21"
44
detekt = "1.23.8"
5-
spotless = "7.2.1"
5+
spotless = "8.0.0"
66
kotlinDokka = "2.0.0"
77
gradlePluginPublish = "2.0.0"
88
mavenPublish = "0.32.0"
99

1010
[libraries]
1111
android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" }
1212
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
13+
spotless-gradle-plugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" }
1314

1415
[plugins]
1516
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

plugin/build.gradle.kts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ dependencies {
2424
compileOnly(gradleKotlinDsl())
2525
compileOnly(libs.android.gradle.plugin)
2626
compileOnly(libs.kotlin.gradle.plugin)
27+
implementation(libs.spotless.gradle.plugin)
2728
}
2829

2930
val repoId = "GetStream/stream-build-conventions-android"
@@ -34,6 +35,13 @@ gradlePlugin {
3435
vcsUrl = repoUrl
3536

3637
plugins {
38+
create("root") {
39+
id = "io.getstream.project"
40+
implementationClass = "io.getstream.android.RootConventionPlugin"
41+
displayName = "Stream Root Convention Plugin"
42+
description = "Root convention plugin for Stream projects - configures project-wide settings"
43+
tags = listOf("stream", "conventions", "configuration")
44+
}
3745
create("androidLibrary") {
3846
id = "io.getstream.android.library"
3947
implementationClass = "io.getstream.android.AndroidLibraryConventionPlugin"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-build-conventions-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.android
17+
18+
import java.io.BufferedReader
19+
import org.gradle.api.DefaultTask
20+
import org.gradle.api.file.ConfigurableFileCollection
21+
import org.gradle.api.file.RegularFileProperty
22+
import org.gradle.api.provider.Property
23+
import org.gradle.api.tasks.CacheableTask
24+
import org.gradle.api.tasks.Classpath
25+
import org.gradle.api.tasks.Input
26+
import org.gradle.api.tasks.OutputFile
27+
import org.gradle.api.tasks.TaskAction
28+
29+
/**
30+
* Task that generates license header files from templates bundled in the plugin.
31+
*
32+
* This task uses [@Classpath] to track the plugin JAR that contains the template resources. When
33+
* the plugin is updated with new template content, Gradle will automatically detect the change and
34+
* regenerate the license files.
35+
*/
36+
@CacheableTask
37+
abstract class GenerateLicenseFileTask : DefaultTask() {
38+
@get:Input abstract val repositoryName: Property<String>
39+
40+
@get:Input abstract val templateName: Property<String>
41+
42+
/**
43+
* The classpath containing the plugin resources (the plugin JAR itself). Used to track changes
44+
* to the template files so the task is re-run.
45+
*/
46+
@get:Classpath abstract val pluginClasspath: ConfigurableFileCollection
47+
48+
@get:OutputFile abstract val outputFile: RegularFileProperty
49+
50+
@TaskAction
51+
fun generate() {
52+
val templateContent =
53+
javaClass.classLoader
54+
.getResourceAsStream(templateName.get())
55+
?.bufferedReader()
56+
?.use(BufferedReader::readText)
57+
?: throw IllegalStateException(
58+
"Could not find bundled ${templateName.get()} resource"
59+
)
60+
61+
outputFile
62+
.get()
63+
.asFile
64+
.writeText(templateContent.replace("\$PROJECT", repositoryName.get()))
65+
}
66+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-build-conventions-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.android
17+
18+
import com.diffplug.gradle.spotless.SpotlessExtension
19+
import org.gradle.api.Project
20+
import org.gradle.api.Task
21+
import org.gradle.api.tasks.TaskContainer
22+
import org.gradle.kotlin.dsl.configure
23+
import org.gradle.kotlin.dsl.findByType
24+
import org.gradle.kotlin.dsl.named
25+
import org.gradle.kotlin.dsl.register
26+
27+
internal fun Project.configureSpotless() {
28+
pluginManager.apply("com.diffplug.spotless")
29+
30+
afterEvaluate {
31+
// Get configuration from root project
32+
val projectExtension = requireStreamProjectExtension()
33+
34+
// Get module-specific configuration
35+
val moduleExtension = extensions.findByType<StreamModuleExtension>()
36+
37+
// Check if Spotless should be disabled for this module
38+
if (moduleExtension?.disableSpotless?.getOrElse(false) == true) {
39+
return@afterEvaluate
40+
}
41+
42+
val repositoryName =
43+
projectExtension.repositoryName.orNull
44+
?: error("streamProject.repositoryName must be configured in the root project")
45+
46+
val useKtfmt = projectExtension.useKtfmt.getOrElse(false)
47+
48+
val generateKotlinLicenseTask =
49+
registerLicenseGenerationTask(
50+
taskName = "generateKotlinLicenseHeader",
51+
repositoryName = repositoryName,
52+
templateName = "license-header.txt",
53+
fileName = "license.kt",
54+
)
55+
56+
val generateXmlLicenseTask =
57+
registerLicenseGenerationTask(
58+
taskName = "generateXmlLicenseHeader",
59+
repositoryName = repositoryName,
60+
templateName = "license-header.xml",
61+
fileName = "license.xml",
62+
)
63+
64+
extensions.configure<SpotlessExtension> {
65+
val kotlinLicenseFile = generateKotlinLicenseTask.get().outputFile.get().asFile
66+
val xmlLicenseFile = generateXmlLicenseTask.get().outputFile.get().asFile
67+
68+
encoding = Charsets.UTF_8
69+
kotlin {
70+
target("**/*.kt")
71+
targetExclude("**/build/**/*.kt")
72+
73+
if (useKtfmt) {
74+
ktfmt().kotlinlangStyle()
75+
} else {
76+
// For now, we are fixing the ktlint version to the one currently used by Chat &
77+
// Video to dodge this issue: https://github.com/diffplug/spotless/issues/1913
78+
ktlint("0.50.0")
79+
.editorConfigOverride(
80+
mapOf("ktlint_standard_max-line-length" to "disabled")
81+
)
82+
}
83+
84+
trimTrailingWhitespace()
85+
endWithNewline()
86+
licenseHeaderFile(kotlinLicenseFile)
87+
}
88+
java {
89+
importOrder()
90+
removeUnusedImports()
91+
googleJavaFormat()
92+
trimTrailingWhitespace()
93+
endWithNewline()
94+
licenseHeaderFile(kotlinLicenseFile)
95+
}
96+
kotlinGradle {
97+
target("*.gradle.kts")
98+
ktlint()
99+
trimTrailingWhitespace()
100+
endWithNewline()
101+
}
102+
format("xml") {
103+
target("**/*.xml")
104+
targetExclude("**/build/**/*.xml", "**/detekt-baseline.xml")
105+
licenseHeaderFile(xmlLicenseFile, "(<[^!?])")
106+
}
107+
}
108+
109+
// Make Spotless tasks depend on license generation tasks
110+
tasks
111+
.matching { it.name.startsWith("spotless") }
112+
.configureEach { dependsOn(generateKotlinLicenseTask, generateXmlLicenseTask) }
113+
}
114+
}
115+
116+
/**
117+
* Registers a task to generate a license file from a bundled template. Safe to call from multiple
118+
* subprojects, as it will reuse the task if it already exists.
119+
*/
120+
private fun Project.registerLicenseGenerationTask(
121+
taskName: String,
122+
repositoryName: String,
123+
templateName: String,
124+
fileName: String,
125+
) =
126+
rootProject.tasks.findOrRegister<GenerateLicenseFileTask>(taskName) {
127+
this.repositoryName.set(repositoryName)
128+
this.templateName.set(templateName)
129+
130+
// Set the plugin's classpath as an input so Gradle tracks when the plugin JAR changes
131+
pluginClasspath.from(
132+
GenerateLicenseFileTask::class.java.protectionDomain.codeSource.location
133+
)
134+
135+
// Set the output file location
136+
val outputDir = rootProject.layout.buildDirectory.dir("stream-spotless-config")
137+
outputFile.set(outputDir.map { it.file(fileName) })
138+
}
139+
140+
private inline fun <reified T : Task> TaskContainer.findOrRegister(
141+
name: String,
142+
noinline configuration: T.() -> Unit,
143+
) = findByName(name)?.let { named<T>(name) } ?: register<T>(name, configuration)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-build-conventions-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.android
17+
18+
import javax.inject.Inject
19+
import org.gradle.api.Project
20+
import org.gradle.api.model.ObjectFactory
21+
import org.gradle.api.provider.Property
22+
import org.gradle.kotlin.dsl.create
23+
import org.gradle.kotlin.dsl.findByType
24+
import org.gradle.kotlin.dsl.property
25+
26+
/**
27+
* Extension for configuring Stream project-wide settings. Apply the `io.getstream.project` plugin
28+
* to the root project to use this extension.
29+
*/
30+
abstract class StreamProjectExtension
31+
@Inject
32+
constructor(project: Project, objects: ObjectFactory) {
33+
34+
/** The repository name used for inferring the repository URL. Example: "stream-core-android" */
35+
val repositoryName: Property<String> =
36+
objects.property<String>().convention(project.provider { project.rootProject.name })
37+
38+
/** Whether to apply ktfmt formatting instead of ktlint to Kotlin files. Default: false */
39+
val useKtfmt: Property<Boolean> = objects.property<Boolean>().convention(false)
40+
}
41+
42+
/**
43+
* Extension for configuring Stream module-specific settings. This extension is created in each
44+
* module where a Stream convention plugin is applied.
45+
*/
46+
abstract class StreamModuleExtension @Inject constructor(objects: ObjectFactory) {
47+
48+
/** Whether to disable Spotless formatting in this specific module. Default: false */
49+
val disableSpotless: Property<Boolean> = objects.property<Boolean>().convention(false)
50+
}
51+
52+
internal fun Project.createModuleExtension() {
53+
extensions.create<StreamModuleExtension>("streamModule")
54+
}
55+
56+
internal fun Project.createProjectExtension(): StreamProjectExtension =
57+
extensions.create<StreamProjectExtension>("streamProject")
58+
59+
internal fun Project.requireStreamProjectExtension(): StreamProjectExtension =
60+
rootProject.extensions.findByType<StreamProjectExtension>()
61+
?: error(
62+
"${StreamProjectExtension::class.simpleName} not found. " +
63+
"Apply the io.getstream.project plugin to the root project"
64+
)

plugin/src/main/kotlin/io/getstream/android/StreamConventionPlugins.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,31 @@ import com.android.build.api.dsl.LibraryExtension
2020
import org.gradle.api.Plugin
2121
import org.gradle.api.Project
2222

23+
/**
24+
* Root-level convention plugin for Stream projects. Apply this plugin to the root project to
25+
* configure project-wide settings.
26+
*/
27+
class RootConventionPlugin : Plugin<Project> {
28+
override fun apply(target: Project) {
29+
with(target) {
30+
require(this == rootProject) {
31+
"The io.getstream.project plugin should be applied to the root project only"
32+
}
33+
34+
createProjectExtension()
35+
}
36+
}
37+
}
38+
2339
class AndroidApplicationConventionPlugin : Plugin<Project> {
2440
override fun apply(target: Project) {
2541
with(target) {
2642
pluginManager.apply("com.android.application")
2743

44+
createModuleExtension()
2845
configureAndroid<ApplicationExtension>()
2946
configureKotlin()
47+
configureSpotless()
3048
}
3149
}
3250
}
@@ -36,8 +54,10 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
3654
with(target) {
3755
pluginManager.apply("com.android.library")
3856

57+
createModuleExtension()
3958
configureAndroid<LibraryExtension>()
4059
configureKotlin()
60+
configureSpotless()
4161
}
4262
}
4363
}
@@ -46,9 +66,12 @@ class JavaLibraryConventionPlugin : Plugin<Project> {
4666
override fun apply(target: Project) {
4767
with(target) {
4868
pluginManager.apply("java-library")
69+
pluginManager.apply("org.jetbrains.kotlin.jvm")
4970

71+
createModuleExtension()
5072
configureJava()
5173
configureKotlin()
74+
configureSpotless()
5275
}
5376
}
5477
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright (c) 2014-$YEAR Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/$PROJECT/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+

0 commit comments

Comments
 (0)