|  | #!/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). | 
|  |  | 
|  | Common usage: | 
|  | Run across all default paths in the project (in .bazel_to_cmake.cfg.py): | 
|  | $ python build_tools/bazel_to_cmake/bazel_to_cmake.py | 
|  |  | 
|  | Run on an individual file: | 
|  | $ python build_tools/bazel_to_cmake/bazel_to_cmake.py runtime/src/iree/base/BUILD.bazel | 
|  |  | 
|  | Run on multiple files (e.g. as part of a pre-commit hook): | 
|  | $ python build_tools/bazel_to_cmake/bazel_to_cmake.py runtime/src/iree/base/BUILD.bazel runtime/src/iree/vm/BUILD.bazel | 
|  |  | 
|  | Run on a single directory: | 
|  | $ python build_tools/bazel_to_cmake/bazel_to_cmake.py --dir runtime/src/iree/base/ | 
|  |  | 
|  | Run on all files under a root directory: | 
|  | $ python build_tools/bazel_to_cmake/bazel_to_cmake.py --root-dir runtime/src/iree/ | 
|  |  | 
|  | 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 importlib | 
|  | import importlib.util | 
|  | import os | 
|  | import re | 
|  | import sys | 
|  | import textwrap | 
|  | from enum import Enum | 
|  | from pathlib import Path | 
|  |  | 
|  | 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( | 
|  | "files", nargs="*", help="Converts (or generates) the given files", default=[] | 
|  | ) | 
|  | 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() | 
|  |  | 
|  | # 'files' and '--dir' take precedence over '--root_dir'. | 
|  | # They are mutually exclusive, but the default value is still set. | 
|  | if args.files or 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.files: | 
|  | convert_directories( | 
|  | [Path(file).parent for file in args.files], | 
|  | write_files=write_files, | 
|  | allow_partial_conversion=args.allow_partial_conversion, | 
|  | verbosity=args.verbosity, | 
|  | ) | 
|  | elif 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 (positional) 'file', '--root-dir', or '--dir' " | 
|  | f"arguments or DEFAULT_ROOT_DIRS in .bazel_to_cmake.cfg.py: No " | 
|  | f"conversion will be done" | 
|  | ) | 
|  | sys.exit(1) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | setup_environment() | 
|  | main(parse_arguments()) |