feat(build): replace homebrew ktfmt formatter with Spotless
Spotless has fixed their Gradle Configuration Cache woes in the past couple months which gets rid of my primary complaint.
This commit is contained in:
parent
3af68b45c4
commit
b699b4db71
15 changed files with 65 additions and 243 deletions
10
.github/renovate.json5
vendored
10
.github/renovate.json5
vendored
|
@ -68,6 +68,16 @@
|
||||||
"datasourceTemplate": "maven",
|
"datasourceTemplate": "maven",
|
||||||
"depNameTemplate": "androidx.compose.compiler:compiler",
|
"depNameTemplate": "androidx.compose.compiler:compiler",
|
||||||
"registryUrlTemplate": "https://maven.google.com",
|
"registryUrlTemplate": "https://maven.google.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fileMatch": [
|
||||||
|
"build-logic/src/main/kotlin/app/passwordstore/gradle/SpotlessPlugin.kt"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"KTFMT_VERSION = \"(?<currentValue>.*)\""
|
||||||
|
],
|
||||||
|
"datasourceTemplate": "maven",
|
||||||
|
"depNameTemplate": "com.facebook:ktfmt"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
2
.github/workflows/pull_request.yml
vendored
2
.github/workflows/pull_request.yml
vendored
|
@ -20,7 +20,7 @@ jobs:
|
||||||
|
|
||||||
- name: Check codestyle
|
- name: Check codestyle
|
||||||
shell: bash
|
shell: bash
|
||||||
run: ./gradlew ktfmtCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
||||||
- name: Upload Kotlin build report
|
- name: Upload Kotlin build report
|
||||||
if: "${{ always() }}"
|
if: "${{ always() }}"
|
||||||
|
|
|
@ -41,10 +41,6 @@ gradlePlugin {
|
||||||
id = "com.github.android-password-store.kotlin-jvm-library"
|
id = "com.github.android-password-store.kotlin-jvm-library"
|
||||||
implementationClass = "app.passwordstore.gradle.KotlinJVMLibrary"
|
implementationClass = "app.passwordstore.gradle.KotlinJVMLibrary"
|
||||||
}
|
}
|
||||||
register("ktfmt") {
|
|
||||||
id = "com.github.android-password-store.ktfmt"
|
|
||||||
implementationClass = "app.passwordstore.gradle.KtfmtPlugin"
|
|
||||||
}
|
|
||||||
register("published-android-library") {
|
register("published-android-library") {
|
||||||
id = "com.github.android-password-store.published-android-library"
|
id = "com.github.android-password-store.published-android-library"
|
||||||
implementationClass = "app.passwordstore.gradle.PublishedAndroidLibraryPlugin"
|
implementationClass = "app.passwordstore.gradle.PublishedAndroidLibraryPlugin"
|
||||||
|
@ -61,6 +57,10 @@ gradlePlugin {
|
||||||
id = "com.github.android-password-store.sentry"
|
id = "com.github.android-password-store.sentry"
|
||||||
implementationClass = "app.passwordstore.gradle.SentryPlugin"
|
implementationClass = "app.passwordstore.gradle.SentryPlugin"
|
||||||
}
|
}
|
||||||
|
register("spotless") {
|
||||||
|
id = "com.github.android-password-store.spotless"
|
||||||
|
implementationClass = "app.passwordstore.gradle.SpotlessPlugin"
|
||||||
|
}
|
||||||
register("versioning") {
|
register("versioning") {
|
||||||
id = "com.github.android-password-store.versioning-plugin"
|
id = "com.github.android-password-store.versioning-plugin"
|
||||||
implementationClass = "app.passwordstore.gradle.versioning.VersioningPlugin"
|
implementationClass = "app.passwordstore.gradle.versioning.VersioningPlugin"
|
||||||
|
@ -79,7 +79,6 @@ dependencies {
|
||||||
implementation(libs.build.download)
|
implementation(libs.build.download)
|
||||||
implementation(libs.build.javapoet)
|
implementation(libs.build.javapoet)
|
||||||
implementation(libs.build.kotlin)
|
implementation(libs.build.kotlin)
|
||||||
implementation(libs.build.ktfmt)
|
|
||||||
implementation(libs.build.mavenpublish)
|
implementation(libs.build.mavenpublish)
|
||||||
implementation(libs.build.metalava)
|
implementation(libs.build.metalava)
|
||||||
implementation(libs.build.moshi)
|
implementation(libs.build.moshi)
|
||||||
|
@ -88,6 +87,7 @@ dependencies {
|
||||||
implementation(libs.build.r8)
|
implementation(libs.build.r8)
|
||||||
implementation(libs.build.semver)
|
implementation(libs.build.semver)
|
||||||
implementation(libs.build.sentry)
|
implementation(libs.build.sentry)
|
||||||
|
implementation(libs.build.spotless)
|
||||||
implementation(libs.build.vcu)
|
implementation(libs.build.vcu)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
package app.passwordstore.gradle
|
|
||||||
|
|
||||||
import app.passwordstore.gradle.ktfmt.KtfmtCheckTask
|
|
||||||
import app.passwordstore.gradle.ktfmt.KtfmtFormatTask
|
|
||||||
import com.facebook.ktfmt.format.FormattingOptions
|
|
||||||
import java.util.concurrent.Callable
|
|
||||||
import org.gradle.api.Plugin
|
|
||||||
import org.gradle.api.Project
|
|
||||||
import org.gradle.kotlin.dsl.register
|
|
||||||
|
|
||||||
class KtfmtPlugin : Plugin<Project> {
|
|
||||||
|
|
||||||
override fun apply(target: Project) {
|
|
||||||
val input = Callable {
|
|
||||||
target.layout.projectDirectory.asFileTree.filter { file ->
|
|
||||||
file.extension == "kt" || file.extension == "kts" && !file.canonicalPath.contains("build/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target.tasks.register<KtfmtFormatTask>("ktfmtFormat") { source(input) }
|
|
||||||
target.tasks.register<KtfmtCheckTask>("ktfmtCheck") {
|
|
||||||
source(input)
|
|
||||||
projectDirectory.set(target.layout.projectDirectory)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val DEFAULT_FORMATTING_OPTIONS =
|
|
||||||
FormattingOptions(
|
|
||||||
maxWidth = FormattingOptions.DEFAULT_MAX_WIDTH,
|
|
||||||
blockIndent = 2,
|
|
||||||
continuationIndent = 2,
|
|
||||||
removeUnusedImports = true,
|
|
||||||
debuggingPrintOpsAfterFormatting = false,
|
|
||||||
manageTrailingCommas = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package app.passwordstore.gradle
|
||||||
|
|
||||||
|
import com.diffplug.gradle.spotless.SpotlessExtension
|
||||||
|
import com.diffplug.gradle.spotless.SpotlessPlugin
|
||||||
|
import org.gradle.api.Plugin
|
||||||
|
import org.gradle.api.Project
|
||||||
|
import org.gradle.kotlin.dsl.apply
|
||||||
|
import org.gradle.kotlin.dsl.getByType
|
||||||
|
|
||||||
|
@Suppress("Unused")
|
||||||
|
class SpotlessPlugin : Plugin<Project> {
|
||||||
|
|
||||||
|
override fun apply(project: Project) {
|
||||||
|
project.pluginManager.apply(SpotlessPlugin::class)
|
||||||
|
project.extensions.getByType<SpotlessExtension>().run {
|
||||||
|
kotlin {
|
||||||
|
ktfmt(KTFMT_VERSION).googleStyle()
|
||||||
|
target("**/*.kt")
|
||||||
|
targetExclude("**/build/")
|
||||||
|
}
|
||||||
|
kotlinGradle {
|
||||||
|
ktfmt(KTFMT_VERSION).googleStyle()
|
||||||
|
target("**/*.kts")
|
||||||
|
targetExclude("**/build/")
|
||||||
|
}
|
||||||
|
format("xml") {
|
||||||
|
target("**/*.xml")
|
||||||
|
targetExclude("**/build/", ".idea/")
|
||||||
|
trimTrailingWhitespace()
|
||||||
|
indentWithSpaces()
|
||||||
|
endWithNewline()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val KTFMT_VERSION = "0.51"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
package app.passwordstore.gradle.ktfmt
|
|
||||||
|
|
||||||
import app.passwordstore.gradle.KtfmtPlugin
|
|
||||||
import com.facebook.ktfmt.format.Formatter
|
|
||||||
import java.io.File
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import org.gradle.api.GradleException
|
|
||||||
import org.gradle.api.file.DirectoryProperty
|
|
||||||
import org.gradle.api.file.FileCollection
|
|
||||||
import org.gradle.api.tasks.IgnoreEmptyDirectories
|
|
||||||
import org.gradle.api.tasks.InputFiles
|
|
||||||
import org.gradle.api.tasks.Internal
|
|
||||||
import org.gradle.api.tasks.PathSensitive
|
|
||||||
import org.gradle.api.tasks.PathSensitivity
|
|
||||||
import org.gradle.api.tasks.SourceTask
|
|
||||||
import org.gradle.api.tasks.TaskAction
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
abstract class KtfmtCheckTask : SourceTask() {
|
|
||||||
|
|
||||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
|
||||||
@get:InputFiles
|
|
||||||
@get:IgnoreEmptyDirectories
|
|
||||||
protected val inputFiles: FileCollection
|
|
||||||
get() = super.getSource()
|
|
||||||
|
|
||||||
@get:Internal abstract val projectDirectory: DirectoryProperty
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
fun execute() {
|
|
||||||
runBlocking(Dispatchers.IO.limitedParallelism(PARALLEL_TASK_LIMIT)) {
|
|
||||||
coroutineScope {
|
|
||||||
val results = inputFiles.map { async { checkFile(it) } }.awaitAll()
|
|
||||||
if (results.any { (notFormatted, _) -> notFormatted }) {
|
|
||||||
val prettyDiff =
|
|
||||||
results
|
|
||||||
.map { (_, diffs) -> diffs }
|
|
||||||
.flatten()
|
|
||||||
.joinToString(separator = "\n") { diff -> diff.toString() }
|
|
||||||
throw GradleException("[ktfmt] Found unformatted files\n${prettyDiff}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkFile(input: File): Pair<Boolean, List<KtfmtDiffEntry>> {
|
|
||||||
val originCode = input.readText()
|
|
||||||
val formattedCode = Formatter.format(KtfmtPlugin.DEFAULT_FORMATTING_OPTIONS, originCode)
|
|
||||||
val pathNormalizer = { file: File -> file.toRelativeString(projectDirectory.asFile.get()) }
|
|
||||||
return (originCode != formattedCode) to
|
|
||||||
KtfmtDiffer.computeDiff(input, formattedCode, pathNormalizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val PARALLEL_TASK_LIMIT = 4
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
package app.passwordstore.gradle.ktfmt
|
|
||||||
|
|
||||||
data class KtfmtDiffEntry(val input: String, val lineNumber: Int, val message: String) {
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return "$input:$lineNumber - $message"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
package app.passwordstore.gradle.ktfmt
|
|
||||||
|
|
||||||
import com.github.difflib.DiffUtils
|
|
||||||
import com.github.difflib.patch.ChangeDelta
|
|
||||||
import com.github.difflib.patch.DeleteDelta
|
|
||||||
import com.github.difflib.patch.InsertDelta
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
object KtfmtDiffer {
|
|
||||||
fun computeDiff(
|
|
||||||
inputFile: File,
|
|
||||||
formattedCode: String,
|
|
||||||
pathNormalizer: (File) -> String,
|
|
||||||
): List<KtfmtDiffEntry> {
|
|
||||||
val originCode = inputFile.readText()
|
|
||||||
return DiffUtils.diff(originCode, formattedCode, null).deltas.map {
|
|
||||||
val line = it.source.position + 1
|
|
||||||
val message: String =
|
|
||||||
when (it) {
|
|
||||||
is ChangeDelta -> "Line changed: ${it.source.lines.first()}"
|
|
||||||
is DeleteDelta -> "Line deleted"
|
|
||||||
is InsertDelta -> "Line added"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
KtfmtDiffEntry(pathNormalizer(inputFile), line, message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
package app.passwordstore.gradle.ktfmt
|
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
import org.gradle.api.GradleException
|
|
||||||
import org.gradle.api.file.ProjectLayout
|
|
||||||
import org.gradle.api.tasks.SourceTask
|
|
||||||
import org.gradle.api.tasks.TaskAction
|
|
||||||
import org.gradle.internal.exceptions.MultiCauseException
|
|
||||||
import org.gradle.workers.WorkerExecutor
|
|
||||||
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
|
|
||||||
|
|
||||||
abstract class KtfmtFormatTask
|
|
||||||
@Inject
|
|
||||||
constructor(private val workerExecutor: WorkerExecutor, private val projectLayout: ProjectLayout) :
|
|
||||||
SourceTask() {
|
|
||||||
|
|
||||||
@TaskAction
|
|
||||||
fun execute() {
|
|
||||||
val result =
|
|
||||||
with(workerExecutor.noIsolation()) {
|
|
||||||
submit(KtfmtWorkerAction::class.java) {
|
|
||||||
name.set("ktfmt-worker")
|
|
||||||
files.from(source)
|
|
||||||
projectDirectory.set(projectLayout.projectDirectory.asFile)
|
|
||||||
}
|
|
||||||
runCatching { await() }
|
|
||||||
}
|
|
||||||
|
|
||||||
result.exceptionOrNull()?.workErrorCauses<Exception>()?.ifNotEmpty {
|
|
||||||
forEach { logger.error(it.message, it.cause) }
|
|
||||||
throw GradleException("error formatting sources for $name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T : Throwable> Throwable.workErrorCauses(): List<Throwable> {
|
|
||||||
return when (this) {
|
|
||||||
is MultiCauseException -> this.causes.map { it.cause }
|
|
||||||
else -> listOf(this.cause)
|
|
||||||
}
|
|
||||||
.filter {
|
|
||||||
// class instance comparison doesn't work due to different classloaders
|
|
||||||
it?.javaClass?.canonicalName == T::class.java.canonicalName
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package app.passwordstore.gradle.ktfmt
|
|
||||||
|
|
||||||
import app.passwordstore.gradle.KtfmtPlugin
|
|
||||||
import com.facebook.ktfmt.format.Formatter
|
|
||||||
import java.io.File
|
|
||||||
import org.gradle.api.logging.LogLevel
|
|
||||||
import org.gradle.api.logging.Logger
|
|
||||||
import org.gradle.api.logging.Logging
|
|
||||||
import org.gradle.internal.logging.slf4j.DefaultContextAwareTaskLogger
|
|
||||||
import org.gradle.workers.WorkAction
|
|
||||||
|
|
||||||
abstract class KtfmtWorkerAction : WorkAction<KtfmtWorkerParameters> {
|
|
||||||
private val logger: Logger =
|
|
||||||
DefaultContextAwareTaskLogger(Logging.getLogger(KtfmtFormatTask::class.java))
|
|
||||||
private val files: List<File> = parameters.files.toList()
|
|
||||||
private val projectDirectory: File = parameters.projectDirectory.asFile.get()
|
|
||||||
private val name: String = parameters.name.get()
|
|
||||||
|
|
||||||
override fun execute() {
|
|
||||||
try {
|
|
||||||
files.forEach { file ->
|
|
||||||
val sourceText = file.readText()
|
|
||||||
val relativePath = file.toRelativeString(projectDirectory)
|
|
||||||
|
|
||||||
logger.log(LogLevel.DEBUG, "$name checking format: $relativePath")
|
|
||||||
|
|
||||||
val formattedText = Formatter.format(KtfmtPlugin.DEFAULT_FORMATTING_OPTIONS, sourceText)
|
|
||||||
|
|
||||||
if (!formattedText.contentEquals(sourceText)) {
|
|
||||||
logger.log(LogLevel.QUIET, "${file.toRelativeString(projectDirectory)}: Format fixed")
|
|
||||||
file.writeText(formattedText)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
throw Exception("format worker execution error", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
package app.passwordstore.gradle.ktfmt
|
|
||||||
|
|
||||||
import org.gradle.api.file.ConfigurableFileCollection
|
|
||||||
import org.gradle.api.file.RegularFileProperty
|
|
||||||
import org.gradle.api.provider.Property
|
|
||||||
import org.gradle.workers.WorkParameters
|
|
||||||
|
|
||||||
interface KtfmtWorkerParameters : WorkParameters {
|
|
||||||
val name: Property<String>
|
|
||||||
val files: ConfigurableFileCollection
|
|
||||||
val projectDirectory: RegularFileProperty
|
|
||||||
}
|
|
|
@ -5,6 +5,6 @@
|
||||||
plugins {
|
plugins {
|
||||||
id("com.github.android-password-store.git-hooks")
|
id("com.github.android-password-store.git-hooks")
|
||||||
id("com.github.android-password-store.kotlin-common")
|
id("com.github.android-password-store.kotlin-common")
|
||||||
id("com.github.android-password-store.ktfmt")
|
id("com.github.android-password-store.spotless")
|
||||||
id("com.github.android-password-store.versions")
|
id("com.github.android-password-store.versions")
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ build-diffutils = "io.github.java-diff-utils:java-diff-utils:4.12"
|
||||||
build-download = "de.undercouch:gradle-download-task:5.6.0"
|
build-download = "de.undercouch:gradle-download-task:5.6.0"
|
||||||
build-javapoet = "com.squareup:javapoet:1.13.0"
|
build-javapoet = "com.squareup:javapoet:1.13.0"
|
||||||
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
build-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
build-ktfmt = "com.facebook:ktfmt:0.51"
|
|
||||||
build-mavenpublish = "com.vanniktech:gradle-maven-publish-plugin:0.29.0"
|
build-mavenpublish = "com.vanniktech:gradle-maven-publish-plugin:0.29.0"
|
||||||
build-metalava = "me.tylerbwong.gradle.metalava:plugin:0.3.5"
|
build-metalava = "me.tylerbwong.gradle.metalava:plugin:0.3.5"
|
||||||
build-moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
build-moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||||
|
@ -48,6 +47,7 @@ build-okhttp = "com.squareup.okhttp3:okhttp:5.0.0-alpha.14"
|
||||||
build-r8 = "com.android.tools:r8:8.4.6-dev"
|
build-r8 = "com.android.tools:r8:8.4.6-dev"
|
||||||
build-semver = "com.github.zafarkhaja:java-semver:0.10.2"
|
build-semver = "com.github.zafarkhaja:java-semver:0.10.2"
|
||||||
build-sentry = "io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin:4.11.0"
|
build-sentry = "io.sentry.android.gradle:io.sentry.android.gradle.gradle.plugin:4.11.0"
|
||||||
|
build-spotless = "com.diffplug.spotless:spotless-plugin-gradle:6.25.0"
|
||||||
build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin:0.8.4"
|
build-vcu = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin:0.8.4"
|
||||||
compose-bom = "androidx.compose:compose-bom:2024.06.00"
|
compose-bom = "androidx.compose:compose-bom:2024.06.00"
|
||||||
compose-foundation-core = { module = "androidx.compose.foundation:foundation" }
|
compose-foundation-core = { module = "androidx.compose.foundation:foundation" }
|
||||||
|
|
|
@ -13,6 +13,6 @@ while read -r local_ref local_oid remote_ref remote_oid; do
|
||||||
_=$remote_ref
|
_=$remote_ref
|
||||||
_=$remote_oid
|
_=$remote_oid
|
||||||
if [ "${local_oid}" != "${ZERO}" ]; then
|
if [ "${local_oid}" != "${ZERO}" ]; then
|
||||||
CI=true "${GRADLE_EXEC}" metalavaCheckCompatibilityRelease lint ktfmtCheck test -PslimTests
|
CI=true "${GRADLE_EXEC}" metalavaCheckCompatibilityRelease lint spotlessCheck test -PslimTests
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
Loading…
Reference in a new issue