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