blob: efb95292ed8c5076a2b4e06fe56294fe077d0080 [file] [log] [blame]
Scott Toddd0a06292020-01-24 14:41:50 -08001#!/usr/bin/env python3
Geoffrey Martin-Noble552d3f82021-05-25 17:56:09 -07002# Copyright 2020 The IREE Authors
Scott Toddd0a06292020-01-24 14:41:50 -08003#
Geoffrey Martin-Noble552d3f82021-05-25 17:56:09 -07004# Licensed under the Apache License v2.0 with LLVM Exceptions.
5# See https://llvm.org/LICENSE.txt for license information.
6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
Geoffrey Martin-Noblebdeb1ab2020-03-31 13:27:14 -07007"""This script assists with converting from Bazel BUILD files to CMakeLists.txt.
Scott Toddd0a06292020-01-24 14:41:50 -08008
Geoffrey Martin-Noblebdeb1ab2020-03-31 13:27:14 -07009Bazel BUILD files should, where possible, be written to use simple features
10that can be directly evaluated and avoid more advanced features like
11variables, list comprehensions, etc.
Scott Toddd0a06292020-01-24 14:41:50 -080012
Geoffrey Martin-Noblebdeb1ab2020-03-31 13:27:14 -070013Generated CMake files will be similar in structure to their source BUILD
14files by using the functions in build_tools/cmake/ that imitate corresponding
15Bazel rules (e.g. cc_library -> iree_cc_library.cmake).
16
17For usage, see:
18 python3 build_tools/bazel_to_cmake/bazel_to_cmake.py --help
Stella Laurenzo6f24a2f2023-03-27 17:08:20 -070019
20Configuration
21-------------
22When invoked, bazel_to_cmake will traverse up from the current directory until
23it finds a ".bazel_to_cmake.cfg.py" file. This file both serves as a marker
24for the repository root and provides repository specific configuration.
25
26The file is evaluated as a module and can have the following customizations:
27
28* DEFAULT_ROOT_DIRS: A list of root directory names that should be processed
29 (relative to the repository root) when invoked without a --repo_root or --dir.
30* REPO_MAP: Mapping of canonical Bazel repo name (i.e. "@iree_core") to what it
31 is known as locally (most commonly the empty string). This is used in global
32 target rules to make sure that they work either in the defining or referencing
33 repository.
34* CustomBuildFileFunctions: A class that extends
35 `bazel_to_cmake_converter.BuildFileFunctions` and injects globals for
36 processing the BUILD file. All symbols that do not start with "_" are
37 available.
38* CustomTargetConverter: A class that extends
39 `bazel_to_cmake_targets.TargetConverter` and customizes target mapping.
40 Typically, this is used for purely local targets in leaf projects (as global
41 targets will be encoded in the main bazel_to_cmake_targets.py file).
Geoffrey Martin-Noblebdeb1ab2020-03-31 13:27:14 -070042"""
Geoffrey Martin-Nobleea9c6832020-02-29 20:26:35 -080043# pylint: disable=missing-docstring
Geoffrey Martin-Nobleea9c6832020-02-29 20:26:35 -080044
Scott Toddd0a06292020-01-24 14:41:50 -080045import argparse
Scott Toddd0a06292020-01-24 14:41:50 -080046import datetime
Stella Laurenzo6f24a2f2023-03-27 17:08:20 -070047import importlib
48import importlib.util
Scott Toddd0a06292020-01-24 14:41:50 -080049import os
Geoffrey Martin-Noble2a36c382020-01-28 15:04:30 -080050import re
Geoffrey Martin-Noblebdeb1ab2020-03-31 13:27:14 -070051import sys
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -080052import textwrap
Stella Laurenzo6f24a2f2023-03-27 17:08:20 -070053import types
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -080054from enum import Enum
Geoffrey Martin-Nobleea9c6832020-02-29 20:26:35 -080055
Geoffrey Martin-Noblebdeb1ab2020-03-31 13:27:14 -070056import bazel_to_cmake_converter
Scott Toddd0a06292020-01-24 14:41:50 -080057
58repo_root = None
Stella Laurenzo6f24a2f2023-03-27 17:08:20 -070059repo_cfg = None
Scott Toddd0a06292020-01-24 14:41:50 -080060
Geoffrey Martin-Noble2a36c382020-01-28 15:04:30 -080061EDIT_BLOCKING_PATTERN = re.compile(
Jakub Kuderskibe24f022023-06-21 14:44:18 -040062 r"bazel[\s_]*to[\s_]*cmake[\s_]*:?[\s_]*do[\s_]*not[\s_]*edit", flags=re.IGNORECASE
63)
Geoffrey Martin-Noble2a36c382020-01-28 15:04:30 -080064
Stella Laurenzobe0f1e12023-04-03 13:34:38 -070065PRESERVE_ABOVE_TAG = "### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_ABOVE_THIS_LINE ###"
66PRESERVE_BELOW_TAG = "### BAZEL_TO_CMAKE_PRESERVES_ALL_CONTENT_BELOW_THIS_LINE ###"
Stella Laurenzo6f24a2f2023-03-27 17:08:20 -070067REPO_CFG_FILE = ".bazel_to_cmake.cfg.py"
68REPO_CFG_MODULE_NAME = "bazel_to_cmake_repo_config"
Geoffrey Martin-Noble98a806c2021-02-25 16:57:31 -080069
Scott Toddd0a06292020-01-24 14:41:50 -080070
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -080071class Status(Enum):
Jakub Kuderskibe24f022023-06-21 14:44:18 -040072 UPDATED = 1
73 NOOP = 2
74 FAILED = 3
75 SKIPPED = 4
76 NO_BUILD_FILE = 5
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -080077
78
Scott Toddd0a06292020-01-24 14:41:50 -080079def parse_arguments():
Jakub Kuderskibe24f022023-06-21 14:44:18 -040080 parser = argparse.ArgumentParser(description="Bazel to CMake conversion helper.")
81 parser.add_argument(
82 "--preview",
83 help="Prints results instead of writing files",
84 action="store_true",
85 default=False,
86 )
87 parser.add_argument(
88 "--allow_partial_conversion",
89 help="Generates partial files, ignoring errors during conversion.",
90 action="store_true",
91 default=False,
92 )
93 parser.add_argument(
94 "--verbosity",
95 "-v",
96 type=int,
97 default=0,
98 help="Specify verbosity level where higher verbosity emits more logging."
99 " 0 (default): Only output errors and summary statistics."
100 " 1: Also output the name of each directory as it's being processed and"
101 " whether the directory is skipped."
102 " 2: Also output when conversion was successful.",
103 )
Scott Toddd0a06292020-01-24 14:41:50 -0800104
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400105 # Specify only one of these (defaults to --root_dir=<main source dirs>).
106 group = parser.add_mutually_exclusive_group()
107 group.add_argument(
108 "--dir", help="Converts the BUILD file in the given directory", default=None
109 )
110 default_root_dirs = (
111 repo_cfg.DEFAULT_ROOT_DIRS if hasattr(repo_cfg, "DEFAULT_ROOT_DIRS") else []
112 )
113 group.add_argument(
114 "--root_dir",
115 nargs="+",
116 help="Converts all BUILD files under a root directory",
117 default=default_root_dirs,
118 )
Scott Toddd0a06292020-01-24 14:41:50 -0800119
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400120 args = parser.parse_args()
Scott Toddd0a06292020-01-24 14:41:50 -0800121
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400122 # --dir takes precedence over --root_dir.
123 # They are mutually exclusive, but the default value is still set.
124 if args.dir:
125 args.root_dir = None
Scott Toddd0a06292020-01-24 14:41:50 -0800126
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400127 return args
Scott Toddd0a06292020-01-24 14:41:50 -0800128
129
130def setup_environment():
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400131 """Sets up some environment globals."""
132 global repo_root
133 global repo_cfg
Scott Toddd0a06292020-01-24 14:41:50 -0800134
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400135 # Scan up the directory tree for a repo config file.
136 check_dir = os.getcwd()
137 while not os.path.exists(os.path.join(check_dir, REPO_CFG_FILE)):
138 new_check_dir = os.path.dirname(check_dir)
139 if not new_check_dir or new_check_dir == check_dir:
140 print(
141 f"ERROR: Could not find {REPO_CFG_FILE} in a parent directory "
142 f"of {os.getcwd()}"
143 )
144 sys.exit(1)
145 check_dir = new_check_dir
146 repo_root = check_dir
147 log(f"Using repo root {repo_root}")
Stella Laurenzo6f24a2f2023-03-27 17:08:20 -0700148
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400149 # Dynamically load the config file as a module.
150 orig_dont_write_bytecode = sys.dont_write_bytecode
151 sys.dont_write_bytecode = True # Don't generate __pycache__ dir
152 repo_cfg_path = os.path.join(repo_root, REPO_CFG_FILE)
153 spec = importlib.util.spec_from_file_location(REPO_CFG_MODULE_NAME, repo_cfg_path)
154 if spec and spec.loader:
155 repo_cfg = importlib.util.module_from_spec(spec)
156 sys.modules[REPO_CFG_MODULE_NAME] = repo_cfg
157 spec.loader.exec_module(repo_cfg)
158 sys.dont_write_bytecode = orig_dont_write_bytecode
159 else:
160 print(f"INTERNAL ERROR: Could not evaluate {repo_cfg_path} as module")
161 sys.exit(1)
Scott Toddd0a06292020-01-24 14:41:50 -0800162
163
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -0800164def repo_relpath(path):
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400165 return os.path.relpath(path, repo_root).replace("\\", "/")
Scott Toddd0a06292020-01-24 14:41:50 -0800166
167
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -0800168def log(string, *args, indent=0, **kwargs):
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400169 print(
170 textwrap.indent(string, prefix=(indent * " ")), *args, **kwargs, file=sys.stderr
171 )
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -0800172
173
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400174def convert_directories(directories, write_files, allow_partial_conversion, verbosity):
175 failure_dirs = []
176 skip_count = 0
177 success_count = 0
178 noop_count = 0
179 for directory in directories:
180 status = convert_directory(
181 directory,
182 write_files=write_files,
183 allow_partial_conversion=allow_partial_conversion,
184 verbosity=verbosity,
185 )
186 if status == Status.FAILED:
187 failure_dirs.append(repo_relpath(directory))
188 elif status == Status.SKIPPED:
189 skip_count += 1
190 elif status == Status.UPDATED:
191 success_count += 1
192 elif status == Status.NOOP:
193 noop_count += 1
Geoffrey Martin-Nobled2cb9962021-02-19 17:30:26 -0800194
Geoffrey Martin-Noble0e34ffa2021-02-25 18:16:45 -0800195 log(
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400196 f"{success_count} CMakeLists.txt files were updated, {skip_count} were"
197 f" skipped, and {noop_count} required no change."
198 )
199 if failure_dirs:
200 log(
201 f"ERROR: Encountered unexpected errors converting {len(failure_dirs)}"
202 " directories:"
203 )
204 log("\n".join(failure_dirs), indent=2)
205 sys.exit(1)
Geoffrey Martin-Noble0e34ffa2021-02-25 18:16:45 -0800206
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400207
208def convert_directory(directory_path, write_files, allow_partial_conversion, verbosity):
209 if not os.path.isdir(directory_path):
210 raise FileNotFoundError(f"Cannot find directory '{directory_path}'")
211
212 rel_dir_path = repo_relpath(directory_path)
213 if verbosity >= 1:
214 log(f"Processing {rel_dir_path}")
215
216 # Scan for a BUILD file.
217 build_file_found = False
218 build_file_basenames = ["BUILD", "BUILD.bazel"]
219 for build_file_basename in build_file_basenames:
220 build_file_path = os.path.join(directory_path, build_file_basename)
221
222 rel_build_file_path = repo_relpath(build_file_path)
223 if os.path.isfile(build_file_path):
224 build_file_found = True
225 break
226 cmakelists_file_path = os.path.join(directory_path, "CMakeLists.txt")
227 rel_cmakelists_file_path = repo_relpath(cmakelists_file_path)
228
229 if not build_file_found:
230 return Status.NO_BUILD_FILE
231
232 autogeneration_tag = f"Autogenerated by {repo_relpath(os.path.abspath(__file__))}"
233
234 header = "\n".join(
235 ["#" * 80]
236 + [
237 l.ljust(79) + "#"
238 for l in [
239 f"# {autogeneration_tag} from",
240 f"# {rel_build_file_path}",
241 "#",
242 "# Use iree_cmake_extra_content from iree/build_defs.oss.bzl to add arbitrary",
243 "# CMake-only content.",
244 "#",
245 f"# To disable autogeneration for this file entirely, delete this header.",
246 ]
247 ]
248 + ["#" * 80]
249 )
250
251 old_lines = []
252 possible_preserved_header_lines = []
253 preserved_footer_lines = ["\n" + PRESERVE_BELOW_TAG + "\n"]
254
255 # Read CMakeLists.txt and check if it has the auto-generated header.
256 found_preserve_below_tag = False
257 found_preserve_above_tag = False
258 if os.path.isfile(cmakelists_file_path):
259 found_autogeneration_tag = False
260 with open(cmakelists_file_path) as f:
261 old_lines = f.readlines()
262
263 for line in old_lines:
264 if not found_preserve_above_tag:
265 possible_preserved_header_lines.append(line)
266 if not found_autogeneration_tag and autogeneration_tag in line:
267 found_autogeneration_tag = True
268 if not found_preserve_below_tag and PRESERVE_BELOW_TAG in line:
269 found_preserve_below_tag = True
270 elif not found_preserve_above_tag and PRESERVE_ABOVE_TAG in line:
271 found_preserve_above_tag = True
272 elif found_preserve_below_tag:
273 preserved_footer_lines.append(line)
274 if not found_autogeneration_tag:
275 if verbosity >= 1:
276 log(f"Skipped. Did not find autogeneration line.", indent=2)
277 return Status.SKIPPED
278 preserved_header = (
279 "".join(possible_preserved_header_lines) if found_preserve_above_tag else ""
280 )
281 preserved_footer = "".join(preserved_footer_lines)
282
283 # Read the Bazel BUILD file and interpret it.
284 with open(build_file_path, "rt") as build_file:
285 build_file_contents = build_file.read()
286 if "bazel-to-cmake: skip" in build_file_contents:
287 return Status.SKIPPED
288 build_file_code = compile(build_file_contents, build_file_path, "exec")
289 try:
290 converted_build_file = bazel_to_cmake_converter.convert_build_file(
291 build_file_code,
292 repo_cfg=repo_cfg,
293 allow_partial_conversion=allow_partial_conversion,
294 )
295 except (NameError, NotImplementedError) as e:
296 log(
297 f"ERROR generating {rel_dir_path}.\n"
298 f"Missing a rule handler in bazel_to_cmake_converter.py?\n"
299 f"Reason: `{type(e).__name__}: {e}`",
300 indent=2,
301 )
302 return Status.FAILED
303 except KeyError as e:
304 log(
305 f"ERROR generating {rel_dir_path}.\n"
306 f"Missing a conversion in bazel_to_cmake_targets.py?\n"
307 f"Reason: `{type(e).__name__}: {e}`",
308 indent=2,
309 )
310 return Status.FAILED
311 converted_content = (
312 preserved_header + header + converted_build_file + preserved_footer
313 )
314 if write_files:
315 with open(cmakelists_file_path, "wt") as cmakelists_file:
316 cmakelists_file.write(converted_content)
317 else:
318 print(converted_content, end="")
319
320 if converted_content == "".join(old_lines):
321 if verbosity >= 2:
322 log(f"{rel_cmakelists_file_path} required no update", indent=2)
323 return Status.NOOP
324
Geoffrey Martin-Noble0e34ffa2021-02-25 18:16:45 -0800325 if verbosity >= 2:
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400326 log(
327 f"Successfly generated {rel_cmakelists_file_path}"
328 f" from {rel_build_file_path}",
329 indent=2,
330 )
331 return Status.UPDATED
Scott Toddd0a06292020-01-24 14:41:50 -0800332
333
334def main(args):
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400335 """Runs Bazel to CMake conversion."""
336 global repo_root
Scott Toddd0a06292020-01-24 14:41:50 -0800337
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400338 write_files = not args.preview
Scott Toddd0a06292020-01-24 14:41:50 -0800339
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400340 if args.root_dir:
341 for root_dir in args.root_dir:
342 root_directory_path = os.path.join(repo_root, root_dir)
343 log(f"Converting directory tree rooted at: {root_directory_path}")
344 convert_directories(
345 (root for root, _, _ in os.walk(root_directory_path)),
346 write_files=write_files,
347 allow_partial_conversion=args.allow_partial_conversion,
348 verbosity=args.verbosity,
349 )
350 elif args.dir:
351 convert_directories(
352 [os.path.join(repo_root, args.dir)],
353 write_files=write_files,
354 allow_partial_conversion=args.allow_partial_conversion,
355 verbosity=args.verbosity,
356 )
357 else:
358 log(
359 f"ERROR: None of --root-dir, --dir arguments or DEFAULT_ROOT_DIRS in "
360 f".bazel_to_cmake.cfg.py: No conversion will be done"
361 )
362 sys.exit(1)
Scott Toddd0a06292020-01-24 14:41:50 -0800363
364
365if __name__ == "__main__":
Jakub Kuderskibe24f022023-06-21 14:44:18 -0400366 setup_environment()
367 main(parse_arguments())