| #!/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 | |
| Configuration | |
| ------------- | |
| When invoked, bazel_to_cmake will traverse up from the current directory until | |
| it finds a ".bazel_to_cmake.cfg.py" file. This file both serves as a marker | |
| for the repository root and provides repository specific configuration. | |
| The file is evaluated as a module and can have the following customizations: | |
| * DEFAULT_ROOT_DIRS: A list of root directory names that should be processed | |
| (relative to the repository root) when invoked without a --repo_root or --dir. | |
| * REPO_MAP: Mapping of canonical Bazel repo name (i.e. "@iree_core") to what it | |
| is known as locally (most commonly the empty string). This is used in global | |
| target rules to make sure that they work either in the defining or referencing | |
| repository. | |
| * CustomBuildFileFunctions: A class that extends | |
| `bazel_to_cmake_converter.BuildFileFunctions` and injects globals for | |
| processing the BUILD file. All symbols that do not start with "_" are | |
| available. | |
| * CustomTargetConverter: A class that extends | |
| `bazel_to_cmake_targets.TargetConverter` and customizes target mapping. | |
| Typically, this is used for purely local targets in leaf projects (as global | |
| targets will be encoded in the main bazel_to_cmake_targets.py file). | |
| """ | |
| # pylint: disable=missing-docstring | |
| import argparse | |
| import datetime | |
| import importlib | |
| import importlib.util | |
| import os | |
| import re | |
| import sys | |
| import textwrap | |
| import types | |
| from enum import Enum | |
| import bazel_to_cmake_converter | |
| repo_root = None | |
| repo_cfg = None | |
| EDIT_BLOCKING_PATTERN = re.compile( | |
| r"bazel[\s_]*to[\s_]*cmake[\s_]*:?[\s_]*do[\s_]*not[\s_]*edit", flags=re.IGNORECASE | |
| ) | |
| PRESERVE_ABOVE_TAG = "### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_ABOVE_THIS_LINE ###" | |
| PRESERVE_BELOW_TAG = "### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###" | |
| REPO_CFG_FILE = ".bazel_to_cmake.cfg.py" | |
| REPO_CFG_MODULE_NAME = "bazel_to_cmake_repo_config" | |
| class Status(Enum): | |
| UPDATED = 1 | |
| NOOP = 2 | |
| FAILED = 3 | |
| SKIPPED = 4 | |
| NO_BUILD_FILE = 5 | |
| def parse_arguments(): | |
| 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 | |
| ) | |
| default_root_dirs = ( | |
| repo_cfg.DEFAULT_ROOT_DIRS if hasattr(repo_cfg, "DEFAULT_ROOT_DIRS") else [] | |
| ) | |
| group.add_argument( | |
| "--root_dir", | |
| nargs="+", | |
| help="Converts all BUILD files under a root directory", | |
| default=default_root_dirs, | |
| ) | |
| 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 | |
| global repo_cfg | |
| # Scan up the directory tree for a repo config file. | |
| check_dir = os.getcwd() | |
| while not os.path.exists(os.path.join(check_dir, REPO_CFG_FILE)): | |
| new_check_dir = os.path.dirname(check_dir) | |
| if not new_check_dir or new_check_dir == check_dir: | |
| print( | |
| f"ERROR: Could not find {REPO_CFG_FILE} in a parent directory " | |
| f"of {os.getcwd()}" | |
| ) | |
| sys.exit(1) | |
| check_dir = new_check_dir | |
| repo_root = check_dir | |
| log(f"Using repo root {repo_root}") | |
| # Dynamically load the config file as a module. | |
| orig_dont_write_bytecode = sys.dont_write_bytecode | |
| sys.dont_write_bytecode = True # Don't generate __pycache__ dir | |
| repo_cfg_path = os.path.join(repo_root, REPO_CFG_FILE) | |
| spec = importlib.util.spec_from_file_location(REPO_CFG_MODULE_NAME, repo_cfg_path) | |
| if spec and spec.loader: | |
| repo_cfg = importlib.util.module_from_spec(spec) | |
| sys.modules[REPO_CFG_MODULE_NAME] = repo_cfg | |
| spec.loader.exec_module(repo_cfg) | |
| sys.dont_write_bytecode = orig_dont_write_bytecode | |
| else: | |
| print(f"INTERNAL ERROR: Could not evaluate {repo_cfg_path} as module") | |
| sys.exit(1) | |
| 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}") | |
| # Scan for a BUILD file. | |
| build_file_found = False | |
| build_file_basenames = ["BUILD", "BUILD.bazel"] | |
| for build_file_basename in build_file_basenames: | |
| build_file_path = os.path.join(directory_path, build_file_basename) | |
| rel_build_file_path = repo_relpath(build_file_path) | |
| if os.path.isfile(build_file_path): | |
| build_file_found = True | |
| break | |
| cmakelists_file_path = os.path.join(directory_path, "CMakeLists.txt") | |
| rel_cmakelists_file_path = repo_relpath(cmakelists_file_path) | |
| if not build_file_found: | |
| 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 = [] | |
| possible_preserved_header_lines = [] | |
| preserved_footer_lines = ["\n" + PRESERVE_BELOW_TAG + "\n"] | |
| # Read CMakeLists.txt and check if it has the auto-generated header. | |
| found_preserve_below_tag = False | |
| found_preserve_above_tag = False | |
| if os.path.isfile(cmakelists_file_path): | |
| found_autogeneration_tag = False | |
| with open(cmakelists_file_path) as f: | |
| old_lines = f.readlines() | |
| for line in old_lines: | |
| if not found_preserve_above_tag: | |
| possible_preserved_header_lines.append(line) | |
| if not found_autogeneration_tag and autogeneration_tag in line: | |
| found_autogeneration_tag = True | |
| if not found_preserve_below_tag and PRESERVE_BELOW_TAG in line: | |
| found_preserve_below_tag = True | |
| elif not found_preserve_above_tag and PRESERVE_ABOVE_TAG in line: | |
| found_preserve_above_tag = True | |
| elif found_preserve_below_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_header = ( | |
| "".join(possible_preserved_header_lines) if found_preserve_above_tag else "" | |
| ) | |
| preserved_footer = "".join(preserved_footer_lines) | |
| # Read the Bazel BUILD file and interpret it. | |
| with open(build_file_path, "rt") as build_file: | |
| build_file_contents = build_file.read() | |
| if "bazel-to-cmake: skip" in build_file_contents: | |
| return Status.SKIPPED | |
| build_file_code = compile(build_file_contents, build_file_path, "exec") | |
| try: | |
| converted_build_file = bazel_to_cmake_converter.convert_build_file( | |
| build_file_code, | |
| repo_cfg=repo_cfg, | |
| build_dir=directory_path, | |
| 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 = ( | |
| preserved_header + 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, | |
| ) | |
| else: | |
| log( | |
| f"ERROR: None of --root-dir, --dir arguments or DEFAULT_ROOT_DIRS in " | |
| f".bazel_to_cmake.cfg.py: No conversion will be done" | |
| ) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| setup_environment() | |
| main(parse_arguments()) |