blob: 4918d6fef51969919889c77395b6228b1d8471a0 [file] [log] [blame]
#!/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
from collections import OrderedDict
from itertools import repeat
import glob
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_cc_namespace_block(self, **kwargs):
# CC_NAMESPACE
# "cc_namespace"
return " CC_NAMESPACE\n \"%s\"\n" % (kwargs["cc_namespace"])
def _convert_cpp_namespace_block(self, **kwargs):
# CPP_NAMESPACE
# "cpp_namespace"
return " CPP_NAMESPACE\n \"%s\"\n" % (kwargs["cpp_namespace"])
def _convert_translation_block(self, **kwargs):
return " TRANSLATION\n \"%s\"\n" % (kwargs["translation"])
def _convert_translate_tool_block(self, **kwargs):
translate_tool = kwargs.get("translate_tool")
if translate_tool:
# Bazel `//iree/base` -> CMake `iree::base`
# Bazel `//iree/base:api` -> CMake `iree::base::api`
translate_tool = translate_tool.replace("//", "") # iree/base:api
translate_tool = translate_tool.replace(":", "_") # iree/base::api
translate_tool = translate_tool.replace("/", "_") # iree::base::api
return " TRANSLATION_TOOL\n %s\n" % (translate_tool)
else:
return ""
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_flatten_block(self, **kwargs):
return self._convert_option_block("FLATTEN", kwargs.get("flatten"))
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_cc_file_output_block(self, **kwargs):
return " CC_FILE_OUTPUT\n \"%s\"\n" % kwargs.get("cc_file_output")
def _convert_h_file_output_block(self, **kwargs):
return " H_FILE_OUTPUT\n \"%s\"\n" % kwargs.get("h_file_output")
def _convert_td_file_block(self, **kwargs):
td_file = kwargs.get("td_file")
if td_file.startswith("//iree"):
# Bazel `//iree/dir/td_file.td`
# -> CMake `${IREE_ROOT_DIR}/iree/dir/td_file.td
# Bazel `//iree/dir/IR:td_file.td`
# -> CMake `${IREE_ROOT_DIR}/iree/dir/IR/td_file.td
td_file = td_file.replace("//", "${IREE_ROOT_DIR}/")
td_file = td_file.replace(":", "/")
return " TD_FILE\n \"%s\"\n" % (td_file)
def _convert_tbl_outs_block(self, **kwargs):
tbl_outs = kwargs.get("tbl_outs")
outs_list = "\n".join([" %s %s" % tbl_out for tbl_out in tbl_outs])
return " OUTS\n%s\n" % (outs_list)
def _convert_tblgen_block(self, **kwargs):
tblgen = kwargs.get("tblgen")
if tblgen.endswith("iree-tblgen"):
return " TBLGEN\n IREE\n"
else:
return ""
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
if target.endswith("_gen"):
# Files created by gentbl have to be included as source and header files
# and not as a dependency. Adding these targets to the dependencies list,
# results in linkage failures if the library including the gentbl dep is
# marked as ALWAYSLINK.
# TODO: Some targets not to be included end to "Gen", but others like
# LLVMTableGen still need to be included.
return ""
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_data_block(self, **kwargs):
if not kwargs.get("data"):
return ""
# DATA
# package1::target1
# package1::target2
# package2::target
data = kwargs.get("data")
data_list = [self._convert_target(dep) for dep in data]
# Remove Falsey (None and empty string) values and duplicates, preserving the original ordering.
data_list = list(filter(None, OrderedDict(zip(data_list, repeat(None)))))
data_list = "\n".join([" %s" % (data,) for data in data_list])
return " DATA\n%s\n" % (data_list,)
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]
# Remove Falsey (None and empty string) values and duplicates, preserving the original ordering.
deps_list = list(filter(None, OrderedDict(zip(deps_list, repeat(None)))))
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, include, exclude=[], exclude_directories=1):
# Rather than converting bazel globs into CMake globs, we evaluate the glob at
# conversion time. This avoids issues with different glob semantics and dire
# warnings about not knowing when to reevaluate the glob.
# See https://cmake.org/cmake/help/v3.12/command/file.html#filesystem
if exclude_directories != 1:
raise ValueError("Non-default exclude_directories not supported")
filepaths = []
for pattern in include:
if "**" in pattern:
# bazel's glob has some specific restrictions about crossing package boundaries.
# We have no uses of recursive globs. Rather than try to emulate them or
# silently give different behavior, just error out.
# See https://docs.bazel.build/versions/master/be/functions.html#glob
raise ValueError("Recursive globs not supported")
filepaths += glob.glob(self.converter.directory_path + "/" + pattern)
exclude_filepaths = set([])
for pattern in exclude:
if "**" in pattern:
# See comment above
raise ValueError("Recursive globs not supported")
exclude_filepaths.update(
glob.glob(self.converter.directory_path + "/" + pattern))
basenames = sorted([
os.path.basename(path)
for path in filepaths
if path not in exclude_filepaths
])
return basenames
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 cc_embed_data(self, **kwargs):
name_block = self._convert_name_block(**kwargs)
srcs_block = self._convert_srcs_block(**kwargs)
cc_file_output_block = self._convert_cc_file_output_block(**kwargs)
h_file_output_block = self._convert_h_file_output_block(**kwargs)
namespace_block = self._convert_cpp_namespace_block(**kwargs)
flatten_block = self._convert_flatten_block(**kwargs)
self.converter.body += """iree_cc_embed_data(
%(name_block)s%(srcs_block)s%(cc_file_output_block)s%(h_file_output_block)s%(namespace_block)s%(flatten_block)s PUBLIC\n)\n\n""" % {
"name_block": name_block,
"srcs_block": srcs_block,
"cc_file_output_block": cc_file_output_block,
"h_file_output_block": h_file_output_block,
"namespace_block": namespace_block,
"flatten_block": flatten_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_cc_namespace_block(**kwargs)
translate_tool_block = self._convert_translate_tool_block(**kwargs)
translation_block = self._convert_translation_block(**kwargs)
self.converter.body += """iree_bytecode_module(
%(name_block)s%(src_block)s%(namespace_block)s%(translate_tool_block)s%(translation_block)s PUBLIC\n)\n\n""" % {
"name_block": name_block,
"src_block": src_block,
"namespace_block": namespace_block,
"translate_tool_block": translate_tool_block,
"translation_block": translation_block,
}
def gentbl(self, **kwargs):
name_block = self._convert_name_block(**kwargs)
td_file_block = self._convert_td_file_block(**kwargs)
outs_block = self._convert_tbl_outs_block(**kwargs)
tblgen_block = self._convert_tblgen_block(**kwargs)
self.converter.body += """iree_tablegen_library(
%(name_block)s%(td_file_block)s%(outs_block)s%(tblgen_block)s)\n\n""" % {
"name_block": name_block,
"td_file_block": td_file_block,
"outs_block": outs_block,
"tblgen_block": tblgen_block,
}
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)
def iree_lit_test_suite(self, **kwargs):
name_block = self._convert_name_block(**kwargs)
srcs_block = self._convert_srcs_block(**kwargs)
data_block = self._convert_data_block(**kwargs)
self.converter.body += ("iree_lit_test_suite(\n"
"%(name_block)s"
"%(srcs_block)s"
"%(data_block)s)\n\n" % {
"name_block": name_block,
"srcs_block": srcs_block,
"data_block": data_block,
})
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())