Merge pull request #2697 from square/jwilson.0806.kts

Convert builds to KTS
diff --git a/build.gradle b/build.gradle
deleted file mode 100644
index 385673d..0000000
--- a/build.gradle
+++ /dev/null
@@ -1,179 +0,0 @@
-import org.jetbrains.dokka.gradle.DokkaTask
-import com.vanniktech.maven.publish.SonatypeHost
-
-buildscript {
-  ext.versions = [
-    'minSdk'    : 14,
-    'compileSdk': 34,
-  ]
-  repositories {
-    google()
-    gradlePluginPortal()
-    mavenCentral()
-  }
-  dependencies {
-    classpath libs.gradlePlugin.android
-    classpath libs.gradlePlugin.kotlin
-    classpath libs.gradlePlugin.dokka
-    classpath libs.gradlePlugin.mavenPublish
-    classpath libs.gradlePlugin.detekt
-    classpath libs.gradlePlugin.binaryCompatibility
-    classpath libs.gradlePlugin.keeper
-    classpath libs.gradlePlugin.sqldelight
-    classpath 'com.google.dagger:hilt-android-gradle-plugin:2.43.2'
-  }
-}
-
-// We use JetBrain's Kotlin Binary Compatibility Validator to track changes to our public binary
-// APIs.
-// When making a change that results in a public ABI change, the apiCheck task will fail. When this
-// happens, run ./gradlew apiDump to generate updated *.api files, and add those to your commit.
-// See https://github.com/Kotlin/binary-compatibility-validator
-apply plugin: 'binary-compatibility-validator'
-
-apiValidation {
-  // Ignore projects that are not uploaded to Maven Central
-  ignoredProjects += ["leakcanary-app", "leakcanary-android-sample", "shark-test", "shark-hprof-test", "shark-cli"]
-}
-
-// This plugin needs to be applied to the root projects for the dokkaGfmCollector task we use to
-// generate the documentation site.
-apply plugin: 'org.jetbrains.dokka'
-
-repositories {
-  // Needed for the Dokka plugin.
-  gradlePluginPortal()
-}
-
-// Config shared for all subprojects
-subprojects {
-
-  repositories {
-    google()
-    mavenCentral()
-    //    maven {
-    //      url 'https://oss.sonatype.org/content/repositories/snapshots/'
-    //    }
-    //    mavenLocal()
-    jcenter()
-  }
-
-  apply plugin: 'io.gitlab.arturbosch.detekt'
-
-  tasks.withType(JavaCompile).configureEach {
-    options.compilerArgs += [
-        '-Xlint:all',
-        '-Xlint:-serial',
-        '-Xlint:-deprecation',
-        // espresso-core classes say they're compiled with 51.0 but contain 52.0 attributes.
-        // warning: [classfile] MethodParameters attribute introduced in version 52.0 class files is ignored in version 51.0 class files
-        // '-Werror'
-    ]
-  }
-
-  tasks.withType(Test).configureEach {
-    testLogging {
-      exceptionFormat 'FULL'
-      showCauses true
-      showExceptions true
-      showStackTraces true
-    }
-  }
-
-  detekt {
-    config = rootProject.files('config/detekt-config.yml')
-    parallel = true
-    reports {
-      xml.enabled = false
-    }
-  }
-
-  pluginManager.withPlugin("java") {
-    tasks.named("check") { dependsOn("detekt") }
-    tasks.named("assemble") { dependsOn(rootProject.tasks.named("installGitHooks")) }
-    tasks.named("clean") { dependsOn(rootProject.tasks.named("installGitHooks")) }
-  }
-}
-
-// Config shared for subprojects except leakcanary-deobfuscation-gradle-plugin
-configure(subprojects.findAll {
-  !(["leakcanary-deobfuscation-gradle-plugin"].contains(it.name))
-}) {
-  tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
-    kotlinOptions {
-      jvmTarget = '1.8'
-    }
-  }
-}
-
-// Config shared for subprojects except apps
-configure(subprojects.findAll {
-    !(["leakcanary-app", "leakcanary-android-sample"].contains(it.name))
-}) {
-  // Note: to skip Dokka on some projects we could add it individually to projects we actually
-  // want.
-  apply plugin: 'org.jetbrains.dokka'
-  group = GROUP
-  version = VERSION_NAME
-
-  tasks.withType(DokkaTask.class).configureEach {
-    dokkaSourceSets.configureEach {
-      reportUndocumented.set(false)
-      displayName.set(null)
-      platform.set(org.jetbrains.dokka.Platform.jvm)
-
-      perPackageOption {
-        // will match all .internal packages and sub-packages
-        matchingRegex.set("(.*\\.internal.*)")
-        suppress.set(true)
-      }
-      perPackageOption {
-        // BuildConfig files
-        matchingRegex.set("com.squareup.leakcanary\\..*")
-        suppress.set(true)
-      }
-      skipDeprecated.set(true)
-      externalDocumentationLink {
-        url.set(new URL("https://square.github.io/okio/2.x/okio/"))
-      }
-      externalDocumentationLink {
-        url.set(new URL("https://square.github.io/moshi/1.x/moshi/"))
-      }
-    }
-  }
-
-  pluginManager.withPlugin("com.vanniktech.maven.publish") {
-    mavenPublishing {
-      publishToMavenCentral(SonatypeHost.S01)
-      signAllPublications()
-    }
-  }
-}
-
-//Copies git hooks from /hooks folder into .git; currently used to run Detekt during push
-//Git hook installation
-tasks.register("installGitHooks", Copy) {
-  from new File(rootProject.rootDir, 'config/hooks')
-  into { new File(rootProject.rootDir, '.git/hooks') }
-  fileMode 0777 //Make files executable
-}
-
-tasks.register("siteDokka", Copy) {
-  description = "Generate dokka Github-flavored Markdown for the documentation site."
-  group = "documentation"
-  dependsOn(":dokkaGfmCollector")
-
-  // Copy the files instead of configuring a different output directory on the dokka task itself
-  // since the default output directories disambiguate between different types of outputs, and our
-  // custom directory doesn't.
-  from(layout.buildDirectory.dir("dokka/gfmCollector/leakcanary-repo"))
-  // For whatever reason Dokka doesn't want to ignore the packages we told it to ignore.
-  // Fine, we'll just ignore it here.
-  exclude '**/com.example.leakcanary/**'
-  into(rootProject.file("docs/api"))
-
-  filter { line ->
-    // Dokka adds [main]\ and [main]<br> everywhere, this just removes it.
-    line.replaceAll("\\[main\\]\\\\", "").replaceAll("\\[main\\]<br>", "")
-  }
-}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..c8aaa56
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,183 @@
+import com.vanniktech.maven.publish.MavenPublishBaseExtension
+import com.vanniktech.maven.publish.SonatypeHost
+import io.gitlab.arturbosch.detekt.extensions.DetektExtension
+import java.net.URL
+import kotlinx.validation.ApiValidationExtension
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+import org.jetbrains.dokka.gradle.DokkaTask
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+buildscript {
+  repositories {
+    google()
+    gradlePluginPortal()
+    mavenCentral()
+  }
+  dependencies {
+    classpath(libs.gradlePlugin.android)
+    classpath(libs.gradlePlugin.kotlin)
+    classpath(libs.gradlePlugin.dokka)
+    classpath(libs.gradlePlugin.mavenPublish)
+    classpath(libs.gradlePlugin.detekt)
+    classpath(libs.gradlePlugin.binaryCompatibility)
+    classpath(libs.gradlePlugin.keeper)
+    classpath(libs.gradlePlugin.sqldelight)
+    classpath("com.google.dagger:hilt-android-gradle-plugin:2.43.2")
+  }
+}
+
+// We use JetBrain's Kotlin Binary Compatibility Validator to track changes to our public binary
+// APIs.
+// When making a change that results in a public ABI change, the apiCheck task will fail. When this
+// happens, run ./gradlew apiDump to generate updated *.api files, and add those to your commit.
+// See https://github.com/Kotlin/binary-compatibility-validator
+apply(plugin = "binary-compatibility-validator")
+
+extensions.configure<ApiValidationExtension> {
+  // Ignore projects that are not uploaded to Maven Central
+  ignoredProjects += listOf("leakcanary-app", "leakcanary-android-sample", "shark-test", "shark-hprof-test", "shark-cli")
+}
+
+// This plugin needs to be applied to the root projects for the dokkaGfmCollector task we use to
+// generate the documentation site.
+apply(plugin = "org.jetbrains.dokka")
+
+repositories {
+  // Needed for the Dokka plugin.
+  gradlePluginPortal()
+}
+
+// Config shared for all subprojects
+subprojects {
+
+  repositories {
+    google()
+    mavenCentral()
+    //    maven {
+    //      url 'https://oss.sonatype.org/content/repositories/snapshots/'
+    //    }
+    //    mavenLocal()
+    jcenter()
+  }
+
+  apply(plugin = "io.gitlab.arturbosch.detekt")
+
+  tasks.withType<JavaCompile> {
+    options.compilerArgs.addAll(
+      listOf(
+        "-Xlint:all",
+        "-Xlint:-serial",
+        "-Xlint:-deprecation",
+        // espresso-core classes say they're compiled with 51.0 but contain 52.0 attributes.
+        // warning: [classfile] MethodParameters attribute introduced in version 52.0 class files is ignored in version 51.0 class files
+        // "-Werror"
+      )
+    )
+  }
+
+  tasks.withType<Test> {
+    testLogging {
+      exceptionFormat = TestExceptionFormat.FULL
+      showCauses = true
+      showExceptions = true
+      showStackTraces = true
+    }
+  }
+
+  extensions.configure<DetektExtension> {
+    config = rootProject.files("config/detekt-config.yml")
+    parallel = true
+    reports {
+      xml.enabled = false
+    }
+  }
+
+  pluginManager.withPlugin("java") {
+    tasks.named("check") { dependsOn("detekt") }
+    tasks.named("assemble") { dependsOn(rootProject.tasks.named("installGitHooks")) }
+    tasks.named("clean") { dependsOn(rootProject.tasks.named("installGitHooks")) }
+  }
+}
+
+// Config shared for subprojects except leakcanary-deobfuscation-gradle-plugin
+configure(subprojects.filter {
+  it.name !in listOf("leakcanary-deobfuscation-gradle-plugin")
+}) {
+  tasks.withType<KotlinCompile> {
+    kotlinOptions {
+      jvmTarget = "1.8"
+    }
+  }
+}
+
+// Config shared for subprojects except apps
+configure(subprojects.filter {
+  it.name !in listOf("leakcanary-app", "leakcanary-android-sample")
+}) {
+  // Note: to skip Dokka on some projects we could add it individually to projects we actually
+  // want.
+  apply(plugin = "org.jetbrains.dokka")
+  group = property("GROUP").toString()
+  version = property("VERSION_NAME").toString()
+
+  tasks.withType<DokkaTask> {
+    dokkaSourceSets.configureEach {
+      reportUndocumented.set(false)
+      displayName.set(null as String?)
+      platform.set(org.jetbrains.dokka.Platform.jvm)
+
+      perPackageOption {
+        // will match all .internal packages and sub-packages
+        matchingRegex.set("(.*\\.internal.*)")
+        suppress.set(true)
+      }
+      perPackageOption {
+        // BuildConfig files
+        matchingRegex.set("com.squareup.leakcanary\\..*")
+        suppress.set(true)
+      }
+      skipDeprecated.set(true)
+      externalDocumentationLink {
+        url.set(URL("https://square.github.io/okio/2.x/okio/"))
+      }
+      externalDocumentationLink {
+        url.set(URL("https://square.github.io/moshi/1.x/moshi/"))
+      }
+    }
+  }
+
+  pluginManager.withPlugin("com.vanniktech.maven.publish") {
+    extensions.configure<MavenPublishBaseExtension> {
+      publishToMavenCentral(SonatypeHost.S01)
+      signAllPublications()
+    }
+  }
+}
+
+//Copies git hooks from /hooks folder into .git; currently used to run Detekt during push
+//Git hook installation
+tasks.register<Copy>("installGitHooks") {
+  from(File(rootProject.rootDir, "config/hooks"))
+  into({ File(rootProject.rootDir, ".git/hooks") })
+  fileMode = "0777".toInt(8) // Make files executable
+}
+
+tasks.register<Copy>("siteDokka") {
+  description = "Generate dokka Github-flavored Markdown for the documentation site."
+  group = "documentation"
+  dependsOn(":dokkaGfmCollector")
+
+  // Copy the files instead of configuring a different output directory on the dokka task itself
+  // since the default output directories disambiguate between different types of outputs, and our
+  // custom directory doesn't.
+  from(layout.buildDirectory.dir("dokka/gfmCollector/leakcanary-repo"))
+  // For whatever reason Dokka doesn't want to ignore the packages we told it to ignore.
+  // Fine, we'll just ignore it here.
+  exclude("**/com.example.leakcanary/**")
+  into(rootProject.file("docs/api"))
+
+  filter { line ->
+    // Dokka adds [main]\ and [main]<br> everywhere, this just removes it.
+    line.replace("\\[main\\]\\\\", "").replace("\\[main\\]<br>", "")
+  }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 43265b8..9cd723a 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -15,11 +15,14 @@
 [versions]
 # We would like to use Kotlin recent language features but keep Kotlin 1.3 library APIs
 # The benefit is that depending clients do not have to upgrade to Kotlin 1.4
+compose = "1.4.3"
 kotlin = "1.8.21"
 coroutines = "1.7.3"
 androidXTest = "1.1.0"
 androidXJunit = "1.1.3"
 workManager = "2.7.0"
+androidMinSdk = "14"
+androidCompileSdk = "34"
 
 [libraries]
 gradlePlugin-android = { module = "com.android.tools.build:gradle", version = "8.0.0" }
diff --git a/leakcanary/leakcanary-android-core/build.gradle b/leakcanary/leakcanary-android-core/build.gradle
deleted file mode 100644
index c912075..0000000
--- a/leakcanary/leakcanary-android-core/build.gradle
+++ /dev/null
@@ -1,59 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.shark.sharkAndroid
-  api projects.objectWatcher.objectWatcherAndroidCore
-  api projects.objectWatcher.objectWatcherAndroidAndroidx
-  api projects.leakcanary.leakcanaryAndroidUtils
-  implementation libs.kotlin.stdlib
-
-  // Optional dependency
-  compileOnly libs.androidX.work.runtime
-  compileOnly libs.androidX.work.multiprocess
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation libs.kotlin.reflect
-  testImplementation libs.mockito
-  testImplementation libs.mockitoKotlin
-  androidTestImplementation libs.androidX.test.espresso
-  androidTestImplementation libs.androidX.test.rules
-  androidTestImplementation libs.androidX.test.runner
-  androidTestImplementation libs.assertjCore
-  androidTestImplementation projects.shark.sharkHprofTest
-  androidTestUtil libs.androidX.test.orchestrator
-}
-
-def gitSha() {
-  return 'git rev-parse --short HEAD'.execute().text.trim()
-}
-
-android {
-  resourcePrefix 'leak_canary_'
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-    // Avoid DeprecatedTargetSdkVersionDialog during UI tests
-    targetSdk versions.compileSdk
-    buildConfigField "String", "LIBRARY_VERSION", "\"${rootProject.ext.VERSION_NAME}\""
-    buildConfigField "String", "GIT_SHA", "\"${gitSha()}\""
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
-    testInstrumentationRunnerArguments clearPackageData: 'true'
-    testOptions {
-      execution 'ANDROIDX_TEST_ORCHESTRATOR'
-    }
-  }
-  namespace 'com.squareup.leakcanary.core'
-  testNamespace 'com.squareup.leakcanary.core.test'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/leakcanary/leakcanary-android-core/build.gradle.kts b/leakcanary/leakcanary-android-core/build.gradle.kts
new file mode 100644
index 0000000..0b20c6c
--- /dev/null
+++ b/leakcanary/leakcanary-android-core/build.gradle.kts
@@ -0,0 +1,66 @@
+import java.io.InputStreamReader
+
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.shark.sharkAndroid)
+  api(projects.objectWatcher.objectWatcherAndroidCore)
+  api(projects.objectWatcher.objectWatcherAndroidAndroidx)
+  api(projects.leakcanary.leakcanaryAndroidUtils)
+  implementation(libs.kotlin.stdlib)
+
+  // Optional dependency
+  compileOnly(libs.androidX.work.runtime)
+  compileOnly(libs.androidX.work.multiprocess)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(libs.kotlin.reflect)
+  testImplementation(libs.mockito)
+  testImplementation(libs.mockitoKotlin)
+  androidTestImplementation(libs.androidX.test.espresso)
+  androidTestImplementation(libs.androidX.test.rules)
+  androidTestImplementation(libs.androidX.test.runner)
+  androidTestImplementation(libs.assertjCore)
+  androidTestImplementation(projects.shark.sharkHprofTest)
+  androidTestUtil(libs.androidX.test.orchestrator)
+}
+
+fun gitSha(): String {
+  val process = ProcessBuilder("git", "rev-parse", "--short", "HEAD").start()
+  return InputStreamReader(process.inputStream).readText().trim()
+}
+
+android {
+  resourcePrefix = "leak_canary_"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    // Avoid DeprecatedTargetSdkVersionDialog during UI tests
+    targetSdk = libs.versions.androidCompileSdk.get().toInt()
+    buildConfigField("String", "LIBRARY_VERSION", "\"${rootProject.property("VERSION_NAME")}\"")
+    buildConfigField("String", "GIT_SHA", "\"${gitSha()}\"")
+    consumerProguardFiles("consumer-proguard-rules.pro")
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+    testInstrumentationRunnerArguments(
+      mapOf(
+        "clearPackageData" to "true",
+      )
+    )
+    testOptions {
+      execution = "ANDROIDX_TEST_ORCHESTRATOR"
+    }
+  }
+  namespace = "com.squareup.leakcanary.core"
+  testNamespace = "com.squareup.leakcanary.core.test"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/leakcanary/leakcanary-android-instrumentation/build.gradle b/leakcanary/leakcanary-android-instrumentation/build.gradle
deleted file mode 100644
index 50539d5..0000000
--- a/leakcanary/leakcanary-android-instrumentation/build.gradle
+++ /dev/null
@@ -1,44 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.leakcanary.leakcanaryAndroidCore
-  api projects.leakcanary.leakcanaryAndroidTest
-  api projects.leakcanary.leakcanaryTestCore
-  api projects.shark.sharkAndroid
-
-  implementation libs.androidX.test.runner
-  implementation libs.kotlin.stdlib
-
-  // AppWatcher auto installer for running tests
-  androidTestImplementation projects.objectWatcher.objectWatcherAndroid
-  // Plumber auto installer for running tests
-  androidTestImplementation projects.plumber.plumberAndroid
-  androidTestImplementation libs.androidX.multidex
-  androidTestImplementation libs.androidX.test.core
-  androidTestImplementation libs.androidX.test.espresso
-  androidTestImplementation libs.androidX.test.rules
-  androidTestImplementation libs.androidX.fragment
-  androidTestImplementation libs.assertjCore
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    targetSdk versions.compileSdk
-    minSdk versions.minSdk
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-    multiDexEnabled true
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.instrumentation'
-  testNamespace 'com.squareup.leakcanary.instrumentation.test'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    ignore 'InvalidPackage'
-  }
-}
diff --git a/leakcanary/leakcanary-android-instrumentation/build.gradle.kts b/leakcanary/leakcanary-android-instrumentation/build.gradle.kts
new file mode 100644
index 0000000..6eaddf1
--- /dev/null
+++ b/leakcanary/leakcanary-android-instrumentation/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryAndroidCore)
+  api(projects.leakcanary.leakcanaryAndroidTest)
+  api(projects.leakcanary.leakcanaryTestCore)
+  api(projects.shark.sharkAndroid)
+
+  implementation(libs.androidX.test.runner)
+  implementation(libs.kotlin.stdlib)
+
+  // AppWatcher auto installer for running tests
+  androidTestImplementation(projects.objectWatcher.objectWatcherAndroid)
+  // Plumber auto installer for running tests
+  androidTestImplementation(projects.plumber.plumberAndroid)
+  androidTestImplementation(libs.androidX.multidex)
+  androidTestImplementation(libs.androidX.test.core)
+  androidTestImplementation(libs.androidX.test.espresso)
+  androidTestImplementation(libs.androidX.test.rules)
+  androidTestImplementation(libs.androidX.fragment)
+  androidTestImplementation(libs.assertjCore)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    targetSdk = libs.versions.androidCompileSdk.get().toInt()
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    multiDexEnabled = true
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.instrumentation"
+  testNamespace = "com.squareup.leakcanary.instrumentation.test"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    ignore += "InvalidPackage"
+  }
+}
diff --git a/leakcanary/leakcanary-android-process/build.gradle b/leakcanary/leakcanary-android-process/build.gradle
deleted file mode 100644
index 674d961..0000000
--- a/leakcanary/leakcanary-android-process/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.shark.sharkLog
-  api projects.objectWatcher.objectWatcherAndroidCore
-
-  implementation libs.kotlin.stdlib
-  implementation libs.androidX.work.multiprocess
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    ignore 'InvalidPackage'
-  }
-}
diff --git a/leakcanary/leakcanary-android-process/build.gradle.kts b/leakcanary/leakcanary-android-process/build.gradle.kts
new file mode 100644
index 0000000..1e79009
--- /dev/null
+++ b/leakcanary/leakcanary-android-process/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.shark.sharkLog)
+  api(projects.objectWatcher.objectWatcherAndroidCore)
+
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.androidX.work.multiprocess)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    ignore += "InvalidPackage"
+  }
+}
diff --git a/leakcanary/leakcanary-android-release/build.gradle b/leakcanary/leakcanary-android-release/build.gradle
deleted file mode 100644
index e630558..0000000
--- a/leakcanary/leakcanary-android-release/build.gradle
+++ /dev/null
@@ -1,34 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.shark.sharkAndroid
-  api projects.leakcanary.leakcanaryAndroidUtils
-
-  implementation libs.kotlin.stdlib
-  implementation libs.okio2
-}
-
-def gitSha() {
-  return 'git rev-parse --short HEAD'.execute().text.trim()
-}
-
-android {
-  resourcePrefix 'leak_canary_'
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk 16
-    buildConfigField "String", "LIBRARY_VERSION", "\"${rootProject.ext.VERSION_NAME}\""
-    buildConfigField "String", "GIT_SHA", "\"${gitSha()}\""
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-  namespace 'com.squareup.leakcanary.release'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/leakcanary/leakcanary-android-release/build.gradle.kts b/leakcanary/leakcanary-android-release/build.gradle.kts
new file mode 100644
index 0000000..6f9ce35
--- /dev/null
+++ b/leakcanary/leakcanary-android-release/build.gradle.kts
@@ -0,0 +1,37 @@
+import java.io.InputStreamReader
+
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.shark.sharkAndroid)
+  api(projects.leakcanary.leakcanaryAndroidUtils)
+
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.okio2)
+}
+
+fun gitSha(): String {
+  val process = ProcessBuilder("git", "rev-parse", "--short", "HEAD").start()
+  return InputStreamReader(process.inputStream).readText().trim()
+}
+
+android {
+  resourcePrefix = "leak_canary_"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = 16
+    buildConfigField("String", "LIBRARY_VERSION", "\"${rootProject.property("VERSION_NAME")}\"")
+    buildConfigField("String", "GIT_SHA", "\"${gitSha()}\"")
+    consumerProguardFiles("consumer-proguard-rules.pro")
+  }
+  namespace = "com.squareup.leakcanary.release"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/leakcanary/leakcanary-android-startup/build.gradle b/leakcanary/leakcanary-android-startup/build.gradle
deleted file mode 100644
index c39f402..0000000
--- a/leakcanary/leakcanary-android-startup/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.leakcanary.leakcanaryAndroidCore
-  // AppWatcher AndroidX Startup installer
-  implementation projects.objectWatcher.objectWatcherAndroidStartup
-  // Plumber AndroidX Startup installer
-  implementation projects.plumber.plumberAndroidStartup
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.startup'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    ignore 'InvalidPackage'
-  }
-}
diff --git a/leakcanary/leakcanary-android-startup/build.gradle.kts b/leakcanary/leakcanary-android-startup/build.gradle.kts
new file mode 100644
index 0000000..a3783ac
--- /dev/null
+++ b/leakcanary/leakcanary-android-startup/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryAndroidCore)
+  // AppWatcher AndroidX Startup installer
+  implementation(projects.objectWatcher.objectWatcherAndroidStartup)
+  // Plumber AndroidX Startup installer
+  implementation(projects.plumber.plumberAndroidStartup)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.startup"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    ignore += "InvalidPackage"
+  }
+}
diff --git a/leakcanary/leakcanary-android-test/build.gradle b/leakcanary/leakcanary-android-test/build.gradle
deleted file mode 100644
index e921acc..0000000
--- a/leakcanary/leakcanary-android-test/build.gradle
+++ /dev/null
@@ -1,32 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.leakcanary.leakcanaryCore
-  api projects.leakcanary.leakcanaryAndroidUtils
-  api projects.leakcanary.leakcanaryTestCore
-  api projects.shark.sharkAndroid
-
-  implementation libs.androidX.test.runner
-
-  androidTestImplementation libs.androidX.multidex
-  androidTestImplementation libs.androidX.test.core
-  androidTestImplementation libs.androidX.test.runner
-  androidTestImplementation libs.assertjCore
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    targetSdk versions.compileSdk
-    minSdk versions.minSdk
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-    multiDexEnabled true
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.android.test'
-  testNamespace 'com.squareup.leakcanary.android.test.test'
-}
diff --git a/leakcanary/leakcanary-android-test/build.gradle.kts b/leakcanary/leakcanary-android-test/build.gradle.kts
new file mode 100644
index 0000000..8fb501a
--- /dev/null
+++ b/leakcanary/leakcanary-android-test/build.gradle.kts
@@ -0,0 +1,32 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryCore)
+  api(projects.leakcanary.leakcanaryAndroidUtils)
+  api(projects.leakcanary.leakcanaryTestCore)
+  api(projects.shark.sharkAndroid)
+
+  implementation(libs.androidX.test.runner)
+
+  androidTestImplementation(libs.androidX.multidex)
+  androidTestImplementation(libs.androidX.test.core)
+  androidTestImplementation(libs.androidX.test.runner)
+  androidTestImplementation(libs.assertjCore)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    targetSdk = libs.versions.androidCompileSdk.get().toInt()
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    multiDexEnabled = true
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.android.test"
+  testNamespace = "com.squareup.leakcanary.android.test.test"
+}
diff --git a/leakcanary/leakcanary-android-uiautomator/build.gradle b/leakcanary/leakcanary-android-uiautomator/build.gradle
deleted file mode 100644
index 00279f7..0000000
--- a/leakcanary/leakcanary-android-uiautomator/build.gradle
+++ /dev/null
@@ -1,24 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.leakcanary.leakcanaryCore
-  api projects.leakcanary.leakcanaryTestCore
-  api projects.shark.sharkAndroid
-  api libs.androidX.test.uiautomator
-
-  implementation libs.androidX.test.monitor
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    targetSdk versions.compileSdk
-    minSdk 18
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.android.uiautomator'
-}
diff --git a/leakcanary/leakcanary-android-uiautomator/build.gradle.kts b/leakcanary/leakcanary-android-uiautomator/build.gradle.kts
new file mode 100644
index 0000000..ebf7398
--- /dev/null
+++ b/leakcanary/leakcanary-android-uiautomator/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryCore)
+  api(projects.leakcanary.leakcanaryTestCore)
+  api(projects.shark.sharkAndroid)
+  api(libs.androidX.test.uiautomator)
+
+  implementation(libs.androidX.test.monitor)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    targetSdk = libs.versions.androidCompileSdk.get().toInt()
+    minSdk = 18
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.android.uiautomator"
+}
diff --git a/leakcanary/leakcanary-android-utils/build.gradle b/leakcanary/leakcanary-android-utils/build.gradle
deleted file mode 100644
index 548972b..0000000
--- a/leakcanary/leakcanary-android-utils/build.gradle
+++ /dev/null
@@ -1,26 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.leakcanary.leakcanaryCore
-  api projects.shark.sharkLog
-
-  implementation libs.kotlin.stdlib
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.utils'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/leakcanary/leakcanary-android-utils/build.gradle.kts b/leakcanary/leakcanary-android-utils/build.gradle.kts
new file mode 100644
index 0000000..6a811cd
--- /dev/null
+++ b/leakcanary/leakcanary-android-utils/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryCore)
+  api(projects.shark.sharkLog)
+
+  implementation(libs.kotlin.stdlib)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.utils"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/leakcanary/leakcanary-android/build.gradle b/leakcanary/leakcanary-android/build.gradle
deleted file mode 100644
index 87fb7b1..0000000
--- a/leakcanary/leakcanary-android/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.leakcanary.leakcanaryAndroidCore
-  // AppWatcher auto installer
-  api projects.objectWatcher.objectWatcherAndroid
-  // Plumber auto installer
-  implementation projects.plumber.plumberAndroid
-  implementation libs.kotlin.stdlib
-
-  androidTestImplementation libs.androidX.test.espresso
-  androidTestImplementation libs.androidX.test.rules
-  androidTestImplementation libs.androidX.test.runner
-  androidTestImplementation libs.assertjCore
-  androidTestImplementation projects.shark.sharkHprofTest
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    ignore 'InvalidPackage'
-  }
-}
diff --git a/leakcanary/leakcanary-android/build.gradle.kts b/leakcanary/leakcanary-android/build.gradle.kts
new file mode 100644
index 0000000..9205655
--- /dev/null
+++ b/leakcanary/leakcanary-android/build.gradle.kts
@@ -0,0 +1,35 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryAndroidCore)
+  // AppWatcher auto installer
+  api(projects.objectWatcher.objectWatcherAndroid)
+  // Plumber auto installer
+  implementation(projects.plumber.plumberAndroid)
+  implementation(libs.kotlin.stdlib)
+
+  androidTestImplementation(libs.androidX.test.espresso)
+  androidTestImplementation(libs.androidX.test.rules)
+  androidTestImplementation(libs.androidX.test.runner)
+  androidTestImplementation(libs.assertjCore)
+  androidTestImplementation(projects.shark.sharkHprofTest)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    ignore += "InvalidPackage"
+  }
+}
diff --git a/leakcanary/leakcanary-app-aidl/build.gradle b/leakcanary/leakcanary-app-aidl/build.gradle
deleted file mode 100644
index f711b55..0000000
--- a/leakcanary/leakcanary-app-aidl/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  implementation libs.kotlin.stdlib
-  implementation projects.shark.shark
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures {
-    aidl true
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.app.aidl'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    ignore 'InvalidPackage'
-  }
-}
diff --git a/leakcanary/leakcanary-app-aidl/build.gradle.kts b/leakcanary/leakcanary-app-aidl/build.gradle.kts
new file mode 100644
index 0000000..67ce3ea
--- /dev/null
+++ b/leakcanary/leakcanary-app-aidl/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+  implementation(projects.shark.shark)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures {
+    aidl = true
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.app.aidl"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    ignore += "InvalidPackage"
+  }
+}
diff --git a/leakcanary/leakcanary-app-service/build.gradle b/leakcanary/leakcanary-app-service/build.gradle
deleted file mode 100644
index ccf54c1..0000000
--- a/leakcanary/leakcanary-app-service/build.gradle
+++ /dev/null
@@ -1,30 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  implementation libs.kotlin.stdlib
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.app.service'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    ignore 'InvalidPackage'
-  }
-}
-
-dependencies {
-  implementation projects.leakcanary.leakcanaryAppAidl
-  implementation projects.leakcanary.leakcanaryAndroidCore
-  implementation projects.shark.shark
-  implementation projects.shark.sharkAndroid
-}
diff --git a/leakcanary/leakcanary-app-service/build.gradle.kts b/leakcanary/leakcanary-app-service/build.gradle.kts
new file mode 100644
index 0000000..b013c7d
--- /dev/null
+++ b/leakcanary/leakcanary-app-service/build.gradle.kts
@@ -0,0 +1,30 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.app.service"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    ignore += "InvalidPackage"
+  }
+}
+
+dependencies {
+  implementation(projects.leakcanary.leakcanaryAppAidl)
+  implementation(projects.leakcanary.leakcanaryAndroidCore)
+  implementation(projects.shark.shark)
+  implementation(projects.shark.sharkAndroid)
+}
diff --git a/leakcanary/leakcanary-app/build.gradle b/leakcanary/leakcanary-app/build.gradle
deleted file mode 100644
index ad4ece3..0000000
--- a/leakcanary/leakcanary-app/build.gradle
+++ /dev/null
@@ -1,104 +0,0 @@
-plugins {
-  id("com.android.application")
-  id("org.jetbrains.kotlin.android")
-  id("app.cash.sqldelight")
-  id("com.google.dagger.hilt.android")
-  id("kotlin-kapt")
-  id("kotlin-parcelize")
-}
-
-ext {
-  compose_version = '1.4.3'
-}
-
-def gitSha() {
-  return 'git rev-parse --short HEAD'.execute().text.trim()
-}
-
-android {
-  namespace 'org.leakcanary'
-  compileSdk versions.compileSdk
-
-  defaultConfig {
-    applicationId "org.leakcanary"
-    // 21 required by Compose
-    minSdk 21
-    targetSdk versions.compileSdk
-
-    buildConfigField "String", "GIT_SHA", "\"${gitSha()}\""
-
-    // TODO Figure out versioning scheme. Should this follow LeakCanary releases?
-    versionCode 1
-    versionName "1.0"
-
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-    vectorDrawables {
-      useSupportLibrary true
-    }
-  }
-
-  buildTypes {
-    debug
-    release {
-      // TODO Enable R8 minification
-      // minifyEnabled true
-      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-      // TODO Proper signing config
-      signingConfig signingConfigs.debug
-    }
-  }
-
-  compileOptions {
-    sourceCompatibility JavaVersion.VERSION_1_8
-    targetCompatibility JavaVersion.VERSION_1_8
-  }
-
-  kotlinOptions {
-    jvmTarget = '1.8'
-  }
-
-  buildFeatures {
-    compose true
-  }
-
-  composeOptions {
-    kotlinCompilerExtensionVersion '1.4.7'
-  }
-
-  packagingOptions {
-    resources {
-      excludes += '/META-INF/{AL2.0,LGPL2.1}'
-    }
-  }
-}
-
-dependencies {
-  implementation projects.leakcanary.leakcanaryAppAidl
-  // TODO Move these to ./gradle/libs/versions/toml
-  implementation "app.cash.sqldelight:android-driver:2.0.0-alpha05"
-  implementation "app.cash.sqldelight:coroutines-extensions:2.0.0-alpha05"
-  implementation 'androidx.core:core-ktx:1.9.0'
-  implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
-  implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
-  implementation 'androidx.activity:activity-compose:1.5.1'
-  implementation "androidx.compose.ui:ui:$compose_version"
-  implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
-  implementation 'androidx.compose.material3:material3:1.0.0-beta02'
-  implementation 'androidx.sqlite:sqlite-framework:2.2.0'
-  implementation 'me.saket.extendedspans:extendedspans:1.3.0'
-  testImplementation 'junit:junit:4.13.2'
-  androidTestImplementation 'androidx.test.ext:junit:1.1.3'
-  androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
-  androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
-  debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
-  debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version"
-  // TODO Split out what's included in debug vs the subset for release
-  implementation projects.leakcanary.leakcanaryAndroid
-  implementation 'com.google.dagger:hilt-android:2.43.2'
-  implementation libs.okio2
-  kapt 'com.google.dagger:hilt-compiler:2.43.2'
-}
-
-kapt {
-  correctErrorTypes true
-}
diff --git a/leakcanary/leakcanary-app/build.gradle.kts b/leakcanary/leakcanary-app/build.gradle.kts
new file mode 100644
index 0000000..6388419
--- /dev/null
+++ b/leakcanary/leakcanary-app/build.gradle.kts
@@ -0,0 +1,104 @@
+import java.io.InputStreamReader
+
+plugins {
+  id("com.android.application")
+  id("org.jetbrains.kotlin.android")
+  id("app.cash.sqldelight")
+  id("com.google.dagger.hilt.android")
+  id("kotlin-kapt")
+  id("kotlin-parcelize")
+}
+
+fun gitSha(): String {
+  val process = ProcessBuilder("git", "rev-parse", "--short", "HEAD").start()
+  return InputStreamReader(process.inputStream).readText().trim()
+}
+
+android {
+  namespace = "org.leakcanary"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+
+  defaultConfig {
+    applicationId = "org.leakcanary"
+    // 21 required by Compose
+    minSdk = 21
+    targetSdk = libs.versions.androidCompileSdk.get().toInt()
+
+    buildConfigField("String", "GIT_SHA", "\"${gitSha()}\"")
+
+    // TODO Figure out versioning scheme. Should this follow LeakCanary releases?
+    versionCode = 1
+    versionName = "1.0"
+
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    vectorDrawables {
+      useSupportLibrary = true
+    }
+  }
+
+  buildTypes {
+    debug {
+    }
+    release {
+      // TODO Enable R8 minification
+      // minifyEnabled true
+      proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+      // TODO Proper signing config
+      signingConfig = signingConfigs["debug"]
+    }
+  }
+
+  compileOptions {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+  }
+
+  kotlinOptions {
+    jvmTarget = "1.8"
+  }
+
+  buildFeatures {
+    compose = true
+  }
+
+  composeOptions {
+    kotlinCompilerExtensionVersion = "1.4.7"
+  }
+
+  packagingOptions {
+    resources {
+      excludes += "/META-INF/{AL2.0,LGPL2.1}"
+    }
+  }
+}
+
+dependencies {
+  implementation(projects.leakcanary.leakcanaryAppAidl)
+  // TODO Move these to ./gradle/libs/versions/toml
+  implementation("app.cash.sqldelight:android-driver:2.0.0-alpha05")
+  implementation("app.cash.sqldelight:coroutines-extensions:2.0.0-alpha05")
+  implementation("androidx.core:core-ktx:1.9.0")
+  implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+  implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
+  implementation("androidx.activity:activity-compose:1.5.1")
+  implementation("androidx.compose.ui:ui:${libs.versions.compose.get()}")
+  implementation("androidx.compose.ui:ui-tooling-preview:${libs.versions.compose.get()}")
+  implementation("androidx.compose.material3:material3:1.0.0-beta02")
+  implementation("androidx.sqlite:sqlite-framework:2.2.0")
+  implementation("me.saket.extendedspans:extendedspans:1.3.0")
+  testImplementation("junit:junit:4.13.2")
+  androidTestImplementation("androidx.test.ext:junit:1.1.3")
+  androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
+  androidTestImplementation("androidx.compose.ui:ui-test-junit4:${libs.versions.compose.get()}")
+  debugImplementation("androidx.compose.ui:ui-tooling:${libs.versions.compose.get()}")
+  debugImplementation("androidx.compose.ui:ui-test-manifest:${libs.versions.compose.get()}")
+  // TODO Split out what's included in debug vs the subset for release
+  implementation(projects.leakcanary.leakcanaryAndroid)
+  implementation("com.google.dagger:hilt-android:2.43.2")
+  implementation(libs.okio2)
+  kapt("com.google.dagger:hilt-compiler:2.43.2")
+}
+
+kapt {
+  correctErrorTypes = true
+}
diff --git a/leakcanary/leakcanary-core/build.gradle b/leakcanary/leakcanary-core/build.gradle
deleted file mode 100644
index 35ba8a2..0000000
--- a/leakcanary/leakcanary-core/build.gradle
+++ /dev/null
@@ -1,17 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  api projects.leakcanary.leakcanaryGc
-  api projects.shark.shark
-  implementation libs.okio2
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation projects.shark.sharkHprofTest
-}
diff --git a/leakcanary/leakcanary-core/build.gradle.kts b/leakcanary/leakcanary-core/build.gradle.kts
new file mode 100644
index 0000000..063abc2
--- /dev/null
+++ b/leakcanary/leakcanary-core/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryGc)
+  api(projects.shark.shark)
+  implementation(libs.okio2)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(projects.shark.sharkHprofTest)
+}
diff --git a/leakcanary/leakcanary-deobfuscation-gradle-plugin/build.gradle b/leakcanary/leakcanary-deobfuscation-gradle-plugin/build.gradle
deleted file mode 100644
index dcebe74..0000000
--- a/leakcanary/leakcanary-deobfuscation-gradle-plugin/build.gradle
+++ /dev/null
@@ -1,39 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("java-gradle-plugin")
-  id("com.vanniktech.maven.publish")
-}
-
-gradlePlugin {
-  plugins {
-    leakCanary {
-      id = 'com.squareup.leakcanary.deobfuscation'
-      implementationClass =
-        'com.squareup.leakcanary.deobfuscation.LeakCanaryLeakDeobfuscationPlugin'
-    }
-  }
-
-  sourceSets {
-    test.java.srcDirs += 'src/test/test-project/src/main/java'
-  }
-}
-
-dependencies {
-  implementation libs.kotlin.stdlib
-  implementation libs.gradlePlugin.kotlin
-  implementation libs.gradlePlugin.android
-  compileOnly gradleApi()
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-}
-
-sourceCompatibility = JavaVersion.VERSION_11
-targetCompatibility = JavaVersion.VERSION_11
-
-tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
-      kotlinOptions {
-        jvmTarget = '11'
-      }
-}
-
diff --git a/leakcanary/leakcanary-deobfuscation-gradle-plugin/build.gradle.kts b/leakcanary/leakcanary-deobfuscation-gradle-plugin/build.gradle.kts
new file mode 100644
index 0000000..65b2b8c
--- /dev/null
+++ b/leakcanary/leakcanary-deobfuscation-gradle-plugin/build.gradle.kts
@@ -0,0 +1,45 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("java-gradle-plugin")
+  id("com.vanniktech.maven.publish")
+}
+
+gradlePlugin {
+  plugins {
+    register("leakCanary") {
+      id = "com.squareup.leakcanary.deobfuscation"
+      implementationClass =
+        "com.squareup.leakcanary.deobfuscation.LeakCanaryLeakDeobfuscationPlugin"
+    }
+  }
+
+  sourceSets {
+    test {
+      java.srcDirs.add(file("src/test/test-project/src/main/java"))
+    }
+  }
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.gradlePlugin.kotlin)
+  implementation(libs.gradlePlugin.android)
+  compileOnly(gradleApi())
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_11
+  targetCompatibility = JavaVersion.VERSION_11
+}
+
+tasks.withType<KotlinCompile> {
+  kotlinOptions {
+    jvmTarget = "11"
+  }
+}
+
diff --git a/leakcanary/leakcanary-gc/build.gradle b/leakcanary/leakcanary-gc/build.gradle
deleted file mode 100644
index 3ff8340..0000000
--- a/leakcanary/leakcanary-gc/build.gradle
+++ /dev/null
@@ -1,11 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  implementation libs.kotlin.stdlib
-}
diff --git a/leakcanary/leakcanary-gc/build.gradle.kts b/leakcanary/leakcanary-gc/build.gradle.kts
new file mode 100644
index 0000000..1fe48b8
--- /dev/null
+++ b/leakcanary/leakcanary-gc/build.gradle.kts
@@ -0,0 +1,13 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+}
diff --git a/leakcanary/leakcanary-jvm-test/build.gradle b/leakcanary/leakcanary-jvm-test/build.gradle
deleted file mode 100644
index b8bac03..0000000
--- a/leakcanary/leakcanary-jvm-test/build.gradle
+++ /dev/null
@@ -1,16 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  api projects.leakcanary.leakcanaryTestCore
-  api projects.shark.shark
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation projects.shark.sharkHprofTest
-}
diff --git a/leakcanary/leakcanary-jvm-test/build.gradle.kts b/leakcanary/leakcanary-jvm-test/build.gradle.kts
new file mode 100644
index 0000000..9096ca5
--- /dev/null
+++ b/leakcanary/leakcanary-jvm-test/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryTestCore)
+  api(projects.shark.shark)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(projects.shark.sharkHprofTest)
+}
diff --git a/leakcanary/leakcanary-test-core/build.gradle b/leakcanary/leakcanary-test-core/build.gradle
deleted file mode 100644
index 588a444..0000000
--- a/leakcanary/leakcanary-test-core/build.gradle
+++ /dev/null
@@ -1,15 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  api projects.leakcanary.leakcanaryCore
-  api projects.shark.shark
-  api libs.junit
-
-  testImplementation libs.assertjCore
-}
diff --git a/leakcanary/leakcanary-test-core/build.gradle.kts b/leakcanary/leakcanary-test-core/build.gradle.kts
new file mode 100644
index 0000000..706be70
--- /dev/null
+++ b/leakcanary/leakcanary-test-core/build.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  api(projects.leakcanary.leakcanaryCore)
+  api(projects.shark.shark)
+  api(libs.junit)
+
+  testImplementation(libs.assertjCore)
+}
diff --git a/object-watcher/object-watcher-android-androidx/build.gradle b/object-watcher/object-watcher-android-androidx/build.gradle
deleted file mode 100644
index 4ce89e9..0000000
--- a/object-watcher/object-watcher-android-androidx/build.gradle
+++ /dev/null
@@ -1,28 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.objectWatcher.objectWatcherAndroidCore
-
-  implementation libs.kotlin.stdlib
-  // Optional dependency
-  compileOnly libs.androidX.fragment
-}
-
-android {
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.fragments.androidx'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/object-watcher/object-watcher-android-androidx/build.gradle.kts b/object-watcher/object-watcher-android-androidx/build.gradle.kts
new file mode 100644
index 0000000..80343c1
--- /dev/null
+++ b/object-watcher/object-watcher-android-androidx/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.objectWatcher.objectWatcherAndroidCore)
+
+  implementation(libs.kotlin.stdlib)
+  // Optional dependency
+  compileOnly(libs.androidX.fragment)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    consumerProguardFiles("consumer-proguard-rules.pro")
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.fragments.androidx"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/object-watcher/object-watcher-android-core/build.gradle b/object-watcher/object-watcher-android-core/build.gradle
deleted file mode 100644
index 5f782ef..0000000
--- a/object-watcher/object-watcher-android-core/build.gradle
+++ /dev/null
@@ -1,36 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.objectWatcher.objectWatcher
-  api projects.leakcanary.leakcanaryAndroidUtils
-
-  implementation libs.curtains
-  implementation libs.kotlin.stdlib
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation libs.kotlin.reflect
-}
-
-android {
-  resourcePrefix 'leak_canary_watcher_'
-  compileSdk versions.compileSdk
-
-  defaultConfig {
-    minSdk versions.minSdk
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-
-  buildFeatures {
-    buildConfig = false
-  }
-  namespace 'com.squareup.leakcanary.objectwatcher.core'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-  }
-}
diff --git a/object-watcher/object-watcher-android-core/build.gradle.kts b/object-watcher/object-watcher-android-core/build.gradle.kts
new file mode 100644
index 0000000..ce5c794
--- /dev/null
+++ b/object-watcher/object-watcher-android-core/build.gradle.kts
@@ -0,0 +1,36 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.objectWatcher.objectWatcher)
+  api(projects.leakcanary.leakcanaryAndroidUtils)
+
+  implementation(libs.curtains)
+  implementation(libs.kotlin.stdlib)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(libs.kotlin.reflect)
+}
+
+android {
+  resourcePrefix = "leak_canary_watcher_"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    consumerProguardFiles("consumer-proguard-rules.pro")
+  }
+
+  buildFeatures {
+    buildConfig = false
+  }
+  namespace = "com.squareup.leakcanary.objectwatcher.core"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+  }
+}
diff --git a/object-watcher/object-watcher-android-startup/build.gradle b/object-watcher/object-watcher-android-startup/build.gradle
deleted file mode 100644
index 73e37bb..0000000
--- a/object-watcher/object-watcher-android-startup/build.gradle
+++ /dev/null
@@ -1,26 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.objectWatcher.objectWatcherAndroidCore
-
-  implementation libs.androidX.startup
-}
-
-android {
-  resourcePrefix 'leak_canary_watcher_'
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.objectwatcher.startup'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/object-watcher/object-watcher-android-startup/build.gradle.kts b/object-watcher/object-watcher-android-startup/build.gradle.kts
new file mode 100644
index 0000000..a3d4e2b
--- /dev/null
+++ b/object-watcher/object-watcher-android-startup/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.objectWatcher.objectWatcherAndroidCore)
+
+  implementation(libs.androidX.startup)
+}
+
+android {
+  resourcePrefix = "leak_canary_watcher_"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.objectwatcher.startup"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/object-watcher/object-watcher-android/build.gradle b/object-watcher/object-watcher-android/build.gradle
deleted file mode 100644
index 3d0d009..0000000
--- a/object-watcher/object-watcher-android/build.gradle
+++ /dev/null
@@ -1,28 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.objectWatcher.objectWatcherAndroidCore
-}
-
-android {
-  resourcePrefix 'leak_canary_watcher_'
-  compileSdk versions.compileSdk
-
-  defaultConfig {
-    minSdk versions.minSdk
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-
-  buildFeatures {
-    buildConfig = false
-  }
-  namespace 'com.squareup.leakcanary.objectwatcher'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-  }
-}
diff --git a/object-watcher/object-watcher-android/build.gradle.kts b/object-watcher/object-watcher-android/build.gradle.kts
new file mode 100644
index 0000000..f861ae7
--- /dev/null
+++ b/object-watcher/object-watcher-android/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.objectWatcher.objectWatcherAndroidCore)
+}
+
+android {
+  resourcePrefix = "leak_canary_watcher_"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    consumerProguardFiles("consumer-proguard-rules.pro")
+  }
+
+  buildFeatures {
+    buildConfig = false
+  }
+  namespace = "com.squareup.leakcanary.objectwatcher"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+  }
+}
diff --git a/object-watcher/object-watcher/build.gradle b/object-watcher/object-watcher/build.gradle
deleted file mode 100644
index 9386604..0000000
--- a/object-watcher/object-watcher/build.gradle
+++ /dev/null
@@ -1,16 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  implementation libs.kotlin.stdlib
-  api projects.shark.sharkLog
-  api projects.leakcanary.leakcanaryGc
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-}
diff --git a/object-watcher/object-watcher/build.gradle.kts b/object-watcher/object-watcher/build.gradle.kts
new file mode 100644
index 0000000..9d1921a
--- /dev/null
+++ b/object-watcher/object-watcher/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+  api(projects.shark.sharkLog)
+  api(projects.leakcanary.leakcanaryGc)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+}
diff --git a/plumber/plumber-android-core/build.gradle b/plumber/plumber-android-core/build.gradle
deleted file mode 100644
index b849e4e..0000000
--- a/plumber/plumber-android-core/build.gradle
+++ /dev/null
@@ -1,31 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.shark.sharkLog
-  api projects.leakcanary.leakcanaryAndroidUtils
-
-  implementation libs.kotlin.stdlib
-  implementation libs.curtains
-  // Optional dependency
-  compileOnly libs.androidX.fragment
-}
-
-android {
-  resourcePrefix 'leak_canary_plumber'
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.plumber.core'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/plumber/plumber-android-core/build.gradle.kts b/plumber/plumber-android-core/build.gradle.kts
new file mode 100644
index 0000000..13e670a
--- /dev/null
+++ b/plumber/plumber-android-core/build.gradle.kts
@@ -0,0 +1,31 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.shark.sharkLog)
+  api(projects.leakcanary.leakcanaryAndroidUtils)
+
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.curtains)
+  // Optional dependency
+  compileOnly(libs.androidX.fragment)
+}
+
+android {
+  resourcePrefix = "leak_canary_plumber"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    consumerProguardFiles("consumer-proguard-rules.pro")
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.plumber.core"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/plumber/plumber-android-startup/build.gradle b/plumber/plumber-android-startup/build.gradle
deleted file mode 100644
index a7adca5..0000000
--- a/plumber/plumber-android-startup/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.plumber.plumberAndroidCore
-
-  implementation libs.kotlin.stdlib
-  implementation libs.androidX.startup
-}
-
-android {
-  resourcePrefix 'leak_canary_plumber'
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.plumber.startup'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/plumber/plumber-android-startup/build.gradle.kts b/plumber/plumber-android-startup/build.gradle.kts
new file mode 100644
index 0000000..b57fc23
--- /dev/null
+++ b/plumber/plumber-android-startup/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.plumber.plumberAndroidCore)
+
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.androidX.startup)
+}
+
+android {
+  resourcePrefix = "leak_canary_plumber"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.plumber.startup"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/plumber/plumber-android/build.gradle b/plumber/plumber-android/build.gradle
deleted file mode 100644
index d43f683..0000000
--- a/plumber/plumber-android/build.gradle
+++ /dev/null
@@ -1,27 +0,0 @@
-plugins {
-  id("com.android.library")
-  id("org.jetbrains.kotlin.android")
-  id("com.vanniktech.maven.publish")
-}
-
-dependencies {
-  api projects.plumber.plumberAndroidCore
-
-  implementation libs.kotlin.stdlib
-}
-
-android {
-  resourcePrefix 'leak_canary_plumber'
-  compileSdk versions.compileSdk
-  defaultConfig {
-    minSdk versions.minSdk
-    consumerProguardFiles 'consumer-proguard-rules.pro'
-  }
-  buildFeatures.buildConfig = false
-  namespace 'com.squareup.leakcanary.plumber'
-  lint {
-    checkOnly 'Interoperability'
-    disable 'GoogleAppIndexingWarning'
-    error 'ObsoleteSdkInt'
-  }
-}
diff --git a/plumber/plumber-android/build.gradle.kts b/plumber/plumber-android/build.gradle.kts
new file mode 100644
index 0000000..350b1c3
--- /dev/null
+++ b/plumber/plumber-android/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+  id("com.android.library")
+  id("org.jetbrains.kotlin.android")
+  id("com.vanniktech.maven.publish")
+}
+
+dependencies {
+  api(projects.plumber.plumberAndroidCore)
+
+  implementation(libs.kotlin.stdlib)
+}
+
+android {
+  resourcePrefix = "leak_canary_plumber"
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+  defaultConfig {
+    minSdk = libs.versions.androidMinSdk.get().toInt()
+    consumerProguardFiles("consumer-proguard-rules.pro")
+  }
+  buildFeatures.buildConfig = false
+  namespace = "com.squareup.leakcanary.plumber"
+  lint {
+    checkOnly += "Interoperability"
+    disable += "GoogleAppIndexingWarning"
+    error += "ObsoleteSdkInt"
+  }
+}
diff --git a/samples/leakcanary-android-sample/build.gradle b/samples/leakcanary-android-sample/build.gradle
deleted file mode 100644
index 442faea..0000000
--- a/samples/leakcanary-android-sample/build.gradle
+++ /dev/null
@@ -1,98 +0,0 @@
-plugins {
-  id("com.android.application")
-  id("org.jetbrains.kotlin.android")
-  // Required to run obfuscated instrumentation tests:
-  // ./gradlew leakcanary-android-sample:connectedCheck -Pminify
-  id("com.slack.keeper")
-}
-
-keeper {
-  variantFilter {
-    setIgnore(!project.hasProperty('minify'))
-  }
-}
-
-dependencies {
-  debugImplementation projects.leakcanary.leakcanaryAndroid
-  debugImplementation projects.leakcanary.leakcanaryAppService
-  // debugImplementation projects.leakcanary.leakcanaryAndroidStartup
-
-  // Uncomment to use the :leakcanary process
-  // debugImplementation projects.leakcanary.leakcanaryAndroidProcess
-  releaseImplementation projects.leakcanary.leakcanaryAndroidRelease
-  // Optional
-  releaseImplementation projects.objectWatcher.objectWatcherAndroid
-
-  implementation libs.kotlin.stdlib
-  // Uncomment to use WorkManager
-  // implementation libs.androidX.work.runtime
-
-  testImplementation libs.junit
-  testImplementation libs.robolectric
-
-  androidTestImplementation projects.leakcanary.leakcanaryAndroidInstrumentation
-  androidTestImplementation libs.androidX.test.espresso
-  androidTestImplementation libs.androidX.test.rules
-  androidTestImplementation libs.androidX.test.runner
-  androidTestImplementation libs.androidX.test.junit
-  androidTestImplementation libs.androidX.test.junitKtx
-  androidTestUtil libs.androidX.test.orchestrator
-}
-
-android {
-  compileSdk versions.compileSdk
-
-  compileOptions {
-    sourceCompatibility JavaVersion.VERSION_1_8
-    targetCompatibility JavaVersion.VERSION_1_8
-  }
-
-  defaultConfig {
-    applicationId "com.example.leakcanary"
-    minSdk 16
-    targetSdk versions.compileSdk
-
-    versionCode 1
-    versionName "1.0"
-
-    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
-
-    // Run ./gradlew leakcanary-android-sample:connectedCheck -Porchestrator
-    if (project.hasProperty('orchestrator')) {
-      testInstrumentationRunnerArguments clearPackageData: 'true'
-      testOptions {
-        execution 'ANDROIDX_TEST_ORCHESTRATOR'
-      }
-    }
-  }
-
-  buildTypes {
-    // Build with ./gradlew leakcanary-android-sample:installDebug -Pminify
-    if (project.hasProperty('minify')) {
-      debug {
-        minifyEnabled true
-        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
-      }
-    } else {
-      debug
-    }
-    release {
-      signingConfig signingConfigs.debug
-    }
-  }
-
-  dexOptions {
-    dexInProcess false
-  }
-
-  testOptions {
-    unitTests {
-      includeAndroidResources = true
-    }
-  }
-  namespace 'com.example.leakcanary'
-  testNamespace 'com.squareup.leakcanary.instrumentation.test'
-  lint {
-    disable 'GoogleAppIndexingWarning'
-  }
-}
diff --git a/samples/leakcanary-android-sample/build.gradle.kts b/samples/leakcanary-android-sample/build.gradle.kts
new file mode 100644
index 0000000..0c86817
--- /dev/null
+++ b/samples/leakcanary-android-sample/build.gradle.kts
@@ -0,0 +1,99 @@
+plugins {
+  id("com.android.application")
+  id("org.jetbrains.kotlin.android")
+  // Required to run obfuscated instrumentation tests:
+  // ./gradlew leakcanary-android-sample:connectedCheck -Pminify
+  id("com.slack.keeper")
+}
+
+keeper {
+  variantFilter {
+    setIgnore(!project.hasProperty("minify"))
+  }
+}
+
+dependencies {
+  debugImplementation(projects.leakcanary.leakcanaryAndroid)
+  debugImplementation(projects.leakcanary.leakcanaryAppService)
+  // debugImplementation(projects.leakcanary.leakcanaryAndroidStartup)
+
+  // Uncomment to use the :leakcanary process
+  // debugImplementation(projects.leakcanary.leakcanaryAndroidProcess)
+  releaseImplementation(projects.leakcanary.leakcanaryAndroidRelease)
+  // Optional
+  releaseImplementation(projects.objectWatcher.objectWatcherAndroid)
+
+  implementation(libs.kotlin.stdlib)
+  // Uncomment to use WorkManager
+  // implementation(libs.androidX.work.runtime)
+
+  testImplementation(libs.junit)
+  testImplementation(libs.robolectric)
+
+  androidTestImplementation(projects.leakcanary.leakcanaryAndroidInstrumentation)
+  androidTestImplementation(libs.androidX.test.espresso)
+  androidTestImplementation(libs.androidX.test.rules)
+  androidTestImplementation(libs.androidX.test.runner)
+  androidTestImplementation(libs.androidX.test.junit)
+  androidTestImplementation(libs.androidX.test.junitKtx)
+  androidTestUtil(libs.androidX.test.orchestrator)
+}
+
+android {
+  compileSdk = libs.versions.androidCompileSdk.get().toInt()
+
+  compileOptions {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+  }
+
+  defaultConfig {
+    applicationId = "com.example.leakcanary"
+    minSdk = 16
+    targetSdk = libs.versions.androidCompileSdk.get().toInt()
+
+    versionCode = 1
+    versionName = "1.0"
+
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+
+    // Run ./gradlew leakcanary-android-sample:connectedCheck -Porchestrator
+    if (project.hasProperty("orchestrator")) {
+      testInstrumentationRunnerArguments(mapOf("clearPackageData" to "true"))
+      testOptions {
+        execution = "ANDROIDX_TEST_ORCHESTRATOR"
+      }
+    }
+  }
+
+  buildTypes {
+    // Build with ./gradlew leakcanary-android-sample:installDebug -Pminify
+    if (project.hasProperty("minify")) {
+      debug {
+        isMinifyEnabled = true
+        proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"))
+      }
+    } else {
+      debug {
+      }
+    }
+    release {
+      signingConfig = signingConfigs["debug"]
+    }
+  }
+
+  dexOptions {
+    dexInProcess = false
+  }
+
+  testOptions {
+    unitTests {
+      isIncludeAndroidResources = true
+    }
+  }
+  namespace = "com.example.leakcanary"
+  testNamespace = "com.squareup.leakcanary.instrumentation.test"
+  lint {
+    disable += "GoogleAppIndexingWarning"
+  }
+}
diff --git a/shark/shark-android/build.gradle b/shark/shark-android/build.gradle
deleted file mode 100644
index 5bc2c0a..0000000
--- a/shark/shark-android/build.gradle
+++ /dev/null
@@ -1,22 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  api projects.shark.shark
-
-  implementation libs.kotlin.stdlib
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation libs.kotlinStatistics
-  testImplementation libs.mockito
-  testImplementation libs.mockitoKotlin
-  testImplementation libs.okio2
-  testImplementation projects.shark.sharkTest
-  testImplementation projects.shark.sharkHprofTest
-}
diff --git a/shark/shark-android/build.gradle.kts b/shark/shark-android/build.gradle.kts
new file mode 100644
index 0000000..9ae207c
--- /dev/null
+++ b/shark/shark-android/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  api(projects.shark.shark)
+
+  implementation(libs.kotlin.stdlib)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(libs.kotlinStatistics)
+  testImplementation(libs.mockito)
+  testImplementation(libs.mockitoKotlin)
+  testImplementation(libs.okio2)
+  testImplementation(projects.shark.sharkTest)
+  testImplementation(projects.shark.sharkHprofTest)
+}
diff --git a/shark/shark-cli/build.gradle b/shark/shark-cli/build.gradle
deleted file mode 100644
index a2d4d4e..0000000
--- a/shark/shark-cli/build.gradle
+++ /dev/null
@@ -1,51 +0,0 @@
-import org.jetbrains.kotlin.gradle.dsl.KotlinCompile
-
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("application")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-// Workaround for https://stackoverflow.com/questions/48988778
-// /cannot-inline-bytecode-built-with-jvm-target-1-8-into-bytecode-that-is-being-bui
-tasks.withType(KotlinCompile).configureEach {
-  kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
-}
-
-dependencies {
-  api projects.shark.sharkAndroid
-
-  implementation libs.clikt
-  implementation libs.neo4j
-  implementation libs.jline
-  implementation libs.kotlin.stdlib
-}
-
-application {
-  mainClassName = 'shark.MainKt'
-}
-
-def generatedVersionDir = "${buildDir}/generated-version"
-
-sourceSets {
-  main {
-    output.dir(generatedVersionDir, builtBy: 'generateVersionProperties')
-  }
-}
-
-tasks.register("generateVersionProperties") {
-  doLast {
-    def propertiesFile = file "$generatedVersionDir/version.properties"
-    propertiesFile.parentFile.mkdirs()
-    def properties = new Properties()
-    properties.setProperty("version_name", rootProject.VERSION_NAME.toString())
-    propertiesFile.withWriter { properties.store(it, null) }
-  }
-}
-tasks.named("processResources") {
-  dependsOn("generateVersionProperties")
-}
-
diff --git a/shark/shark-cli/build.gradle.kts b/shark/shark-cli/build.gradle.kts
new file mode 100644
index 0000000..78e5bde
--- /dev/null
+++ b/shark/shark-cli/build.gradle.kts
@@ -0,0 +1,56 @@
+import java.util.Properties
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("application")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+// Workaround for https://stackoverflow.com/questions/48988778
+// /cannot-inline-bytecode-built-with-jvm-target-1-8-into-bytecode-that-is-being-bui
+tasks.withType<KotlinCompile> {
+  kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString()
+}
+
+dependencies {
+  api(projects.shark.sharkAndroid)
+
+  implementation(libs.clikt)
+  implementation(libs.neo4j)
+  implementation(libs.jline)
+  implementation(libs.kotlin.stdlib)
+}
+
+application {
+  mainClass.set("shark.MainKt")
+}
+
+val generatedVersionDir = "${buildDir}/generated-version"
+
+sourceSets {
+  main {
+    output.dir(generatedVersionDir, "builtBy" to "generateVersionProperties")
+  }
+}
+
+tasks.register("generateVersionProperties") {
+  doLast {
+    val propertiesFile = file("$generatedVersionDir/version.properties")
+    propertiesFile.parentFile.mkdirs()
+    val properties = Properties()
+    properties.setProperty("version_name", rootProject.property("VERSION_NAME").toString())
+    propertiesFile.writer().use {
+      properties.store(it, null)
+    }
+  }
+}
+tasks.named("processResources") {
+  dependsOn("generateVersionProperties")
+}
+
diff --git a/shark/shark-graph/build.gradle b/shark/shark-graph/build.gradle
deleted file mode 100644
index ab83749..0000000
--- a/shark/shark-graph/build.gradle
+++ /dev/null
@@ -1,20 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  api projects.shark.sharkHprof
-  api libs.androidX.collections
-
-  implementation libs.kotlin.stdlib
-  implementation libs.okio2
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation projects.shark.sharkTest
-  testImplementation projects.shark.sharkHprofTest
-}
diff --git a/shark/shark-graph/build.gradle.kts b/shark/shark-graph/build.gradle.kts
new file mode 100644
index 0000000..403773f
--- /dev/null
+++ b/shark/shark-graph/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  api(projects.shark.sharkHprof)
+  api(libs.androidX.collections)
+
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.okio2)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(projects.shark.sharkTest)
+  testImplementation(projects.shark.sharkHprofTest)
+}
diff --git a/shark/shark-hprof-test/build.gradle b/shark/shark-hprof-test/build.gradle
deleted file mode 100644
index 66a1a66..0000000
--- a/shark/shark-hprof-test/build.gradle
+++ /dev/null
@@ -1,14 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-    implementation libs.kotlin.stdlib
-    implementation libs.junit
-    implementation libs.okio2
-
-    implementation projects.shark.sharkHprof
-}
diff --git a/shark/shark-hprof-test/build.gradle.kts b/shark/shark-hprof-test/build.gradle.kts
new file mode 100644
index 0000000..7698f5a
--- /dev/null
+++ b/shark/shark-hprof-test/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+    implementation(libs.kotlin.stdlib)
+    implementation(libs.junit)
+    implementation(libs.okio2)
+
+    implementation(projects.shark.sharkHprof)
+}
diff --git a/shark/shark-hprof/build.gradle b/shark/shark-hprof/build.gradle.kts
similarity index 65%
rename from shark/shark-hprof/build.gradle
rename to shark/shark-hprof/build.gradle.kts
index f03a1df..765ce9f 100644
--- a/shark/shark-hprof/build.gradle
+++ b/shark/shark-hprof/build.gradle.kts
@@ -3,23 +3,25 @@
   id("com.vanniktech.maven.publish")
 }
 
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
 
 dependencies {
-  api projects.shark.sharkLog
+  api(projects.shark.sharkLog)
 
-  implementation libs.kotlin.stdlib
+  implementation(libs.kotlin.stdlib)
   // compileOnly ensures this dependency is not exposed through this artifact's pom.xml in Maven Central.
   // Okio is a required dependency, but we're making it required on the "shark" artifact which is the main artifact that
   // should generally be used. The shark artifact depends on Okio 2.x (ensure compatibility with modern Okio). Depending on 1.x here
   // enables us to ensure binary compatibility with Okio 1.x and allow us to use the deprecated (error level) Okio APIs to keep that
   // compatibility.
   // See https://github.com/square/leakcanary/issues/1624
-  compileOnly libs.okio1
-  testImplementation libs.okio1
+  compileOnly(libs.okio1)
+  testImplementation(libs.okio1)
 
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation projects.shark.sharkTest
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(projects.shark.sharkTest)
 }
diff --git a/shark/shark-log/build.gradle b/shark/shark-log/build.gradle
deleted file mode 100644
index 8ebe092..0000000
--- a/shark/shark-log/build.gradle
+++ /dev/null
@@ -1,14 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  implementation libs.kotlin.stdlib
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-}
diff --git a/shark/shark-log/build.gradle.kts b/shark/shark-log/build.gradle.kts
new file mode 100644
index 0000000..46920a1
--- /dev/null
+++ b/shark/shark-log/build.gradle.kts
@@ -0,0 +1,16 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  implementation(libs.kotlin.stdlib)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+}
diff --git a/shark/shark-test/build.gradle b/shark/shark-test/build.gradle
deleted file mode 100644
index 5a46a6c..0000000
--- a/shark/shark-test/build.gradle
+++ /dev/null
@@ -1,13 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-    implementation libs.kotlin.stdlib
-    implementation libs.assertjCore
-    implementation libs.junit
-}
-
diff --git a/shark/shark-test/build.gradle.kts b/shark/shark-test/build.gradle.kts
new file mode 100644
index 0000000..7f39667
--- /dev/null
+++ b/shark/shark-test/build.gradle.kts
@@ -0,0 +1,15 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+    implementation(libs.kotlin.stdlib)
+    implementation(libs.assertjCore)
+    implementation(libs.junit)
+}
+
diff --git a/shark/shark/build.gradle b/shark/shark/build.gradle
deleted file mode 100644
index 84310f2..0000000
--- a/shark/shark/build.gradle
+++ /dev/null
@@ -1,20 +0,0 @@
-plugins {
-  id("org.jetbrains.kotlin.jvm")
-  id("com.vanniktech.maven.publish")
-}
-
-sourceCompatibility = JavaVersion.VERSION_1_8
-targetCompatibility = JavaVersion.VERSION_1_8
-
-dependencies {
-  api projects.shark.sharkGraph
-
-  implementation libs.coroutines.core
-  implementation libs.kotlin.stdlib
-  implementation libs.okio2
-
-  testImplementation libs.assertjCore
-  testImplementation libs.junit
-  testImplementation projects.shark.sharkTest
-  testImplementation projects.shark.sharkHprofTest
-}
diff --git a/shark/shark/build.gradle.kts b/shark/shark/build.gradle.kts
new file mode 100644
index 0000000..25eb0bf
--- /dev/null
+++ b/shark/shark/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+  id("org.jetbrains.kotlin.jvm")
+  id("com.vanniktech.maven.publish")
+}
+
+java {
+  sourceCompatibility = JavaVersion.VERSION_1_8
+  targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+dependencies {
+  api(projects.shark.sharkGraph)
+
+  implementation(libs.coroutines.core)
+  implementation(libs.kotlin.stdlib)
+  implementation(libs.okio2)
+
+  testImplementation(libs.assertjCore)
+  testImplementation(libs.junit)
+  testImplementation(projects.shark.sharkTest)
+  testImplementation(projects.shark.sharkHprofTest)
+}