| # 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. |
| """Script to translate Zephyr's driver/ CMakeLists.txt to BUILD.bazel for zephyr-bazel.""" |
| |
| import argparse |
| from pathlib import Path |
| import re |
| import sys |
| from typing import Any |
| import cmake_converter_common as common |
| |
| |
| _SCRIPT_PATH = Path(__file__).resolve() |
| ZEPHYR_BAZEL_ROOT = _SCRIPT_PATH.parents[2] |
| |
| |
| def generate_cc_library_from_ast( |
| ast: list[dict[str, Any]], target_name: str |
| ) -> list[str]: |
| """Generates driver libraries from the AST.""" |
| |
| def update_rules( |
| rules: list[str], |
| target_name: str, |
| srcs_content: str, |
| unconditional_deps: list[str], |
| deps_selects: list[str], |
| is_root: bool, |
| ) -> list[str]: |
| deps_content = '' |
| if deps_selects: |
| deps_content = '\n'.join([ |
| ',', |
| ' deps = [', |
| *[f' {dep},' for dep in unconditional_deps], |
| ' ]', |
| ]) |
| deps_content += ''.join(deps_selects) |
| |
| hdr_content = ',\n hdrs = glob(["*.h"], allow_empty=True)' |
| |
| main_rule = f"""zephyr_cc_library( |
| name = "{target_name}", |
| srcs = {srcs_content}{hdr_content}{deps_content}, |
| )""" |
| rules.insert(0, main_rule) # Main rule comes first |
| return rules |
| |
| return common.traverse_ast(ast, target_name, update_rules) |
| |
| |
| def _transform_syscall_client_file_path(filepath: str) -> str: |
| """Takes a path string for a file and returns the Bazel label.""" |
| if not filepath.startswith('${ZEPHYR_BASE}'): |
| # Assume it's a path relative to the directory we are processing. |
| return filepath |
| |
| script_path = Path(__file__).resolve() |
| zephyr_bazel_root = script_path.parent.parent.parent |
| |
| # Convert file paths to labels pointing to the file. |
| # ${ZEPHYR_BASE} -> // |
| # renaminder -> look at each subdirectory level in the file path in the |
| # overlay trees. Add a colon to mark the Bazel package boundary. |
| relative_path = Path(filepath).relative_to('${ZEPHYR_BASE}') |
| for parent in relative_path.parents: |
| overlay_path = ( |
| ZEPHYR_BAZEL_ROOT / 'bazel_overlay' / parent / 'BUILD.bazel' |
| ) |
| third_party_path = ( |
| ZEPHYR_BAZEL_ROOT |
| / 'third_party' |
| / 'zephyr' |
| / parent |
| / 'BUILD.bazel' |
| ) |
| if overlay_path.exists() or third_party_path.exists(): |
| label = f'//{parent}:{relative_path.relative_to(parent)}' |
| return label |
| # Failed to find the Bazel package for this file. |
| print(f'Failed to find Bazel package for file {filepath}', file=sys.stderr) |
| sys.exit(1) |
| |
| |
| def extract_syscall_client_files_from_ast( |
| ast: list[dict[str, Any]], |
| ) -> list[str]: |
| """Extracts syscall client header files from AST.""" |
| files = [] |
| |
| for node in ast: |
| if 'files' in node: |
| node['files'] = [ |
| _transform_syscall_client_file_path(file) |
| for file in node['files'] |
| ] |
| if node['type'] == 'unconditional_syscall_client_files': |
| files.extend(node['files']) |
| elif node['type'] == 'conditional_syscall_client_files': |
| # There are complications if we respect the conditionals. |
| # Not all syscalls in all client files are "emitted", and to |
| # strictly follow Zephyr's approach we would have to parse JSON in |
| # a custom rule. |
| # For now, just treat conditionals as unconditionals. |
| files.extend(node['files']) |
| elif node['type'] == 'if_block': |
| # Process if/elseif branches |
| for i, branch in enumerate(node['branches']): |
| # Recursively generate rules for the branch body |
| files.extend( |
| extract_syscall_client_files_from_ast(branch['body']) |
| ) |
| |
| # Process else branch |
| if node['else_body'] is not None: |
| # Recursively generate rules for the else body |
| files.extend( |
| extract_syscall_client_files_from_ast(node['else_body']) |
| ) |
| |
| return files |
| |
| |
| def generate_syscall_client_filegroup_from_ast( |
| ast: list[dict[str, Any]], |
| ) -> str | None: |
| """Generates one filegroup rule for syscall client files, from AST.""" |
| syscall_client_files = extract_syscall_client_files_from_ast(ast) |
| if syscall_client_files: |
| return '\n'.join([ |
| 'filegroup(', |
| ' name = "syscall_client_files",', |
| ' srcs = [', |
| *[f' "{file}",' for file in syscall_client_files], |
| ' ],', |
| ')', |
| ]) |
| return None |
| |
| |
| def convert_cmakelists_to_bazel(cmake_content: str, target_name: str) -> str: |
| """Top-level conversion function.""" |
| cmake_content = common.merge_bracketed_string(cmake_content) |
| lines = cmake_content.splitlines() |
| ast = common.build_ast(lines) |
| bazel_rules = generate_cc_library_from_ast(ast, target_name) |
| |
| syscall_client_files_rule = generate_syscall_client_filegroup_from_ast(ast) |
| if syscall_client_files_rule: |
| bazel_rules.append(syscall_client_files_rule) |
| |
| header = """# SPDX-License-Identifier: Apache-2.0 |
| |
| # Auto-generated from corresponding CMakeLists.txt in Zephyr using scripts |
| # in zephyr-bazel. |
| |
| load("@bazel_skylib//lib:selects.bzl", "selects") |
| load("//:cc.bzl", "zephyr_cc_library") |
| |
| package(default_visibility = ["//:__subpackages__"]) |
| """ |
| return header + '\n' + '\n\n'.join(bazel_rules) |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser( |
| description='Convert CMakeLists.txt to BUILD.bazel' |
| ) |
| parser.add_argument( |
| 'input_file', help='Path to the input CMakeLists.txt file' |
| ) |
| parser.add_argument( |
| 'output_file', help='Path to the output BUILD.bazel file' |
| ) |
| args = parser.parse_args() |
| |
| with open(args.input_file, 'r') as f: |
| cmake_content = f.read() |
| |
| target_name = Path(args.input_file).parent.name |
| bazel_content = convert_cmakelists_to_bazel(cmake_content, target_name) |
| |
| with open(args.output_file, 'w') as f: |
| f.write(bazel_content) |
| |
| print(f'Successfully converted {args.input_file} to {args.output_file}') |