Skip to content

Commit 7ca1b42

Browse files
lukmccallmeta-codesync[bot]
authored andcommitted
Introduce PCH for codegen targets (#58317)
Summary: Upstream precompiled headers configuration on Android that was added to Expo in SDK 56. Related Expo PRs: - expo/expo#47133 - expo/expo#46915 - expo/expo#45835 - expo/expo#45921 - expo/expo#46274 In an Expo test app with ~17 turbomodules, I was able to reduce the C++ build time from ~16 min to ~5 min. These changes only affect the codegen targets. They shouldn't affect user code. ## Changelog: [ANDROID] [ADDED] - Introduce PCH for codegen targets to improve compilation time Pull Request resolved: #58317 Test Plan: - `assembleDebug` and `assembleRelease` in `rn-tester` ✅ - Manually confirmed that .pch was generated ✅ - Opened `rn-tester` in Android Studio to check if the sync workaround works ✅ Similar changes were added to Expo some time ago and have worked well in production. Reviewed By: cipolleschi Differential Revision: D119519543 Pulled By: cortinico fbshipit-source-id: b75497b11850b48554800bbc3abef98d814f9d33
1 parent efc5161 commit 7ca1b42

11 files changed

Lines changed: 630 additions & 0 deletions

File tree

‎packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/ReactPlugin.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings
2929
import com.facebook.react.utils.JdkConfiguratorUtils.configureJavaToolChains
3030
import com.facebook.react.utils.JsonUtils
3131
import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk
32+
import com.facebook.react.utils.NdkConfiguratorUtils.configureStubPchGeneration
3233
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
3334
import com.facebook.react.utils.PropertyUtils
3435
import com.facebook.react.utils.findPackageJsonFile
@@ -99,6 +100,7 @@ class ReactPlugin : Plugin<Project> {
99100
}
100101

101102
configureReactNativeNdk(project, extension)
103+
configureStubPchGeneration(project)
102104
configureBuildConfigFieldsForApp(project, extension)
103105
configureDevServerLocation(project)
104106
configureBackwardCompatibilityReactMap(project)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.internal
9+
10+
import com.facebook.react.utils.StubPchUtils
11+
import org.gradle.api.file.DirectoryProperty
12+
import org.gradle.api.logging.Logging
13+
import org.gradle.api.services.BuildService
14+
import org.gradle.api.services.BuildServiceParameters
15+
16+
/**
17+
* Generates stub precompiled headers when the build finishes. See [StubPchUtils].
18+
*
19+
* Android Studio configures the C++ projects of the selected variant while it fetches the Gradle
20+
* models (during sync), which happens after all tasks have run. A build service is closed after
21+
* that, so by the time [close] runs the `compile_commands.json` files of exactly the variant and
22+
* ABI that Studio requested are on disk.
23+
*/
24+
abstract class StubPchBuildService : BuildService<StubPchBuildService.Params>, AutoCloseable {
25+
26+
interface Params : BuildServiceParameters {
27+
val cxxDirectory: DirectoryProperty
28+
}
29+
30+
override fun close() {
31+
try {
32+
StubPchUtils.generateStubs(parameters.cxxDirectory.get().asFile)
33+
} catch (e: Exception) {
34+
logger.warn("RNGP - Could not generate stub precompiled headers: ${e.message}")
35+
}
36+
}
37+
38+
companion object {
39+
private val logger = Logging.getLogger(StubPchBuildService::class.java)
40+
}
41+
}

‎packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTask.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() {
7070
if(EXISTS "$nativeFolderPath")
7171
add_subdirectory("$nativeFolderPath" ${libraryName}_autolinked_build)
7272
list(APPEND AUTOLINKED_LIBRARIES $CODEGEN_LIB_PREFIX${libraryName})
73+
list(APPEND AUTOLINKED_CODEGEN_LIBRARIES $CODEGEN_LIB_PREFIX${libraryName})
7374
else()
7475
message(WARNING "React Native: Skipping autolinked library '$CODEGEN_LIB_PREFIX${libraryName}' because the source directory does not exist: $nativeFolderPath")
7576
endif()
@@ -209,6 +210,7 @@ abstract class GenerateAutolinkingNewArchitecturesFileTask : DefaultTask() {
209210
set(REACTNATIVE_MERGED_SO true)
210211
211212
set(AUTOLINKED_LIBRARIES)
213+
set(AUTOLINKED_CODEGEN_LIBRARIES)
212214
213215
{{ libraryIncludes }}
214216
"""

‎packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/NdkConfiguratorUtils.kt‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ package com.facebook.react.utils
1010
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
1111
import com.android.build.api.variant.Variant
1212
import com.facebook.react.ReactExtension
13+
import com.facebook.react.internal.StubPchBuildService
1314
import com.facebook.react.utils.ProjectUtils.getReactNativeArchitectures
1415
import java.io.File
1516
import org.gradle.api.Project
1617

1718
internal object NdkConfiguratorUtils {
19+
/** Set by Android Studio only on every Gradle */
20+
private const val BUILD_MODEL_ONLY_PROPERTY = "android.injected.build.model.only"
21+
1822
@Suppress("UnstableApiUsage")
1923
fun configureReactNativeNdk(project: Project, extension: ReactExtension) {
2024
project.pluginManager.withPlugin("com.android.application") {
@@ -65,6 +69,29 @@ internal object NdkConfiguratorUtils {
6569
}
6670
}
6771

72+
/**
73+
* The codegen targets share a precompiled header, which only a real build produces. Android
74+
* Studio's C++ engine needs one at sync time, so during a sync we write stubs for the variant
75+
* Studio is indexing.
76+
*/
77+
fun configureStubPchGeneration(project: Project) {
78+
val isIdeSync =
79+
project.providers
80+
.gradleProperty(BUILD_MODEL_ONLY_PROPERTY)
81+
.map { it.toBoolean() }
82+
.getOrElse(false)
83+
if (!isIdeSync) {
84+
return
85+
}
86+
87+
project.gradle.sharedServices
88+
.registerIfAbsent("StubPchBuildService", StubPchBuildService::class.java) { spec ->
89+
spec.parameters.cxxDirectory.set(project.layout.projectDirectory.dir(".cxx"))
90+
}
91+
// Gradle only closes services that were instantiated, so resolve it right away.
92+
.get()
93+
}
94+
6895
/**
6996
* This method is used to configure the .so Packaging Options for the given variant. It will make
7097
* sure we specify the correct .pickFirsts for all the .so files we are producing or that we're
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.utils
9+
10+
import com.google.gson.Gson
11+
import com.google.gson.JsonArray
12+
import java.io.File
13+
import org.gradle.api.GradleException
14+
15+
internal object StubPchUtils {
16+
private const val COMPILE_COMMANDS_FILENAME = "compile_commands.json"
17+
private const val PCH_SOURCE_SUFFIX = "cmake_pch.hxx.cxx"
18+
private const val SOURCE_EXTENSION = ".cxx"
19+
private const val PCH_EXTENSION = ".pch"
20+
private const val STUB_HEADER_FILENAME = "stub_pch.hxx"
21+
22+
/** Generates the missing stub precompiled headers for every configuration under [cxxDir]. */
23+
fun generateStubs(cxxDir: File) {
24+
if (!cxxDir.isDirectory) {
25+
return
26+
}
27+
28+
cxxDir
29+
.walkTopDown()
30+
.filter { it.isFile && it.name == COMPILE_COMMANDS_FILENAME }
31+
.forEach { generateStubsFor(it) }
32+
}
33+
34+
internal fun generateStubsFor(compileCommands: File) {
35+
val entries =
36+
runCatching { Gson().fromJson(compileCommands.readText(), JsonArray::class.java) }
37+
.getOrNull() ?: return
38+
39+
for (element in entries) {
40+
val entry = element.asJsonObject
41+
val source = entry.get("file")?.asString ?: continue
42+
if (!source.endsWith(PCH_SOURCE_SUFFIX)) {
43+
continue
44+
}
45+
46+
val header = source.removeSuffix(SOURCE_EXTENSION)
47+
val pchFile = File(header + PCH_EXTENSION)
48+
// Anything already on disk was either built for real or stubbed by an earlier sync.
49+
if (pchFile.length() > 0L) {
50+
continue
51+
}
52+
53+
val command = entry.get("command")?.asString ?: continue
54+
val directory = entry.get("directory")?.asString ?: continue
55+
compileEmptyPch(command, header, File(directory), pchFile)
56+
57+
// A stub is not a valid input for the real compilation, so keep it older than its source.
58+
// That way the next build treats it as stale and replaces it before anything consumes it.
59+
pchFile.setLastModified(File(source).lastModified() - 1)
60+
}
61+
}
62+
63+
private fun compileEmptyPch(
64+
command: String,
65+
header: String,
66+
workingDir: File,
67+
pchFile: File,
68+
) {
69+
pchFile.parentFile.mkdirs()
70+
val stubHeader = File(pchFile.parentFile, STUB_HEADER_FILENAME).apply { writeText("") }
71+
72+
val process =
73+
ProcessBuilder(stubCompilerArguments(command, header, stubHeader))
74+
.directory(workingDir)
75+
.redirectErrorStream(true)
76+
.start()
77+
78+
val output = process.inputStream.bufferedReader().use { it.readText() }
79+
process.outputStream.close()
80+
81+
if (process.waitFor() != 0) {
82+
throw GradleException("RNGP - Stub precompiled header generation failed:\n$output")
83+
}
84+
}
85+
86+
internal fun stubCompilerArguments(
87+
command: String,
88+
header: String,
89+
stubHeader: File,
90+
windows: Boolean = Os.isWindows(),
91+
): List<String> {
92+
val arguments = splitCompilerCommand(command, windows)
93+
if (arguments.none { it == header }) {
94+
throw GradleException("RNGP - Could not find $header in: $command")
95+
}
96+
97+
return arguments.map { if (it == header) stubHeader.absolutePath else it }
98+
}
99+
100+
private fun splitCompilerCommand(command: String, windows: Boolean): List<String> {
101+
val arguments = mutableListOf<String>()
102+
var argument: StringBuilder? = null
103+
var quote: Char? = null
104+
var index = 0
105+
106+
while (index < command.length) {
107+
val char = command[index++]
108+
if (char.isWhitespace() && quote == null) {
109+
argument?.let { arguments.add(it.toString()) }
110+
argument = null
111+
continue
112+
}
113+
114+
// POSIX line continuations don't start an argument or contribute any characters.
115+
if (!windows && char == '\\' && quote != '\'' && command.getOrNull(index) == '\n') {
116+
index++
117+
continue
118+
}
119+
120+
val token = argument ?: StringBuilder().also { argument = it }
121+
when {
122+
char == '\\' && windows -> {
123+
val start = index - 1
124+
while (command.getOrNull(index) == '\\') {
125+
index++
126+
}
127+
val count = index - start
128+
val followedByQuote = command.getOrNull(index) == '"'
129+
repeat(if (followedByQuote) count / 2 else count) { token.append('\\') }
130+
if (followedByQuote && count % 2 != 0) {
131+
token.append(command[index++])
132+
}
133+
}
134+
char == '\\' && quote != '\'' -> {
135+
val next =
136+
command.getOrNull(index)
137+
?: throw GradleException("RNGP - Trailing escape in compiler command: $command")
138+
// Inside POSIX double quotes, backslashes only escape these shell characters.
139+
val escaped = quote != '"' || next in "\"\\\$`"
140+
token.append(if (escaped) command[index++] else char)
141+
}
142+
quote == char && windows && command.getOrNull(index) == '"' ->
143+
token.append(command[index++])
144+
quote == char -> quote = null
145+
quote == null && (char == '"' || (!windows && char == '\'')) -> quote = char
146+
else -> token.append(char)
147+
}
148+
}
149+
150+
if (quote != null) {
151+
throw GradleException("RNGP - Unclosed quote in compiler command: $command")
152+
}
153+
154+
argument?.let { arguments.add(it.toString()) }
155+
156+
return arguments
157+
}
158+
}

‎packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/GenerateAutolinkingNewArchitecturesFileTaskTest.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
208208
set(REACTNATIVE_MERGED_SO true)
209209
210210
set(AUTOLINKED_LIBRARIES)
211+
set(AUTOLINKED_CODEGEN_LIBRARIES)
211212
212213
213214
"""
@@ -233,16 +234,19 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
233234
set(REACTNATIVE_MERGED_SO true)
234235
235236
set(AUTOLINKED_LIBRARIES)
237+
set(AUTOLINKED_CODEGEN_LIBRARIES)
236238
237239
if(EXISTS "./a/directory/")
238240
add_subdirectory("./a/directory/" aPackage_autolinked_build)
239241
list(APPEND AUTOLINKED_LIBRARIES react_codegen_aPackage)
242+
list(APPEND AUTOLINKED_CODEGEN_LIBRARIES react_codegen_aPackage)
240243
else()
241244
message(WARNING "React Native: Skipping autolinked library 'react_codegen_aPackage' because the source directory does not exist: ./a/directory/")
242245
endif()
243246
if(EXISTS "./another/directory/with\ spaces/")
244247
add_subdirectory("./another/directory/with\ spaces/" anotherPackage_autolinked_build)
245248
list(APPEND AUTOLINKED_LIBRARIES react_codegen_anotherPackage)
249+
list(APPEND AUTOLINKED_CODEGEN_LIBRARIES react_codegen_anotherPackage)
246250
else()
247251
message(WARNING "React Native: Skipping autolinked library 'react_codegen_anotherPackage' because the source directory does not exist: ./another/directory/with\ spaces/")
248252
endif()
@@ -289,6 +293,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
289293
if(EXISTS "$generatedNativeFolderPath")
290294
add_subdirectory("$generatedNativeFolderPath" aPackage_autolinked_build)
291295
list(APPEND AUTOLINKED_LIBRARIES react_codegen_aPackage)
296+
list(APPEND AUTOLINKED_CODEGEN_LIBRARIES react_codegen_aPackage)
292297
else()
293298
message(WARNING "React Native: Skipping autolinked library 'react_codegen_aPackage' because the source directory does not exist: $generatedNativeFolderPath")
294299
endif()
@@ -327,6 +332,7 @@ class GenerateAutolinkingNewArchitecturesFileTaskTest {
327332
if(EXISTS "./a/directory/")
328333
add_subdirectory("./a/directory/" aPackage_autolinked_build)
329334
list(APPEND AUTOLINKED_LIBRARIES react_codegen_aPackage)
335+
list(APPEND AUTOLINKED_CODEGEN_LIBRARIES react_codegen_aPackage)
330336
else()
331337
message(WARNING "React Native: Skipping autolinked library 'react_codegen_aPackage' because the source directory does not exist: ./a/directory/")
332338
endif()

0 commit comments

Comments
 (0)