Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import com.facebook.react.utils.DependencyUtils.readVersionAndGroupStrings
import com.facebook.react.utils.JdkConfiguratorUtils.configureJavaToolChains
import com.facebook.react.utils.JsonUtils
import com.facebook.react.utils.NdkConfiguratorUtils.configureReactNativeNdk
import com.facebook.react.utils.NdkConfiguratorUtils.configureStubPchGeneration
import com.facebook.react.utils.ProjectUtils.needsCodegenFromPackageJson
import com.facebook.react.utils.PropertyUtils
import com.facebook.react.utils.findPackageJsonFile
Expand Down Expand Up @@ -99,6 +100,7 @@ class ReactPlugin : Plugin<Project> {
}

configureReactNativeNdk(project, extension)
configureStubPchGeneration(project)
configureBuildConfigFieldsForApp(project, extension)
configureDevServerLocation(project)
configureBackwardCompatibilityReactMap(project)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.tasks

import com.google.gson.Gson
import com.google.gson.JsonArray
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.TaskAction

abstract class GenerateStubPchTask : DefaultTask() {

init {
group = "react"
description = "Generates stub precompiled headers so Android Studio can sync C++ sources."
outputs.upToDateWhen { false }
}

/** The AGP-managed `.cxx` directory. */
@get:Internal abstract val cxxDirectory: DirectoryProperty

@TaskAction
fun taskAction() {
val cxxDir = cxxDirectory.get().asFile
if (!cxxDir.isDirectory) {
return
}

cxxDir
.walkTopDown()
.filter { it.isFile && it.name == COMPILE_COMMANDS_FILENAME }
.forEach { generateStubsFor(it) }
}

internal fun generateStubsFor(compileCommands: File) {
val entries = runCatching {
Gson().fromJson(compileCommands.readText(), JsonArray::class.java)
}.getOrNull() ?: return

for (element in entries) {
val entry = element.asJsonObject
val source = entry.get("file")?.asString ?: continue
if (!source.endsWith(PCH_SOURCE_SUFFIX)) {
continue
}

val pchFile = File(source.removeSuffix(SOURCE_EXTENSION) + PCH_EXTENSION)
// Anything already on disk was either built for real or stubbed by an earlier sync.
if (pchFile.length() > 0L) {
continue
}

val command = entry.get("command")?.asString ?: continue
val directory = entry.get("directory")?.asString ?: continue
compileEmptyPch(command, File(directory), pchFile)

// A stub is not a valid input for the real compilation, so keep it older than its source.
// That way the next build treats it as stale and replaces it before anything consumes it.
pchFile.setLastModified(File(source).lastModified() - 1)
}
}

private fun compileEmptyPch(command: String, workingDir: File, pchFile: File) {
pchFile.parentFile.mkdirs()
val stubHeader = File(pchFile.parentFile, STUB_HEADER_FILENAME).apply { writeText("") }

val process = ProcessBuilder(stubCompilerArguments(command, pchFile, stubHeader))
.directory(workingDir)
.redirectErrorStream(true)
.start()

val output = process.inputStream.bufferedReader().use { it.readText() }
process.outputStream.close()

if (process.waitFor() != 0) {
throw GradleException("RNGP - Stub precompiled header generation failed:\n$output")
}
}

internal fun stubCompilerArguments(
command: String,
pchFile: File,
stubHeader: File,
): List<String> {
val target =
TARGET_FLAG.find(command)?.value
?: throw GradleException("RNGP - Could not find --target in: $command")
val sysroot =
SYSROOT_FLAG.find(command)?.value
?: throw GradleException("RNGP - Could not find --sysroot in: $command")

return listOf(
command.substringBefore(' '),
target,
sysroot,
"-x",
"c++-header",
"-o",
pchFile.absolutePath,
stubHeader.absolutePath,
)
}

companion object {
private const val COMPILE_COMMANDS_FILENAME = "compile_commands.json"
private const val PCH_SOURCE_SUFFIX = "cmake_pch.hxx.cxx"
private const val SOURCE_EXTENSION = ".cxx"
private const val PCH_EXTENSION = ".pch"
private const val STUB_HEADER_FILENAME = "stub_pch.hxx"
private val TARGET_FLAG = Regex("""--target=\S+""")
private val SYSROOT_FLAG = Regex("""--sysroot=\S+""")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package com.facebook.react.utils
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.Variant
import com.facebook.react.ReactExtension
import com.facebook.react.tasks.GenerateStubPchTask
import com.facebook.react.utils.ProjectUtils.getReactNativeArchitectures
import java.io.File
import org.gradle.api.Project
Expand Down Expand Up @@ -65,6 +66,20 @@ internal object NdkConfiguratorUtils {
}
}

/**
* The codegen targets share a precompiled header, which only a real build produces. Android
* Studio's C++ engine needs one at sync time, so we hook a task that writes stubs into the sync
* itself. See [GenerateStubPchTask].
*/
fun configureStubPchGeneration(project: Project) {
val generateStubPchTask = project.tasks.register("generateStubPch", GenerateStubPchTask::class.java) { task ->
task.cxxDirectory.set(project.layout.projectDirectory.dir(".cxx"))
task.dependsOn(project.tasks.matching { it.name.startsWith("configureCMakeDebug") })
}

project.tasks.maybeCreate("prepareKotlinBuildScriptModel").dependsOn(generateStubPchTask)
}

/**
* This method is used to configure the .so Packaging Options for the given variant. It will make
* sure we specify the correct .pickFirsts for all the .so files we are producing or that we're
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.facebook.react.tasks

import com.facebook.react.tests.createTestTask
import java.io.File
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.gradle.api.GradleException
import org.junit.Assume.assumeFalse
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder

class GenerateStubPchTaskTest {

@get:Rule val tempFolder = TemporaryFolder()

@Test
fun generateStubPchTask_groupIsSetCorrectly() {
val task = createTestTask<GenerateStubPchTask> {}
assertThat(task.group).isEqualTo("react")
}

@Test
fun stubCompilerArguments_extractsCompilerAndFlags() {
val task = createTestTask<GenerateStubPchTask>()
val pchFile = tempFolder.newFile("cmake_pch.hxx.pch")
val stubHeader = tempFolder.newFile("stub_pch.hxx")

val arguments =
task.stubCompilerArguments(
"/ndk/clang++ --target=aarch64-none-linux-android24 --sysroot=/ndk/sysroot -Wall -c x.cxx",
pchFile,
stubHeader,
)

assertThat(arguments)
.containsExactly(
"/ndk/clang++",
"--target=aarch64-none-linux-android24",
"--sysroot=/ndk/sysroot",
"-x",
"c++-header",
"-o",
pchFile.absolutePath,
stubHeader.absolutePath,
)
}

@Test
fun stubCompilerArguments_withoutTarget_fails() {
val task = createTestTask<GenerateStubPchTask>()

assertThatThrownBy {
task.stubCompilerArguments(
"/ndk/clang++ --sysroot=/ndk/sysroot -c x.cxx",
tempFolder.newFile("a.pch"),
tempFolder.newFile("a.hxx"),
)
}
.isInstanceOf(GradleException::class.java)
.hasMessageContaining("--target")
}

@Test
fun stubCompilerArguments_withoutSysroot_fails() {
val task = createTestTask<GenerateStubPchTask>()

assertThatThrownBy {
task.stubCompilerArguments(
"/ndk/clang++ --target=aarch64-none-linux-android24 -c x.cxx",
tempFolder.newFile("a.pch"),
tempFolder.newFile("a.hxx"),
)
}
.isInstanceOf(GradleException::class.java)
.hasMessageContaining("--sysroot")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,19 @@ target_link_libraries(${CMAKE_PROJECT_NAME}
add_library(common_flags INTERFACE)
target_compile_options(common_flags INTERFACE ${folly_FLAGS})

# Defines the `reactnative_pch` target and `target_reuse_reactnative_pch()`, so
# the codegen targets below share a single precompiled header. Has to come after
# `common_flags`, as the precompiled header is built with the same flags as its
# consumers.
include(${CMAKE_CURRENT_LIST_DIR}/ReactNative-precompiled-header.cmake)

# If project is on RN CLI v9, then we can use the following lines to link against the autolinked 3rd party libraries.
if(EXISTS ${PROJECT_BUILD_DIR}/generated/autolinking/src/main/jni/Android-autolinking.cmake)
include(${PROJECT_BUILD_DIR}/generated/autolinking/src/main/jni/Android-autolinking.cmake)
target_link_libraries(${CMAKE_PROJECT_NAME} ${AUTOLINKED_LIBRARIES})
foreach(autolinked_library ${AUTOLINKED_LIBRARIES})
target_link_libraries(${autolinked_library} common_flags)
target_reuse_reactnative_pch(${autolinked_library})
endforeach()
endif()

Expand All @@ -104,6 +111,7 @@ if(EXISTS ${PROJECT_BUILD_DIR}/generated/source/codegen/jni/CMakeLists.txt)
get_property(APP_CODEGEN_TARGET DIRECTORY ${PROJECT_BUILD_DIR}/generated/source/codegen/jni/ PROPERTY BUILDSYSTEM_TARGETS)
target_link_libraries(${CMAKE_PROJECT_NAME} ${APP_CODEGEN_TARGET})
target_link_libraries(${APP_CODEGEN_TARGET} common_flags)
target_reuse_reactnative_pch(${APP_CODEGEN_TARGET})

# We need to pass the generated header and module provider to the OnLoad.cpp file so
# local app modules can properly be linked.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

# Clang stamps a .pch with the time it was built, which makes it
# non-reproducible: ccache can never reuse one, and a PCH restored from the cache
# gets rejected as "modified since built" by the sources that consume it. Both
# the owner and the consumers need the flag, as it changes how the header is
# both written and read.
set(REACT_NATIVE_PCH_FLAGS "$<$<COMPILE_LANGUAGE:CXX>:-Xclang;-fno-pch-timestamp>")

add_library(reactnative_pch STATIC EXCLUDE_FROM_ALL
${CMAKE_CURRENT_LIST_DIR}/precompiled-header/pch-owner.cpp)

target_compile_reactnative_options(reactnative_pch PRIVATE)
set_target_properties(reactnative_pch PROPERTIES CXX_STANDARD 20 CXX_EXTENSIONS OFF)

target_link_libraries(reactnative_pch PRIVATE common_flags fbjni jsi reactnative)

target_compile_options(reactnative_pch PRIVATE ${REACT_NATIVE_PCH_FLAGS})
target_precompile_headers(reactnative_pch PRIVATE
"$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_CURRENT_LIST_DIR}/precompiled-header/pch.h>")

# Points `target` at the precompiled header owned by `reactnative_pch`. Targets
# that can't consume one - imported and interface libraries - are skipped, as
# are names that aren't targets at all: an autolinked library is allowed to skip
# itself when its source directory is missing.
function(target_reuse_reactnative_pch target)
if (NOT TARGET ${target})
return()
endif ()

get_target_property(is_imported ${target} IMPORTED)
if (is_imported)
return()
endif ()

get_target_property(target_type ${target} TYPE)
if (target_type STREQUAL "INTERFACE_LIBRARY")
return()
endif ()

# See the note on REACT_NATIVE_PCH_FLAGS above: the flag has to be on both
# sides of the reuse.
target_compile_options(${target} PRIVATE ${REACT_NATIVE_PCH_FLAGS})

# clang rejects a precompiled header built with a different C++ dialect.
set_target_properties(${target} PROPERTIES CXX_STANDARD 20 CXX_EXTENSIONS OFF)

target_precompile_headers(${target} REUSE_FROM reactnative_pch)
endfunction()
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

// This translation unit exists solely so the `reactnative_pch` target has
// something to compile, and therefore produces a precompiled header that the
// codegen targets can reuse.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <folly/dynamic.h>
#include <jsi/jsi.h>
#include <react/bridging/Bridging.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/ViewEventEmitter.h>
#include <react/renderer/core/ConcreteComponentDescriptor.h>
#include <react/renderer/core/PropsParserContext.h>
#include <react/renderer/core/StateData.h>
#include <react/renderer/core/propsConversions.h>
Loading