@@ -49,17 +49,27 @@ logger.lifecycle("[HarnessCoverage] Will instrument modules: ${targetModules.joi
4949def scriptDir = buildscript. sourceFile?. parentFile
5050def packageDir = scriptDir?. parentFile
5151
52- gradle. projectsEvaluated {
53- def jacocoVersion = ' 0.8.12'
52+ def jacocoVersion = ' 0.8.12'
5453
55- rootProject. allprojects { project ->
56- if (! targetModules. contains(project. path)) return
54+ // Use allprojects + afterEvaluate so that source sets, dependencies, and
55+ // BuildConfig fields are registered BEFORE the compile tasks finalize their
56+ // inputs. gradle.projectsEvaluated is too late for source-set changes.
57+ gradle. allprojects { project ->
58+ if (! targetModules. contains(project. path)) return
5759
60+ project. afterEvaluate {
5861 logger. lifecycle(" [HarnessCoverage] Configuring ${ project.path} " )
5962
63+ def androidExt = project. extensions. findByName(' android' )
64+ if (! androidExt) {
65+ logger. warn(" [HarnessCoverage] ${ project.path} has no android extension, skipping" )
66+ return
67+ }
68+
6069 // --- Dependencies ---
6170 project. configurations. maybeCreate(' jacocoAnt' )
62- project. configurations. maybeCreate(' jacocoCli' )
71+ def jacocoCliConf = project. configurations. maybeCreate(' jacocoCli' )
72+ jacocoCliConf. transitive = false
6373
6474 project. dependencies {
6575 implementation " org.jacoco:org.jacoco.agent:${ jacocoVersion} :runtime"
@@ -68,15 +78,13 @@ gradle.projectsEvaluated {
6878 }
6979
7080 // --- BuildConfig field ---
71- def androidExt = project. extensions. findByName(' android' )
72- if (androidExt) {
73- androidExt. defaultConfig {
74- buildConfigField " boolean" , " COVERAGE_ENABLED" , " true"
75- }
81+ def namespace = androidExt. namespace
82+ androidExt. defaultConfig {
83+ buildConfigField " boolean" , " COVERAGE_ENABLED" , " true"
7684 }
7785
7886 // --- Inject coverage runtime sources ---
79- if (androidExt && packageDir) {
87+ if (packageDir) {
8088 def coverageSrcDir = new File (packageDir, ' android/src/main/kotlin' )
8189 def coverageResources = new File (packageDir, ' android/src/main/resources' )
8290
@@ -88,86 +96,78 @@ gradle.projectsEvaluated {
8896 }
8997
9098 // Generate a debug-overlay manifest that merges the ContentProvider
91- // into the app's manifest (avoids replacing the main manifest)
9299 def generatedManifestDir = project. file(" ${ project.buildDir} /generated/harness-coverage" )
93100 generatedManifestDir. mkdirs()
94101 def generatedManifest = new File (generatedManifestDir, ' AndroidManifest.xml' )
95- generatedManifest. text = ''' \
102+ generatedManifest. text = """ \
96103<manifest xmlns:android="http://schemas.android.com/apk/res/android">
97104 <application>
98105 <provider
99106 android:name="com.harness.coverage.CoverageInitProvider"
100- android:authorities="${applicationId}.harness_coverage"
107+ android:authorities="\ $ {applicationId}.harness_coverage"
101108 android:exported="false"
102109 android:initOrder="999" />
103110 </application>
104111</manifest>
105- '''
112+ """
106113 androidExt. sourceSets. debug. manifest. srcFile generatedManifest
107114 }
108115
109116 // --- Offline instrumentation + stash build artifacts ---
110- project. afterEvaluate {
111- def coverageDir = project. file(" ${ project.buildDir} /harness-coverage" )
117+ def coverageDir = project. file(" ${ project.buildDir} /harness-coverage" )
112118
113- // Reusable closure that saves originals, instruments, and stashes jacococli
114- def instrumentClasses = { File classesDir , String label ->
115- if (! classesDir. exists()) return
119+ def instrumentClasses = { File classesDir , String label ->
120+ if (! classesDir. exists()) return
116121
117- coverageDir. mkdirs()
122+ coverageDir. mkdirs()
118123
119- // Save original classes for report generation
120- def origDir = new File (coverageDir, " original-classes-${ label} " )
121- if (origDir. exists()) origDir. deleteDir()
122- ant. copy(todir : origDir) {
123- fileset(dir : classesDir)
124- }
125-
126- // Stash jacococli.jar for use at report time
127- def destJar = new File (coverageDir, ' jacococli.jar' )
128- if (! destJar. exists()) {
129- def cliJar = project. configurations. jacocoCli. singleFile
130- ant. copy(file : cliJar, tofile : destJar)
131- logger. lifecycle(" [HarnessCoverage] Stashed ${ destJar} " )
132- }
124+ // Save original classes for report generation
125+ def origDir = new File (coverageDir, " original-classes-${ label} " )
126+ if (origDir. exists()) origDir. deleteDir()
127+ ant. copy(todir : origDir) {
128+ fileset(dir : classesDir)
129+ }
133130
134- // Instrument to temp dir, then replace
135- def instrumentedDir = project. file(" ${ project.buildDir} /tmp/jacoco-instrumented-${ label} " )
136- if (instrumentedDir. exists()) instrumentedDir. deleteDir()
137- instrumentedDir. mkdirs()
138-
139- ant. taskdef(
140- name : ' instrument' ,
141- classname : ' org.jacoco.ant.InstrumentTask' ,
142- classpath : project. configurations. jacocoAnt. asPath
143- )
144- ant. instrument(destdir : instrumentedDir) {
145- fileset(dir : classesDir, includes : ' **/*.class' )
146- }
131+ // Stash jacococli.jar for use at report time
132+ def destJar = new File (coverageDir, ' jacococli.jar' )
133+ if (! destJar. exists()) {
134+ def cliJar = project. configurations. jacocoCli. singleFile
135+ ant. copy(file : cliJar, tofile : destJar)
136+ logger. lifecycle(" [HarnessCoverage] Stashed ${ destJar} " )
137+ }
147138
148- ant. copy(todir : classesDir, overwrite : true ) {
149- fileset(dir : instrumentedDir)
150- }
139+ // Instrument to temp dir, then replace originals
140+ def instrumentedDir = project. file(" ${ project.buildDir} /tmp/jacoco-instrumented-${ label} " )
141+ if (instrumentedDir. exists()) instrumentedDir. deleteDir()
142+ instrumentedDir. mkdirs()
143+
144+ ant. taskdef(
145+ name : ' instrument' ,
146+ classname : ' org.jacoco.ant.InstrumentTask' ,
147+ classpath : project. configurations. jacocoAnt. asPath
148+ )
149+ ant. instrument(destdir : instrumentedDir) {
150+ fileset(dir : classesDir, includes : ' **/*.class' )
151+ }
151152
152- logger. lifecycle(" [HarnessCoverage] Instrumented ${ label} classes in ${ classesDir} " )
153+ ant. copy(todir : classesDir, overwrite : true ) {
154+ fileset(dir : instrumentedDir)
153155 }
154156
155- // Kotlin classes
156- def kotlinTask = project. tasks. findByName(' compileDebugKotlin' )
157- if (kotlinTask) {
157+ logger. lifecycle(" [HarnessCoverage] Instrumented ${ label} classes in ${ classesDir} " )
158+ }
159+
160+ // Hook into compile tasks. Use configureEach with name matching so it
161+ // works with lazily-registered tasks (AGP + RN Gradle plugin register
162+ // compile tasks after project evaluation, during task graph resolution).
163+ project. tasks. configureEach { task ->
164+ if (task. name == ' compileDebugKotlin' ) {
158165 def kotlinClasses = project. file(" ${ project.buildDir} /tmp/kotlin-classes/debug" )
159- kotlinTask . doLast { instrumentClasses(kotlinClasses, ' kotlin' ) }
166+ task . doLast { instrumentClasses(kotlinClasses, ' kotlin' ) }
160167 }
161-
162- // Java classes
163- def javaTask = project. tasks. findByName(' compileDebugJavaWithJavac' )
164- if (javaTask) {
168+ if (task. name == ' compileDebugJavaWithJavac' ) {
165169 def javaClasses = project. file(" ${ project.buildDir} /intermediates/javac/debug/classes" )
166- javaTask. doLast { instrumentClasses(javaClasses, ' java' ) }
167- }
168-
169- if (! kotlinTask && ! javaTask) {
170- logger. warn(" [HarnessCoverage] No compile tasks found in ${ project.path} , skipping instrumentation" )
170+ task. doLast { instrumentClasses(javaClasses, ' java' ) }
171171 }
172172 }
173173 }
0 commit comments