xref: /aosp_15_r20/external/ruy/cmake/bazel_to_cmake.py (revision bb86c7ed5fb1b98a7eac808e443a46cc8b90dfc0)
1#!/usr/bin/env python3
2# Copyright 2021 Google LLC
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      https://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""This is yet another bazel-to-cmake converter. It's independently written from
17scratch but relies on the same basic idea as others (including IREE's), namely:
18to let the python interpreter do the bulk of the work, exploiting the fact that
19both Bazel's BUILD syntax and Starlark (".bzl") languages are more or less
20subsets of Python.
21
22The main features that this converter supports and that others don't, justifying
23its existence as of early 2021, are:
24  1. Ad-hoc support for select(), generating CMake if()...elseif()... chains
25     parsing the condition keys (e.g. anything ending in ":windows" is
26     interpreted as the condition "the target platform is Windows"). This allows
27     to just ignore config_setting, as we only care about the config_setting
28     names, not their actual implementation, as well as all the variants from
29     the Bazel 'selects' library.
30  2. Support for load(), loading macros from Starlark files.
31"""
32
33import re
34import os
35import os.path
36import pickle
37import sys
38import datetime
39import itertools
40
41# Ruy's dependencies.
42external_targets = ['gtest', 'gtest_main', 'cpuinfo']
43
44# Text replacements [oldstring, newstring] pairs, applied on all BUILD and
45# Starlark files that we load. Only used by preprocess_input_text.
46replacements = [
47    ['$(STACK_FRAME_UNLIMITED)', ''],
48    ['native.cc_', 'cc_'],
49    ['selects.config_setting_group', 'config_setting_group'],
50    ['@com_google_googletest//:gtest', 'gtest'],
51    ['@com_google_googletest//:gtest_main', 'gtest_main'],
52    ['@cpuinfo', 'cpuinfo::cpuinfo'],
53]
54
55
56def preprocess_input_text(text):
57  result = text
58  for replacement in replacements:
59    result = result.replace(replacement[0], replacement[1])
60  return result
61
62
63def set_cmake_list(list_name, values, indent):
64  semicolon_separated = ';'.join(values)
65  print(f'{indent}set({list_name} "{semicolon_separated}")')
66
67
68def generate_cmake_select(select_name, dict):
69  new_if_branch_keyword = 'if'
70  default_value = []
71  for key in dict:
72    condition = ''
73    if key == '//conditions:default':
74      default_value = dict[key]
75      continue
76    elif re.search(r':windows$', key):
77      condition = 'CMAKE_SYSTEM_NAME STREQUAL Windows'
78    elif re.search(r':ppc$', key):
79      condition = ('CMAKE_SYSTEM_PROCESSOR STREQUAL ppc64 OR '
80                   'CMAKE_SYSTEM_PROCESSOR STREQUAL ppc64le')
81    elif re.search(r':s390x$', key):
82      condition = ('CMAKE_SYSTEM_PROCESSOR STREQUAL s390 OR '
83                   'CMAKE_SYSTEM_PROCESSOR STREQUAL s390x')
84    elif re.search(r':fuchsia$', key):
85      condition = 'CMAKE_SYSTEM_NAME STREQUAL Fuchsia'
86    elif re.search(r':arm32_assuming_neon$', key):
87      condition = 'CMAKE_SYSTEM_PROCESSOR STREQUAL arm'
88    elif re.search(r':do_not_want_O3$', key):
89      # Ruy is a specialist library: we always want code to be compiled
90      # with -O3 unless the build type is Debug or the compiler does not
91      # support that flag syntax.
92      condition = '(CMAKE_BUILD_TYPE STREQUAL Debug) OR MSVC'
93    elif re.search(r':x86_64_and_not_msvc$', key):
94      condition = ('(CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64 OR '
95                   'CMAKE_SYSTEM_PROCESSOR STREQUAL amd64) AND NOT MSVC')
96    elif re.search(r':windows_msvc$', key):
97      condition = 'MSVC'
98    elif re.search(r':ruy_profiler$', key):
99      condition = '${RUY_PROFILER}'
100    else:
101      raise ValueError(f'Unhandled key in select: {key}')
102
103    print(f'{new_if_branch_keyword}({condition})')
104    set_cmake_list(select_name, dict[key], '  ')
105    new_if_branch_keyword = 'elseif'
106
107  print('else()')
108  set_cmake_list(select_name, default_value, '  ')
109
110  print('endif()\n')
111
112
113def trim_multiple_ruy_prefixes(name):
114  return re.sub(r'(ruy_)+ruy', 'ruy', name)
115
116
117def get_cmake_local_target_name(name):
118  global package_prefix
119  return trim_multiple_ruy_prefixes(f'ruy_{package_prefix}_{name}')
120
121
122def get_cmake_dep_target_name(name):
123  if name in external_targets:
124    return name
125  if name.startswith('$'):
126    # Happens for deps that are the result of expanding a select() that we
127    # have compiled to expanding a variable.
128    return name
129  if name.startswith('//'):
130    after_last_slash = name.split('/')[-1]
131    if ':' not in after_last_slash:
132      name = f'{name}:{after_last_slash}'
133    raw = name[2:].replace('/', '_').replace(':', '_')
134    return trim_multiple_ruy_prefixes(raw)
135  if name.startswith(':'):
136    name = name[1:]
137  return get_cmake_local_target_name(name)
138
139
140#
141# Functions implementing BUILD functions
142#
143
144
145def package(**kwargs):
146  pass
147
148
149def exports_files(*args):
150  pass
151
152
153def load(filename, *args):
154  if filename.startswith('@'):
155    return
156  elif filename.startswith(':'):
157    filename = os.path.join(bazel_package_dir, filename[1:])
158  elif filename.startswith('//'):
159    split = filename[2:].split(':')
160    filename = os.path.join(bazel_workspace_dir, split[0], split[1])
161
162  src_file_content = open(filename).read()
163  processed_file_content = preprocess_input_text(src_file_content)
164  exec(processed_file_content, globals(), globals())
165
166
167def config_setting(**kwargs):
168  # Nothing to do since our implementation of select() is based on parsing
169  # the names of config_settings, not looking deep into their actual
170  # implementation.
171  pass
172
173
174def filegroup(**kwargs):
175  pass
176
177
178def config_setting_group(**kwargs):
179  # See config_setting.
180  pass
181
182
183def bzl_library(**kwargs):
184  pass
185
186
187select_index = 0
188select_cache = {}
189
190
191def select(select_dict):
192  global select_index
193  global select_cache
194  global package_prefix
195  key = pickle.dumps(sorted(select_dict.items()))
196  if key in select_cache:
197    select_name = select_cache[key]
198  else:
199    unique_values = sorted(
200        set(itertools.chain.from_iterable(select_dict.values()))
201    )  # sorting ensures determinism, no spurious diffs
202    description = '_'.join(unique_values)
203    select_name = f'{package_prefix}_{select_index}_{description}'
204    select_name = select_name.replace('c++', 'cxx')
205    select_name = re.sub(r'[^a-zA-Z0-9]+', '_', select_name)
206    select_index = select_index + 1
207    select_cache[key] = select_name
208    generate_cmake_select(select_name, select_dict)
209
210  return [f'${{{select_name}}}']
211
212
213def generic_rule(rule_name, **kwargs):
214  print(f'{rule_name}(')
215  for key in kwargs.keys():
216    values = kwargs[key]
217    if type(values) is bool:
218      if values:
219        print(f'  {key.upper()}')
220        continue
221      else:
222        raise ValueError('Cannot specify FALSE boolean args in CMake')
223    if key == 'visibility':
224      if values == ['//visibility:public']:
225        print(f'  PUBLIC')
226      continue
227    if key == 'tags':
228      values = list(filter(lambda x: not x.startswith('req_dep'), values))
229    if not values:
230      continue
231    print(f'  {key.upper()}')
232    if type(values) is list:
233      for value in values:
234        if key == 'deps':
235          target_name = get_cmake_dep_target_name(value)
236          print(f'    {target_name}')
237        else:
238          print(f'    {value}')
239    else:
240      if key == 'name':
241        target_name = get_cmake_local_target_name(values)
242        print(f'    {target_name}')
243      else:
244        print(f'    {values}')
245  print(')\n')
246
247
248def cc_library(**kwargs):
249  generic_rule('ruy_cc_library', **kwargs)
250
251
252def cc_test(**kwargs):
253  generic_rule('ruy_cc_test', **kwargs)
254
255
256def cc_binary(**kwargs):
257  generic_rule('ruy_cc_binary', **kwargs)
258
259
260#
261# Program entry point.
262#
263if __name__ == "__main__":
264  if len(sys.argv) != 3:
265    print('Usage: bazel_to_cmake.py bazel_workspace_dir bazel_package_dir')
266    sys.exit(1)
267
268  bazel_workspace_dir = sys.argv[1]
269  bazel_package_dir = sys.argv[2]
270  bazel_package_relative_dir = os.path.relpath(bazel_package_dir,
271                                               bazel_workspace_dir)
272  package_prefix = bazel_package_relative_dir.replace(os.path.sep, '_')
273
274  print("""# This file is generated (whence no license header). Do not edit!
275# To regenerate, run:
276#   cmake/bazel_to_cmake.sh
277""")
278
279  src_build_file = os.path.join(bazel_package_dir, 'BUILD')
280  src_build_content = open(src_build_file).read()
281  processed_build_content = preprocess_input_text(src_build_content)
282  exec(processed_build_content)
283
284  print('ruy_add_all_subdirs()')
285