blob: fb2d788e2b92ca5af09ea23e56e92ce4ba1a8c9c [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.
"""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}')