Add bazel_to_cmake script to assist with synchronizing build systems.
See discussion at https://groups.google.com/forum/#!topic/iree-discuss/Y_Z2n5A-ofY
The primary script here was inspired by https://github.com/google/bazel-to-cmake and contains substantial changes to support generating CMake files with the structure used throughout IREE.
Closes https://github.com/google/iree/pull/556
Co-authored-by: Marius Brehler <marius.brehler@iml.fraunhofer.de>
Co-authored-by: Geoffrey Martin-Noble <gcmn@google.com>
PiperOrigin-RevId: 291446425
diff --git a/build_tools/scripts/bazel_to_cmake.py b/build_tools/scripts/bazel_to_cmake.py
new file mode 100755
index 0000000..3ee92d6
--- /dev/null
+++ b/build_tools/scripts/bazel_to_cmake.py
@@ -0,0 +1,424 @@
+#!/usr/bin/env python3
+# Copyright 2020 Google LLC
+#
+# 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.
+
+# 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/scripts/bazel_to_cmake.py --help
+
+import argparse
+import bazel_to_cmake_targets
+import datetime
+import os
+import textwrap
+
+repo_root = None
+
+
+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)
+
+ # Specify only one of these (defaults to --root_dir=iree).
+ 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",
+ help="Converts all BUILD files under a root directory (defaults to iree/)",
+ default="iree")
+
+ # TODO(scotttodd): --check option that returns success/failure depending on
+ # if files match the converted versions
+
+ 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__))))
+
+
+class BuildFileFunctions(object):
+ """Object passed to `exec` that has handlers for BUILD file functions."""
+
+ def __init__(self, converter):
+ self.converter = converter
+
+ # ------------------------------------------------------------------------- #
+ # Conversion utilities, written to reduce boilerplate and allow for reuse #
+ # between similar rule conversions (e.g. cc_library and cc_binary). #
+ # ------------------------------------------------------------------------- #
+
+ def _convert_name_block(self, **kwargs):
+ # NAME
+ # rule_name
+ return " NAME\n %s\n" % (kwargs["name"])
+
+ def _convert_namespace_block(self, **kwargs):
+ # CC_NAMESPACE
+ # "cc_namespace"
+ return " CC_NAMESPACE\n \"%s\"\n" % (kwargs["cc_namespace"])
+
+ def _convert_translation_block(self, **kwargs):
+ return " TRANSLATION\n \"%s\"\n" % (kwargs["translation"])
+
+ def _convert_option_block(self, option, option_value):
+ if option_value:
+ # Note: this is a truthiness check as well as an existence check, i.e.
+ # Bazel `testonly = False` will be handled correctly by this condition.
+ return " %s\n" % option
+ else:
+ return ""
+
+ def _convert_alwayslink_block(self, **kwargs):
+ return self._convert_option_block("ALWAYSLINK", kwargs.get("alwayslink"))
+
+ def _convert_testonly_block(self, **kwargs):
+ return self._convert_option_block("TESTONLY", kwargs.get("testonly"))
+
+ def _convert_filelist_block(self, list_name, files):
+ if not files:
+ return ""
+
+ # list_name
+ # "file_1.h"
+ # "file_2.h"
+ # "file_3.h"
+ files_list = "\n".join([" \"%s\"" % (file) for file in files])
+ return " %s\n%s\n" % (list_name, files_list)
+
+ def _convert_hdrs_block(self, **kwargs):
+ return self._convert_filelist_block("HDRS", kwargs.get("hdrs"))
+
+ def _convert_srcs_block(self, **kwargs):
+ return self._convert_filelist_block("SRCS", kwargs.get("srcs"))
+
+ def _convert_src_block(self, **kwargs):
+ return " SRC\n \"%s\"\n" % kwargs.get("src")
+
+ def _convert_target(self, target):
+ if target.startswith(":"):
+ # Bazel package-relative `:logging` -> CMake absolute `iree::base::logging`
+ package = os.path.dirname(self.converter.rel_build_file_path)
+ package = package.replace(os.path.sep, "::")
+ if package.endswith(target):
+ target = package # Omit target if it matches the package name
+ else:
+ target = package + ":" + target
+ elif not target.startswith("//iree"):
+ # External target, call helper method for special case handling.
+ target = bazel_to_cmake_targets.convert_external_target(target)
+ else:
+ # Bazel `//iree/base` -> CMake `iree::base`
+ # Bazel `//iree/base:api` -> CMake `iree::base::api`
+ target = target.replace("//", "") # iree/base:api
+ target = target.replace(":", "::") # iree/base::api
+ target = target.replace("/", "::") # iree::base::api
+ return target
+
+ def _convert_deps_block(self, **kwargs):
+ if not kwargs.get("deps"):
+ return ""
+
+ # DEPS
+ # package1::target1
+ # package1::target2
+ # package2::target
+ deps = kwargs.get("deps")
+ deps_list = [self._convert_target(dep) for dep in deps]
+ deps_list = sorted(list(set(deps_list))) # Remove duplicates and sort.
+ deps_list = "\n".join([" %s" % (dep,) for dep in deps_list])
+ return " DEPS\n%s\n" % (deps_list,)
+
+ def _convert_unimplemented_function(self, rule, *args, **kwargs):
+ name = kwargs.get("name", "unnamed")
+ self.converter.body += "# Unimplemented %(rule)s %(name)s\n" % {
+ "rule": rule,
+ "name": name
+ }
+
+ # ------------------------------------------------------------------------- #
+ # Function handlers that convert BUILD definitions to CMake definitions. #
+ # #
+ # Names and signatures must match 1:1 with those expected in BUILD files. #
+ # Each function that may be found in a BUILD file must be listed here. #
+ # ------------------------------------------------------------------------- #
+
+ def load(self, *args):
+ pass
+
+ def package(self, **kwargs):
+ # No mapping to CMake, ignore.
+ pass
+
+ def iree_build_test(self, **kwargs):
+ pass
+
+ def filegroup(self, **kwargs):
+ # Not implemented yet. Might be a no-op, or may want to evaluate the srcs
+ # attribute and pass them along to any targets that depend on the filegroup.
+ # Cross-package dependencies and complicated globs could be hard to handle.
+ self._convert_unimplemented_function("filegroup", **kwargs)
+
+ def exports_files(self, *args, **kwargs):
+ pass
+
+ def glob(self, *args):
+ # Not supported during conversion (yet?).
+ self._convert_unimplemented_function("glob", *args)
+
+ def config_setting(self, **kwargs):
+ # No mapping to CMake, ignore.
+ pass
+
+ def cc_library(self, **kwargs):
+ name_block = self._convert_name_block(**kwargs)
+ hdrs_block = self._convert_hdrs_block(**kwargs)
+ srcs_block = self._convert_srcs_block(**kwargs)
+ deps_block = self._convert_deps_block(**kwargs)
+ alwayslink_block = self._convert_alwayslink_block(**kwargs)
+ testonly_block = self._convert_testonly_block(**kwargs)
+
+ self.converter.body += """iree_cc_library(
+%(name_block)s%(hdrs_block)s%(srcs_block)s%(deps_block)s%(alwayslink_block)s%(testonly_block)s PUBLIC
+)\n\n""" % {
+ "name_block": name_block,
+ "hdrs_block": hdrs_block,
+ "srcs_block": srcs_block,
+ "deps_block": deps_block,
+ "alwayslink_block": alwayslink_block,
+ "testonly_block": testonly_block,
+ }
+
+ def cc_test(self, **kwargs):
+ name_block = self._convert_name_block(**kwargs)
+ hdrs_block = self._convert_hdrs_block(**kwargs)
+ srcs_block = self._convert_srcs_block(**kwargs)
+ deps_block = self._convert_deps_block(**kwargs)
+
+ self.converter.body += """iree_cc_test(
+%(name_block)s%(hdrs_block)s%(srcs_block)s%(deps_block)s)\n\n""" % {
+ "name_block": name_block,
+ "hdrs_block": hdrs_block,
+ "srcs_block": srcs_block,
+ "deps_block": deps_block,
+ }
+
+ def cc_binary(self, **kwargs):
+ name_block = self._convert_name_block(**kwargs)
+ srcs_block = self._convert_srcs_block(**kwargs)
+ deps_block = self._convert_deps_block(**kwargs)
+
+ self.converter.body += """iree_cc_binary(
+%(name_block)s%(srcs_block)s%(deps_block)s)\n\n""" % {
+ "name_block": name_block,
+ "srcs_block": srcs_block,
+ "deps_block": deps_block,
+ }
+
+ def spirv_kernel_cc_library(self, **kwargs):
+ name_block = self._convert_name_block(**kwargs)
+ srcs_block = self._convert_srcs_block(**kwargs)
+
+ self.converter.body += """iree_spirv_kernel_cc_library(
+%(name_block)s%(srcs_block)s)\n\n""" % {
+ "name_block": name_block,
+ "srcs_block": srcs_block,
+ }
+
+ def iree_bytecode_module(self, **kwargs):
+ name_block = self._convert_name_block(**kwargs)
+ src_block = self._convert_src_block(**kwargs)
+ namespace_block = self._convert_namespace_block(**kwargs)
+ translation_block = self._convert_translation_block(**kwargs)
+
+ self.converter.body += """iree_bytecode_module(
+%(name_block)s%(src_block)s%(namespace_block)s%(translation_block)s PUBLIC\n)\n\n""" % {
+ "name_block": name_block,
+ "src_block": src_block,
+ "namespace_block": namespace_block,
+ "translation_block": translation_block,
+ }
+
+ def gentbl(self, **kwargs):
+ self._convert_unimplemented_function("gentbl", **kwargs)
+
+ def cc_embed_data(self, **kwargs):
+ self._convert_unimplemented_function("cc_embed_data", **kwargs)
+
+ def iree_setup_lit_package(self, **kwargs):
+ self._convert_unimplemented_function("iree_setup_lit_package", **kwargs)
+
+ def iree_glob_lit_tests(self, **kwargs):
+ self._convert_unimplemented_function("iree_glob_lit_tests", **kwargs)
+
+
+class Converter(object):
+ """Conversion state tracking and full file template substitution."""
+
+ def __init__(self, directory_path, rel_build_file_path):
+ self.body = ""
+ self.directory_path = directory_path
+ self.rel_build_file_path = rel_build_file_path
+
+ def convert(self):
+ # One `add_subdirectory(name)` per subdirectory.
+ add_subdirectories = ""
+ for root, dirs, file_names in os.walk(self.directory_path):
+ add_subdirectories = "\n".join(
+ ["add_subdirectory(%s)" % (dir,) for dir in dirs])
+ # Stop walk, only add direct subdirectories.
+ break
+
+ converted_file = self.template % {
+ "date_year": datetime.date.today().year,
+ "add_subdirectories": add_subdirectories,
+ "body": self.body,
+ }
+
+ # Cleanup newline characters. This is more convenient than ensuring all
+ # conversions are careful with where they insert newlines.
+ converted_file = converted_file.replace("\n\n\n", "\n")
+ converted_file = converted_file.rstrip() + "\n"
+
+ return converted_file
+
+ template = textwrap.dedent("""\
+ # Copyright %(date_year)s Google LLC
+ #
+ # 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.
+
+ %(add_subdirectories)s
+
+ %(body)s""")
+
+
+def GetDict(obj):
+ ret = {}
+ for k in dir(obj):
+ if not k.startswith("_"):
+ ret[k] = getattr(obj, k)
+ return ret
+
+
+def convert_directory_tree(root_directory_path, write_files):
+ print("convert_directory_tree: %s" % (root_directory_path,))
+ for root, dirs, file_names in os.walk(root_directory_path):
+ convert_directory(root, write_files)
+
+
+def convert_directory(directory_path, write_files):
+ if not os.path.isdir(directory_path):
+ raise FileNotFoundError("Cannot find directory '%s'" % (directory_path,))
+
+ build_file_path = os.path.join(directory_path, "BUILD")
+ cmakelists_file_path = os.path.join(directory_path, "CMakeLists.txt")
+
+ if not os.path.isfile(build_file_path):
+ # No Bazel BUILD file in this directory to convert, skip.
+ return
+
+ global repo_root
+ rel_build_file_path = os.path.relpath(build_file_path, repo_root)
+ rel_cmakelists_file_path = os.path.relpath(cmakelists_file_path, repo_root)
+ print("Converting %s to %s" % (rel_build_file_path, rel_cmakelists_file_path))
+
+ if write_files:
+ # TODO(scotttodd): Attempt to merge instead of overwrite?
+ # Existing CMakeLists.txt may have special logic that should be preserved
+ if os.path.isfile(cmakelists_file_path):
+ print(" %s already exists, overwritting" % (rel_cmakelists_file_path,))
+ else:
+ print(" %s does not exist yet, creating" % (rel_cmakelists_file_path,))
+ print("")
+
+ with open(build_file_path, "rt") as build_file:
+ build_file_code = compile(build_file.read(), build_file_path, "exec")
+ converter = Converter(directory_path, rel_build_file_path)
+ try:
+ exec(build_file_code, GetDict(BuildFileFunctions(converter)))
+ converted_text = converter.convert()
+
+ if write_files:
+ with open(cmakelists_file_path, "wt") as cmakelists_file:
+ cmakelists_file.write(converted_text)
+ else:
+ print(converted_text)
+ except NameError as e:
+ print(
+ "Failed to convert %s. Missing a rule handler in bazel_to_cmake.py?" %
+ (rel_build_file_path))
+ print(" Reason: `%s: %s`" % (type(e).__name__, e))
+ except KeyError as e:
+ print(
+ "Failed to convert %s. Missing a conversion in bazel_to_cmake_targets.py?"
+ % (rel_build_file_path))
+ print(" Reason: `%s: %s`" % (type(e).__name__, e))
+
+
+def main(args):
+ """Runs Bazel to CMake conversion."""
+ global repo_root
+
+ write_files = not args.preview
+
+ if args.root_dir:
+ convert_directory_tree(os.path.join(repo_root, args.root_dir), write_files)
+ elif args.dir:
+ convert_directory(os.path.join(repo_root, args.dir), write_files)
+
+
+if __name__ == "__main__":
+ setup_environment()
+ main(parse_arguments())
diff --git a/build_tools/scripts/bazel_to_cmake_targets.py b/build_tools/scripts/bazel_to_cmake_targets.py
new file mode 100644
index 0000000..e3ad9cc
--- /dev/null
+++ b/build_tools/scripts/bazel_to_cmake_targets.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+# Copyright 2020 Google LLC
+#
+# 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.
+
+# Bazel to CMake target name conversions used by bazel_to_cmake.py.
+
+ABSL_EXPLICIT_TARGET_MAPPING = {
+ "@com_google_absl//absl/flags:flag": "absl::flags",
+ "@com_google_absl//absl/flags:parse": "absl::flags_parse",
+}
+
+
+def _convert_absl_target(target):
+ if target in ABSL_EXPLICIT_TARGET_MAPPING:
+ return ABSL_EXPLICIT_TARGET_MAPPING[target]
+
+ # Default to a pattern substitution approach.
+ # Take "absl::" and append the name part of the full target identifier, e.g.
+ # "@com_google_absl//absl/memory" -> "absl::memory"
+ # "@com_google_absl//absl/types:optional" -> "absl::optional"
+ # "@com_google_absl//absl/types:span" -> "absl::span"
+ if ":" in target:
+ target_name = target.rsplit(":")[-1]
+ else:
+ target_name = target.rsplit("/")[-1]
+ return "absl::" + target_name
+
+
+LLVM_TARGET_MAPPING = {
+ "@llvm-project//llvm:support": "LLVMSupport",
+}
+
+VULKAN_HEADERS_MAPPING = {
+ # TODO(scotttodd): Set -DVK_NO_PROTOTYPES to COPTS for _no_prototypes.
+ # Maybe add a wrapper CMake lib within build_tools/third_party/?
+ "@vulkan_headers//:vulkan_headers": "Vulkan::Headers",
+ "@vulkan_headers//:vulkan_headers_no_prototypes": "Vulkan::Headers",
+}
+
+MLIR_EXPLICIT_TARGET_MAPPING = {
+ "@llvm-project//mlir:AffineDialectRegistration":
+ "MLIRAffineOps",
+ "@llvm-project//mlir:AffineToStandardTransforms":
+ "MLIRAffineToStandard",
+ "@llvm-project//mlir:GPUToSPIRVTransforms":
+ "MLIRGPUtoSPIRVTransforms",
+ "@llvm-project//mlir:GPUTransforms":
+ "MLIRGPU",
+ "@llvm-project//mlir:LinalgDialectRegistration":
+ "MLIRLinalgOps",
+ "@llvm-project//mlir:LoopsToGPUPass":
+ "MLIRLoopsToGPU",
+ "@llvm-project//mlir:SPIRVDialect":
+ "MLIRSPIRV",
+ "@llvm-project//mlir:SPIRVDialectRegistration":
+ "MLIRSPIRV",
+ "@llvm-project//mlir:SPIRVLowering":
+ "MLIRSPIRV",
+ "@llvm-project//mlir:SPIRVTranslateRegistration":
+ "MLIRSPIRVSerialization",
+ "@llvm-project//mlir:StandardDialectRegistration":
+ "MLIRStandardOps",
+ "@llvm-project//mlir:StandardToSPIRVConversions":
+ "MLIRStandardToSPIRVTransforms",
+ "@llvm-project//mlir:MlirOptMain":
+ "MLIROptMain",
+}
+
+
+def _convert_mlir_target(target):
+ if target in MLIR_EXPLICIT_TARGET_MAPPING:
+ return MLIR_EXPLICIT_TARGET_MAPPING[target]
+
+ # Default to a pattern substitution approach.
+ # Take "MLIR" and append the name part of the full target identifier, e.g.
+ # "@llvm-project//mlir:IR" -> "MLIRIR"
+ # "@llvm-project//mlir:Pass" -> "MLIRPass"
+ return "MLIR" + target.rsplit(":")[-1]
+
+
+def convert_external_target(target):
+ """Converts an external (doesn't start with //iree) Bazel target to Cmake.
+
+ IREE targets are expected to follow a standard form between Bazel and CMake
+ that facilitates conversion. External targets *may* have their own patterns,
+ or they may be purely special cases.
+
+ Multiple target in Bazel may map to a single target in CMake.
+ A Bazel target may *not* map to multiple CMake targets.
+
+ Returns:
+ The converted target if it was successfully converted.
+
+ Raises:
+ KeyError: No conversion was found for the target.
+ """
+ if target.startswith("@com_google_absl"):
+ return _convert_absl_target(target)
+ if target == "@com_google_benchmark//:benchmark":
+ return "benchmark"
+ if target == "@com_github_google_flatbuffers//:flatbuffers":
+ return "flatbuffers"
+ if target == "@com_google_googletest//:gtest":
+ return "gtest"
+ if target.startswith("@llvm-project//llvm"):
+ return LLVM_TARGET_MAPPING[target]
+ if target.startswith("@llvm-project//mlir"):
+ return _convert_mlir_target(target)
+ if target.startswith("@org_tensorflow//tensorflow/compiler/mlir"):
+ # All Bazel targets map to a single CMake target.
+ return "tensorflow::mlir_xla"
+ if target.startswith("@org_tensorflow//tensorflow/lite/experimental/ruy"):
+ # All Bazel targets map to a single CMake target.
+ return "ruy"
+ if target.startswith("@vulkan_headers"):
+ return VULKAN_HEADERS_MAPPING[target]
+
+ raise KeyError("No conversion found for target '%s'" % target)