| # 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 |