blob: e5ea60947040c893944ef02b91fa67878dff3909 [file] [log] [blame]
#!/usr/bin/env vpython3
# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""
Add feature flags for Clank.
This script can be used to quickly add Chrome feature flags and export
them to the Java-side code for Clank. It also creates the necessary
code to add the flag to chrome://flags. It does NOT create a fieldtrial.
The script presents the user with a series of prompts to specify the
basic parts of a flag that are unique to each flag. It then uses the
standard naming conventions and file conventions (e.g. alphabetical
ordering) to add the new flag to the code.
Example (Basic):
# Run the script from the base directory with:
tools/flags/generate_clank_feature_flag.py
# You will be presented with a series of prompts. In order, the answers
# to the prompts may look something like:
1. MyNewFeatureFlag
2. My new feature
3. When enabled, this feature does ...
4. foo@google.com, bar@google.com
5. 157
The script will assume that "MyNewFeatureFlag" should be "kMyNewFeatureFlag"
in the code, with a string identifier as "MyNewFeatureFlag". For the
Java-side it will use "MY_NEW_FEATURE_FLAG" as the variable name. The
answers to the 2nd and 3rd prompts are what appear in chrome://flags. The
answers to the 4th and 5th prompts are what is used in flag-metadata.json.
Example (Advanced):
# Run the script from the base directory with arguments:
tools/flags/generate_clank_feature_flag.py --name "MyNewFeatureFlag" \
--display-name "Name for chrome://flags" \
--description "Description for chrome://flags" \
--owners "foo@google.com,bar@chromium.org" \
--milestone 145
This will set each of the necessary values as above, using the same assumptions
above variable naming etc. You do not need to include every argument. All of
these pieces of information are required for the script to add a complete flag,
so the user will be presented with a series of prompts to fill in any of the
remaining information not included in the arguments.
The script will also run: tools/metrics/histograms/generate_flag_enums.py
to generate the enums.xml values for the flag.
For a simple/standard flag (e.g. no caching, no FeatureParams, etc), you
should be able to raise a CL to add your flag once the script finishes.
If there are any bugs/issues, please file them in the Flags component:
Chromium > Internals > Flags (id: 1456387)
"""
import re
import json
import subprocess
import os
import sys
import argparse
# --- Helper Functions for Name Formatting ---
def to_java_upper_snake_case(name):
"""Converts 'MyNewFeatureFlagName' to 'MY_NEW_FEATURE_FLAG_NAME'."""
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).upper()
def to_cpp_lower_camel_case_k(name):
"""Converts 'MyNewFeatureFlagName' to 'kMyNewFeatureFlagName'."""
return 'k' + name[0].upper() + name[1:] if name else 'k'
def to_kebab_case(name):
"""Converts 'MyNewFeatureFlagName' to 'my-new-feature-flag-name'."""
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', name)
return re.sub('([a-z0-9])([A-Z])', r'\1-\2', s1).lower()
def format_flag_names(user_flag_name):
"""Generates all required name formats from the user's flag name."""
return {
"user_flag_name":
user_flag_name,
"java_constant_name":
to_java_upper_snake_case(user_flag_name),
"cpp_variable_name":
to_cpp_lower_camel_case_k(user_flag_name),
"cpp_lower_camel_case_name":
to_kebab_case(user_flag_name),
"flag_description_name":
to_cpp_lower_camel_case_k(user_flag_name) + "Name",
"flag_description_description":
to_cpp_lower_camel_case_k(user_flag_name) + "Description",
}
# --- File Paths ---
FILE_PATHS = {
"java_feature_list": ("chrome/browser/flags/android/java/src/org/chromium/"
"chrome/browser/flags/ChromeFeatureList.java"),
"cpp_feature_list_h":
"chrome/browser/flags/android/chrome_feature_list.h",
"cpp_feature_list_cc":
"chrome/browser/flags/android/chrome_feature_list.cc",
"about_flags_cc":
"chrome/browser/about_flags.cc",
"flag_descriptions_h":
"chrome/browser/flag_descriptions.h",
"flag_metadata_json":
"chrome/browser/flag-metadata.json",
}
# --- File Operations Helper Functions ---
def read_file(filepath):
"""Reads content from a file."""
if not os.path.exists(filepath):
print(f"Error: File not found at {filepath}")
return None
with open(filepath, 'r') as f:
return f.readlines()
def write_file(filepath, content_lines):
"""Writes a list of lines back to a file."""
with open(filepath, 'w') as f:
f.writelines(content_lines)
def insert_block_before_line(lines, new_block_lines, target_line_pattern):
"""Inserts a block of lines before a specific target line."""
for i, line in enumerate(lines):
if target_line_pattern in line:
return lines[:i] + new_block_lines + lines[i:]
print(f"Error: Target line pattern '{target_line_pattern}'"
" not found for insertion.")
return lines
def get_current_username():
"""Gets the current user's username and formats as an email."""
try:
username = subprocess.check_output("git config user.email",
shell=True).decode('utf-8').strip()
if not username:
print("Error: 'git config user.email' returned an empty value.",
file=sys.stderr)
sys.exit(1)
return username
except subprocess.CalledProcessError:
print(
"Error: Could not determine username. Please ensure"
"'git config user.email' is set.",
file=sys.stderr)
sys.exit(1)
def add_metadata_entry_textually(file_path, new_entry_dict):
"""
Updates flag-metadata.json by treating it as a text file to preserve
comments and formatting. It finds the correct alphabetical insertion
point and adds the new flag metadata block.
"""
if not os.path.exists(file_path):
print(f"Error: File not found at {file_path}. Cannot add metadata.")
return
file_lines = read_file(file_path)
if not file_lines:
print(f"Warning: {file_path} is empty. Cannot add metadata.")
return
new_flag_name = new_entry_dict["name"]
# --- Step 1: Find the insertion point ---
insertion_line_index = -1
last_name_line_index = -1
# Find the line index of the first "name" entry that should come after this.
for i, line in enumerate(file_lines):
# Regex to find a "name" entry and capture its value
match = re.search(r'^\s*"name":\s*"(?P<name>.*?)",?', line)
if match:
last_name_line_index = i
existing_flag_name = match.group("name")
if new_flag_name < existing_flag_name:
# We found the spot. Now, find the opening brace `{` of this block by
# searching backwards.
for j in range(i - 1, -1, -1):
if "{" in file_lines[j]:
insertion_line_index = j
break
if insertion_line_index != -1:
break # Exit the main loop once we've found our insertion point
# --- Step 2: Prepare the text block for the new entry ---
owners_json_string = json.dumps(new_entry_dict["owners"])
new_block_lines = [
' {\n',
f' "name": "{new_flag_name}",\n',
f' "owners": {owners_json_string},\n',
f' "expiry_milestone": {new_entry_dict["expiry_milestone"]}\n',
]
# --- Step 3: Insert the block and handle trailing commas ---
if insertion_line_index != -1:
# Case 1: Inserting in the middle of the list.
# The new block needs a trailing comma.
new_block_lines.append(' },\n')
# Insert the new block before the block of the next alphabetical feature.
file_lines = (file_lines[:insertion_line_index] + new_block_lines +
file_lines[insertion_line_index:])
else:
# Case 2: Appending to the end of the list.
# The new block does NOT get a trailing comma.
new_block_lines.append(' }\n')
# Find the closing brace of the *previous* last entry and add a comma to it.
if last_name_line_index != -1:
# Search forward from the last known "name" to find its closing brace `}`.
for i in range(last_name_line_index, len(file_lines)):
if "}" in file_lines[i]:
# Add a comma to that line.
file_lines[i] = file_lines[i].rstrip() + ',\n'
break
# Find the final closing bracket `]` of the whole list to insert before it.
closing_bracket_index = -1
for i in range(len(file_lines) - 1, -1, -1):
if "]" in file_lines[i]:
closing_bracket_index = i
break
if closing_bracket_index != -1:
file_lines = (file_lines[:closing_bracket_index] + new_block_lines +
file_lines[closing_bracket_index:])
else:
print(f"Error: Could not find closing bracket ']' in {file_path}."
"Appending to end of file as a fallback.")
file_lines.extend(new_block_lines)
write_file(file_path, file_lines)
# --- Main Script Logic ---
def add_chrome_android_feature_flag(user_provided_flag_name, display_name,
description, owners_list, expiry_milestone):
"""Performs all file modifications to add the new feature flag."""
print("\nStarting to add new feature flag to the codebase...")
names = format_flag_names(user_provided_flag_name)
java_constant_name = names["java_constant_name"]
cpp_variable_name = names["cpp_variable_name"]
cpp_lower_camel_case_name = names["cpp_lower_camel_case_name"]
flag_desc_name_var = names["flag_description_name"]
flag_desc_description_var = names["flag_description_description"]
# Step 3: Add Java-side functional component
print("\nStep 3: Adding Java-side functional code...")
java_file_lines = read_file(FILE_PATHS["java_feature_list"])
if java_file_lines is None: return
# The new flag definition will always be a single line.
# A code formatter can split it later if necessary.
new_java_line = (f' public static final String {java_constant_name}'
f' = "{user_provided_flag_name}";\n')
insert_idx = -1
last_flag_block_end_idx = -1
# Iterate through the file to find the correct alphabetical insertion point.
for i, line in enumerate(java_file_lines):
# We only care about the "anchor" lines that start a definition.
if line.strip().startswith("public static final String"):
# Extract the constant name from this anchor line.
match = re.search(r'public static final String ([A-Z_]+)', line)
if not match:
continue
existing_flag_constant_name = match.group(1)
# Check if our new flag comes before the current one.
if java_constant_name < existing_flag_constant_name:
# If it does, this is our insertion point. We insert *before*
# the anchor line of the entry that should come after ours.
insert_idx = i
break
else:
# If not, we keep track of the end of the current flag's
# definition block. This is in case our new flag needs to
# be added at the very end of the list.
# We scan forward from the current anchor line to find the ';'.
current_block_end = -1
for j in range(i, len(java_file_lines)):
if ";" in java_file_lines[j]:
current_block_end = j
break
if current_block_end != -1:
last_flag_block_end_idx = current_block_end
# Perform the insertion.
if insert_idx != -1:
# Case 1: Insert in the middle of the list.
java_file_lines.insert(insert_idx, new_java_line)
elif last_flag_block_end_idx != -1:
# Case 2: Append after the last known flag definition block.
java_file_lines.insert(last_flag_block_end_idx + 1, new_java_line)
else:
# Fallback case: No feature flags were found. We can't safely insert.
print(f"Warning: No existing feature flags found in",
f"{FILE_PATHS['java_feature_list']}. Could not add new flag.")
return # Exit the step to prevent incorrect file modification.
write_file(FILE_PATHS["java_feature_list"], java_file_lines)
print(f"Added Java constant: {java_constant_name} to ",
f"{FILE_PATHS['java_feature_list']}")
# Step 4: Add C++-side functional component
print("\nStep 4: Adding C++-side functional code...")
# .h file
h_file_lines = read_file(FILE_PATHS["cpp_feature_list_h"])
if h_file_lines is None: return
new_h_line = f'BASE_DECLARE_FEATURE({cpp_variable_name});\n'
h_insert_idx, last_h_decl_idx = -1, -1
for i, line in enumerate(h_file_lines):
match = re.search(r'BASE_DECLARE_FEATURE\((k[A-Za-z0-9]+)\);', line)
if match:
last_h_decl_idx = i
if cpp_variable_name < match.group(1):
h_insert_idx = i
break
h_file_lines.insert(
h_insert_idx if h_insert_idx != -1 else last_h_decl_idx + 1, new_h_line)
write_file(FILE_PATHS["cpp_feature_list_h"], h_file_lines)
print(f"Added C++ declaration to {FILE_PATHS['cpp_feature_list_h']}")
# .cc file
cc_file_lines = read_file(FILE_PATHS["cpp_feature_list_cc"])
if cc_file_lines is None: return
# Determine what to insert based on the context.
lines_to_insert = [
f'BASE_FEATURE({cpp_variable_name}, base::FEATURE_DISABLED_BY_DEFAULT);\n'
]
insertion_anchor_idx = -1
prev_feature_idx = -1
last_feature_idx = -1
# Find the insertion anchor (the next feature) and track the
# previous/last feature.
for i, line in enumerate(cc_file_lines):
if line.strip().startswith("BASE_FEATURE"):
match = re.search(r'BASE_FEATURE\((k[A-Za-z0-9]+),', line)
if not match:
continue
last_feature_idx = i
existing_cpp_var_name = match.group(1)
if cpp_variable_name < existing_cpp_var_name:
insertion_anchor_idx = i
break
else:
prev_feature_idx = i
# --- Case 1: Inserting in the middle of the list ---
if insertion_anchor_idx != -1:
# Find the true start of the next block to insert before.
true_insertion_point = insertion_anchor_idx
for j in range(insertion_anchor_idx - 1, -1, -1):
stripped_line = cc_file_lines[j].strip()
if stripped_line.startswith('//') or not stripped_line:
true_insertion_point = j
else:
break
cc_file_lines = (cc_file_lines[:true_insertion_point] + lines_to_insert +
cc_file_lines[true_insertion_point:])
# --- Case 2: Appending to the end of the list ---
else:
if last_feature_idx > 0 and cc_file_lines[last_feature_idx -
1].strip().startswith('//'):
# If the last feature in the file was commented, add a preceding newline.
lines_to_insert.insert(0, '\n')
# Find the closing brace of the last feature block to insert after.
last_block_end_idx = -1
if last_feature_idx != -1:
for i in range(last_feature_idx, len(cc_file_lines)):
if ');' in cc_file_lines[i]:
last_block_end_idx = i
break
if last_block_end_idx != -1:
cc_file_lines = (cc_file_lines[:last_block_end_idx + 1] +
lines_to_insert + cc_file_lines[last_block_end_idx + 1:])
else:
print(f"Warning: No BASE_FEATUREs found in "
f"{FILE_PATHS['cpp_feature_list_cc']}. Appending to end of file.")
cc_file_lines.extend(lines_to_insert)
write_file(FILE_PATHS["cpp_feature_list_cc"], cc_file_lines)
print(f"Added C++ feature definition to {FILE_PATHS['cpp_feature_list_cc']}")
# Add to kFeaturesExposedToJava
start_pattern = "FEATURE_EXPORT_LIST_START"
end_marker = "FEATURE_EXPORT_LIST_END"
start_idx = next(
(i for i, line in enumerate(cc_file_lines) if start_pattern in line), -1)
if start_idx != -1:
end_idx = next((i for i, line in enumerate(cc_file_lines)
if end_marker in line and i > start_idx), -1)
if end_idx != -1:
insertion_point, last_feature_line = -1, -1
for i in range(start_idx + 1, end_idx):
match = re.search(r'&\s*(k[A-Za-z0-9]+)', cc_file_lines[i])
if match:
last_feature_line = i
if cpp_variable_name < match.group(1):
insertion_point = i
break
new_exposed_line = f' &{cpp_variable_name},\n'
cc_file_lines.insert(
insertion_point if insertion_point != -1 else last_feature_line + 1,
new_exposed_line)
write_file(FILE_PATHS["cpp_feature_list_cc"], cc_file_lines)
print(
f"Added C++ feature and exposed it in {FILE_PATHS['cpp_feature_list_cc']}"
)
# Step 5: Add about_flags portion
print("\nStep 5: Adding about_flags entry...")
about_flags_lines = read_file(FILE_PATHS["about_flags_cc"])
if about_flags_lines is None: return
new_about_flags_entry = [
'#if BUILDFLAG(IS_ANDROID)\n', f' {{"{cpp_lower_camel_case_name}",\n',
f' flag_descriptions::{flag_desc_name_var},\n',
f' flag_descriptions::{flag_desc_description_var}, kOsAndroid,\n',
f' FEATURE_VALUE_TYPE(chrome::android::{cpp_variable_name})}},\n',
'#endif\n'
]
about_flags_lines = insert_block_before_line(
about_flags_lines, new_about_flags_entry,
"// Add new entries above this line.")
write_file(FILE_PATHS["about_flags_cc"], about_flags_lines)
print(f"Added about_flags entry for {cpp_lower_camel_case_name} to "
f"{FILE_PATHS['about_flags_cc']}")
# Step 6: Add flag_descriptions portion
print("\nStep 6: Adding flag_descriptions...")
# --- Modify flag_descriptions.h ---
flag_desc_cc_lines = read_file(FILE_PATHS["flag_descriptions_h"])
if flag_desc_cc_lines is None: return
cc_start_marker = "FLAG_DESCRIPTIONS_ANDROID_START"
cc_end_marker = "FLAG_DESCRIPTIONS_ANDROID_END"
cc_start_idx = next(
(i
for i, line in enumerate(flag_desc_cc_lines) if cc_start_marker in line),
-1)
cc_end_idx = next(
(i for i, line in enumerate(flag_desc_cc_lines) if cc_end_marker in line),
-1)
if cc_start_idx != -1 and cc_end_idx != -1:
cc_insertion_point = -1
# Search within the block for the alphabetical insertion point.
for i in range(cc_start_idx + 1, cc_end_idx):
line = flag_desc_cc_lines[i]
match = re.search(r'inline constexpr char (k[A-Za-z0-9]+Name)\[\]', line)
if match:
existing_name_var = match.group(1)
if flag_desc_name_var < existing_name_var:
# Found the insertion point. We need to find the start of this entire
# block to ensure we insert before it.
true_insertion_point = i
# Scan backwards to find the start of the block (the "Name" line).
for j in range(i - 1, cc_start_idx, -1):
if 'inline constexpr char' in flag_desc_cc_lines[
j] and 'Name[]' in flag_desc_cc_lines[j]:
true_insertion_point = j
elif flag_desc_cc_lines[j].strip(
) == '': # Stop at the blank line of the previous entry
break
cc_insertion_point = true_insertion_point
break
new_cc_pair = [
f'inline constexpr char {flag_desc_name_var}[] = "{display_name}";\n',
f'inline constexpr char {flag_desc_description_var}[] =\n',
f' "{description}";\n', '\n'
]
# If no later entry was found, insert before the end marker. Otherwise,
# insert at the found point.
final_cc_insertion_idx = (cc_end_idx if cc_insertion_point == -1 else
cc_insertion_point)
flag_desc_cc_lines = (flag_desc_cc_lines[:final_cc_insertion_idx] +
new_cc_pair +
flag_desc_cc_lines[final_cc_insertion_idx:])
write_file(FILE_PATHS["flag_descriptions_h"], flag_desc_cc_lines)
print(f"Added definitions to {FILE_PATHS['flag_descriptions_h']}")
else:
print(f"Error: Could not find START/END markers in"
f"{FILE_PATHS['flag_descriptions_h']}")
# Step 7: Add flag-metadata portion
print("\nStep 7: Adding flag-metadata entry...")
new_metadata_entry = {
"name": cpp_lower_camel_case_name,
"owners": owners_list,
"expiry_milestone": expiry_milestone
}
add_metadata_entry_textually(FILE_PATHS["flag_metadata_json"],
new_metadata_entry)
print(f"Added metadata for {cpp_lower_camel_case_name} to "
f"{FILE_PATHS['flag_metadata_json']}")
# Step 12: Run generate_flag_enums.py
print(
"\nStep 12: Running tools/metrics/histograms/generate_flag_enums.py ...")
try:
subprocess.run([
"python3", "tools/metrics/histograms/generate_flag_enums.py",
"--feature", user_provided_flag_name
],
check=True,
capture_output=True,
text=True)
print("Successfully ran generate_flag_enums.py.")
except (subprocess.CalledProcessError, FileNotFoundError) as e:
print(f"Error running generate_flag_enums.py: {e}")
# Step 13: Give confirmation
print(
"\n\nFeature flag addition process complete. Remember to check the diff!")
def parse_arguments():
"""Parses command line arguments."""
parser = argparse.ArgumentParser(
description="New Chrome Android Feature Flag Creator. "
"If arguments are not provided, the script will prompt interactively.",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument(
'--name',
type=str,
help='Internal feature flag name (e.g., MyNewFeatureFlag).')
parser.add_argument('--display-name',
type=str,
help='User-facing name for chrome://flags.')
parser.add_argument('--description',
type=str,
help='Description for chrome://flags.')
parser.add_argument(
'--owners',
type=str,
help=('Additional owners as a comma-separated list of emails '
'(e.g., "user1@a.com,user2@b.com").'))
parser.add_argument(
'--milestone',
type=int,
help='The expiry milestone as a positive whole number (e.g., 152).')
return parser.parse_args()
def main():
# Parse command-line arguments first
args = parse_arguments()
print("--- New Chrome Android Feature Flag Creator ---")
print("Please answer the following questions to generate the new flag.")
# 1. Get the internal flag name
user_provided_flag_name = args.name
while not user_provided_flag_name:
user_provided_flag_name = input(
"\nEnter the internal feature flag name (e.g., MyNewFeatureFlag): "
).strip()
if not user_provided_flag_name:
print("The internal name cannot be empty.")
# 2. Get the user-facing name
display_name = args.display_name
default_display_name = re.sub(r'([a-z])([A-Z])', r'\1 \2',
user_provided_flag_name)
if not display_name:
display_name = input(
f"Enter the user-facing name for chrome://flags "
f"(press Enter for default: '{default_display_name}'): ").strip()
if not display_name:
display_name = default_display_name
# 3. Get the description
description = args.description
default_description = f"Enables the {display_name} feature."
if not description:
description = input(
f"Enter the description for chrome://flags "
f"(press Enter for default: '{default_description}'): ").strip()
if not description:
description = default_description
# 4. Get owners
current_user = get_current_username()
additional_owners_input = args.owners
if not additional_owners_input:
print(f"\nYou ({current_user}) will be added as an owner automatically.")
additional_owners_input = input(
"Enter any additional owners as a comma-separated list of emails "
"(or press Enter to skip): ").strip()
additional_owners = [
owner.strip() for owner in additional_owners_input.split(',')
if owner.strip()
]
# Remove duplicates while preserving the order of the first occurrence.
all_owners_with_potential_dupes = [current_user] + additional_owners
seen = set()
all_owners = []
for owner in all_owners_with_potential_dupes:
if owner not in seen:
all_owners.append(owner)
seen.add(owner)
# 5. Get expiry milestone
expiry_milestone = args.milestone
# Ensure milestone is a positive integer
while not isinstance(expiry_milestone, int) or expiry_milestone <= 0:
milestone_input = input(
"Enter the expiry milestone as a number (e.g., 152): ").strip()
if milestone_input.isdigit():
expiry_milestone = int(milestone_input)
else:
print("Invalid input. Please enter a positive whole number.")
print("\nThank you. Using the following information:")
print(f" - Internal Name: {user_provided_flag_name}")
print(f" - Display Name: {display_name}")
print(f" - Description: {description}")
print(f" - Owners: {all_owners}")
print(f" - Milestone: {expiry_milestone}")
add_chrome_android_feature_flag(user_provided_flag_name, display_name,
description, all_owners, expiry_milestone)
if __name__ == "__main__":
sys.exit(main())