blob: 1770ee2070035f73dce2bf241440fa074204c2e9 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 The IREE Authors
#
# Licensed under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
"""This script assists with converting from Bazel BUILD files to CMakeLists.txt.
Bazel BUILD files should, where possible, be written to use simple features
that can be directly evaluated and avoid more advanced features like
variables, list comprehensions, etc.
Generated CMake files will be similar in structure to their source BUILD
files by using the functions in build_tools/cmake/ that imitate corresponding
Bazel rules (e.g. cc_library -> iree_cc_library.cmake).
For usage, see:
python3 build_tools/bazel_to_cmake/bazel_to_cmake.py --help
"""
# pylint: disable=missing-docstring
import argparse
import datetime
import os
import re
import sys
import textwrap
from enum import Enum
import bazel_to_cmake_converter
repo_root = None
EDIT_BLOCKING_PATTERN = re.compile(
r"bazel[\s_]*to[\s_]*cmake[\s_]*:?[\s_]*do[\s_]*not[\s_]*edit",
flags=re.IGNORECASE)
PRESERVE_TAG = "### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###"
class Status(Enum):
UPDATED = 1
NOOP = 2
FAILED = 3
SKIPPED = 4
NO_BUILD_FILE = 5
def parse_arguments():
global repo_root
parser = argparse.ArgumentParser(
description="Bazel to CMake conversion helper.")
parser.add_argument("--preview",
help="Prints results instead of writing files",
action="store_true",
default=False)
parser.add_argument(
"--allow_partial_conversion",
help="Generates partial files, ignoring errors during conversion.",
action="store_true",
default=False)
parser.add_argument(
"--verbosity",
"-v",
type=int,
default=0,
help="Specify verbosity level where higher verbosity emits more logging."
" 0 (default): Only output errors and summary statistics."
" 1: Also output the name of each directory as it's being processed and"
" whether the directory is skipped."
" 2: Also output when conversion was successful.")
# Specify only one of these (defaults to --root_dir=<main source dirs>).
group = parser.add_mutually_exclusive_group()
group.add_argument("--dir",
help="Converts the BUILD file in the given directory",
default=None)
group.add_argument(
"--root_dir",
nargs="+",
help="Converts all BUILD files under a root directory",
default=["compiler", "iree", "runtime", "samples", "tests", "tools"])
args = parser.parse_args()
# --dir takes precedence over --root_dir.
# They are mutually exclusive, but the default value is still set.
if args.dir:
args.root_dir = None
return args
def setup_environment():
"""Sets up some environment globals."""
global repo_root
# Determine the repository root (two dir-levels up).
repo_root = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def repo_relpath(path):
return os.path.relpath(path, repo_root).replace("\\", "/")
def log(string, *args, indent=0, **kwargs):
print(textwrap.indent(string, prefix=(indent * " ")),
*args,
**kwargs,
file=sys.stderr)
def convert_directories(directories, write_files, allow_partial_conversion,
verbosity):
failure_dirs = []
skip_count = 0
success_count = 0
noop_count = 0
for directory in directories:
status = convert_directory(
directory,
write_files=write_files,
allow_partial_conversion=allow_partial_conversion,
verbosity=verbosity)
if status == Status.FAILED:
failure_dirs.append(repo_relpath(directory))
elif status == Status.SKIPPED:
skip_count += 1
elif status == Status.UPDATED:
success_count += 1
elif status == Status.NOOP:
noop_count += 1
log(f"{success_count} CMakeLists.txt files were updated, {skip_count} were"
f" skipped, and {noop_count} required no change.")
if failure_dirs:
log(f"ERROR: Encountered unexpected errors converting {len(failure_dirs)}"
" directories:")
log("\n".join(failure_dirs), indent=2)
sys.exit(1)
def convert_directory(directory_path, write_files, allow_partial_conversion,
verbosity):
if not os.path.isdir(directory_path):
raise FileNotFoundError(f"Cannot find directory '{directory_path}'")
rel_dir_path = repo_relpath(directory_path)
if verbosity >= 1:
log(f"Processing {rel_dir_path}")
build_file_path = os.path.join(directory_path, "BUILD")
cmakelists_file_path = os.path.join(directory_path, "CMakeLists.txt")
rel_cmakelists_file_path = repo_relpath(cmakelists_file_path)
rel_build_file_path = repo_relpath(build_file_path)
if not os.path.isfile(build_file_path):
return Status.NO_BUILD_FILE
autogeneration_tag = f"Autogenerated by {repo_relpath(os.path.abspath(__file__))}"
header = "\n".join(["#" * 80] + [
l.ljust(79) + "#" for l in [
f"# {autogeneration_tag} from",
f"# {rel_build_file_path}",
"#",
"# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary",
"# CMake-only content.",
"#",
f"# To disable autogeneration for this file entirely, delete this header.",
]
] + ["#" * 80])
old_lines = []
preserved_footer_lines = ["\n" + PRESERVE_TAG + "\n"]
if os.path.isfile(cmakelists_file_path):
found_autogeneration_tag = False
found_preserve_tag = False
with open(cmakelists_file_path) as f:
old_lines = f.readlines()
for line in old_lines:
if not found_autogeneration_tag and autogeneration_tag in line:
found_autogeneration_tag = True
if not found_preserve_tag and PRESERVE_TAG in line:
found_preserve_tag = True
elif found_preserve_tag:
preserved_footer_lines.append(line)
if not found_autogeneration_tag:
if verbosity >= 1:
log(f"Skipped. Did not find autogeneration line.", indent=2)
return Status.SKIPPED
preserved_footer = "".join(preserved_footer_lines)
with open(build_file_path, "rt") as build_file:
build_file_code = compile(build_file.read(), build_file_path, "exec")
try:
converted_build_file = bazel_to_cmake_converter.convert_build_file(
build_file_code, allow_partial_conversion=allow_partial_conversion)
except (NameError, NotImplementedError) as e:
log(
f"ERROR generating {rel_dir_path}.\n"
f"Missing a rule handler in bazel_to_cmake_converter.py?\n"
f"Reason: `{type(e).__name__}: {e}`",
indent=2)
return Status.FAILED
except KeyError as e:
log(
f"ERROR generating {rel_dir_path}.\n"
f"Missing a conversion in bazel_to_cmake_targets.py?\n"
f"Reason: `{type(e).__name__}: {e}`",
indent=2)
return Status.FAILED
converted_content = header + converted_build_file + preserved_footer
if write_files:
with open(cmakelists_file_path, "wt") as cmakelists_file:
cmakelists_file.write(converted_content)
else:
print(converted_content, end="")
if converted_content == "".join(old_lines):
if verbosity >= 2:
log(f"{rel_cmakelists_file_path} required no update", indent=2)
return Status.NOOP
if verbosity >= 2:
log(
f"Successfly generated {rel_cmakelists_file_path}"
f" from {rel_build_file_path}",
indent=2)
return Status.UPDATED
def main(args):
"""Runs Bazel to CMake conversion."""
global repo_root
write_files = not args.preview
if args.root_dir:
for root_dir in args.root_dir:
root_directory_path = os.path.join(repo_root, root_dir)
log(f"Converting directory tree rooted at: {root_directory_path}")
convert_directories(
(root for root, _, _ in os.walk(root_directory_path)),
write_files=write_files,
allow_partial_conversion=args.allow_partial_conversion,
verbosity=args.verbosity)
elif args.dir:
convert_directories([os.path.join(repo_root, args.dir)],
write_files=write_files,
allow_partial_conversion=args.allow_partial_conversion,
verbosity=args.verbosity)
if __name__ == "__main__":
setup_environment()
main(parse_arguments())