blob: 3b3c0fea4d65a063fc5340031f17999af4209a8e [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 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}')