Add gflags (#52015)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52015

Changelog: [Internal]
Add gflags to fantom_tester so we can pass in data like featureFlags

Reviewed By: cortinico

Differential Revision: D76618409

fbshipit-source-id: a18e642a02c405eef972a7418a606a5980253b6a
This commit is contained in:
Andrew Datsenko
2025-06-17 04:18:08 -07:00
committed by Facebook GitHub Bot
parent a91e598e6a
commit 74b6acb1f0
10 changed files with 475 additions and 7 deletions
@@ -0,0 +1,116 @@
/*
* 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.internal
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.CopySpec
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.provider.Property
import org.gradle.api.tasks.*
/**
* A task that takes care of extracting gflags from a source folder/zip and preparing it to be
* consumed by the NDK. This task will also take care of applying the mapping for gflags parameters.
*/
abstract class PrepareGflagsTask : DefaultTask() {
@get:InputFiles abstract val gflagsPath: ConfigurableFileCollection
@get:InputDirectory abstract val gflagsThirdPartyPath: DirectoryProperty
@get:Input abstract val gflagsVersion: Property<String>
@get:OutputDirectory abstract val outputDir: DirectoryProperty
@get:Inject abstract val fs: FileSystemOperations
@TaskAction
fun taskAction() {
val commonCopyConfig: (action: CopySpec) -> Unit = { action ->
action.from(gflagsPath)
action.from(gflagsThirdPartyPath)
action.duplicatesStrategy = DuplicatesStrategy.INCLUDE
action.includeEmptyDirs = false
action.into(outputDir)
}
fs.copy { action ->
commonCopyConfig(action)
action.include(
"gflags-${gflagsVersion.get()}/src/*.h",
"gflags-${gflagsVersion.get()}/src/*.cc",
"CMakeLists.txt")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.path = "gflags/${matchedFile.name}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags_declare.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line ->
// Replace all placeholders with appropriate values
// see https://github.com/gflags/gflags/blob/v2.2.0/src/gflags_declare.h.in
line
.replace(Regex("@GFLAGS_NAMESPACE@"), "gflags")
.replace(
Regex(
"@(HAVE_STDINT_H|HAVE_SYS_TYPES_H|HAVE_INTTYPES_H|GFLAGS_INTTYPES_FORMAT_C99)@"),
"1")
.replace(Regex("@([A-Z0-9_]+)@"), "1")
}
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/config.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line -> line.replace(Regex("^#cmakedefine"), "//cmakedefine") }
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags_ns.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line ->
line.replace(Regex("@ns@"), "google").replace(Regex("@NS@"), "google".uppercase())
}
matchedFile.path = "gflags/gflags_google.h"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line ->
line
.replace(Regex("@GFLAGS_ATTRIBUTE_UNUSED@"), "")
.replace(Regex("@INCLUDE_GFLAGS_NS_H@"), "#include \"gflags/gflags_google.h\"")
}
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
fs.copy { action ->
commonCopyConfig(action)
action.include("gflags-${gflagsVersion.get()}/src/gflags_completions.h.in")
action.filesMatching("*/src/*") { matchedFile ->
matchedFile.filter { line -> line.replace(Regex("@GFLAGS_NAMESPACE@"), "gflags") }
matchedFile.path = "gflags/${matchedFile.name.removeSuffix(".in")}"
}
}
}
}
@@ -0,0 +1,209 @@
/*
* 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.internal
import com.facebook.react.tests.createProject
import com.facebook.react.tests.createTestTask
import java.io.*
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
class PrepareGflagsTaskTest {
@get:Rule val tempFolder = TemporaryFolder()
@Test(expected = IllegalStateException::class)
fun prepareGflagsTask_withMissingConfiguration_fails() {
val task = createTestTask<PrepareGflagsTask>()
task.taskAction()
}
@Test
fun prepareGflagsTask_copiesCMakefile() {
val gflagspath = tempFolder.newFolder("gflagspath")
val output = tempFolder.newFolder("output")
val project = createProject()
val gflagsThirdPartyPath = File(project.projectDir, "src/main/jni/third-party/gflags/")
val task =
createTestTask<PrepareGflagsTask>(project = project) {
it.gflagsPath.setFrom(gflagspath)
it.gflagsThirdPartyPath.set(gflagsThirdPartyPath)
it.gflagsVersion.set("1.0.0")
it.outputDir.set(output)
}
File(gflagsThirdPartyPath, "CMakeLists.txt").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(output.listFiles()!!.any { it.name == "CMakeLists.txt" }).isTrue()
}
@Test
fun prepareGflagsTask_copiesSourceCodeAndHeaders() {
val gflagspath = tempFolder.newFolder("gflagspath")
val gflagsThirdPartyPath = tempFolder.newFolder("gflagspath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGflagsTask> {
it.gflagsPath.setFrom(gflagspath)
it.gflagsThirdPartyPath.set(gflagsThirdPartyPath)
it.gflagsVersion.set("1.0.0")
it.outputDir.set(output)
}
File(gflagspath, "gflags-1.0.0/src/gflags.cc").apply {
parentFile.mkdirs()
createNewFile()
}
File(gflagspath, "gflags-1.0.0/src/util.h").apply {
parentFile.mkdirs()
createNewFile()
}
task.taskAction()
assertThat(File(output, "gflags/gflags.cc").exists()).isTrue()
assertThat(File(output, "gflags/util.h").exists()).isTrue()
}
@Test
fun prepareGflagsTask_replacesTokenCorrectly() {
val gflagspath = tempFolder.newFolder("gflagspath")
val gflagsThirdPartyPath = tempFolder.newFolder("gflagspath/jni")
val output = tempFolder.newFolder("output")
val task =
createTestTask<PrepareGflagsTask> {
it.gflagsPath.setFrom(gflagspath)
it.gflagsThirdPartyPath.set(gflagsThirdPartyPath)
it.gflagsVersion.set("1.0.0")
it.outputDir.set(output)
}
File(gflagspath, "gflags-1.0.0/src/gflags_declare.h.in").apply {
parentFile.mkdirs()
writeText(
"""
#define GFLAGS_NAMESPACE @GFLAGS_NAMESPACE@
#include <string>
#if @HAVE_STDINT_H@
# include <stdint.h>
#elif @HAVE_SYS_TYPES_H@
# include <sys/types.h>
#elif @HAVE_INTTYPES_H@
# include <inttypes.h>
#endif
namespace GFLAGS_NAMESPACE {
#if @GFLAGS_INTTYPES_FORMAT_C99@ // C99
typedef int32_t int32;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
#elif @GFLAGS_INTTYPES_FORMAT_BSD@ // BSD
typedef int32_t int32;
typedef u_int32_t uint32;
typedef int64_t int64;
typedef u_int64_t uint64;
#elif @GFLAGS_INTTYPES_FORMAT_VC7@ // Windows
typedef __int32 int32;
typedef unsigned __int32 uint32;
typedef __int64 int64;
typedef unsigned __int64 uint64;
#else
# error Do not know how to define a 32-bit integer quantity on your system
#endif
} // namespace GFLAGS_NAMESPACE
""")
}
File(gflagspath, "gflags-1.0.0/src/config.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("#cmakedefine")
}
File(gflagspath, "gflags-1.0.0/src/gflags_ns.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("@ns@ @NS@")
}
File(gflagspath, "gflags-1.0.0/src/gflags.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("@GFLAGS_ATTRIBUTE_UNUSED@\n@INCLUDE_GFLAGS_NS_H@")
}
File(gflagspath, "gflags-1.0.0/src/gflags_completions.h.in").apply {
parentFile.mkdirs()
createNewFile()
writeText("@GFLAGS_NAMESPACE@")
}
task.taskAction()
val declareFile = File(output, "gflags/gflags_declare.h")
assertThat(declareFile.exists()).isTrue()
assertEquals(
declareFile.readText(),
"""
#define GFLAGS_NAMESPACE gflags
#include <string>
#if 1
# include <stdint.h>
#elif 1
# include <sys/types.h>
#elif 1
# include <inttypes.h>
#endif
namespace GFLAGS_NAMESPACE {
#if 1 // C99
typedef int32_t int32;
typedef uint32_t uint32;
typedef int64_t int64;
typedef uint64_t uint64;
#elif 1 // BSD
typedef int32_t int32;
typedef u_int32_t uint32;
typedef int64_t int64;
typedef u_int64_t uint64;
#elif 1 // Windows
typedef __int32 int32;
typedef unsigned __int32 uint32;
typedef __int64 int64;
typedef unsigned __int64 uint64;
#else
# error Do not know how to define a 32-bit integer quantity on your system
#endif
} // namespace GFLAGS_NAMESPACE
""")
val configFile = File(output, "gflags/config.h")
assertThat(configFile.exists()).isTrue()
assertEquals(configFile.readText(), "//cmakedefine")
val nsFile = File(output, "gflags/gflags_google.h")
assertThat(nsFile.exists()).isTrue()
assertEquals(nsFile.readText(), "google GOOGLE")
val gflagsFile = File(output, "gflags/gflags.h")
assertThat(gflagsFile.exists()).isTrue()
assertEquals(gflagsFile.readText(), "\n#include \"gflags/gflags_google.h\"")
val completionsFile = File(output, "gflags/gflags_completions.h")
assertThat(completionsFile.exists()).isTrue()
assertEquals(completionsFile.readText(), "gflags")
}
}
@@ -46,6 +46,7 @@ fastFloat="8.0.0"
fmt="11.0.2"
folly="2024.11.18.00"
glog="0.3.5"
gflags="2.2.0"
[libraries]
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "androidx-annotation" }
@@ -0,0 +1,63 @@
/*
* 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.
*/
import com.android.build.gradle.internal.tasks.factory.dependsOn
import com.facebook.react.tasks.internal.*
import com.facebook.react.tasks.internal.utils.*
import de.undercouch.gradle.tasks.download.Download
plugins {
id("com.facebook.react")
alias(libs.plugins.download)
}
val GFLAGS_VERSION = libs.versions.gflags.get()
val buildDir = project.layout.buildDirectory.get().asFile
val downloadsDir =
if (System.getenv("REACT_NATIVE_DOWNLOADS_DIR") != null) {
File(System.getenv("REACT_NATIVE_DOWNLOADS_DIR"))
} else {
File("$buildDir/downloads")
}
val thirdParty = File("$buildDir/third-party")
val reactNativeRootDir = projectDir.parent
val createNativeDepsDirectories by
tasks.registering {
downloadsDir.mkdirs()
thirdParty.mkdirs()
}
val downloadGflagsDest = File(downloadsDir, "gflags-${GFLAGS_VERSION}.tar.gz")
val downloadGflags by
tasks.registering(Download::class) {
dependsOn(createNativeDepsDirectories)
src("https://github.com/gflags/gflags/archive/v${GFLAGS_VERSION}.tar.gz")
onlyIfModified(true)
overwrite(false)
retries(5)
quiet(true)
dest(downloadGflagsDest)
}
val prepareGflags by
tasks.registering(PrepareGflagsTask::class) {
dependsOn(listOf(downloadGflags))
gflagsPath.setFrom(tarTree(downloadGflagsDest))
gflagsThirdPartyPath.set(project.file("tester/third-party/gflags/"))
gflagsVersion.set(GFLAGS_VERSION)
outputDir.set(File(thirdParty, "gflags"))
}
// Tasks used by Fantom to download the Native 3p dependencies used.
val prepareNative3pDependencies by
tasks.registering {
dependsOn(
prepareGflags,
)
}
+2 -1
View File
@@ -7,5 +7,6 @@
set -e
pushd ../..
./gradlew prepareNative3pDependencies
./gradlew :packages:react-native:ReactAndroid:prepareNative3pDependencies
./gradlew :private:react-native-fantom:prepareNative3pDependencies
popd
@@ -26,6 +26,10 @@ function(add_react_common_subdir relative_path)
add_subdirectory(${REACT_COMMON_DIR}/${relative_path} src/${relative_path})
endfunction()
function(add_fantom_third_party_subdir relative_path)
add_subdirectory(${FANTOM_THIRD_PARTY_DIR}/${relative_path} ${relative_path})
endfunction()
# Third-party downloaded targets
add_react_third_party_ndk_subdir(glog)
# Boost in NDK is not compatible with desktop build
@@ -34,6 +38,7 @@ add_react_third_party_ndk_subdir(double-conversion)
add_react_third_party_ndk_subdir(fast_float)
add_react_third_party_ndk_subdir(fmt)
add_react_third_party_ndk_subdir(folly)
add_fantom_third_party_subdir(gflags)
# Common targets
add_react_common_subdir(yoga)
@@ -45,6 +50,7 @@ add_executable(fantom_tester ${SOURCES})
target_link_libraries(fantom_tester
PRIVATE
glog
gflags
boost
double-conversion
fast_float
+18 -2
View File
@@ -11,6 +11,7 @@ BUILD_DIR="$SCRIPT_DIR/build"
REACT_NATIVE_ROOT_DIR=$(readlink -f "$SCRIPT_DIR/../../../packages/react-native")
cmake -S "$SCRIPT_DIR" -B "$BUILD_DIR" \
-DFANTOM_THIRD_PARTY_DIR="${SCRIPT_DIR}/../build/third-party" \
-DREACT_THIRD_PARTY_NDK_DIR="${REACT_NATIVE_ROOT_DIR}/ReactAndroid/build/third-party-ndk" \
-DREACT_COMMON_DIR="${REACT_NATIVE_ROOT_DIR}/ReactCommon"
@@ -18,7 +19,22 @@ cmake --build "$BUILD_DIR" --target fantom_tester
while getopts ":r" opt; do
case $opt in
r) "$BUILD_DIR/fantom_tester" ;;
\?) echo "Invalid option: -$OPTARG"; exit 1;;
r) execute_tester=true ;;
\?) ;;
esac
done
for arg in "$@"; do
if [ "$arg" = "--featureFlags" ]; then
feature_flags="--featureFlags=${@:$OPTIND:1}"
break
fi
done
if [ "$execute_tester" = true ]; then
if [ -n "$feature_flags" ]; then
"$BUILD_DIR/fantom_tester" "$feature_flags"
else
"$BUILD_DIR/fantom_tester"
fi
fi
@@ -5,15 +5,23 @@
* LICENSE file in the root directory of this source tree.
*/
#include <folly/json.h>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h>
#include <yoga/YGEnums.h>
#include <yoga/YGValue.h>
#include <format>
#include <iostream>
#include <memory>
DEFINE_string(
featureFlags,
"",
"JSON representation of the common feature flags to set for the app");
using namespace facebook::react;
static void setUpLogging() {
@@ -21,25 +29,40 @@ static void setUpLogging() {
FLAGS_logtostderr = true;
}
static void setUpFeatureFlags() {
static folly::dynamic setUpFeatureFlags() {
folly::dynamic dynamicFeatureFlags = folly::dynamic::object();
dynamicFeatureFlags["enableBridgelessArchitecture"] = true;
dynamicFeatureFlags["cxxNativeAnimatedEnabled"] = true;
if (!FLAGS_featureFlags.empty()) {
dynamicFeatureFlags.update(folly::parseJson(FLAGS_featureFlags));
}
ReactNativeFeatureFlags::override(
std::make_unique<ReactNativeFeatureFlagsDynamicProvider>(
dynamicFeatureFlags));
return dynamicFeatureFlags;
}
int main() {
int main(int argc, char* argv[]) {
if (argc > 0 && argv != nullptr) {
// Don't exit app on unknown flags, as some of those may be provided when
// debugging via XCode:
gflags::AllowCommandLineReparsing();
gflags::ParseCommandLineFlags(&argc, &argv, false);
}
setUpLogging();
setUpFeatureFlags();
auto dynamicFeatureFlags = setUpFeatureFlags();
LOG(INFO) << "Hello, I am fantom_tester using glog!";
LOG(INFO) << std::format(
"[Yoga] undefined == zero: {}", YGValueZero == YGValueUndefined);
LOG(INFO) << fmt::format(
"[FeatureFlags] overrides: {}", folly::toJson(dynamicFeatureFlags));
return 0;
}
@@ -0,0 +1,31 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)
add_library(gflags STATIC
gflags/gflags.cc
gflags/gflags_completions.cc
gflags/gflags_reporting.cc)
target_include_directories(gflags PUBLIC .)
target_compile_options(gflags PRIVATE
-DHAVE_STDINT_H
-DHAVE_SYS_TYPES_H
-DHAVE_INTTYPES_H
-DHAVE_SYS_STAT_H
-DHAVE_UNISTD_H
-DHAVE_STRTOLL
-DHAVE_STRTOQ
-DHAVE_RWLOCK
-DGFLAGS_INTTYPES_FORMAT_C99
-DGFLAGS_IS_A_DLL=0
-DHAVE_FNMATCH_H
-DHAVE_PTHREAD
-lpthread)
+3 -1
View File
@@ -19,7 +19,9 @@ include(
":packages:react-native:ReactAndroid:hermes-engine",
":packages:react-native:ReactAndroid:external-artifacts",
":packages:rn-tester:android:app",
":packages:rn-tester:android:app:benchmark")
":packages:rn-tester:android:app:benchmark",
":private:react-native-fantom",
)
includeBuild("packages/gradle-plugin/")