From 7cffcfcf63a99193b480ef87a389702aea5f2d72 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Thu, 2 Oct 2025 15:39:37 -0400 Subject: [PATCH 1/5] Use isolated APIs --- backstack/build.gradle.kts | 7 +++---- build.gradle.kts | 14 +++++++++----- circuitx/effects/build.gradle.kts | 7 +++---- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/backstack/build.gradle.kts b/backstack/build.gradle.kts index 25df77acf..ccc148b87 100644 --- a/backstack/build.gradle.kts +++ b/backstack/build.gradle.kts @@ -34,10 +34,9 @@ kotlin { useKarma { useChromeHeadless() useConfigDirectory( - rootProject.projectDir - .resolve("internal-test-utils") - .resolve("karma.config.d") - .resolve("wasm") + rootProject.isolated.projectDirectory + .dir("internal-test-utils/karma.config.d/wasm") + .asFile ) } } diff --git a/build.gradle.kts b/build.gradle.kts index 948d8497f..191e4e862 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -91,14 +91,17 @@ allprojects { trimTrailingWhitespace() endWithNewline() licenseHeaderFile( - rootProject.file("spotless/spotless.kt"), + rootProject.isolated.projectDirectory.file("spotless/spotless.kt"), "(import|plugins|buildscript|dependencies|pluginManagement|dependencyResolutionManagement)", ) } // Apply license formatting separately for kotlin files so we can prevent it from overwriting // copied files format("license") { - licenseHeaderFile(rootProject.file("spotless/spotless.kt"), "(package|@file:)") + licenseHeaderFile( + rootProject.isolated.projectDirectory.file("spotless/spotless.kt"), + "(package|@file:)", + ) target("src/**/*.kt") targetExclude( "**/circuit/backstack/**/*.kt", @@ -205,7 +208,7 @@ subprojects { configure { toolVersion = detektVersion allRules = true - config.from(rootProject.file("config/detekt/detekt.yml")) + config.from(rootProject.isolated.projectDirectory.file("config/detekt/detekt.yml")) buildUponDefaultConfig = true } @@ -260,7 +263,8 @@ subprojects { // Add source links sourceLink { localDirectory.set(layout.projectDirectory.dir("src")) - val relPath = rootProject.projectDir.toPath().relativize(projectDir.toPath()) + val relPath = + rootProject.isolated.projectDirectory.asFile.toPath().relativize(projectDir.toPath()) remoteUrl( providers.gradleProperty("POM_SCM_URL").map { scmUrl -> "$scmUrl/tree/main/$relPath/src" @@ -327,7 +331,7 @@ subprojects { // https://issuetracker.google.com/issues/243267012 disable += "Instantiatable" checkTestSources = true - lintConfig = rootProject.file("config/lint/lint.xml") + lintConfig = rootProject.isolated.projectDirectory.file("config/lint/lint.xml").asFile } dependencies { add("lintChecks", libs.lints.compose) } } diff --git a/circuitx/effects/build.gradle.kts b/circuitx/effects/build.gradle.kts index 250ed57f2..437747e0e 100644 --- a/circuitx/effects/build.gradle.kts +++ b/circuitx/effects/build.gradle.kts @@ -32,10 +32,9 @@ kotlin { useKarma { useChromeHeadless() useConfigDirectory( - rootProject.projectDir - .resolve("internal-test-utils") - .resolve("karma.config.d") - .resolve("wasm") + rootProject.isolated.projectDirectory + .dir("internal-test-utils/karma.config.d/wasm") + .asFile ) } } From 60caba62e42f8dcf597f4e0b606d1b71d293a881 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Thu, 2 Oct 2025 15:40:25 -0400 Subject: [PATCH 2/5] Remove kapt --- build.gradle.kts | 41 +++++++++++++--------------------- gradle/libs.versions.toml | 1 - samples/tacos/build.gradle.kts | 3 --- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 191e4e862..4b3ad167f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,7 +23,6 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension import org.jetbrains.kotlin.gradle.dsl.KotlinNativeCompilerOptions import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension -import org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest @@ -36,7 +35,6 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.kotlin.android) apply false - alias(libs.plugins.kotlin.kapt) apply false alias(libs.plugins.kotlin.plugin.parcelize) apply false alias(libs.plugins.kotlin.plugin.serialization) apply false alias(libs.plugins.agp.application) apply false @@ -152,10 +150,6 @@ subprojects { val hasCompose = !project.hasProperty("circuit.noCompose") plugins.withType { tasks.withType>().configureEach { - if (this is KaptGenerateStubsTask) { - // Don't double apply to stub gen - return@configureEach - } val isWasmTask = name.contains("wasm", ignoreCase = true) compilerOptions { if (isWasmTask && this is KotlinJsCompilerOptions) { @@ -174,25 +168,22 @@ subprojects { .map { it.toString() } .map(JvmTarget::fromTarget) ) - // Stub gen copies args from the parent compilation - if (this@configureEach !is KaptGenerateStubsTask) { - freeCompilerArgs.addAll( - "-Xjsr305=strict", - // Match JVM assertion behavior: - // https://publicobject.com/2019/11/18/kotlins-assert-is-not-like-javas-assert/ - "-Xassertions=jvm", - // Potentially useful for static analysis tools or annotation processors. - "-Xemit-jvm-type-annotations", - // Enable new jvm-default behavior - // https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/ - "-Xjvm-default=all", - // https://kotlinlang.org/docs/whatsnew1520.html#support-for-jspecify-nullness-annotations - "-Xtype-enhancement-improvements-strict-mode", - "-Xjspecify-annotations=strict", - // https://youtrack.jetbrains.com/issue/KT-73255 - "-Xannotation-default-target=param-property", - ) - } + freeCompilerArgs.addAll( + "-Xjsr305=strict", + // Match JVM assertion behavior: + // https://publicobject.com/2019/11/18/kotlins-assert-is-not-like-javas-assert/ + "-Xassertions=jvm", + // Potentially useful for static analysis tools or annotation processors. + "-Xemit-jvm-type-annotations", + // Enable new jvm-default behavior + // https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/ + "-Xjvm-default=all", + // https://kotlinlang.org/docs/whatsnew1520.html#support-for-jspecify-nullness-annotations + "-Xtype-enhancement-improvements-strict-mode", + "-Xjspecify-annotations=strict", + // https://youtrack.jetbrains.com/issue/KT-73255 + "-Xannotation-default-target=param-property", + ) } progressiveMode.set(true) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6dbad2ac8..10ea1501d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,7 +62,6 @@ emulatorWtf = { id = "wtf.emulator.gradle", version = "0.19.3" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-plugin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } diff --git a/samples/tacos/build.gradle.kts b/samples/tacos/build.gradle.kts index 09827a515..ab6c1f76c 100644 --- a/samples/tacos/build.gradle.kts +++ b/samples/tacos/build.gradle.kts @@ -1,12 +1,10 @@ // Copyright (C) 2023 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 -import org.jetbrains.kotlin.gradle.internal.KaptGenerateStubsTask import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.agp.application) alias(libs.plugins.kotlin.android) - alias(libs.plugins.kotlin.kapt) alias(libs.plugins.kotlin.plugin.parcelize) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.plugin.compose) @@ -21,7 +19,6 @@ androidComponents { beforeVariants { it.enable = it.name.contains("debug", ignor tasks .withType() - .matching { it !is KaptGenerateStubsTask } .configureEach { compilerOptions { freeCompilerArgs.addAll( From 89fe6c14bd735220316c347c9615294aaa359c2f Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Thu, 2 Oct 2025 15:42:30 -0400 Subject: [PATCH 3/5] Remove nested subprojects --- build.gradle.kts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 4b3ad167f..262ce8101 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -431,11 +431,9 @@ subprojects { tasks.register("ciConnectedCheck") { dependsOn("connectedCheck") } } - subprojects { - pluginManager.withPlugin("dev.zacsweers.anvil") { - configure { - useKsp(contributesAndFactoryGeneration = true, componentMerging = true) - } + pluginManager.withPlugin("dev.zacsweers.anvil") { + configure { + useKsp(contributesAndFactoryGeneration = true, componentMerging = true) } } } From 031fcba959a6bbf647a799424239284647cd2536 Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Thu, 2 Oct 2025 16:39:48 -0400 Subject: [PATCH 4/5] Migrate logic to build-logic --- build-logic/build.gradle.kts | 43 ++ build-logic/settings.gradle.kts | 22 + .../slack/circuit/gradle/CircuitBasePlugin.kt | 433 ++++++++++++++++++ build.gradle.kts | 396 ---------------- gradle/libs.versions.toml | 23 +- 5 files changed, 517 insertions(+), 400 deletions(-) create mode 100644 build-logic/build.gradle.kts create mode 100644 build-logic/settings.gradle.kts create mode 100644 build-logic/src/main/kotlin/com/slack/circuit/gradle/CircuitBasePlugin.kt diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 000000000..73ad30348 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.plugin.sam) + id("java-gradle-plugin") +} + +// To keep kotlin-dsl-esque DSL APIs +samWithReceiver { annotation("org.gradle.api.HasImplicitReceiver") } + +gradlePlugin { + plugins { + register("base") { + id = "circuit.base" + implementationClass = "com.slack.circuit.gradle.CircuitBasePlugin" + } + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.jdk.get().removeSuffix("-ea").toInt())) + } +} + +kotlin { compilerOptions { allWarningsAsErrors.set(true) } } + +dependencies { + compileOnly(gradleApi()) + api(libs.gradlePlugins.agp) + api(libs.gradlePlugins.spotless) + api(libs.gradlePlugins.dependencyGuard) + api(libs.gradlePlugins.anvil) + api(libs.gradlePlugins.mavenPublish) + api(libs.gradlePlugins.detekt) + api(libs.gradlePlugins.binaryCompatibilityValidator) + api(libs.gradlePlugins.dokka) + api(libs.gradlePlugins.composeCompiler) + api(libs.gradlePlugins.kotlin) + api(libs.gradlePlugins.emulatorWtf) + + // Expose the generated version catalog API to the plugins. + implementation(files(libs::class.java.superclass.protectionDomain.codeSource.location)) +} diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 000000000..dc5a5771b --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1,22 @@ +pluginManagement { + repositories { + mavenCentral() + google() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + google() + gradlePluginPortal() + } + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +rootProject.name = "build-logic" \ No newline at end of file diff --git a/build-logic/src/main/kotlin/com/slack/circuit/gradle/CircuitBasePlugin.kt b/build-logic/src/main/kotlin/com/slack/circuit/gradle/CircuitBasePlugin.kt new file mode 100644 index 000000000..3f84b0002 --- /dev/null +++ b/build-logic/src/main/kotlin/com/slack/circuit/gradle/CircuitBasePlugin.kt @@ -0,0 +1,433 @@ +package com.slack.circuit.gradle + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.variant.LibraryAndroidComponentsExtension +import com.android.build.gradle.LibraryExtension +import com.android.build.gradle.TestExtension +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare +import com.diffplug.spotless.LineEnding +import com.dropbox.gradle.plugins.dependencyguard.DependencyGuardPluginExtension +import com.squareup.anvil.plugin.AnvilExtension +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import io.gitlab.arturbosch.detekt.Detekt +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +import kotlin.apply +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Action +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.plugins.JavaPluginExtension +import org.gradle.api.tasks.TaskCollection +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.jvm.toolchain.JavaLanguageVersion +import org.jetbrains.dokka.gradle.DokkaExtension +import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier +import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompilerOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinNativeCompilerOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin +import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask +import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask +import wtf.emulator.EwExtension + +class CircuitBasePlugin : Plugin { + override fun apply(target: Project) { + val libs = target.extensions.getByType(LibrariesForLibs::class.java) + target.configureAnyProject(libs) + target.configureSubproject(libs) + } + + private fun Project.configureAnyProject(libs: LibrariesForLibs) { + val ktfmtVersion = libs.versions.ktfmt.get() + pluginManager.apply("com.diffplug.spotless") + val spotlessFormatters: SpotlessExtension.() -> Unit = { + lineEndings = LineEnding.PLATFORM_NATIVE + + format("misc") { + target("*.md", ".gitignore") + trimTrailingWhitespace() + endWithNewline() + } + kotlin { + target("src/**/*.kt") + ktfmt(ktfmtVersion).googleStyle() + trimTrailingWhitespace() + endWithNewline() + targetExclude("**/spotless.kt") + } + kotlinGradle { + target("*.kts") + ktfmt(ktfmtVersion).googleStyle() + trimTrailingWhitespace() + endWithNewline() + licenseHeaderFile( + rootProject.isolated.projectDirectory.file("spotless/spotless.kt"), + "(import|plugins|buildscript|dependencies|pluginManagement|dependencyResolutionManagement)", + ) + } + // Apply license formatting separately for kotlin files so we can prevent it from overwriting + // copied files + format("license") { + licenseHeaderFile( + rootProject.isolated.projectDirectory.file("spotless/spotless.kt"), + "(package|@file:)", + ) + target("src/**/*.kt") + targetExclude( + "**/circuit/backstack/**/*.kt", + "**/HorizontalPagerIndicator.kt", + "**/FilterList.kt", + "**/Remove.kt", + "**/Pets.kt", + "**/SystemUiController.kt", + "**/RetainedStateHolderTest.kt", + "**/RetainedStateRestorationTester.kt", + ) + } + } + configure { + spotlessFormatters() + if (project.rootProject == project) { + predeclareDeps() + } + } + if (project.rootProject == project) { + configure { spotlessFormatters() } + } + } + + private fun Project.configureSubproject(libs: LibrariesForLibs) { + val detektVersion = libs.versions.detekt.get() + val twitterDetektPlugin = libs.detektPlugins.twitterCompose + val jvmTargetVersion = libs.versions.jvmTarget + val publishedJvmTargetVersion = libs.versions.publishedJvmTarget + + val isPublished = project.hasProperty("POM_ARTIFACT_ID") + val jvmTargetProject = if (isPublished) publishedJvmTargetVersion else jvmTargetVersion + + pluginManager.withPlugin("java") { + configure { + toolchain { + languageVersion.set( + JavaLanguageVersion.of(libs.versions.jdk.get().removeSuffix("-ea").toInt()) + ) + } + } + + tasks.withType().configureEach { + options.release.set( + jvmTargetProject.map(JavaVersion::toVersion).map { it.majorVersion.toInt() } + ) + } + } + + val hasCompose = !project.hasProperty("circuit.noCompose") + plugins.withType(KotlinBasePlugin::class.java).configureEach { + tasks.withType>().configureEach { + val isWasmTask = name.contains("wasm", ignoreCase = true) + compilerOptions { + if (isWasmTask && this is KotlinJsCompilerOptions) { + // TODO https://youtrack.jetbrains.com/issue/KT-64115 + allWarningsAsErrors.set(false) + } else if (this is KotlinNativeCompilerOptions) { + // TODO https://youtrack.jetbrains.com/issue/KT-38719 + allWarningsAsErrors.set(false) + } else { + allWarningsAsErrors.set(true) + } + if (this is KotlinJvmCompilerOptions) { + jvmTarget.set( + jvmTargetProject + .map(JavaVersion::toVersion) + .map { it.toString() } + .map(JvmTarget::fromTarget) + ) + freeCompilerArgs.addAll( + "-Xjsr305=strict", + // Match JVM assertion behavior: + // https://publicobject.com/2019/11/18/kotlins-assert-is-not-like-javas-assert/ + "-Xassertions=jvm", + // Potentially useful for static analysis tools or annotation processors. + "-Xemit-jvm-type-annotations", + // Enable new jvm-default behavior + // https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/ + "-Xjvm-default=all", + // https://kotlinlang.org/docs/whatsnew1520.html#support-for-jspecify-nullness-annotations + "-Xtype-enhancement-improvements-strict-mode", + "-Xjspecify-annotations=strict", + // https://youtrack.jetbrains.com/issue/KT-73255 + "-Xannotation-default-target=param-property", + ) + } + + progressiveMode.set(true) + } + } + + if (!project.path.startsWith(":samples") && !project.path.startsWith(":internal")) { + configure { explicitApi() } + } + + // region Detekt + project.pluginManager.apply("io.gitlab.arturbosch.detekt") + configure { + toolVersion = detektVersion + allRules = true + config.from(rootProject.isolated.projectDirectory.file("config/detekt/detekt.yml")) + buildUponDefaultConfig = true + } + + val buildDir = project.layout.buildDirectory.asFile.get().canonicalPath + tasks.withType().configureEach { + jvmTarget = jvmTargetProject.get() + exclude { it.file.canonicalPath.startsWith(buildDir) } + reports { + html.required.set(true) + xml.required.set(true) + txt.required.set(true) + } + } + + dependencies.add("detektPlugins", twitterDetektPlugin) + // endregion + } + + // Teach Gradle that full guava replaces listenablefuture. + // This bypasses the dependency resolution that transitively bumps listenablefuture to a 9999.0 + // version that is empty. + dependencies.modules { + module("com.google.guava:listenablefuture") { replacedBy("com.google.guava:guava") } + } + + pluginManager.withPlugin("com.vanniktech.maven.publish") { + pluginManager.apply("org.jetbrains.dokka") + + configure { + moduleName.set(project.path.removePrefix(":").replace(":", "/")) + basePublicationsDirectory.set(layout.buildDirectory.dir("dokkaDir")) + dokkaSourceSets.configureEach { + val readMeProvider = project.layout.projectDirectory.file("README.md") + if (readMeProvider.asFile.exists()) { + includes.from(readMeProvider) + } + + if (name.contains("androidTest", ignoreCase = true)) { + suppress.set(true) + } + skipDeprecated.set(true) + documentedVisibilities.add(VisibilityModifier.Public) + + // Skip internal packages + perPackageOption { + // language=RegExp + matchingRegex.set(".*\\.internal\\..*") + suppress.set(true) + } + // AndroidX and Android docs are automatically added by the Dokka plugin. + + // Add source links + sourceLink { + localDirectory.set(layout.projectDirectory.dir("src")) + @Suppress("NewApi") // lint is confused in the IDE + val relPath = + rootProject.isolated.projectDirectory.asFile.toPath().relativize(projectDir.toPath()) + remoteUrl( + providers.gradleProperty("POM_SCM_URL").map { scmUrl -> + "$scmUrl/tree/main/$relPath/src" + } + ) + remoteLineSuffix.set("#L") + } + } + } + + pluginManager.apply("com.dropbox.dependency-guard") + configure { + if (project.name == "circuit-codegen") { + configuration("runtimeClasspath") { + baselineMap = { + // Remove the version + it.substringBeforeLast(":") + } + } + } else if (project.path == ":circuitx:android") { + // Android-only project + configuration("releaseRuntimeClasspath") { + baselineMap = { + // Remove the version + it.substringBeforeLast(":") + } + } + } else { + configuration("androidReleaseRuntimeClasspath") { + baselineMap = { + // Remove the version + it.substringBeforeLast(":") + } + } + configuration("jvmRuntimeClasspath") { + baselineMap = { + // Remove the version + it.substringBeforeLast(":") + } + } + } + } + + configure { + publishToMavenCentral(automaticRelease = true) + signAllPublications() + } + } + + // Common android config + val commonAndroidConfig: CommonExtension<*, *, *, *, *, *>.() -> Unit = { + compileSdk = 36 + + if (hasCompose) { + buildFeatures { compose = true } + } + + compileOptions { + sourceCompatibility = jvmTargetProject.map(JavaVersion::toVersion).get() + targetCompatibility = jvmTargetProject.map(JavaVersion::toVersion).get() + } + + lint { + // https://issuetracker.google.com/issues/243267012 + disable += "Instantiatable" + checkTestSources = true + lintConfig = rootProject.isolated.projectDirectory.file("config/lint/lint.xml").asFile + } + dependencies.add("lintChecks", libs.lints.compose) + } + + // Android library config + pluginManager.withPlugin("com.android.library") { + configure { + commonAndroidConfig() + defaultConfig { minSdk = 23 } + testOptions { + // TODO update once robolectric supports it + targetSdk = 35 + } + } + + // Single-variant libraries + configure { + beforeVariants { builder -> + if (builder.buildType == "debug") { + builder.enable = false + } + } + } + } + + pluginManager.withPlugin("com.android.test") { + configure { + commonAndroidConfig() + defaultConfig { minSdk = 28 } + } + } + + // Android app config + pluginManager.withPlugin("com.android.application") { + configure { + commonAndroidConfig() + defaultConfig { + minSdk = 23 + targetSdk = 36 + } + buildTypes { + maybeCreate("debug").apply { matchingFallbacks += listOf("release") } + maybeCreate("release").apply { + isMinifyEnabled = true + signingConfig = signingConfigs.getByName("debug") + matchingFallbacks += listOf("release") + } + } + compileOptions { isCoreLibraryDesugaringEnabled = true } + } + dependencies.add("coreLibraryDesugaring", libs.desugarJdkLibs) + } + + pluginManager.withPlugin("org.jetbrains.compose") { + pluginManager.apply("org.jetbrains.kotlin.plugin.compose") + configure { includeSourceInformation.set(true) } + } + + pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { + // Enforce Kotlin BOM + dependencies.apply { add("implementation", platform(libs.kotlin.bom)) } + } + + pluginManager.withPlugin("org.jetbrains.kotlin.android") { + // Enforce Kotlin BOM + dependencies.apply { add("implementation", platform(libs.kotlin.bom)) } + } + + pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { + // Enforce Kotlin BOM + configure { + sourceSets.apply { + commonMain { + dependencies { + // KGP doesn't support catalogs https://youtrack.jetbrains.com/issue/KT-55351 + implementation( + // https://youtrack.jetbrains.com/issue/KT-58759 + project.dependencies.platform( + "org.jetbrains.kotlin:kotlin-bom:${libs.versions.kotlin.get()}" + ) + ) + } + } + } + } + + // Workaround for missing task dependency in WASM + val executableCompileSyncTasks = tasks.withType(DefaultIncrementalSyncTask::class.java) + tasks.withType(KotlinJsTest::class.java).configureEach { + mustRunAfter(executableCompileSyncTasks) + } + } + + pluginManager.withPlugin("wtf.emulator.gradle") { + val emulatorWtfToken = providers.gradleProperty("emulatorWtfToken") + configure { + devices.set(listOf(mapOf("model" to "Pixel2Atd", "version" to "30", "atd" to "true"))) + if (emulatorWtfToken.isPresent) { + token.set(emulatorWtfToken) + } + } + // We don't always run emulator.wtf on CI (forks can't access it), so we add this helper + // lifecycle task that depends on connectedCheck as an alternative. We do this only on + // projects + // that apply emulator.wtf though as we don't want to run _all_ connected checks on CI since + // that would include benchmarks. + tasks.register("ciConnectedCheck") { dependsOn("connectedCheck") } + } + + pluginManager.withPlugin("dev.zacsweers.anvil") { + configure { + useKsp(contributesAndFactoryGeneration = true, componentMerging = true) + } + } + } + + private inline fun Project.configure(action: Action) { + project.extensions.configure(T::class.java, action) + } + + private inline fun TaskCollection.withType(): TaskCollection { + return withType(T::class.java) + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 262ce8101..fed9511d8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,33 +1,6 @@ // Copyright (C) 2022 Slack Technologies, LLC // SPDX-License-Identifier: Apache-2.0 -import com.android.build.api.dsl.ApplicationExtension -import com.android.build.api.dsl.CommonExtension -import com.android.build.api.variant.LibraryAndroidComponentsExtension -import com.android.build.gradle.LibraryExtension -import com.android.build.gradle.TestExtension -import com.diffplug.gradle.spotless.SpotlessExtension -import com.diffplug.gradle.spotless.SpotlessExtensionPredeclare -import com.diffplug.spotless.LineEnding -import com.dropbox.gradle.plugins.dependencyguard.DependencyGuardPluginExtension -import com.squareup.anvil.plugin.AnvilExtension -import com.vanniktech.maven.publish.MavenPublishBaseExtension -import io.gitlab.arturbosch.detekt.Detekt -import io.gitlab.arturbosch.detekt.extensions.DetektExtension import kotlinx.validation.ExperimentalBCVApi -import org.jetbrains.dokka.gradle.DokkaExtension -import org.jetbrains.dokka.gradle.engine.parameters.VisibilityModifier -import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompilerOptions -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions -import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinNativeCompilerOptions -import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension -import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin -import org.jetbrains.kotlin.gradle.targets.js.ir.DefaultIncrementalSyncTask -import org.jetbrains.kotlin.gradle.targets.js.testing.KotlinJsTest -import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask -import wtf.emulator.EwExtension buildscript { dependencies { classpath(platform(libs.kotlin.plugins.bom)) } } @@ -66,378 +39,9 @@ dokka { } } -allprojects { - apply(plugin = "com.diffplug.spotless") - val spotlessFormatters: SpotlessExtension.() -> Unit = { - lineEndings = LineEnding.PLATFORM_NATIVE - - format("misc") { - target("*.md", ".gitignore") - trimTrailingWhitespace() - endWithNewline() - } - kotlin { - target("src/**/*.kt") - ktfmt(ktfmtVersion).googleStyle() - trimTrailingWhitespace() - endWithNewline() - targetExclude("**/spotless.kt") - } - kotlinGradle { - target("*.kts") - ktfmt(ktfmtVersion).googleStyle() - trimTrailingWhitespace() - endWithNewline() - licenseHeaderFile( - rootProject.isolated.projectDirectory.file("spotless/spotless.kt"), - "(import|plugins|buildscript|dependencies|pluginManagement|dependencyResolutionManagement)", - ) - } - // Apply license formatting separately for kotlin files so we can prevent it from overwriting - // copied files - format("license") { - licenseHeaderFile( - rootProject.isolated.projectDirectory.file("spotless/spotless.kt"), - "(package|@file:)", - ) - target("src/**/*.kt") - targetExclude( - "**/circuit/backstack/**/*.kt", - "**/HorizontalPagerIndicator.kt", - "**/FilterList.kt", - "**/Remove.kt", - "**/Pets.kt", - "**/SystemUiController.kt", - "**/RetainedStateHolderTest.kt", - "**/RetainedStateRestorationTester.kt", - ) - } - } - configure { - spotlessFormatters() - if (project.rootProject == project) { - predeclareDeps() - } - } - if (project.rootProject == project) { - configure { spotlessFormatters() } - } -} - val jvmTargetVersion = libs.versions.jvmTarget val publishedJvmTargetVersion = libs.versions.publishedJvmTarget -subprojects { - val isPublished = project.hasProperty("POM_ARTIFACT_ID") - val jvmTargetProject = if (isPublished) publishedJvmTargetVersion else jvmTargetVersion - - pluginManager.withPlugin("java") { - configure { - toolchain { - languageVersion.set( - JavaLanguageVersion.of(libs.versions.jdk.get().removeSuffix("-ea").toInt()) - ) - } - } - - tasks.withType().configureEach { - options.release.set( - jvmTargetProject.map(JavaVersion::toVersion).map { it.majorVersion.toInt() } - ) - } - } - - val hasCompose = !project.hasProperty("circuit.noCompose") - plugins.withType { - tasks.withType>().configureEach { - val isWasmTask = name.contains("wasm", ignoreCase = true) - compilerOptions { - if (isWasmTask && this is KotlinJsCompilerOptions) { - // TODO https://youtrack.jetbrains.com/issue/KT-64115 - allWarningsAsErrors.set(false) - } else if (this is KotlinNativeCompilerOptions) { - // TODO https://youtrack.jetbrains.com/issue/KT-38719 - allWarningsAsErrors.set(false) - } else { - allWarningsAsErrors.set(true) - } - if (this is KotlinJvmCompilerOptions) { - jvmTarget.set( - jvmTargetProject - .map(JavaVersion::toVersion) - .map { it.toString() } - .map(JvmTarget::fromTarget) - ) - freeCompilerArgs.addAll( - "-Xjsr305=strict", - // Match JVM assertion behavior: - // https://publicobject.com/2019/11/18/kotlins-assert-is-not-like-javas-assert/ - "-Xassertions=jvm", - // Potentially useful for static analysis tools or annotation processors. - "-Xemit-jvm-type-annotations", - // Enable new jvm-default behavior - // https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-m3-generating-default-methods-in-interfaces/ - "-Xjvm-default=all", - // https://kotlinlang.org/docs/whatsnew1520.html#support-for-jspecify-nullness-annotations - "-Xtype-enhancement-improvements-strict-mode", - "-Xjspecify-annotations=strict", - // https://youtrack.jetbrains.com/issue/KT-73255 - "-Xannotation-default-target=param-property", - ) - } - - progressiveMode.set(true) - } - } - - if (!project.path.startsWith(":samples") && !project.path.startsWith(":internal")) { - extensions.configure { explicitApi() } - } - - // region Detekt - project.apply(plugin = "io.gitlab.arturbosch.detekt") - configure { - toolVersion = detektVersion - allRules = true - config.from(rootProject.isolated.projectDirectory.file("config/detekt/detekt.yml")) - buildUponDefaultConfig = true - } - - val buildDir = project.layout.buildDirectory.asFile.get().canonicalPath - tasks.withType().configureEach { - jvmTarget = jvmTargetProject.get() - exclude { it.file.canonicalPath.startsWith(buildDir) } - reports { - html.required.set(true) - xml.required.set(true) - txt.required.set(true) - } - } - - dependencies.add("detektPlugins", twitterDetektPlugin) - // endregion - } - - // Teach Gradle that full guava replaces listenablefuture. - // This bypasses the dependency resolution that transitively bumps listenablefuture to a 9999.0 - // version that is empty. - dependencies.modules { - module("com.google.guava:listenablefuture") { replacedBy("com.google.guava:guava") } - } - - pluginManager.withPlugin("com.vanniktech.maven.publish") { - apply(plugin = "org.jetbrains.dokka") - - configure { - moduleName.set(project.path.removePrefix(":").replace(":", "/")) - basePublicationsDirectory.set(layout.buildDirectory.dir("dokkaDir")) - dokkaSourceSets.configureEach { - val readMeProvider = project.layout.projectDirectory.file("README.md") - if (readMeProvider.asFile.exists()) { - includes.from(readMeProvider) - } - - if (name.contains("androidTest", ignoreCase = true)) { - suppress.set(true) - } - skipDeprecated.set(true) - documentedVisibilities.add(VisibilityModifier.Public) - - // Skip internal packages - perPackageOption { - // language=RegExp - matchingRegex.set(".*\\.internal\\..*") - suppress.set(true) - } - // AndroidX and Android docs are automatically added by the Dokka plugin. - - // Add source links - sourceLink { - localDirectory.set(layout.projectDirectory.dir("src")) - val relPath = - rootProject.isolated.projectDirectory.asFile.toPath().relativize(projectDir.toPath()) - remoteUrl( - providers.gradleProperty("POM_SCM_URL").map { scmUrl -> - "$scmUrl/tree/main/$relPath/src" - } - ) - remoteLineSuffix.set("#L") - } - } - } - - apply(plugin = "com.dropbox.dependency-guard") - configure { - if (project.name == "circuit-codegen") { - configuration("runtimeClasspath") { - baselineMap = { - // Remove the version - it.substringBeforeLast(":") - } - } - } else if (project.path == ":circuitx:android") { - // Android-only project - configuration("releaseRuntimeClasspath") { - baselineMap = { - // Remove the version - it.substringBeforeLast(":") - } - } - } else { - configuration("androidReleaseRuntimeClasspath") { - baselineMap = { - // Remove the version - it.substringBeforeLast(":") - } - } - configuration("jvmRuntimeClasspath") { - baselineMap = { - // Remove the version - it.substringBeforeLast(":") - } - } - } - } - - configure { - publishToMavenCentral(automaticRelease = true) - signAllPublications() - } - } - - // Common android config - val commonAndroidConfig: CommonExtension<*, *, *, *, *, *>.() -> Unit = { - compileSdk = 36 - - if (hasCompose) { - buildFeatures { compose = true } - } - - compileOptions { - sourceCompatibility = jvmTargetProject.map(JavaVersion::toVersion).get() - targetCompatibility = jvmTargetProject.map(JavaVersion::toVersion).get() - } - - lint { - // https://issuetracker.google.com/issues/243267012 - disable += "Instantiatable" - checkTestSources = true - lintConfig = rootProject.isolated.projectDirectory.file("config/lint/lint.xml").asFile - } - dependencies { add("lintChecks", libs.lints.compose) } - } - - // Android library config - pluginManager.withPlugin("com.android.library") { - with(extensions.getByType()) { - commonAndroidConfig() - defaultConfig { minSdk = 23 } - testOptions { - // TODO update once robolectric supports it - targetSdk = 35 - } - } - - // Single-variant libraries - extensions.configure { - beforeVariants { builder -> - if (builder.buildType == "debug") { - builder.enable = false - } - } - } - } - - pluginManager.withPlugin("com.android.test") { - with(extensions.getByType()) { - commonAndroidConfig() - defaultConfig { minSdk = 28 } - } - } - - // Android app config - pluginManager.withPlugin("com.android.application") { - with(extensions.getByType()) { - commonAndroidConfig() - defaultConfig { - minSdk = 23 - targetSdk = 36 - } - buildTypes { - maybeCreate("debug").apply { matchingFallbacks += listOf("release") } - maybeCreate("release").apply { - isMinifyEnabled = true - signingConfig = signingConfigs.getByName("debug") - matchingFallbacks += listOf("release") - } - } - compileOptions { isCoreLibraryDesugaringEnabled = true } - } - dependencies.add("coreLibraryDesugaring", libs.desugarJdkLibs) - } - - pluginManager.withPlugin("org.jetbrains.compose") { - apply(plugin = "org.jetbrains.kotlin.plugin.compose") - configure { includeSourceInformation = true } - } - - pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { - // Enforce Kotlin BOM - dependencies { add("implementation", platform(libs.kotlin.bom)) } - } - - pluginManager.withPlugin("org.jetbrains.kotlin.android") { - // Enforce Kotlin BOM - dependencies { add("implementation", platform(libs.kotlin.bom)) } - } - - pluginManager.withPlugin("org.jetbrains.kotlin.multiplatform") { - // Enforce Kotlin BOM - configure { - sourceSets { - commonMain { - dependencies { - // KGP doesn't support catalogs https://youtrack.jetbrains.com/issue/KT-55351 - implementation( - // https://youtrack.jetbrains.com/issue/KT-58759 - project.dependencies.platform( - "org.jetbrains.kotlin:kotlin-bom:${libs.versions.kotlin.get()}" - ) - ) - } - } - } - } - - // Workaround for missing task dependency in WASM - val executableCompileSyncTasks = tasks.withType(DefaultIncrementalSyncTask::class.java) - tasks.withType(KotlinJsTest::class.java).configureEach { - mustRunAfter(executableCompileSyncTasks) - } - } - - pluginManager.withPlugin("wtf.emulator.gradle") { - val emulatorWtfToken = providers.gradleProperty("emulatorWtfToken") - configure { - devices.set(listOf(mapOf("model" to "Pixel2Atd", "version" to "30", "atd" to "true"))) - if (emulatorWtfToken.isPresent) { - token.set(emulatorWtfToken) - } - } - // We don't always run emulator.wtf on CI (forks can't access it), so we add this helper - // lifecycle task that depends on connectedCheck as an alternative. We do this only on projects - // that apply emulator.wtf though as we don't want to run _all_ connected checks on CI since - // that would include benchmarks. - tasks.register("ciConnectedCheck") { dependsOn("connectedCheck") } - } - - pluginManager.withPlugin("dev.zacsweers.anvil") { - configure { - useKsp(contributesAndFactoryGeneration = true, componentMerging = true) - } - } -} - apiValidation { @OptIn(ExperimentalBCVApi::class) klib { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10ea1501d..31fec0fda 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ androidx-compose = "1.9.2" agp = "8.13.0" anvil = "0.4.1" atomicfu = "0.29.0" +bcv = "0.18.1" benchmark = "1.4.1" coil = "3.3.0" compose-hotReload = "1.0.0-beta09" @@ -16,8 +17,10 @@ compose-jb-material-icons-core = "1.7.3" dagger = "2.57.2" datastore = "1.1.7" detekt = "1.23.8" +dg = "0.5.0" dokka = "2.0.0" eithernet = "2.0.0" +ewtf = "0.19.3" jdk = "23" jvmTarget = "11" publishedJvmTarget = "11" @@ -52,13 +55,13 @@ agp-library = { id = "com.android.library", version.ref = "agp" } agp-test = { id = "com.android.test", version.ref = "agp" } anvil = { id = "dev.zacsweers.anvil", version.ref = "anvil" } baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" } -binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.18.1" } +binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "bcv" } compose = { id = "org.jetbrains.compose", version.ref = "compose-jb" } compose-hotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "compose-hotReload" } -dependencyGuard = { id = "com.dropbox.dependency-guard", version = "0.5.0" } +dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dg" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } -emulatorWtf = { id = "wtf.emulator.gradle", version = "0.19.3" } +emulatorWtf = { id = "wtf.emulator.gradle", version.ref = "ewtf" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-atomicfu = { id = "org.jetbrains.kotlin.plugin.atomicfu", version.ref = "kotlin" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } @@ -66,6 +69,7 @@ kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref kotlin-plugin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-plugin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-plugin-sam = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } metro = { id = "dev.zacsweers.metro", version = "0.6.7" } @@ -75,7 +79,18 @@ spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } [libraries] -agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } +gradlePlugins-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } +gradlePlugins-spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } +gradlePlugins-dependencyGuard = { module = "com.dropbox.dependency-guard:dependency-guard", version.ref = "dg" } +gradlePlugins-anvil = { module = "dev.zacsweers.anvil:gradle-plugin", version.ref = "anvil" } +gradlePlugins-mavenPublish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "mavenPublish" } +gradlePlugins-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradlePlugins-binaryCompatibilityValidator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "bcv" } +gradlePlugins-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } +gradlePlugins-composeCompiler = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } +gradlePlugins-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +gradlePlugins-emulatorWtf = { module = "wtf.emulator:gradle-plugin", version.ref = "ewtf" } + androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" } From b913028857a5a06a5f32ed9b3759fa1ae670758e Mon Sep 17 00:00:00 2001 From: Zac Sweers Date: Thu, 2 Oct 2025 16:39:59 -0400 Subject: [PATCH 5/5] Apply in settings --- settings.gradle.kts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/settings.gradle.kts b/settings.gradle.kts index 3ce08ba26..f1ca90e3b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -208,6 +208,8 @@ develocity { rootProject.name = "circuit-root" +includeBuild("build-logic") + // Please keep these in alphabetical order! include( ":backstack", @@ -243,6 +245,10 @@ include( ":internal-test-utils", ) +gradle.lifecycle.beforeProject { + apply(plugin = "circuit.base") +} + // https://docs.gradle.org/5.6/userguide/groovy_plugin.html#sec:groovy_compilation_avoidance enableFeaturePreview("GROOVY_COMPILATION_AVOIDANCE")