Skip to content

Commit 591a49a

Browse files
authored
Merge branch 'code/cash' into chore/delegate-session-controller
2 parents 76b834c + 43049ff commit 591a49a

2 files changed

Lines changed: 127 additions & 48 deletions

File tree

build.gradle.kts

Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -35,61 +35,42 @@ plugins {
3535
alias(libs.plugins.kover)
3636
}
3737

38-
allprojects {
39-
configurations.all {
40-
exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib-jdk7")
41-
resolutionStrategy {
42-
force(libs.kotlinx.serialization.core.get().toString())
43-
force(libs.kotlinx.serialization.json.get().toString())
44-
force(libs.kotlin.metadata.jvm.get().toString())
45-
}
46-
}
38+
// NOTE: The per-project `allprojects { }` configuration that used to live here
39+
// (kotlin-stdlib-jdk7 exclusion, serialization/metadata version forcing, and
40+
// disabling stray kapt tasks) has moved to `settings.gradle.kts` as a
41+
// `gradle.lifecycle.beforeProject { }` hook. Isolated Projects forbids the root
42+
// project from configuring other projects, and that hook is its isolated
43+
// replacement.
4744

48-
tasks.matching { it.name.contains("kapt") }.configureEach {
49-
enabled = false
50-
}
45+
tasks.register("clean", Delete::class) {
46+
delete(rootProject.layout.buildDirectory)
5147
}
5248

49+
// ---- Coverage aggregation (Kover) ----
50+
// The set of modules whose coverage is merged is computed in `settings.gradle.kts`
51+
// (from the actual included projects) and handed over via this extra property,
52+
// because Isolated Projects forbids the root from discovering them by iterating
53+
// `subprojects`. `kover(project(path))` is just a dependency edge, which IP allows.
54+
@Suppress("UNCHECKED_CAST")
55+
val koverModules = rootProject.extra["flipcash.koverModules"] as List<String>
56+
5357
dependencies {
54-
subprojects.forEach { subproject ->
55-
subproject.afterEvaluate {
56-
if (subproject.plugins.hasPlugin("org.jetbrains.kotlinx.kover")
57-
&& (subproject.path.startsWith(":apps:flipcash")
58-
|| subproject.path.startsWith(":services:flipcash")
59-
|| subproject.path.startsWith(":services:opencode")
60-
|| subproject.path.startsWith(":libs:")
61-
|| subproject.path.startsWith(":ui:")
62-
|| subproject.path.startsWith(":definitions:"))
63-
) {
64-
kover(subproject)
65-
}
66-
}
67-
}
58+
koverModules.forEach { kover(project(it)) }
6859
}
6960

70-
tasks.register("clean", Delete::class) {
71-
delete(rootProject.layout.buildDirectory)
72-
}
61+
// ---- Aggregate unit-test task ----
62+
// Replaces a `subprojects { afterEvaluate { rootProject.tasks... } }` wiring that
63+
// Isolated Projects forbids. The module lists come from `settings.gradle.kts`; the
64+
// task depends on each module's test task by *path* — a lazy, cross-project-safe
65+
// dependency that never configures the other project at configuration time.
66+
// Android modules expose `testDebugUnitTest`; pure-JVM modules expose `test`.
67+
@Suppress("UNCHECKED_CAST")
68+
val androidUnitTestModules = rootProject.extra["flipcash.androidUnitTestModules"] as List<String>
69+
@Suppress("UNCHECKED_CAST")
70+
val jvmUnitTestModules = rootProject.extra["flipcash.jvmUnitTestModules"] as List<String>
7371

7472
tasks.register("flipcashTestDebug") {
7573
description = "Run testDebug for all Flipcash modules"
76-
}
77-
78-
subprojects.filter {
79-
it.path.startsWith(":apps:flipcash")
80-
|| it.path == ":services:flipcash"
81-
|| it.path == ":services:opencode"
82-
}.forEach { sub ->
83-
sub.afterEvaluate {
84-
val taskName = when {
85-
sub.plugins.hasPlugin("com.android.library") || sub.plugins.hasPlugin("com.android.application") -> "testDebugUnitTest"
86-
sub.plugins.hasPlugin("org.jetbrains.kotlin.jvm") -> "test"
87-
else -> null
88-
}
89-
if (taskName != null) {
90-
rootProject.tasks.named("flipcashTestDebug").configure {
91-
dependsOn(tasks.named(taskName))
92-
}
93-
}
94-
}
74+
dependsOn(androidUnitTestModules.map { "$it:testDebugUnitTest" })
75+
dependsOn(jvmUnitTestModules.map { "$it:test" })
9576
}

settings.gradle.kts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.gradle.api.initialization.ProjectDescriptor
2+
13
pluginManagement {
24
includeBuild("build-logic")
35
repositories {
@@ -190,3 +192,99 @@ include(
190192
":vendor:tipkit:tipkit-m2",
191193
":vendor:opencv:sdk",
192194
)
195+
196+
// ---- Cross-module aggregation wiring (single source of truth) ----
197+
// The root build script can no longer discover modules by iterating `subprojects`
198+
// (Isolated Projects forbids it), so the module sets used for coverage and the
199+
// aggregate unit-test task are derived here from the actual included projects and
200+
// handed to the root project via extra properties. New modules are picked up
201+
// automatically; only modules that break the usual conventions need to be listed
202+
// in the small denylists below (their plugin type isn't knowable until project
203+
// configuration, which IP forbids the root from inspecting).
204+
// Only real modules, not the intermediate container projects that Gradle
205+
// auto-creates for nested paths (e.g. `:apps:flipcash:shared`) — those have no
206+
// build script and must not receive a `kover(project(...))` / test dependency.
207+
val includedProjectPaths = buildList {
208+
fun collect(descriptor: ProjectDescriptor) {
209+
val hasBuildScript = descriptor.projectDir.resolve("build.gradle.kts").exists() ||
210+
descriptor.projectDir.resolve("build.gradle").exists()
211+
if (descriptor.path != ":" && hasBuildScript) {
212+
add(descriptor.path)
213+
}
214+
descriptor.children.forEach(::collect)
215+
}
216+
collect(rootProject)
217+
}
218+
219+
// Coverage: every module under these paths applies a `flipcash.android.*`
220+
// convention plugin (which pulls in Kover); :apps:flipcash:app applies Kover
221+
// directly. The denylist holds the modules under those paths that have no Kover.
222+
val koverPaths = listOf(":apps:flipcash", ":libs", ":ui", ":definitions", ":services:flipcash", ":services:opencode")
223+
val nonKoverModules = setOf(
224+
":apps:flipcash:benchmark", // com.android.test — no Kover
225+
":apps:flipcash:shared:ksp", // pure-JVM helper — no Kover
226+
":definitions:opencode:protos", // java-library proto jar — no Kover
227+
":definitions:flipcash:protos", // java-library proto jar — no Kover
228+
)
229+
val koverModules = includedProjectPaths.filter { path ->
230+
koverPaths.any { path == it || path.startsWith("$it:") } && path !in nonKoverModules
231+
}
232+
233+
// Aggregate unit tests: :apps:flipcash plus the two service modules. Android
234+
// modules expose `testDebugUnitTest`, pure-JVM modules expose `test`, and the
235+
// androidTest `:benchmark` module has no unit-test task.
236+
val unitTestPaths = listOf(":apps:flipcash", ":services:flipcash", ":services:opencode")
237+
val jvmUnitTestModules = setOf(":apps:flipcash:shared:ksp")
238+
val noUnitTestModules = setOf(":apps:flipcash:benchmark")
239+
val unitTestCandidates = includedProjectPaths.filter { path ->
240+
unitTestPaths.any { path == it || path.startsWith("$it:") } && path !in noUnitTestModules
241+
}
242+
val androidUnitTestModules = unitTestCandidates.filter { it !in jvmUnitTestModules }
243+
244+
// Forced dependency versions for the per-project configuration below. The
245+
// version catalog isn't registered on projects yet at `beforeProject` time, so
246+
// the coordinates are resolved here from the catalog's TOML (keeping them in sync
247+
// with `gradle/libs.versions.toml`) and captured as plain strings.
248+
val versionsToml = rootDir.resolve("gradle/libs.versions.toml").readText()
249+
fun catalogVersion(key: String): String =
250+
Regex("""(?m)^\s*${Regex.escape(key)}\s*=\s*"([^"]+)"""")
251+
.find(versionsToml)?.groupValues?.get(1)
252+
?: error("Version '$key' not found in gradle/libs.versions.toml")
253+
val kotlinVersion = catalogVersion("kotlin")
254+
val serializationVersion = catalogVersion("kotlinx-serialization")
255+
256+
// Per-project configuration that previously lived in the root build script's
257+
// `allprojects { }` block, plus handing the aggregation lists above to the root
258+
// project. Isolated Projects forbids a project (the root) configuring other
259+
// projects, so this runs via the isolated `gradle.lifecycle.beforeProject` hook,
260+
// which applies per-project setup to every project (including the root) without
261+
// crossing project boundaries.
262+
//
263+
// The action is isolated (serialized), so it must not capture a reference to this
264+
// settings script. Everything it needs is therefore copied into local `val`s in
265+
// the block below — captured by value rather than via the script object.
266+
run {
267+
val koverModulesForRoot = koverModules.toList()
268+
val androidUnitTestForRoot = androidUnitTestModules.toList()
269+
val jvmUnitTestForRoot = jvmUnitTestModules.toList()
270+
val forcedDependencies = listOf(
271+
"org.jetbrains.kotlinx:kotlinx-serialization-core:$serializationVersion",
272+
"org.jetbrains.kotlinx:kotlinx-serialization-json:$serializationVersion",
273+
"org.jetbrains.kotlin:kotlin-metadata-jvm:$kotlinVersion",
274+
)
275+
gradle.lifecycle.beforeProject {
276+
if (path == ":") {
277+
extra["flipcash.koverModules"] = koverModulesForRoot
278+
extra["flipcash.androidUnitTestModules"] = androidUnitTestForRoot
279+
extra["flipcash.jvmUnitTestModules"] = jvmUnitTestForRoot
280+
}
281+
282+
configurations.configureEach {
283+
exclude(mapOf("group" to "org.jetbrains.kotlin", "module" to "kotlin-stdlib-jdk7"))
284+
resolutionStrategy.force(*forcedDependencies.toTypedArray())
285+
}
286+
tasks.matching { task -> task.name.contains("kapt") }.configureEach {
287+
enabled = false
288+
}
289+
}
290+
}

0 commit comments

Comments
 (0)