1#!/usr/bin/env python3 2# 3# Copyright (c) 2024 Raspberry Pi (Trading) Ltd. 4# 5# SPDX-License-Identifier: BSD-3-Clause 6# 7# 8# A script to ensure that all declared configuration options match across both 9# CMake and Bazel. 10# 11# Usage: 12# 13# Run from anywhere. 14 15from dataclasses import dataclass 16import glob 17import logging 18import os 19from pathlib import Path 20import re 21import subprocess 22import sys 23from typing import Dict 24 25from bazel_common import SDK_ROOT, setup_logging 26 27_LOG = logging.getLogger(__file__) 28 29CMAKE_FILE_TYPES = ( 30 "**/CMakeLists.txt", 31 "**/*.cmake", 32) 33 34BAZEL_FILE_TYPES = ( 35 "**/BUILD.bazel", 36 "**/*.bzl", 37 "**/*.BUILD", 38) 39 40ATTR_REGEX = re.compile(r",?\s*(?P<key>[^=]+)=(?P<value>[^,]+)") 41 42BAZEL_MODULE_REGEX = re.compile(r'\s*commit\s*=\s*\"(?P<commit>[0-9a-fA-F]+)\"\s*,\s*#\s*keep-in-sync-with-submodule:\s*(?P<dependency>\S*)') 43 44BAZEL_VERSION_REGEX = re.compile(r'module\(\s*name\s*=\s*"pico-sdk",\s*version\s*=\s*"(?P<sdk_version>[^"]+)",?\s*\)') 45 46CMAKE_VERSION_REGEX = re.compile(r'^[^#]*set\(PICO_SDK_VERSION_(?P<part>\S+)\s+(?P<value>\S+)\)') 47 48# Sometimes the build systems are supposed to be implemented differently. This 49# allowlist permits the descriptions to differ between CMake and Bazel. 50BUILD_SYSTEM_DESCRIPTION_DIFFERENCE_ALLOWLIST = ( 51 # Minor semantic differences in Bazel. 52 "PICO_DEFAULT_BOOT_STAGE2_FILE", 53 # In Bazel, not overridable by user environment variables (only flags). 54 "PICO_BOARD", 55 # In Bazel, it's a build label rather than a path. 56 "PICO_CMSIS_PATH", 57 # In Bazel, the semantics of embedded binary info are slightly different. 58 "PICO_PROGRAM_NAME", 59 "PICO_PROGRAM_DESCRIPTION", 60 "PICO_PROGRAM_URL", 61 "PICO_PROGRAM_VERSION_STRING", 62 "PICO_TARGET_NAME", 63) 64 65CMAKE_ONLY_ALLOWLIST = ( 66 # Not relevant to Bazel: toolchain is fetched dynamically, and can be 67 # overridden with native Bazel features. 68 "PICO_TOOLCHAIN_PATH", 69 # Bazel uses native --platforms mechanics. 70 "PICO_PLATFORM", 71 # Named PICO_TOOLCHAIN in Bazel. 72 "PICO_COMPILER", 73 # Entirely irrelevant to Bazel, use Bazel platforms: 74 # https://bazel.build/extending/platforms 75 "PICO_CMAKE_PRELOAD_PLATFORM_FILE", 76 # Both of these are marked as TODO and not actually set up in CMake. 77 "PICO_CMSIS_VENDOR", 78 "PICO_CMSIS_DEVICE", 79 # Bazel build uses PICO_CONFIG_EXTRA_HEADER and PICO_CONFIG_PLATFORM_HEADER 80 # instead. 81 "PICO_CONFIG_HEADER_FILES", 82 "PICO_RP2040_CONFIG_HEADER_FILES", 83 "PICO_HOST_CONFIG_HEADER_FILES", 84 # Bazel uses PICO_CONFIG_HEADER. 85 "PICO_BOARD_CMAKE_DIRS", 86 "PICO_BOARD_HEADER_FILE", 87 "PICO_BOARD_HEADER_DIRS", 88 # Bazel supports this differently. 89 # TODO: Provide a helper rule for explicitly generating a UF2 so users don't 90 # have to write out a bespoke run_binary. 91 "PICO_NO_UF2", 92 # Bazel will not provide a default for this. 93 # TODO: Provide handy rules for PIOASM so users don't have to write out a 94 # bespoke run_binary. 95 "PICO_DEFAULT_PIOASM_OUTPUT_FORMAT", 96 # Bazel always has picotool. 97 "PICO_NO_PICOTOOL", 98 # These aren't supported as build flags in Bazel. Prefer to 99 # set these in board header files like other SDK defines. 100 "CYW43_DEFAULT_PIN_WL_REG_ON", 101 "CYW43_DEFAULT_PIN_WL_DATA_OUT", 102 "CYW43_DEFAULT_PIN_WL_DATA_IN", 103 "CYW43_DEFAULT_PIN_WL_HOST_WAKE", 104 "CYW43_DEFAULT_PIN_WL_CLOCK", 105 "CYW43_DEFAULT_PIN_WL_CS", 106 "CYW43_PIO_CLOCK_DIV_INT", 107 "CYW43_PIO_CLOCK_DIV_FRAC", 108 "CYW43_PIO_CLOCK_DIV_DYNAMIC", 109) 110 111BAZEL_ONLY_ALLOWLIST = ( 112 # Allows users to fully replace the final image for boot_stage2. 113 "PICO_BOOT_STAGE2_LINK_IMAGE", 114 # Allows users to inject an alternative TinyUSB library since TinyUSB 115 # doesn't have native Bazel support. 116 "PICO_TINYUSB_LIB", 117 # Bazel can't do pico_set_* for the binary info defines, so there's a 118 # different mechanism. 119 "PICO_DEFAULT_BINARY_INFO", 120 # Bazel analogue for PICO_CMAKE_BUILD_TYPE. 121 "PICO_BAZEL_BUILD_TYPE", 122 # Different mechanism for setting a linker script that is less complex. 123 "PICO_DEFAULT_LINKER_SCRIPT", 124 # Not yet documented in CMake (but probably should be): 125 "PICO_CMAKE_BUILD_TYPE", 126 # Replaces PICO_RP2040_CONFIG_HEADER_FILES and 127 # PICO_HOST_CONFIG_HEADER_FILES. 128 "PICO_CONFIG_EXTRA_HEADER", 129 "PICO_CONFIG_PLATFORM_HEADER", 130 # Effectively replaces: 131 # - PICO_BOARD_CMAKE_DIRS 132 # - PICO_BOARD_HEADER_FILE 133 # - PICO_BOARD_HEADER_DIRS 134 "PICO_CONFIG_HEADER", 135 # Bazel configuration for 3p deps. 136 "PICO_BTSTACK_CONFIG", 137 "PICO_LWIP_CONFIG", 138 "PICO_FREERTOS_LIB", 139 "PICO_MBEDTLS_LIB", 140 # CMake has PICO_DEFAULT_CLIB, but it's not user-facing. 141 "PICO_CLIB", 142 # Selecting default library implementations. 143 "PICO_MULTICORE_ENABLED", 144 "PICO_DEFAULT_DOUBLE_IMPL", 145 "PICO_DEFAULT_FLOAT_IMPL", 146 "PICO_DEFAULT_DIVIDER_IMPL", 147 "PICO_DEFAULT_PRINTF_IMPL", 148 "PICO_DEFAULT_RAND_IMPL", 149 "PICO_BINARY_INFO_ENABLED", 150 "PICO_ASYNC_CONTEXT_IMPL", 151 # Allows selection of clang/gcc when using the dynamically fetched 152 # toolchains. 153 "PICO_TOOLCHAIN", 154 # In CMake, linking these libraries also sets defines for adjacent 155 # libraries. That's an antipattern in Bazel, so there's flags to control 156 # which modules to enable instead. 157 "PICO_BT_ENABLE_BLE", 158 "PICO_BT_ENABLE_CLASSIC", 159 "PICO_BT_ENABLE_MESH", 160) 161 162 163@dataclass 164class Option: 165 name: str 166 description: str 167 attrs: Dict[str, str] 168 169 def matches(self, other): 170 matches = (self.name == other.name) and (self.attrs == other.attrs) 171 if not self.name in BUILD_SYSTEM_DESCRIPTION_DIFFERENCE_ALLOWLIST: 172 matches = matches and (self.description == other.description) 173 return matches 174 175 176def FindKnownOptions(option_pattern_matcher, file_paths): 177 pattern = re.compile( 178 option_pattern_matcher 179 + r":\s+(?P<name>\w+),\s+(?P<description>[^,]+)(?:,\s+(?P<attrs>.*))?$" 180 ) 181 options = {} 182 for p in file_paths: 183 with open(p, "r") as f: 184 for line in f: 185 match = re.search(pattern, line) 186 if not match: 187 continue 188 189 attrs = { 190 m.group("key"): m.group("value") 191 for m in re.finditer(ATTR_REGEX, match.group("attrs")) 192 } 193 194 options[match.group("name")] = Option( 195 match.group("name"), 196 match.group("description"), 197 attrs, 198 ) 199 return options 200 201 202def OptionsAreEqual(bazel_option, cmake_option, warnings_as_errors): 203 if bazel_option is None: 204 if cmake_option.name in CMAKE_ONLY_ALLOWLIST: 205 return True 206 _LOG.warning(f" {cmake_option.name} does not exist in Bazel") 207 return not warnings_as_errors 208 elif cmake_option is None: 209 if bazel_option.name in BAZEL_ONLY_ALLOWLIST: 210 return True 211 _LOG.warning(f" {bazel_option.name} does not exist in CMake") 212 return not warnings_as_errors 213 elif not bazel_option.matches(cmake_option): 214 _LOG.error(" Bazel and CMAKE definitions do not match:") 215 _LOG.error(f" [CMAKE] {bazel_option}") 216 _LOG.error(f" [BAZEL] {cmake_option}") 217 return False 218 219 return True 220 221 222def CompareOptions(bazel_pattern, bazel_files, cmake_pattern, cmake_files, warnings_as_errors=True): 223 bazel_options = FindKnownOptions(bazel_pattern, bazel_files) 224 cmake_options = FindKnownOptions(cmake_pattern, cmake_files) 225 226 are_equal = True 227 both = {} 228 both.update(bazel_options) 229 both.update(cmake_options) 230 for k in both.keys(): 231 if not OptionsAreEqual( 232 bazel_options.get(k, None), 233 cmake_options.get(k, None), 234 warnings_as_errors, 235 ): 236 are_equal = False 237 return are_equal 238 239def CompareExternalDependencyVersions(): 240 pattern = re.compile(BAZEL_MODULE_REGEX) 241 all_okay = True 242 with open(Path(SDK_ROOT) / "MODULE.bazel", "r") as bazel_module_file: 243 for line in bazel_module_file: 244 maybe_match = pattern.match(line) 245 if not maybe_match: 246 continue 247 248 current_submodule_pin = subprocess.run( 249 ("git", "-C", SDK_ROOT, "rev-parse", f'HEAD:{maybe_match.group("dependency")}'), 250 text=True, 251 check=True, 252 capture_output=True, 253 ).stdout.strip() 254 if current_submodule_pin != maybe_match.group("commit"): 255 _LOG.error(" External pins for %s do not match:", maybe_match.group("dependency")) 256 _LOG.error(" [CMAKE] %s", current_submodule_pin) 257 _LOG.error(" [BAZEL] %s", maybe_match.group("commit")) 258 all_okay = False 259 else: 260 _LOG.info(" External pins for %s match!", maybe_match.group("dependency")) 261 262 return all_okay 263 264def CompareSdkVersion(): 265 # Find version string specified in Bazel. 266 bazel_module_file_path = Path(SDK_ROOT) / "MODULE.bazel" 267 bazel_module_file_contents = bazel_module_file_path.read_text() 268 bazel_sdk_version = BAZEL_VERSION_REGEX.search(bazel_module_file_contents) 269 if not bazel_sdk_version: 270 _LOG.error(" Failed to find Bazel Pico SDK version string") 271 return False 272 bazel_version_string = bazel_sdk_version.group("sdk_version") 273 274 # Find version string specified in CMake. 275 cmake_version_parts = {} 276 with open(Path(SDK_ROOT) / "pico_sdk_version.cmake", "r") as cmake_version_file: 277 for line in cmake_version_file: 278 match = CMAKE_VERSION_REGEX.match(line) 279 if match: 280 cmake_version_parts[match.group("part")] = match.group("value") 281 if len(cmake_version_parts) < 3: 282 _LOG.error(" Failed to find CMake Pico SDK version string") 283 return False 284 cmake_version_string = ".".join(( 285 cmake_version_parts["MAJOR"], 286 cmake_version_parts["MINOR"], 287 cmake_version_parts["REVISION"], 288 )) 289 if "PRE_RELEASE_ID" in cmake_version_parts: 290 cmake_version_string += "-" + cmake_version_parts["PRE_RELEASE_ID"] 291 292 if cmake_version_string != bazel_version_string: 293 _LOG.error(" Declared CMake SDK version is %s and Bazel is %s", cmake_version_string, bazel_version_string) 294 return False 295 296 return True 297 298def compare_build_systems(): 299 cmake_files = [ 300 f 301 for p in CMAKE_FILE_TYPES 302 for f in glob.glob(os.path.join(SDK_ROOT, p), recursive=True) 303 ] 304 bazel_files = [ 305 f 306 for p in BAZEL_FILE_TYPES 307 for f in glob.glob(os.path.join(SDK_ROOT, p), recursive=True) 308 ] 309 310 results = [] 311 _LOG.info("[1/3] Checking build system configuration flags...") 312 results.append(CompareOptions( 313 "PICO_BAZEL_CONFIG", 314 bazel_files, 315 "PICO_CMAKE_CONFIG", 316 cmake_files, 317 # For now, allow CMake and Bazel to go out of sync when it comes to 318 # build configurability since it's a big ask to make contributors 319 # implement the same functionality in both builds. 320 warnings_as_errors=False, 321 )) 322 323 _LOG.info("[2/4] Checking build system defines...") 324 results.append(CompareOptions( 325 "PICO_BUILD_DEFINE", bazel_files, "PICO_BUILD_DEFINE", cmake_files 326 )) 327 328 _LOG.info("[3/4] Checking submodule pins...") 329 results.append(CompareExternalDependencyVersions()) 330 331 _LOG.info("[4/4] Checking version strings...") 332 results.append(CompareSdkVersion()) 333 334 if False not in results: 335 _LOG.info("Passed with no blocking failures") 336 return 0 337 338 _LOG.error("One or more blocking failures detected") 339 return 1 340 341 342if __name__ == "__main__": 343 setup_logging() 344 sys.exit(compare_build_systems()) 345