Apply spotless by default (#2055)

* Apply spotless by default

- Upgrades to spotless 8.2.1
- Moves spotless setup from an init.gradle.kts to build-logic
- Narrows down the scope of `target` in spotless configuration to
  be more precise to workaround https://github.com/diffplug/spotless/issues/2717
- Updates all references to init.gradle.kts

Ran gradle-profiler ./gradlew build --dry-run to validate performance
impact.
Before PR:
Mean 10,527.96 ms with 289.01 ms std dev
After PR:
Mean 11,251.78 ms with 530.29 ms std dev

Regression is there, but quite minor.

Test: ./gradlew spotlessCheck

* Address comments from AI overlords

* Fix usage of rootProject

* Enable spotless for build-logic via root project
pull/2056/head
Aurimas 8 months ago committed by GitHub
parent eee94e4b7d
commit 5ba16edde9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -10,7 +10,7 @@ Thanks for submitting a pull request. To accept your pull request we need you do
**Ensure tests pass and code is formatted correctly** **Ensure tests pass and code is formatted correctly**
- Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug` - Run local tests on the `DemoDebug` variant by running `./gradlew testDemoDebug`
- Fix code formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply` - Fix code formatting: `./gradlew spotlessApply`
**Add a description** **Add a description**

@ -48,7 +48,7 @@ jobs:
run: ./gradlew :build-logic:convention:check run: ./gradlew :build-logic:convention:check
- name: Check spotless - name: Check spotless
run: ./gradlew spotlessCheck --init-script gradle/init.gradle.kts --no-configuration-cache run: ./gradlew spotlessCheck
- name: Check Dependency Guard - name: Check Dependency Guard
id: dependencyguard_verify id: dependencyguard_verify

@ -4,7 +4,6 @@
<option name="executionName" /> <option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" /> <option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--init-script=gradle/init.gradle.kts" />
<option name="taskDescriptions"> <option name="taskDescriptions">
<list /> <list />
</option> </option>

@ -26,7 +26,7 @@ The main Android app lives in the `app/` folder. Feature modules live in `featur
The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`. The app and Android libraries have two product flavors: `demo` and `prod`, and two build types: `debug` and `release`.
- Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`. - Build: `./gradlew assemble{Variant}`. Typically `assembleDemoDebug`.
- Fix linting/formatting: `./gradlew --init-script gradle/init.gradle.kts spotlessApply` - Fix linting/formatting: `./gradlew spotlessApply`
- Run local tests: `./gradlew {variant}Test` - Run local tests: `./gradlew {variant}Test`
- Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"` - Run single test: `./gradlew {variant}Test --tests "com.example.myapp.MyTestClass"`
- Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug` - Run local screenshot tests: `./gradlew verifyRoborazziDemoDebug`

@ -45,6 +45,7 @@ dependencies {
compileOnly(libs.kotlin.gradlePlugin) compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin) compileOnly(libs.ksp.gradlePlugin)
compileOnly(libs.room.gradlePlugin) compileOnly(libs.room.gradlePlugin)
compileOnly(libs.spotless.gradlePlugin)
implementation(libs.truth) implementation(libs.truth)
lintChecks(libs.androidx.lint.gradle) lintChecks(libs.androidx.lint.gradle)
} }

@ -20,6 +20,7 @@ import com.google.samples.apps.nowinandroid.configureBadgingTasks
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import com.google.samples.apps.nowinandroid.configureSpotlessForAndroid
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.apply
@ -42,6 +43,7 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
configurePrintApksTask(this) configurePrintApksTask(this)
configureBadgingTasks(this) configureBadgingTasks(this)
} }
configureSpotlessForAndroid()
} }
} }
} }

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.apply

@ -20,6 +20,7 @@ import com.google.samples.apps.nowinandroid.configureFlavors
import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.google.samples.apps.nowinandroid.configureGradleManagedDevices
import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configureKotlinAndroid
import com.google.samples.apps.nowinandroid.configurePrintApksTask import com.google.samples.apps.nowinandroid.configurePrintApksTask
import com.google.samples.apps.nowinandroid.configureSpotlessForAndroid
import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests import com.google.samples.apps.nowinandroid.disableUnnecessaryAndroidTests
import com.google.samples.apps.nowinandroid.libs import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
@ -52,6 +53,7 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
configurePrintApksTask(this) configurePrintApksTask(this)
disableUnnecessaryAndroidTests(target) disableUnnecessaryAndroidTests(target)
} }
configureSpotlessForAndroid()
dependencies { dependencies {
"androidTestImplementation"(libs.findLibrary("kotlin.test").get()) "androidTestImplementation"(libs.findLibrary("kotlin.test").get())
"testImplementation"(libs.findLibrary("kotlin.test").get()) "testImplementation"(libs.findLibrary("kotlin.test").get())

@ -15,6 +15,7 @@
*/ */
import com.google.samples.apps.nowinandroid.configureKotlinJvm import com.google.samples.apps.nowinandroid.configureKotlinJvm
import com.google.samples.apps.nowinandroid.configureSpotlessForJvm
import com.google.samples.apps.nowinandroid.libs import com.google.samples.apps.nowinandroid.libs
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
@ -28,6 +29,7 @@ class JvmLibraryConventionPlugin : Plugin<Project> {
apply(plugin = "nowinandroid.android.lint") apply(plugin = "nowinandroid.android.lint")
configureKotlinJvm() configureKotlinJvm()
configureSpotlessForJvm()
dependencies { dependencies {
"testImplementation"(libs.findLibrary("kotlin.test").get()) "testImplementation"(libs.findLibrary("kotlin.test").get())
} }

@ -15,6 +15,7 @@
*/ */
import com.google.samples.apps.nowinandroid.configureGraphTasks import com.google.samples.apps.nowinandroid.configureGraphTasks
import com.google.samples.apps.nowinandroid.configureSpotlessForRootProject
import org.gradle.api.Plugin import org.gradle.api.Plugin
import org.gradle.api.Project import org.gradle.api.Project
@ -22,5 +23,6 @@ class RootPlugin : Plugin<Project> {
override fun apply(target: Project) { override fun apply(target: Project) {
require(target.path == ":") require(target.path == ":")
target.subprojects { configureGraphTasks() } target.subprojects { configureGraphTasks() }
target.configureSpotlessForRootProject()
} }
} }

@ -30,6 +30,6 @@ import org.gradle.api.Project
internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests( internal fun LibraryAndroidComponentsExtension.disableUnnecessaryAndroidTests(
project: Project, project: Project,
) = beforeVariants { ) = beforeVariants {
it.androidTest.enable = it.androidTest.enable it.androidTest.enable = it.androidTest.enable &&
&& project.projectDir.resolve("src/androidTest").exists() project.projectDir.resolve("src/androidTest").exists()
} }

@ -123,7 +123,6 @@ fun Project.configureBadgingTasks(
badging = project.layout.buildDirectory.file( badging = project.layout.buildDirectory.file(
"outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt", "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt",
) )
} }
val updateBadgingTaskName = "update${capitalizedVariantName}Badging" val updateBadgingTaskName = "update${capitalizedVariantName}Badging"
@ -141,7 +140,6 @@ fun Project.configureBadgingTasks(
this.updateBadgingTaskName = updateBadgingTaskName this.updateBadgingTaskName = updateBadgingTaskName
output = project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName") output = project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName")
} }
} }
} }

@ -34,7 +34,6 @@ internal fun configureGradleManagedDevices(
val allDevices = listOf(pixel4, pixel6, pixelC) val allDevices = listOf(pixel4, pixel6, pixelC)
val ciDevices = listOf(pixel4, pixelC) val ciDevices = listOf(pixel4, pixelC)
commonExtension.testOptions.apply { commonExtension.testOptions.apply {
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
managedDevices { managedDevices {

@ -17,12 +17,10 @@
package com.google.samples.apps.nowinandroid package com.google.samples.apps.nowinandroid
import com.android.build.api.artifact.ScopedArtifact import com.android.build.api.artifact.ScopedArtifact
import com.android.build.api.dsl.BuildType
import com.android.build.api.dsl.CommonExtension import com.android.build.api.dsl.CommonExtension
import com.android.build.api.variant.AndroidComponentsExtension import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.ScopedArtifacts import com.android.build.api.variant.ScopedArtifacts
import com.android.build.api.variant.SourceDirectories import com.android.build.api.variant.SourceDirectories
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Project import org.gradle.api.Project
import org.gradle.api.file.Directory import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile import org.gradle.api.file.RegularFile
@ -86,7 +84,6 @@ internal fun Project.configureJacoco(
"create${variant.name.capitalize()}CombinedCoverageReport", "create${variant.name.capitalize()}CombinedCoverageReport",
JacocoReport::class, JacocoReport::class,
) { ) {
classDirectories.setFrom( classDirectories.setFrom(
allJars, allJars,
allDirectories.map { dirs -> allDirectories.map { dirs ->
@ -107,7 +104,7 @@ internal fun Project.configureJacoco(
sourceDirectories.setFrom( sourceDirectories.setFrom(
files( files(
variant.sources.java.toFilePaths(), variant.sources.java.toFilePaths(),
variant.sources.kotlin.toFilePaths() variant.sources.kotlin.toFilePaths(),
), ),
) )
@ -120,7 +117,6 @@ internal fun Project.configureJacoco(
) )
} }
variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT) variant.artifacts.forScope(ScopedArtifacts.Scope.PROJECT)
.use(reportTask) .use(reportTask)
.toGet( .toGet(

@ -103,7 +103,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
* The binary signature changes. The error on the declaration is no longer reported. * The binary signature changes. The error on the declaration is no longer reported.
* '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary. * '-Xconsistent-data-class-copy-visibility' compiler flag and ConsistentCopyVisibility annotation are now unnecessary.
*/ */
"-Xconsistent-data-class-copy-visibility" "-Xconsistent-data-class-copy-visibility",
) )
} }
} }

@ -1,3 +1,19 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid package com.google.samples.apps.nowinandroid
import com.android.build.api.dsl.ApplicationExtension import com.android.build.api.dsl.ApplicationExtension
@ -8,7 +24,7 @@ import org.gradle.kotlin.dsl.invoke
@Suppress("EnumEntryName") @Suppress("EnumEntryName")
enum class FlavorDimension { enum class FlavorDimension {
contentType contentType,
} }
// The content for the app can either come from local static data which is useful for demo // The content for the app can either come from local static data which is useful for demo

@ -49,7 +49,9 @@ internal fun Project.configurePrintApksTask(extension: AndroidComponentsExtensio
javaSources.zip(kotlinSources) { javaDirs, kotlinDirs -> javaSources.zip(kotlinSources) { javaDirs, kotlinDirs ->
javaDirs + kotlinDirs javaDirs + kotlinDirs
} }
} else javaSources ?: kotlinSources } else {
javaSources ?: kotlinSources
}
if (artifact != null && testSources != null) { if (artifact != null && testSources != null) {
tasks.register( tasks.register(
@ -96,8 +98,9 @@ internal abstract class PrintApkLocationTask : DefaultTask() {
val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get()) val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
?: throw RuntimeException("Cannot load APKs") ?: throw RuntimeException("Cannot load APKs")
if (builtArtifacts.elements.size != 1) if (builtArtifacts.elements.size != 1) {
throw RuntimeException("Expected one APK !") throw RuntimeException("Expected one APK !")
}
val apk = File(builtArtifacts.elements.single().outputFile).toPath() val apk = File(builtArtifacts.elements.single().outputFile).toPath()
println(apk) println(apk)
} }

@ -0,0 +1,80 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.samples.apps.nowinandroid
import com.diffplug.gradle.spotless.SpotlessExtension
import org.gradle.api.Project
import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.configure
internal fun Project.configureSpotlessForAndroid() {
configureSpotlessCommon()
extensions.configure<SpotlessExtension> {
format("xml") {
target("src/**/*.xml")
// Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
licenseHeaderFile(rootDir.resolve("spotless/copyright.xml"), "(<[^!?])")
endWithNewline()
}
}
}
internal fun Project.configureSpotlessForJvm() {
configureSpotlessCommon()
}
internal fun Project.configureSpotlessForRootProject() {
apply(plugin = "com.diffplug.spotless")
extensions.configure<SpotlessExtension> {
kotlin {
target("build-logic/convention/src/**/*.kt")
ktlint(libs.findVersion("ktlint").get().requiredVersion).editorConfigOverride(
mapOf("android" to "true"),
)
licenseHeaderFile(rootDir.resolve("spotless/copyright.kt"))
endWithNewline()
}
format("kts") {
target("*.kts")
target("build-logic/*.kts")
target("build-logic/convention/*.kts")
// Look for the first line that doesn't have a block comment (assumed to be the license)
licenseHeaderFile(rootDir.resolve("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
endWithNewline()
}
}
}
private fun Project.configureSpotlessCommon() {
apply(plugin = "com.diffplug.spotless")
extensions.configure<SpotlessExtension> {
kotlin {
target("src/**/*.kt")
ktlint(libs.findVersion("ktlint").get().requiredVersion).editorConfigOverride(
mapOf("android" to "true"),
)
licenseHeaderFile(rootDir.resolve("spotless/copyright.kt"))
endWithNewline()
}
format("kts") {
target("*.kts")
// Look for the first line that doesn't have a block comment (assumed to be the license)
licenseHeaderFile(rootDir.resolve("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
endWithNewline()
}
}
}

@ -38,5 +38,6 @@ plugins {
alias(libs.plugins.roborazzi) apply false alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.google.osslicenses) apply false alias(libs.plugins.google.osslicenses) apply false
alias(libs.plugins.room) apply false alias(libs.plugins.room) apply false
alias(libs.plugins.spotless) apply false
alias(libs.plugins.nowinandroid.root) alias(libs.plugins.nowinandroid.root)
} }

@ -25,4 +25,3 @@ android {
dependencies { dependencies {
implementation(projects.core.domain) implementation(projects.core.domain)
} }

@ -1,59 +0,0 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
val ktlintVersion = "1.4.0"
initscript {
val spotlessVersion = "6.25.0"
repositories {
mavenCentral()
}
dependencies {
classpath("com.diffplug.spotless:spotless-plugin-gradle:$spotlessVersion")
}
}
rootProject {
subprojects {
apply<com.diffplug.gradle.spotless.SpotlessPlugin>()
extensions.configure<com.diffplug.gradle.spotless.SpotlessExtension> {
kotlin {
target("**/*.kt")
targetExclude("**/build/**/*.kt")
ktlint(ktlintVersion).editorConfigOverride(
mapOf(
"android" to "true",
),
)
licenseHeaderFile(rootProject.file("spotless/copyright.kt"))
}
format("kts") {
target("**/*.kts")
targetExclude("**/build/**/*.kts")
// Look for the first line that doesn't have a block comment (assumed to be the license)
licenseHeaderFile(rootProject.file("spotless/copyright.kts"), "(^(?![\\/ ]\\*).*$)")
}
format("xml") {
target("**/*.xml")
targetExclude("**/build/**/*.xml")
// Look for the first XML tag that isn't a comment (<!--) or the xml declaration (<?xml)
licenseHeaderFile(rootProject.file("spotless/copyright.xml"), "(<[^!?])")
}
}
}
}

@ -51,6 +51,7 @@ kotlinxCoroutines = "1.10.1"
kotlinxDatetime = "0.6.1" kotlinxDatetime = "0.6.1"
kotlinxSerializationJson = "1.8.0" kotlinxSerializationJson = "1.8.0"
ksp = "2.3.4" ksp = "2.3.4"
ktlint = "1.4.0"
okhttp = "4.12.0" okhttp = "4.12.0"
protobuf = "4.29.2" protobuf = "4.29.2"
protobufPlugin = "0.9.6" protobufPlugin = "0.9.6"
@ -59,6 +60,7 @@ retrofitKotlinxSerializationJson = "1.0.0"
robolectric = "4.16" robolectric = "4.16"
roborazzi = "1.56.0" roborazzi = "1.56.0"
room = "2.8.3" room = "2.8.3"
spotless = "8.2.1"
truth = "1.4.4" truth = "1.4.4"
turbine = "1.2.0" turbine = "1.2.0"
uiTestJunit4 = "1.9.0-rc01" uiTestJunit4 = "1.9.0-rc01"
@ -171,6 +173,7 @@ kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-pl
ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } ksp-gradlePlugin = { group = "com.google.devtools.ksp", name = "com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" }
room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" } room-gradlePlugin = { group = "androidx.room", name = "room-gradle-plugin", version.ref = "room" }
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "uiTestJunit4" }
spotless-gradlePlugin = { group = "com.diffplug.spotless", name = "spotless-plugin-gradle", version.ref = "spotless" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@ -191,6 +194,7 @@ google-osslicenses = { id = "com.google.android.gms.oss-licenses-plugin", versio
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
room = { id = "androidx.room", version.ref = "room" } room = { id = "androidx.room", version.ref = "room" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
# Plugins defined by this project # Plugins defined by this project
nowinandroid-android-application = { id = "nowinandroid.android.application" } nowinandroid-android-application = { id = "nowinandroid.android.application" }

@ -88,7 +88,7 @@ done
if [[ -n "$run_checks" ]]; then if [[ -n "$run_checks" ]]; then
# pre-push usually executes in the repository root, but just to be safe... # pre-push usually executes in the repository root, but just to be safe...
cd "$(git rev-parse --show-toplevel)" cd "$(git rev-parse --show-toplevel)"
./gradlew --init-script gradle/init.gradle.kts --no-configuration-cache check ./gradlew check
exit $? exit $?
fi fi

Loading…
Cancel
Save