blob: 066f24290693cbed11b50479a3a7cb97e496ec11 [file] [edit]
# Copyright 2025 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Common utilities for converting CMakeLists.txt to BUILD.bazel."""
from pathlib import Path
import re
from typing import Any, Callable
def translate_relative_path(
absolute_path: Path, start_dir: Path, target_dir: Path
) -> Path:
"""Translates an absolute_path to a target directory.
For example, start_dir / some / file -> target_dir / some / file.
"""
if not absolute_path.is_relative_to(start_dir):
raise ValueError(f'{absolute_path} is not inside {start_dir}')
relative_path = absolute_path.relative_to(start_dir)
return target_dir / relative_path
def process_if_block_start(line: str) -> dict[str, Any]:
"""Processes the start of a CMake if block."""
condition = re.match(r'^if\s*\((.*)\)', line).group(1).strip()
if_block = {
'type': 'if_block',
'branches': [{'condition': condition, 'body': []}],
'else_body': None,
}
return if_block
def process_if_block_branch(line: str) -> dict[str, Any]:
"""Processes a CMake elseif branch."""
condition = re.match(r'^elseif\s*\((.*)\)', line).group(1).strip()
if_block_branch = {'condition': condition, 'body': []}
return if_block_branch
def merge_bracketed_string(input_text: str) -> str:
"""Merges a CMake statement spanning multiple lines into one line."""
lines = input_text.splitlines()
output_lines = []
buffer = ''
inside_bracket = False
for line in lines:
comment_start_index = line.find('#')
if comment_start_index != -1:
line = line[:comment_start_index]
stripped_line = line.strip()
if not buffer:
buffer = stripped_line
else:
if inside_bracket:
if buffer.endswith('(') or stripped_line.startswith(')'):
buffer += stripped_line
else:
buffer += ' ' + stripped_line
else:
output_lines.append(buffer)
buffer = stripped_line
if '(' in line:
inside_bracket = True
if ')' in line:
inside_bracket = False
if buffer:
output_lines.append(buffer)
return '\n'.join(output_lines)
def _process_zephyr_cmake_functions(
line: str,
) -> dict[str, str | list[str]] | None:
if line.startswith('zephyr_library_sources('):
srcs = (
re.search(r'zephyr_library_sources\((.*)\)', line).group(1).strip()
)
if srcs == 'uart_native_pty.c':
srcs += ' uart_native_pty_bottom.c'
if srcs == 'uart_native_tty.c':
srcs += ' uart_native_tty_bottom.c'
return {'type': 'unconditional_source', 'srcs': srcs.split()}
elif line.startswith('zephyr_library_sources_ifdef('):
config, srcs = re.search(
r'zephyr_library_sources_ifdef\((\S+)\s+(.*)\)', line
).groups()
if srcs == 'uart_native_pty.c':
srcs += ' uart_native_pty_bottom.c'
if srcs == 'uart_native_tty.c':
srcs += ' uart_native_tty_bottom.c'
return {
'type': 'conditional_source',
'config': config.strip(),
'srcs': srcs.strip().split(),
}
elif line.startswith('zephyr_library_sources_ifndef('):
config, srcs = re.search(
r'zephyr_library_sources_ifndef\((\S+)\s+(.*)\)', line
).groups()
return {
'type': 'conditional_source_inverse',
'config': config.strip(),
'srcs': srcs.strip().split(),
}
elif line.startswith('zephyr_sources('):
srcs = re.search(r'zephyr_sources\((.*)\)', line).group(1).strip()
return {'type': 'unconditional_source', 'srcs': srcs.split()}
elif line.startswith('zephyr_sources_ifdef('):
config, srcs = re.search(
r'zephyr_sources_ifdef\((\S+)\s+(.*)\)', line
).groups()
return {
'type': 'conditional_source',
'config': config.strip(),
'srcs': srcs.strip().split(),
}
elif line.startswith('zephyr_sources_ifndef('):
config, srcs = re.search(
r'zephyr_sources_ifndef\((\S+)\s+(.*)\)', line
).groups()
return {
'type': 'conditional_source_inverse',
'config': config.strip(),
'srcs': srcs.strip().split(),
}
elif line.startswith('zephyr_syscall_header('):
syscall_client_files = (
re.search(r'zephyr_syscall_header\((.*)\)', line).group(1).strip()
)
return {
'type': 'unconditional_syscall_client_files',
'files': syscall_client_files.split(),
}
elif line.startswith('zephyr_syscall_header_ifdef('):
config, syscall_client_files = re.search(
r'zephyr_syscall_header_ifdef\((\S+)\s+(.*)\)', line
).groups()
return {
'type': 'conditional_syscall_client_files',
'config': config.strip(),
'files': syscall_client_files.strip().split(),
}
elif line.startswith('add_subdirectory('):
subdir_name = re.search(r'add_subdirectory\((\S+)\)', line).group(1)
return {
'type': 'unconditional_subdir',
'subdir_name': subdir_name,
}
elif line.startswith('add_subdirectory_ifdef('):
config, subdir_name = re.search(
r'add_subdirectory_ifdef\((\S+)\s+(\S+)\)', line
).groups()
if subdir_name.startswith('./'):
subdir_name = subdir_name.removeprefix('./')
return {
'type': 'conditional_subdir',
'config': config,
'subdir_name': subdir_name,
}
elif line.startswith('zephyr_linker_sources('):
# zephyr_linker_sources() supports two usages:
# 1. zephyr_linker_sources(<location> <files>)
# 2. zephyr_linker_sources(<location> SORT_KEY <key> <files>)
#
# Regex breakdown:
# 1. zephyr_linker_sources\(\s* -> Match function name and opening paren
# 2. (?P<location>\S+) -> Capture first arg (location)
# 3. \s+ -> Require whitespace separator
# 4. (?:SORT_KEY\s+(?P<key>\S+)\s+)? -> Optional group: Literal "SORT_KEY", space, capture <key>, space
# 5. (?P<files>.+?) -> Capture a list of files
# 6. \s*\) -> Match trailing whitespace and closing paren
pattern = (
r'zephyr_linker_sources\(\s*'
r'(?P<location>\S+)\s+' # Capture location
r'(?:SORT_KEY\s+(?P<key>\S+)\s+)?' # Optional SORT_KEY group
r'(?P<files>.+?)\s*\)' # Capture files list
)
match = re.search(pattern, line)
if match:
location = match.group('location')
files_list = match.group('files').split()
# If 'key' was not found in the optional group, default it to 'default'
sort_key = match.group('key') if match.group('key') else 'default'
return {
'type': 'linker_fragment',
'location': location,
'sort_key': sort_key,
'files': files_list,
}
print(f'Failed to parse "{line}", skipping...')
return None
elif line.startswith('zephyr_linker_sources_ifdef('):
# zephyr_linker_sources_ifdef() supports two usages:
# 1. zephyr_linker_sources_ifdef(<condition> <location> <files>)
# 2. zephyr_linker_sources_ifdef(<condition> <location> SORT_KEY <key> <files>)
pattern = (
r'zephyr_linker_sources_ifdef\(\s*'
r'(?P<condition>\S+)\s+' # Capture condition
r'(?P<location>\S+)\s+' # Capture location
r'(?:SORT_KEY\s+(?P<key>\S+)\s+)?' # Optional SORT_KEY group
r'(?P<files>.+?)\s*\)' # Capture files list
)
match = re.search(pattern, line)
if match:
return {
'type': 'conditional_linker_fragment',
'condition': match.group('condition'),
'location': match.group('location'),
# Default to "default" if key is missing
'sort_key': (
match.group('key') if match.group('key') else 'default'
),
'files': match.group('files').split(),
}
print(f'Failed to parse "{line}", skipping...')
return None
else:
return None
def build_ast(
lines: list[str],
) -> list[dict[str, Any]]:
"""Builds an Abstract Syntax Tree from CMakeLists.txt lines."""
ast = []
stack = [ast]
for line in lines:
line = line.strip()
if re.match(r'^if\s*\(', line):
if_block = process_if_block_start(line)
stack[-1].append(if_block)
stack.append(if_block)
stack.append(if_block['branches'][0]['body'])
continue
elif re.match(r'^elseif\s*\(', line):
stack.pop()
if_block = stack[-1]
if if_block['type'] != 'if_block':
raise ValueError('elseif without a preceding if')
if_block_branch = process_if_block_branch(line)
if_block['branches'].append(if_block_branch)
stack.append(if_block_branch['body'])
continue
elif re.match(r'^else\s*\(', line):
stack.pop()
if_block = stack[-1]
if if_block['type'] != 'if_block':
raise ValueError('else without a preceding if')
if_block['else_body'] = []
stack.append(if_block['else_body'])
continue
elif re.match(r'^endif\s*\(', line):
stack.pop()
if stack and stack[-1]['type'] == 'if_block':
stack.pop()
continue
source_file_item = _process_zephyr_cmake_functions(line)
if source_file_item:
node_body = stack[-1]
node_body.append(source_file_item)
return ast
# Int type Kconfig symbols that we should take care of in converting
# CMakeLists.txt. An ifdef() on these symbols means the source is added only if
# the int flag value is nonzero.
_INT_CONFIGS_TO_PROCESS = [
'CONFIG_SYS_HEAP_ARRAY_SIZE',
]
def _get_unique_target_name(
start_name: str, names_list: list[str]
) -> tuple[str, str]:
"""Generates a unique target name not already in names_list."""
target_name = start_name + '.target'
target_name_bazel_str = f'":{target_name}"'
while target_name_bazel_str in names_list:
# Name collision. Modify the target name.
target_name += '#'
target_name_bazel_str = f'":{target_name}"'
return target_name, target_name_bazel_str
def traverse_ast(
ast: list[dict[str, Any]],
target_name: str,
update_rules_fn: Callable[[list[str], ...], list[str]],
is_root=False,
# Ignore linker fragments by default.
ignore_node_types=['linker_fragment', 'conditional_linker_fragment'],
) -> list[str]:
"""Recursively generates Bazel rules from the AST."""
rules = []
unconditional_srcs = []
toplevel_conditional_srcs = []
unconditional_deps = []
deps_selects = []
# Process all nodes at the current level
for node in ast:
if node['type'] in ignore_node_types:
continue
if node['type'] == 'unconditional_source':
for src in node['srcs']:
unconditional_srcs.append(f' "{src}",')
elif node['type'] == 'conditional_source':
if node['config'] in _INT_CONFIGS_TO_PROCESS:
# For int symbols in the conditional case, add source if the
# value is NON-ZERO.
toplevel_conditional_srcs.append(
'\n'.join([
' + select({',
f' "@zephyr_kconfig//:{node["config"]}=0": [],',
' "//conditions:default": [',
*[f' "{src}",' for src in node['srcs']],
' ],',
' })',
])
)
else:
toplevel_conditional_srcs.append(
'\n'.join([
' + select({',
' "//conditions:default": [],',
f' "@zephyr_kconfig//:{node["config"]}=true": [',
*[f' "{src}",' for src in node['srcs']],
' ],',
' })',
])
)
elif node['type'] == 'conditional_source_inverse':
toplevel_conditional_srcs.append(
'\n'.join([
' + select({',
f' "@zephyr_kconfig//:{node["config"]}=true": [],',
' "//conditions:default": [',
*[f' "{src}",' for src in node['srcs']],
' ],',
' })',
])
)
elif node['type'] == 'unconditional_subdir':
unconditional_deps.append(
f'"//" + package_name() + "/{node["subdir_name"]}"'
)
elif node['type'] == 'conditional_subdir':
conditional_subdir_target_name = (
f'"//" + package_name() + "/{node["subdir_name"]}"'
)
select_str = '\n'.join([
' + select({',
f' "@zephyr_kconfig//:{node["config"]}=true": [',
f' {conditional_subdir_target_name},',
' ],',
' "//conditions:default": [],',
' })',
])
deps_selects.append(select_str)
elif node['type'] == 'linker_fragment':
# Use the linker file name as the base of the target name.
local_target_name, local_target_name_bazel_str = (
_get_unique_target_name(node['files'][0], unconditional_deps)
)
rules.append(f"""zephyr_linker_fragment(
name = "{local_target_name}",
srcs = {node['files']},
location = "{node['location']}",
sort_key = "{node['sort_key']}",
)
""")
# Track this rule so that the umbrella linker fragment rule can
# depend on it.
unconditional_deps.append(local_target_name_bazel_str)
elif node['type'] == 'conditional_linker_fragment':
# Use the linker file name as the base of the target name.
local_target_name, local_target_name_bazel_str = (
_get_unique_target_name(node['files'][0], unconditional_deps)
)
rules.append(f"""zephyr_linker_fragment(
name = "{local_target_name}",
srcs = select({{
"@zephyr_kconfig//:{node['condition']}=true": {node['files']},
"//conditions:default": [],
}}),
location = "{node['location']}",
sort_key = "{node['sort_key']}",
)
""")
# Track this rule so that the umbrella linker fragment rule can
# depend on it.
unconditional_deps.append(local_target_name_bazel_str)
elif node['type'] == 'if_block':
if_block_rules, deps_select = parse_if_block_ast_node(
node,
target_name,
lambda child, child_name: traverse_ast(
child,
child_name,
update_rules_fn,
ignore_node_types=ignore_node_types,
),
)
rules.extend(if_block_rules)
if deps_select:
deps_selects.append(deps_select)
# Merge unconditional and top-level conditional sources.
srcs_content = '[\n'
for unconditional_src in unconditional_srcs:
srcs_content += unconditional_src + '\n'
srcs_content += ' ]'
srcs_content += ''.join(toplevel_conditional_srcs)
defines_content = ''
if target_name in ('uart_native_pty', 'uart_native_tty'):
defines_content = ',\n defines = ["_POSIX_C_SOURCE=200809L"]'
# AST node has been parsed.
# Call the provided hook to update the Bazel rules.
rules = update_rules_fn(
rules,
target_name,
srcs_content + defines_content,
unconditional_deps,
deps_selects,
is_root,
)
return rules
def _user_ack(prompt: str) -> bool:
while True:
response = input(f'{prompt} (y/n): ').lower()
if response == 'y' or response == 'yes':
return True
elif response == 'n' or response == 'no':
return False
else:
print("Invalid input. Please enter 'y' or 'n'")
def parse_if_block_ast_node(
node: dict[str, Any], target_name: str, recursive_callback: Callable
) -> tuple[list[str], str]:
rules = []
select_dict_entries = []
else_condition = '//conditions:default'
# Process if/elseif branches
for i, branch in enumerate(node['branches']):
# Track compound condition rules created so that we can
# backtrack.
config_setting_rules_created = 0
condition = branch['condition']
branch_body = branch['body']
sanitized_condition = re.sub(r'[^a-zA-Z0-9_]', '_', condition).lower()
child_target_name = f'{target_name}_{sanitized_condition}_deps'
compound_types = 0
for compound_type in [' OR ', ' AND ']:
if compound_type in condition:
compound_types += 1
if compound_types > 1:
if _user_ack(
f'Cannot handle complex compound condition {condition},'
' please confirm okay to skip'
):
continue
else:
raise ValueError(
f'Cannot handle complex compound condition: {condition}'
)
if re.match(r'NOT\s+CONFIG_\S+', condition):
# NOT CONFIG_XXX
real_condition = re.search(r'^NOT\s+(\S+)', condition).group(1)
# Invert else condition.
else_condition = f'@zephyr_kconfig//:{real_condition}=true'
select_dict_entries.append(
f' "//conditions:default": [":{child_target_name}"],'
)
elif re.match(r'\S+\s+OR\s+', condition):
or_conditions = re.split(r'\s+OR\s+', condition)
to_match = []
for or_condition in or_conditions:
if re.match(r'NOT\s+CONFIG_\S+', or_condition):
real_condition = re.search(
r'^NOT\s+(\S+)', or_condition
).group(1)
# Define the inverse config_setting to use in match_any below.
inverse_condition_name = or_condition.replace(
' ', '_'
).lower()
rules.append(f"""config_setting(
name = "{inverse_condition_name}",
flag_values = {{
"@zephyr_kconfig//:{real_condition}": "false",
}},
)""")
to_match.append(f':{inverse_condition_name}')
else:
to_match.append(f'@zephyr_kconfig//:{or_condition}=true')
rules.append(f"""selects.config_setting_group(
name = "{sanitized_condition}",
match_any = {to_match},
)""")
config_setting_rules_created += 1
select_dict_entries.append(
f' ":{sanitized_condition}": [":{child_target_name}"],'
)
elif re.match(r'\S+\s+AND\s+', condition):
and_conditions = re.split(r'\s+AND\s+', condition)
to_match = []
for and_condition in and_conditions:
if re.match(r'NOT\s+CONFIG_\S+', and_condition):
real_condition = re.search(
r'^NOT\s+(\S+)', and_condition
).group(1)
# Define the inverse config_setting to use in match_all below.
inverse_condition_name = and_condition.replace(
' ', '_'
).lower()
rules.append(f"""config_setting(
name = "{inverse_condition_name}",
flag_values = {{
"@zephyr_kconfig//:{real_condition}": "false",
}},
)""")
config_setting_rules_created += 1
to_match.append(f':{inverse_condition_name}')
else:
to_match.append(f'@zephyr_kconfig//:{and_condition}=true')
rules.append(f"""selects.config_setting_group(
name = "{sanitized_condition}",
match_all = {to_match},
)""")
config_setting_rules_created += 1
select_dict_entries.append(
f' ":{sanitized_condition}": [":{child_target_name}"],'
)
elif re.match(r'CONFIG_\S+', condition):
# Regular CONFIG_XXX without AND, OR, NOT.
select_dict_entries.append(
f' "@zephyr_kconfig//:{condition}=true":'
f' [":{child_target_name}"],'
)
else:
print(f'Cannot process condition {condition}, skipping...')
continue
# Recursively generate rules for the branch body
child_rules = recursive_callback(branch_body, child_target_name)
if child_rules:
rules.extend(child_rules)
else:
# Child does not have any content, remove the empty select.
select_dict_entries.pop()
# Since the select is empty, compound condition rules are
# useless too.
for _ in range(config_setting_rules_created):
rules.pop()
# Process else branch
if node['else_body'] is not None:
child_target_name = f'{target_name}_else_deps'
child_rules = recursive_callback(node['else_body'], child_target_name)
if child_rules:
rules.extend(child_rules)
select_dict_entries.append(
f' "{else_condition}": [":{child_target_name}"],'
)
elif select_dict_entries:
# Else condition is only meaningful if there exist other
# conditions already.
select_dict_entries.append(f' "{else_condition}": [],')
deps_select = ''
if select_dict_entries:
deps_select = '\n'.join([
' + select({',
*select_dict_entries,
' })',
])
return rules, deps_select