| # 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 soc/ directory CMakeLists.txt to BUILD.bazel""" |
| |
| import argparse |
| from pathlib import Path |
| import re |
| from typing import Any |
| import cmake_converter_common as common |
| |
| |
| def generate_soc_library_from_ast( |
| ast: list[dict[str, Any]], |
| target_name: str, |
| parent_filegroup_name: str, |
| common_filegroup_names: list[str], |
| ) -> list[str]: |
| """Generates a zephyr_soc_library. |
| |
| The generated zephyr_soc_library may depend on parent source filegroups |
| and header-only libraries, and will compile all sources and headers. |
| """ |
| |
| def update_rules( |
| rules: list[str], |
| target_name: str, |
| srcs_content: str, |
| unconditional_deps: list[str], # Unused |
| deps_selects: list[str], |
| is_root_ast_node: bool, |
| ) -> list[str]: |
| local_parent_filegroup_name = parent_filegroup_name |
| local_common_filegroup_names = common_filegroup_names |
| if not is_root_ast_node: |
| local_parent_filegroup_name = '' |
| local_common_filegroup_names = [] |
| if local_parent_filegroup_name: |
| srcs_content += f' + ["{local_parent_filegroup_name}"]' |
| if local_common_filegroup_names: |
| for filegroup_name in local_common_filegroup_names: |
| srcs_content += f' + ["{filegroup_name}"]' |
| |
| deps_content = ',\n deps = []' |
| if deps_selects: |
| deps_content += ''.join(deps_selects) |
| if local_parent_filegroup_name: |
| # Depend on parent header-only library. |
| parent_hdr_lib_name = ( |
| local_parent_filegroup_name |
| + ':' |
| + local_parent_filegroup_name.split('/')[-1] |
| + '_hdrs' |
| ) |
| deps_content += f' + ["{parent_hdr_lib_name}"]' |
| if local_common_filegroup_names: |
| # Depend on common header-only libraries. |
| for filegroup_name in local_common_filegroup_names: |
| deps_content += f' + ["{filegroup_name}:common_hdrs"]' |
| |
| hdrs_content = """, |
| hdrs = glob(["*.h"], allow_empty=True), |
| # This is unfortunate. Zephyr often uses #include <xxx.h> for headers in |
| # the SoC directories, and expects the SoC directories to be on the system |
| # include paths. |
| includes = ["."]""" |
| |
| # Generate the rule to build the soc. |
| main_rule = f"""zephyr_soc_library( |
| name = "{target_name}", |
| visibility = ["//visibility:public"], |
| srcs = {srcs_content}{hdrs_content}{deps_content}, |
| )""" |
| rules.insert(0, main_rule) # Main rule comes first |
| return rules |
| |
| return common.traverse_ast(ast, target_name, update_rules, is_root=True) |
| |
| |
| def generate_soc_filegroup_and_hdr_lib_from_ast( |
| ast: list[dict[str, Any]], |
| target_name: str, |
| parent_filegroup_name: str, |
| common_filegroup_names: list[str], |
| ) -> list[str]: |
| """Generates Bazel rules for sharing sources and headers with a zephyr_soc_library. |
| |
| We will generate a filegroup containing sources to be shared, and a |
| header-only cc_library containing the headers to be shared. |
| |
| The zephyr_soc_library that represents a specific SoC will depend on these, |
| and will compile all sources and headers, as it has the full context. |
| Otherwise |
| #defines will not propagate correctly. |
| """ |
| |
| def update_rules( |
| rules: list[str], |
| target_name: str, |
| srcs_content: str, |
| unconditional_deps: list[str], # Unused |
| deps_selects: list[str], |
| is_root_ast_node: bool, |
| ) -> list[str]: |
| # filegroups and header-only libraries for non-leaf directories do not |
| # care about common directories. Only leaf directories (which build |
| # zephyr_soc_library()) reference common directories. |
| local_parent_filegroup_name = parent_filegroup_name |
| if not is_root_ast_node: |
| local_parent_filegroup_name = '' |
| if local_parent_filegroup_name: |
| srcs_content += f' + ["{local_parent_filegroup_name}"]' |
| if deps_selects: |
| srcs_content += ''.join(deps_selects) |
| |
| if srcs_content == '[\n ]' and not is_root_ast_node: |
| # Empty. Do not generate anything unless it's the root rule, |
| # because the root filegroup is needed by children directories. |
| return rules |
| |
| # Add the special empty_files target so that our filegroup never |
| # becomes empty, which is disallowed by cc_helper.bzl. |
| filegroup_rule = f"""filegroup( |
| name = "{target_name}", |
| srcs = {srcs_content} + ["empty.h"], |
| )""" |
| # Keep the main rule at the beginning. |
| rules.insert(0, filegroup_rule) |
| |
| # Generate the header-only library alongside the srcs filegroup. |
| hdr_lib_rule = f"""cc_library( |
| name = "{target_name}_hdrs", |
| hdrs = glob(["*.h"], allow_empty=True), |
| # This is unfortunate. Zephyr often uses #include <xxx.h> for headers in |
| # the SoC directories, and expects the SoC directories to be on the system |
| # include paths. |
| includes = ["."], |
| deps = []""" |
| if local_parent_filegroup_name: |
| # In the form of "//soc/some_vendor:some_vendor_hdrs". |
| parent_hdr_lib_name = ( |
| local_parent_filegroup_name |
| + ':' |
| + parent_filegroup_name.split('/')[-1] |
| + '_hdrs' |
| ) |
| hdr_lib_rule += f' + ["{parent_hdr_lib_name}"]' |
| hdr_lib_rule += ',\n)\n' |
| rules.append(hdr_lib_rule) |
| return rules |
| |
| return common.traverse_ast(ast, target_name, update_rules, is_root=True) |
| |
| |
| def generate_linker_fragments_from_ast( |
| ast: list[dict[str, Any]], |
| target_name: str, |
| root: bool, |
| ) -> list[str]: |
| def update_rules( |
| rules: list[str], |
| target_name: str, |
| srcs_content: str, |
| unconditional_deps: list[str], |
| deps_selects: list[str], |
| is_root_ast_node: bool, |
| ) -> list[str]: |
| if not unconditional_deps and not deps_selects: |
| return rules |
| |
| # Writes the umbrella rule that depends on everything else. |
| # If this is an umbrella rule gated by a kconfig condition, then the root |
| # umbrella rule will depend on this rule. |
| umbrella_rule = f"""zephyr_linker_fragment( |
| name = "{target_name}", |
| """ |
| deps_content = '\n'.join([ |
| ' deps = [', |
| *[f' {dep},' for dep in unconditional_deps], |
| ' ]', |
| ]) |
| umbrella_rule += deps_content |
| if deps_selects: |
| umbrella_rule += ''.join(deps_selects) |
| umbrella_rule += """, |
| ) |
| """ |
| # Put the umbrella rule first. |
| rules.insert(0, umbrella_rule) |
| |
| if is_root_ast_node: |
| # Write the cc_library to collect these linker files so that the |
| # C preprocessor can merge them. |
| cc_library_rule = f"""cc_library( |
| name = "linker_includes", |
| hdrs = glob(["**/*.ld"]), |
| ) |
| """ |
| rules.insert(1, cc_library_rule) |
| |
| return rules |
| |
| return common.traverse_ast( |
| ast, |
| target_name, |
| update_rules, |
| is_root=True, |
| ignore_node_types=['unconditional_subdir', 'conditional_subdir'], |
| ) |
| |
| |
| def generate_rules_from_ast( |
| ast: list[dict[str, Any]], |
| target_name: str, |
| leaf: bool, |
| parent_filegroup_name: str, |
| common_filegroup_names: list[str], |
| ) -> list[str]: |
| """Recursively generates Bazel rules from the AST.""" |
| rules = [] |
| if leaf: |
| rules.extend( |
| generate_soc_library_from_ast( |
| ast, target_name, parent_filegroup_name, common_filegroup_names |
| ) |
| ) |
| else: |
| rules.extend( |
| generate_soc_filegroup_and_hdr_lib_from_ast( |
| ast, target_name, parent_filegroup_name, common_filegroup_names |
| ) |
| ) |
| rules.extend( |
| generate_linker_fragments_from_ast(ast, 'linker_fragments', root=True) |
| ) |
| # Remove duplicate rules. This can be from compound if conditions. |
| rules = list(dict.fromkeys(rules)) |
| return rules |
| |
| |
| def convert_cmakelists_to_bazel( |
| cmake_content: str, |
| target_name: str, |
| leaf: bool, |
| parent_filegroup_name: str, |
| common_filegroup_names: list[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_rules_from_ast( |
| ast, target_name, leaf, parent_filegroup_name, common_filegroup_names |
| ) |
| |
| header = """# SPDX-License-Identifier: Apache-2.0 |
| |
| load("@bazel_skylib//lib:selects.bzl", "selects") |
| load("@rules_cc//cc:defs.bzl", "cc_library") |
| load("//:cc.bzl", "zephyr_soc_library") |
| load("//:linker_fragment.bzl", "zephyr_linker_fragment") |
| |
| package(default_visibility = ["//:__subpackages__"]) |
| """ |
| return header + '\n' + '\n\n'.join(bazel_rules) |
| |
| |
| def find_soc_root(path: Path) -> Path: |
| """Finds the Zephyr soc/ directory from a subdirectory path.""" |
| for parent_dir in path.parents: |
| if parent_dir.name == 'soc' and parent_dir.parent.name == 'zephyr': |
| return parent_dir |
| raise ValueError('Input path is not inside zephyr soc directory') |
| |
| |
| def get_filegroup_names( |
| cmake_file: Path, skip_parent: bool |
| ) -> tuple[str, list[str]]: |
| """Calculates parent and common filegroup names.""" |
| # Do not look at any parent directory or common directories in ancestors. |
| if skip_parent: |
| return '', [] |
| |
| soc_root = find_soc_root(cmake_file) |
| zephyr_root = soc_root.parent |
| |
| parent_cmakelists_dir_path = cmake_file.parent.parent |
| parent_cmakelists_dir_relative_path = ( |
| parent_cmakelists_dir_path.relative_to(zephyr_root) |
| ) |
| parent_filegroup_name = '//' + str( |
| parent_cmakelists_dir_relative_path |
| ).replace('\\', '/') |
| |
| common_filegroup_names = [] |
| for ancestor in cmake_file.parents: |
| for item in ancestor.iterdir(): |
| if ( |
| item.is_dir() |
| and item.name == 'common' |
| and (item / 'CMakeLists.txt').is_file() |
| ): |
| common_dir_relative_path = item.relative_to(zephyr_root) |
| common_filegroup_name = '//' + str( |
| common_dir_relative_path |
| ).replace('\\', '/') |
| common_filegroup_names.append(common_filegroup_name) |
| return parent_filegroup_name, common_filegroup_names |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser( |
| description='Convert a Zephyr SoC CMakeLists.txt to BUILD.bazel' |
| ) |
| parser.add_argument( |
| '--leaf', |
| action='store_true', |
| help=( |
| 'Set if the input CMakeLists.txt is the leaf node, i.e. most' |
| ' specific SoC CMake file' |
| ), |
| ) |
| parser.add_argument( |
| '--skip_parent', |
| action='store_true', |
| help=( |
| 'Do not add files from parent directory. Useful for soc/<vendor>' |
| ' and */common' |
| ), |
| ) |
| 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 |
| if target_name == 'common': |
| # common/ stuff will be consumed as if they live in the parent directory. |
| args.skip_parent = True |
| |
| parent_filegroup_name, common_filegroup_names = get_filegroup_names( |
| Path(args.input_file), args.skip_parent |
| ) |
| |
| bazel_content = convert_cmakelists_to_bazel( |
| cmake_content, |
| target_name, |
| args.leaf, |
| parent_filegroup_name, |
| common_filegroup_names, |
| ) |
| |
| with open(args.output_file, 'w') as f: |
| f.write(bazel_content) |
| |
| # Write an empty header file alongside the generated BUILD.bazel so that |
| # our generated filegroups are never empty. |
| empty_header_file = args.output_file.parent / 'empty.h' |
| with open(empty_header_file, 'w') as f: |
| f.write("""// 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. |
| """) |
| |
| print(f'Successfully converted {args.input_file} to {args.output_file}') |