From 44ade83a6e5293eca508ca31de629adbcdbc5433 Mon Sep 17 00:00:00 2001 From: Andrew Datsenko Date: Mon, 16 Jun 2025 11:17:28 -0700 Subject: [PATCH] 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 --- .../react/tasks/internal/PrepareGflagsTask.kt | 116 ++++++++++ .../tasks/internal/PrepareGflagsTaskTest.kt | 209 ++++++++++++++++++ .../react-native/gradle/libs.versions.toml | 1 + private/react-native-fantom/build.gradle.kts | 63 ++++++ private/react-native-fantom/download.sh | 3 +- .../react-native-fantom/tester/CMakeLists.txt | 6 + private/react-native-fantom/tester/build.sh | 20 +- .../react-native-fantom/tester/src/main.cpp | 29 ++- .../tester/third-party/gflags/CMakeLists.txt | 31 +++ settings.gradle.kts | 4 +- 10 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTask.kt create mode 100644 packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTaskTest.kt create mode 100644 private/react-native-fantom/build.gradle.kts create mode 100644 private/react-native-fantom/tester/third-party/gflags/CMakeLists.txt diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTask.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTask.kt new file mode 100644 index 000000000000..7fca69edd17f --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTask.kt @@ -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 + + @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")}" + } + } + } +} diff --git a/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTaskTest.kt b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTaskTest.kt new file mode 100644 index 000000000000..a8194320b162 --- /dev/null +++ b/packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/tasks/internal/PrepareGflagsTaskTest.kt @@ -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() + + 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(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 { + 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 { + 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 +#if @HAVE_STDINT_H@ +# include +#elif @HAVE_SYS_TYPES_H@ +# include +#elif @HAVE_INTTYPES_H@ +# include +#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 +#if 1 +# include +#elif 1 +# include +#elif 1 +# include +#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") + } +} diff --git a/packages/react-native/gradle/libs.versions.toml b/packages/react-native/gradle/libs.versions.toml index 29b5d90ff448..dfd6880b6c70 100644 --- a/packages/react-native/gradle/libs.versions.toml +++ b/packages/react-native/gradle/libs.versions.toml @@ -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" } diff --git a/private/react-native-fantom/build.gradle.kts b/private/react-native-fantom/build.gradle.kts new file mode 100644 index 000000000000..35c6daf1ed55 --- /dev/null +++ b/private/react-native-fantom/build.gradle.kts @@ -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, + ) + } diff --git a/private/react-native-fantom/download.sh b/private/react-native-fantom/download.sh index 1b3fb054b224..1671f783f3b2 100755 --- a/private/react-native-fantom/download.sh +++ b/private/react-native-fantom/download.sh @@ -7,5 +7,6 @@ set -e pushd ../.. -./gradlew prepareNative3pDependencies +./gradlew :packages:react-native:ReactAndroid:prepareNative3pDependencies +./gradlew :private:react-native-fantom:prepareNative3pDependencies popd diff --git a/private/react-native-fantom/tester/CMakeLists.txt b/private/react-native-fantom/tester/CMakeLists.txt index b3548f745754..1845870fa622 100644 --- a/private/react-native-fantom/tester/CMakeLists.txt +++ b/private/react-native-fantom/tester/CMakeLists.txt @@ -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 diff --git a/private/react-native-fantom/tester/build.sh b/private/react-native-fantom/tester/build.sh index a6107f3ed489..7e68d7f3453e 100755 --- a/private/react-native-fantom/tester/build.sh +++ b/private/react-native-fantom/tester/build.sh @@ -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 diff --git a/private/react-native-fantom/tester/src/main.cpp b/private/react-native-fantom/tester/src/main.cpp index 7d6ac4934b0e..ac5dcba560f6 100644 --- a/private/react-native-fantom/tester/src/main.cpp +++ b/private/react-native-fantom/tester/src/main.cpp @@ -5,15 +5,23 @@ * LICENSE file in the root directory of this source tree. */ +#include +#include #include #include #include #include #include + #include #include #include +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( 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; } diff --git a/private/react-native-fantom/tester/third-party/gflags/CMakeLists.txt b/private/react-native-fantom/tester/third-party/gflags/CMakeLists.txt new file mode 100644 index 000000000000..240ba1467214 --- /dev/null +++ b/private/react-native-fantom/tester/third-party/gflags/CMakeLists.txt @@ -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) diff --git a/settings.gradle.kts b/settings.gradle.kts index ce06b908ac73..a30c2d9e1d67 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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/")