Skip to content

Commit cb78dbc

Browse files
motiz88facebook-github-bot
authored andcommitted
Add Buck CLI override for feature flag defaults
Summary: Adds a mechanism to override React Native feature flag default values via a Buck command line argument, without modifying source files. A `genrule` in the featureflags BUCK target always interposes on `ReactNativeFeatureFlagsDefaults.h` before compilation. When `react_native.feature_flag_defaults` is set to a JSON object via `--config`, a Python script rewrites the return values in the matching method bodies. When unset, the header passes through unmodified. The Python script matches each override against the full method signature shape (`<returnType> <flagName>() override { ... return <value>; }`) with lenient whitespace, and fails the build if any requested flag name is not found in the header. Usage: ``` buck2 build --config 'react_native.feature_flag_defaults={"enableViewCulling":true}' //target buck2 build --config 'react_native.feature_flag_defaults={"enableViewCulling":true,"preparedTextCacheSize":500}' //target ``` This modifies defaults only — app-level providers still take priority. Changelog: [Internal] Reviewed By: robhogan Differential Revision: D101484355
1 parent 46c0177 commit cb78dbc

2 files changed

Lines changed: 139 additions & 0 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env fbpython
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+
# pyre-strict
8+
9+
"""Rewrite default return values in ReactNativeFeatureFlagsDefaults.h.
10+
11+
Reads the header from --input, writes the transformed header to stdout.
12+
Overrides are passed as a JSON object via --overrides.
13+
Fails with a non-zero exit code if any requested flag is not found.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import argparse
19+
import json
20+
import re
21+
import sys
22+
23+
24+
def cxx_literal(value: object) -> str:
25+
if isinstance(value, bool):
26+
return "true" if value else "false"
27+
if isinstance(value, (int, float)):
28+
s = str(value)
29+
if isinstance(value, int) or "." not in s:
30+
s += ".0"
31+
return s
32+
raise ValueError(f"Unsupported value type {type(value).__name__} for override")
33+
34+
35+
def rewrite(source: bytes, overrides: dict[str, object]) -> bytes:
36+
text = source.decode("utf-8")
37+
for name, value in overrides.items():
38+
cxx_type = "bool" if isinstance(value, bool) else "double"
39+
pattern = rf"""
40+
( # group 1: everything up to the value
41+
{cxx_type} \s+ # return type
42+
{re.escape(name)} # method name
43+
\s* \( \s* \) # parameter list
44+
\s+ override # override specifier
45+
\s* \{{ # opening brace
46+
[^}}]*? # body before the return (non-greedy, no nested braces)
47+
return \s+ # return keyword
48+
)
49+
[^;]+ # the value to replace
50+
( \s* ; ) # group 2: semicolon
51+
"""
52+
text, n = re.subn(
53+
pattern,
54+
rf"\g<1>{cxx_literal(value)}\2",
55+
text,
56+
count=1,
57+
flags=re.DOTALL | re.VERBOSE,
58+
)
59+
if n != 1:
60+
raise ValueError(f"{name} not matched")
61+
62+
return text.encode("utf-8")
63+
64+
65+
def main() -> None:
66+
parser = argparse.ArgumentParser()
67+
parser.add_argument("--overrides", default="{}")
68+
parser.add_argument("--input", required=True)
69+
args = parser.parse_args()
70+
71+
overrides: dict[str, object] = json.loads(args.overrides)
72+
with open(args.input, "rb") as f:
73+
source = f.read()
74+
75+
sys.stdout.buffer.write(rewrite(source, overrides))
76+
77+
78+
if __name__ == "__main__":
79+
main()
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
# pyre-strict
7+
8+
from __future__ import annotations
9+
10+
import os
11+
import unittest
12+
13+
from rewrite_feature_flag_defaults import cxx_literal, rewrite
14+
15+
16+
def _load_header() -> bytes:
17+
with open(os.environ["HEADER_PATH"], "rb") as f:
18+
return f.read()
19+
20+
21+
class RewriteFeatureFlagDefaultsTest(unittest.TestCase):
22+
def setUp(self) -> None:
23+
self.source = _load_header()
24+
25+
def test_empty_overrides_is_passthrough(self) -> None:
26+
self.assertEqual(rewrite(self.source, {}), self.source)
27+
28+
def test_override_bool_to_true(self) -> None:
29+
result = rewrite(self.source, {"commonTestFlag": True})
30+
start, end = self._method_body_range(result, "commonTestFlag")
31+
self.assertIn(b"return true;", result[start:end])
32+
33+
def test_override_bool_to_false(self) -> None:
34+
result = rewrite(self.source, {"commonTestFlag": False})
35+
start, end = self._method_body_range(result, "commonTestFlag")
36+
self.assertIn(b"return false;", result[start:end])
37+
38+
def test_cxx_literal_int_produces_double(self) -> None:
39+
self.assertEqual(cxx_literal(42), "42.0")
40+
41+
def test_cxx_literal_float(self) -> None:
42+
self.assertEqual(cxx_literal(3.14), "3.14")
43+
44+
def test_unmatched_flag_raises(self) -> None:
45+
with self.assertRaises(ValueError):
46+
rewrite(self.source, {"bogusFlag": True})
47+
48+
def test_only_target_method_body_changes(self) -> None:
49+
result = rewrite(self.source, {"commonTestFlag": True})
50+
src_start, src_end = self._method_body_range(self.source, "commonTestFlag")
51+
res_start, res_end = self._method_body_range(result, "commonTestFlag")
52+
self.assertEqual(self.source[:src_start], result[:res_start])
53+
self.assertEqual(self.source[src_end:], result[res_end:])
54+
55+
def _method_body_range(self, source: bytes, name: str) -> tuple[int, int]:
56+
idx = source.find(name.encode())
57+
self.assertNotEqual(idx, -1, f"{name} not found in output")
58+
open_brace = source.find(b"{", idx)
59+
close_brace = source.find(b"}", open_brace)
60+
return (open_brace, close_brace + 1)

0 commit comments

Comments
 (0)