blob: 971b24cfbbae58e64a802b5ebd3012807d922b69 [file]
# 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
import os
from pathlib import Path
import re
import subprocess
import sys
from typing import Any, Callable
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,
path_transformer: Callable[[str], str] | None = None,
created_config_settings: set[str] | None = None,
) -> 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,
includes: list[str] | None = None,
) -> 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)'
includes_content = ''
if includes:
rel_includes = [
inc
for inc in includes
if not inc.startswith('/') and '$' not in inc
]
if rel_includes:
includes_content = f',\n includes = {rel_includes}'
main_rule = f"""zephyr_cc_library(
name = "{target_name}",
srcs = {srcs_content}{hdr_content}{includes_content}{deps_content},
)"""
rules.insert(0, main_rule) # Main rule comes first
return rules
return common.traverse_ast(
ast,
target_name,
update_rules,
path_transformer=path_transformer,
created_config_settings=created_config_settings,
)
get_path_transformer = common.get_path_transformer
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,
path_transformer: Callable[[str], str] | None = None,
) -> str:
"""Top-level conversion function."""
cmake_content = common.merge_bracketed_string(cmake_content)
lines = cmake_content.splitlines()
ast = common.build_ast(lines)
created_config_settings = set()
bazel_rules = generate_cc_library_from_ast(
ast,
target_name,
path_transformer=path_transformer,
created_config_settings=created_config_settings,
)
syscall_client_files_rule = generate_syscall_client_filegroup_from_ast(ast)
if syscall_client_files_rule:
bazel_rules.append(syscall_client_files_rule)
linker_fragment_rules = common.generate_linker_fragments_from_ast(
ast,
'linker_fragments',
path_transformer=path_transformer,
created_config_settings=created_config_settings,
)
has_linker_fragments = bool(linker_fragment_rules)
if has_linker_fragments:
bazel_rules.extend(linker_fragment_rules)
header_lines = [
"# 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")',
]
if has_linker_fragments:
header_lines.append('load("@rules_cc//cc:defs.bzl", "cc_library")')
header_lines.append('load("//:cc.bzl", "zephyr_cc_library")')
if has_linker_fragments:
header_lines.append('load("//:linker_fragment.bzl", "zephyr_linker_fragment")')
header_lines.extend([
"",
'package(default_visibility = ["//:__subpackages__"])',
])
header = '\n'.join(header_lines)
return header + '\n\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
path_transformer = get_path_transformer(Path(args.input_file).parent)
bazel_content = convert_cmakelists_to_bazel(
cmake_content, target_name, path_transformer=path_transformer
)
with open(args.output_file, 'w') as f:
f.write(bazel_content)
try:
subprocess.run(['buildifier', args.output_file], check=True)
except subprocess.CalledProcessError as e:
print(
f'Warning: failed to run buildifier on {args.output_file}: {e}',
file=sys.stderr,
)
except FileNotFoundError:
print('Warning: buildifier not found in PATH', file=sys.stderr)
print(f'Successfully converted {args.input_file} to {args.output_file}')