@react-native/gradle-plugin 0.75.2 → 0.75.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native/gradle-plugin",
3
- "version": "0.75.2",
3
+ "version": "0.75.3",
4
4
  "description": "Gradle Plugin for React Native",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,7 +7,6 @@
7
7
 
8
8
  import org.gradle.api.internal.classpath.ModuleRegistry
9
9
  import org.gradle.api.tasks.testing.logging.TestExceptionFormat
10
- import org.gradle.configurationcache.extensions.serviceOf
11
10
  import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
12
11
 
13
12
  plugins {
@@ -52,14 +51,6 @@ dependencies {
52
51
  testImplementation(libs.junit)
53
52
  testImplementation(libs.assertj)
54
53
  testImplementation(project(":shared-testutil"))
55
-
56
- testRuntimeOnly(
57
- files(
58
- serviceOf<ModuleRegistry>()
59
- .getModule("gradle-tooling-api-builders")
60
- .classpath
61
- .asFiles
62
- .first()))
63
54
  }
64
55
 
65
56
  // We intentionally don't build for Java 17 as users will see a cryptic bytecode version
@@ -188,6 +188,7 @@ abstract class ReactExtension @Inject constructor(val project: Project) {
188
188
  ?.dependencies
189
189
  ?.values
190
190
  ?.filter { it.platforms?.android !== null }
191
+ ?.filterNot { it.platforms?.android?.isPureCxxDependency == true }
191
192
  ?.forEach { deps ->
192
193
  val nameCleansed = deps.nameCleansed
193
194
  val dependencyConfiguration = deps.platforms?.android?.dependencyConfiguration
@@ -189,6 +189,44 @@ class ReactExtensionTest {
189
189
  assertThat(deps).isEmpty()
190
190
  }
191
191
 
192
+ @Test
193
+ fun getGradleDependenciesToApply_withIsPureCxxDeps_filtersCorrectly() {
194
+ val validJsonFile =
195
+ createJsonFile(
196
+ """
197
+ {
198
+ "reactNativeVersion": "1000.0.0",
199
+ "dependencies": {
200
+ "@react-native/oss-library-example": {
201
+ "root": "./node_modules/@react-native/android-example",
202
+ "name": "@react-native/android-example",
203
+ "platforms": {
204
+ "android": {
205
+ "sourceDir": "src/main/java",
206
+ "packageImportPath": "com.facebook.react"
207
+ }
208
+ }
209
+ },
210
+ "@react-native/another-library-for-testing": {
211
+ "root": "./node_modules/@react-native/cxx-testing",
212
+ "name": "@react-native/cxx-testing",
213
+ "platforms": {
214
+ "android": {
215
+ "sourceDir": "src/main/java",
216
+ "packageImportPath": "com.facebook.react",
217
+ "isPureCxxDependency": true
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ """
224
+ .trimIndent())
225
+
226
+ val deps = getGradleDependenciesToApply(validJsonFile)
227
+ assertThat(deps).containsExactly("implementation" to ":react-native_android-example")
228
+ }
229
+
192
230
  private fun createJsonFile(@Language("JSON") input: String) =
193
231
  tempFolder.newFile().apply { writeText(input) }
194
232
  }
@@ -7,7 +7,6 @@
7
7
 
8
8
  import org.gradle.api.internal.classpath.ModuleRegistry
9
9
  import org.gradle.api.tasks.testing.logging.TestExceptionFormat
10
- import org.gradle.configurationcache.extensions.serviceOf
11
10
  import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
12
11
 
13
12
  plugins {
@@ -41,14 +40,6 @@ dependencies {
41
40
 
42
41
  testImplementation(libs.junit)
43
42
  testImplementation(project(":shared-testutil"))
44
-
45
- testRuntimeOnly(
46
- files(
47
- serviceOf<ModuleRegistry>()
48
- .getModule("gradle-tooling-api-builders")
49
- .classpath
50
- .asFiles
51
- .first()))
52
43
  }
53
44
 
54
45
  // We intentionally don't build for Java 17 as users will see a cryptic bytecode version
@@ -7,6 +7,7 @@
7
7
 
8
8
  package com.facebook.react
9
9
 
10
+ import com.facebook.react.model.ModelAutolinkingConfigJson
10
11
  import com.facebook.react.utils.JsonUtils
11
12
  import com.facebook.react.utils.windowsAwareCommandLine
12
13
  import java.io.File
@@ -53,30 +54,22 @@ abstract class ReactSettingsExtension @Inject constructor(val settings: Settings
53
54
  .files("yarn.lock", "package-lock.json", "package.json", "react-native.config.js")
54
55
  ) {
55
56
  outputFile.parentFile.mkdirs()
56
- val lockFilesChanged = checkAndUpdateLockfiles(lockFiles, outputFolder)
57
- if (lockFilesChanged || outputFile.exists().not() || outputFile.length() != 0L) {
58
- val process =
59
- ProcessBuilder(command)
60
- .directory(workingDirectory)
61
- .redirectOutput(ProcessBuilder.Redirect.to(outputFile))
62
- .redirectError(ProcessBuilder.Redirect.INHERIT)
63
- .start()
64
- val finished = process.waitFor(5, TimeUnit.MINUTES)
65
- if (!finished || (process.exitValue() != 0)) {
66
- val prefixCommand =
67
- "ERROR: autolinkLibrariesFromCommand: process ${command.joinToString(" ")}"
68
- val message =
69
- if (!finished) "${prefixCommand} timed out"
70
- else "${prefixCommand} exited with error code: ${process.exitValue()}"
71
- val logger = Logging.getLogger("ReactSettingsExtension")
72
- logger.error(message)
73
- if (outputFile.length() != 0L) {
74
- logger.error(outputFile.readText().substring(0, 1024))
57
+
58
+ val updateConfig =
59
+ object : GenerateConfig {
60
+ private val pb =
61
+ ProcessBuilder(command)
62
+ .directory(workingDirectory)
63
+ .redirectOutput(ProcessBuilder.Redirect.to(outputFile))
64
+ .redirectError(ProcessBuilder.Redirect.INHERIT)
65
+
66
+ override fun command(): List<String> = pb.command()
67
+
68
+ override fun start(): Process = pb.start()
75
69
  }
76
- outputFile.delete()
77
- throw GradleException(message)
78
- }
79
- }
70
+
71
+ checkAndUpdateCache(updateConfig, outputFile, outputFolder, lockFiles)
72
+
80
73
  linkLibraries(getLibrariesToAutolink(outputFile))
81
74
  }
82
75
 
@@ -107,9 +100,73 @@ abstract class ReactSettingsExtension @Inject constructor(val settings: Settings
107
100
  }
108
101
  }
109
102
 
103
+ internal interface GenerateConfig {
104
+ fun command(): List<String>
105
+
106
+ fun start(): Process
107
+ }
108
+
110
109
  companion object {
111
110
  private val md = MessageDigest.getInstance("SHA-256")
112
111
 
112
+ /**
113
+ * Determine if our cache is out-of-date
114
+ *
115
+ * @param cacheJsonConfig Our current cached autolinking.json config, which may exist
116
+ * @param cacheFolder The folder we store our cached SHAs and config
117
+ * @param lockFiles The [FileCollection] of the lockfiles to check.
118
+ * @return `true` if the cache needs to be rebuilt, `false` otherwise
119
+ */
120
+ internal fun isCacheDirty(
121
+ cacheJsonConfig: File,
122
+ cacheFolder: File,
123
+ lockFiles: FileCollection,
124
+ ): Boolean {
125
+ if (cacheJsonConfig.exists().not() || cacheJsonConfig.length() == 0L) {
126
+ return true
127
+ }
128
+ val lockFilesChanged = checkAndUpdateLockfiles(lockFiles, cacheFolder)
129
+ if (lockFilesChanged) {
130
+ return true
131
+ }
132
+ return isConfigModelInvalid(JsonUtils.fromAutolinkingConfigJson(cacheJsonConfig))
133
+ }
134
+
135
+ /**
136
+ * Utility function to update the settings cache only if it's entries are dirty
137
+ *
138
+ * @param updateJsonConfig A [GenerateConfig] to update the project's autolinking config
139
+ * @param cacheJsonConfig Our current cached autolinking.json config, which may exist
140
+ * @param cacheFolder The folder we store our cached SHAs and config
141
+ * @param lockFiles The [FileCollection] of the lockfiles to check.
142
+ */
143
+ internal fun checkAndUpdateCache(
144
+ updateJsonConfig: GenerateConfig,
145
+ cacheJsonConfig: File,
146
+ cacheFolder: File,
147
+ lockFiles: FileCollection,
148
+ ) {
149
+ if (isCacheDirty(cacheJsonConfig, cacheFolder, lockFiles)) {
150
+ val process = updateJsonConfig.start()
151
+
152
+ val finished = process.waitFor(5, TimeUnit.MINUTES)
153
+ if (!finished || (process.exitValue() != 0)) {
154
+ val command = updateJsonConfig.command().joinToString(" ")
155
+ val prefixCommand = "ERROR: autolinkLibrariesFromCommand: process $command"
156
+ val message =
157
+ if (!finished) "$prefixCommand timed out"
158
+ else "$prefixCommand exited with error code: ${process.exitValue()}"
159
+ val logger = Logging.getLogger("ReactSettingsExtension")
160
+ logger.error(message)
161
+ if (cacheJsonConfig.length() != 0L) {
162
+ logger.error(cacheJsonConfig.readText().substring(0, 1024))
163
+ }
164
+ cacheJsonConfig.delete()
165
+ throw GradleException(message)
166
+ }
167
+ }
168
+ }
169
+
113
170
  /**
114
171
  * Utility function to check if the provided lockfiles have been updated or not. This function
115
172
  * will both check and update the lockfiles hashes if necessary.
@@ -150,5 +207,8 @@ abstract class ReactSettingsExtension @Inject constructor(val settings: Settings
150
207
 
151
208
  internal fun computeSha256(lockFile: File) =
152
209
  String.format("%032x", BigInteger(1, md.digest(lockFile.readBytes())))
210
+
211
+ internal fun isConfigModelInvalid(model: ModelAutolinkingConfigJson?) =
212
+ model?.project?.android?.packageName == null
153
213
  }
154
214
  }
@@ -7,10 +7,12 @@
7
7
 
8
8
  package com.facebook.react
9
9
 
10
+ import com.facebook.react.ReactSettingsExtension.Companion.checkAndUpdateCache
10
11
  import com.facebook.react.ReactSettingsExtension.Companion.checkAndUpdateLockfiles
11
12
  import com.facebook.react.ReactSettingsExtension.Companion.computeSha256
12
13
  import com.facebook.react.ReactSettingsExtension.Companion.getLibrariesToAutolink
13
14
  import groovy.test.GroovyTestCase.assertEquals
15
+ import com.facebook.react.ReactSettingsExtension.GenerateConfig
14
16
  import java.io.File
15
17
  import org.gradle.testfixtures.ProjectBuilder
16
18
  import org.intellij.lang.annotations.Language
@@ -236,6 +238,241 @@ class ReactSettingsExtensionTest {
236
238
  File(buildFolder, "package-lock.json.sha").readText())
237
239
  }
238
240
 
241
+ @Test
242
+ fun skipUpdateIfConfigInCacheIsValid() {
243
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
244
+ val buildFolder = tempFolder.newFolder("build")
245
+ val generatedFolder = tempFolder.newFolder("build", "generated")
246
+ val outputFile =
247
+ File(generatedFolder, "autolinking.json").apply {
248
+ writeText(
249
+ """
250
+ {
251
+ "root": "/",
252
+ "reactNativePath": "/node_modules/react-native",
253
+ "reactNativeVersion": "0.75",
254
+ "dependencies": {},
255
+ "healthChecks": [],
256
+ "platforms": {
257
+ "ios": {},
258
+ "android": {}
259
+ },
260
+ "assets": [],
261
+ "project": {
262
+ "ios": {},
263
+ "android": {
264
+ "sourceDir": "/",
265
+ "appName": "app",
266
+ "packageName": "com.TestApp",
267
+ "applicationId": "com.TestApp",
268
+ "mainActivity": ".MainActivity",
269
+ "assets": []
270
+ }
271
+ }
272
+ }
273
+ """
274
+ .trimIndent())
275
+ }
276
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
277
+ val lockfileCollection = project.files("yarn.lock")
278
+
279
+ // Prebuild the shas with the invalid empty autolinking.json
280
+ checkAndUpdateLockfiles(lockfileCollection, buildFolder)
281
+
282
+ val monitoredUpdateConfig = createMonitoredUpdateConfig()
283
+
284
+ checkAndUpdateCache(monitoredUpdateConfig, outputFile, buildFolder, lockfileCollection)
285
+
286
+ // The autolinking.json file is valid, SHA's are untouched therefore config should NOT be
287
+ // refreshed
288
+ assertThat(monitoredUpdateConfig.run).isFalse()
289
+ }
290
+
291
+ @Test
292
+ fun checkAndUpdateConfigIfEmpty() {
293
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
294
+ val buildFolder = tempFolder.newFolder("build")
295
+ val generatedFolder = tempFolder.newFolder("build", "generated")
296
+ val outputFile = File(generatedFolder, "autolinking.json").apply { writeText("") }
297
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
298
+ val lockfileCollection = project.files("yarn.lock")
299
+
300
+ // Prebuild the shas with the invalid empty autolinking.json
301
+ checkAndUpdateLockfiles(lockfileCollection, buildFolder)
302
+
303
+ val monitoredUpdateConfig = createMonitoredUpdateConfig()
304
+
305
+ checkAndUpdateCache(monitoredUpdateConfig, outputFile, buildFolder, lockfileCollection)
306
+
307
+ // The autolinking.json file is invalid and should be refreshed
308
+ assertThat(monitoredUpdateConfig.run).isTrue()
309
+ }
310
+
311
+ @Test
312
+ fun checkAndUpdateConfigIfCachedConfigInvalid() {
313
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
314
+ val buildFolder = tempFolder.newFolder("build")
315
+ val generatedFolder = tempFolder.newFolder("build", "generated")
316
+ val outputFile =
317
+ File(generatedFolder, "autolinking.json").apply {
318
+ writeText(
319
+ """
320
+ {
321
+ "project": {
322
+ "ios": {},
323
+ "android": {}
324
+ }
325
+ }
326
+ """
327
+ .trimIndent())
328
+ }
329
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
330
+ val lockfileCollection = project.files("yarn.lock")
331
+
332
+ // Prebuild the shas with the invalid empty autolinking.json
333
+ checkAndUpdateLockfiles(lockfileCollection, buildFolder)
334
+
335
+ val monitoredUpdateConfig = createMonitoredUpdateConfig()
336
+
337
+ checkAndUpdateCache(monitoredUpdateConfig, outputFile, buildFolder, lockfileCollection)
338
+
339
+ // The autolinking.json file is invalid and should be refreshed
340
+ assertThat(monitoredUpdateConfig.run).isTrue()
341
+ }
342
+
343
+ @Test
344
+ fun isCacheDirty_withMissingAutolinkingFile_returnsTrue() {
345
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
346
+ val buildFolder =
347
+ tempFolder.newFolder("build").apply {
348
+ File(this, "yarn.lock.sha")
349
+ .writeText("76046b72442ee7eb130627e56c3db7c9907eef4913b17ad130335edc0eb702a8")
350
+ }
351
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
352
+ val lockfiles = project.files("yarn.lock")
353
+ val emptyConfigFile = File(tempFolder.newFolder("build", "autolinking"), "autolinking.json")
354
+
355
+ assertThat(ReactSettingsExtension.isCacheDirty(emptyConfigFile, buildFolder, lockfiles))
356
+ .isTrue()
357
+ }
358
+
359
+ @Test
360
+ fun isCacheDirty_withInvalidAutolinkingFile_returnsTrue() {
361
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
362
+ val buildFolder =
363
+ tempFolder.newFolder("build").apply {
364
+ File(this, "yarn.lock.sha")
365
+ .writeText("76046b72442ee7eb130627e56c3db7c9907eef4913b17ad130335edc0eb702a8")
366
+ }
367
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
368
+ val lockfiles = project.files("yarn.lock")
369
+ val invalidConfigFile =
370
+ createJsonFile(
371
+ """
372
+ {}
373
+ """
374
+ .trimIndent())
375
+
376
+ assertThat(ReactSettingsExtension.isCacheDirty(invalidConfigFile, buildFolder, lockfiles))
377
+ .isTrue()
378
+ }
379
+
380
+ @Test
381
+ fun isCacheDirty_withMissingDependenciesInJson_returnsFalse() {
382
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
383
+ val buildFolder =
384
+ tempFolder.newFolder("build").apply {
385
+ File(this, "yarn.lock.sha")
386
+ .writeText("76046b72442ee7eb130627e56c3db7c9907eef4913b17ad130335edc0eb702a8")
387
+ }
388
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
389
+ val lockfiles = project.files("yarn.lock")
390
+ val invalidConfigFile =
391
+ createJsonFile(
392
+ """
393
+ {
394
+ "reactNativeVersion": "1000.0.0"
395
+ }
396
+ """
397
+ .trimIndent())
398
+
399
+ assertThat(ReactSettingsExtension.isCacheDirty(invalidConfigFile, buildFolder, lockfiles))
400
+ .isTrue()
401
+ }
402
+
403
+ @Test
404
+ fun isCacheDirty_withExistingEmptyDependenciesInJson_returnsTrue() {
405
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
406
+ val buildFolder =
407
+ tempFolder.newFolder("build").apply {
408
+ File(this, "yarn.lock.sha")
409
+ .writeText("76046b72442ee7eb130627e56c3db7c9907eef4913b17ad130335edc0eb702a8")
410
+ }
411
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
412
+ val lockfiles = project.files("yarn.lock")
413
+ val invalidConfigFile =
414
+ createJsonFile(
415
+ """
416
+ {
417
+ "reactNativeVersion": "1000.0.0",
418
+ "dependencies": {}
419
+ }
420
+ """
421
+ .trimIndent())
422
+
423
+ assertThat(ReactSettingsExtension.isCacheDirty(invalidConfigFile, buildFolder, lockfiles))
424
+ .isTrue()
425
+ }
426
+
427
+ @Test
428
+ fun isCacheDirty_withExistingDependenciesInJson_returnsTrue() {
429
+ val project = ProjectBuilder.builder().withProjectDir(tempFolder.root).build()
430
+ val buildFolder =
431
+ tempFolder.newFolder("build").apply {
432
+ File(this, "yarn.lock.sha")
433
+ .writeText("76046b72442ee7eb130627e56c3db7c9907eef4913b17ad130335edc0eb702a8")
434
+ }
435
+ tempFolder.newFile("yarn.lock").apply { writeText("I'm a lockfile") }
436
+ val lockfiles = project.files("yarn.lock")
437
+ val invalidConfigFile =
438
+ createJsonFile(
439
+ """
440
+ {
441
+ "reactNativeVersion": "1000.0.0",
442
+ "dependencies": {
443
+ "@react-native/oss-library-example": {
444
+ "root": "./node_modules/@react-native/oss-library-example",
445
+ "name": "@react-native/oss-library-example",
446
+ "platforms": {
447
+ "ios": {
448
+ "podspecPath": "./node_modules/@react-native/oss-library-example/OSSLibraryExample.podspec",
449
+ "version": "0.0.1",
450
+ "configurations": [],
451
+ "scriptPhases": []
452
+ }
453
+ }
454
+ }
455
+ }
456
+ }
457
+ """
458
+ .trimIndent())
459
+
460
+ assertThat(ReactSettingsExtension.isCacheDirty(invalidConfigFile, buildFolder, lockfiles))
461
+ .isTrue()
462
+ }
463
+
239
464
  private fun createJsonFile(@Language("JSON") input: String) =
240
465
  tempFolder.newFile().apply { writeText(input) }
466
+
467
+ private fun createMonitoredUpdateConfig() =
468
+ object : GenerateConfig {
469
+ var run = false
470
+
471
+ override fun start(): Process {
472
+ run = true
473
+ return ProcessBuilder("true").start()
474
+ }
475
+
476
+ override fun command(): List<String> = listOf("true")
477
+ }
241
478
  }