1# pylint: disable=g-bad-file-header 2# Copyright 2016 The Bazel Authors. All rights reserved. 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# http://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"""Base library for configuring the C++ toolchain.""" 16 17def resolve_labels(repository_ctx, labels): 18 """Resolves a collection of labels to their paths. 19 20 Label resolution can cause the evaluation of Starlark functions to restart. 21 For functions with side-effects (like the auto-configuration functions, which 22 inspect the system and touch the file system), such restarts are costly. 23 We cannot avoid the restarts, but we can minimize their penalty by resolving 24 all labels upfront. 25 26 Among other things, doing less work on restarts can cut analysis times by 27 several seconds and may also prevent tickling kernel conditions that cause 28 build failures. See https://github.com/bazelbuild/bazel/issues/5196 for 29 more details. 30 31 Args: 32 repository_ctx: The context with which to resolve the labels. 33 labels: Labels to be resolved expressed as a list of strings. 34 35 Returns: 36 A dictionary with the labels as keys and their paths as values. 37 """ 38 return dict([(label, repository_ctx.path(Label(label))) for label in labels]) 39 40def escape_string(arg): 41 """Escape percent sign (%) in the string so it can appear in the Crosstool.""" 42 if arg != None: 43 return str(arg).replace("%", "%%") 44 else: 45 return None 46 47def split_escaped(string, delimiter): 48 """Split string on the delimiter unless %-escaped. 49 50 Examples: 51 Basic usage: 52 split_escaped("a:b:c", ":") -> [ "a", "b", "c" ] 53 54 Delimiter that is not supposed to be splitten on has to be %-escaped: 55 split_escaped("a%:b", ":") -> [ "a:b" ] 56 57 Literal % can be represented by escaping it as %%: 58 split_escaped("a%%b", ":") -> [ "a%b" ] 59 60 Consecutive delimiters produce empty strings: 61 split_escaped("a::b", ":") -> [ "a", "", "", "b" ] 62 63 Args: 64 string: The string to be split. 65 delimiter: Non-empty string not containing %-sign to be used as a 66 delimiter. 67 68 Returns: 69 A list of substrings. 70 """ 71 if delimiter == "": 72 fail("Delimiter cannot be empty") 73 if delimiter.find("%") != -1: 74 fail("Delimiter cannot contain %-sign") 75 76 i = 0 77 result = [] 78 accumulator = [] 79 length = len(string) 80 delimiter_length = len(delimiter) 81 82 if not string: 83 return [] 84 85 # Iterate over the length of string since Starlark doesn't have while loops 86 for _ in range(length): 87 if i >= length: 88 break 89 if i + 2 <= length and string[i:i + 2] == "%%": 90 accumulator.append("%") 91 i += 2 92 elif (i + 1 + delimiter_length <= length and 93 string[i:i + 1 + delimiter_length] == "%" + delimiter): 94 accumulator.append(delimiter) 95 i += 1 + delimiter_length 96 elif i + delimiter_length <= length and string[i:i + delimiter_length] == delimiter: 97 result.append("".join(accumulator)) 98 accumulator = [] 99 i += delimiter_length 100 else: 101 accumulator.append(string[i]) 102 i += 1 103 104 # Append the last group still in accumulator 105 result.append("".join(accumulator)) 106 return result 107 108def auto_configure_fail(msg): 109 """Output failure message when auto configuration fails.""" 110 red = "\033[0;31m" 111 no_color = "\033[0m" 112 fail("\n%sAuto-Configuration Error:%s %s\n" % (red, no_color, msg)) 113 114def auto_configure_warning(msg): 115 """Output warning message during auto configuration.""" 116 yellow = "\033[1;33m" 117 no_color = "\033[0m" 118 119 # buildifier: disable=print 120 print("\n%sAuto-Configuration Warning:%s %s\n" % (yellow, no_color, msg)) 121 122def get_env_var(repository_ctx, name, default = None, enable_warning = True): 123 """Find an environment variable in system path. Doesn't %-escape the value! 124 125 Args: 126 repository_ctx: The repository context. 127 name: Name of the environment variable. 128 default: Default value to be used when such environment variable is not present. 129 enable_warning: Show warning if the variable is not present. 130 Returns: 131 value of the environment variable or default. 132 """ 133 134 if name in repository_ctx.os.environ: 135 return repository_ctx.os.environ[name] 136 if default != None: 137 if enable_warning: 138 auto_configure_warning("'%s' environment variable is not set, using '%s' as default" % (name, default)) 139 return default 140 auto_configure_fail("'%s' environment variable is not set" % name) 141 return None 142 143def which(repository_ctx, cmd, default = None): 144 """A wrapper around repository_ctx.which() to provide a fallback value. Doesn't %-escape the value! 145 146 Args: 147 repository_ctx: The repository context. 148 cmd: name of the executable to resolve. 149 default: Value to be returned when such executable couldn't be found. 150 Returns: 151 absolute path to the cmd or default when not found. 152 """ 153 result = repository_ctx.which(cmd) 154 return default if result == None else str(result) 155 156def which_cmd(repository_ctx, cmd, default = None): 157 """Find cmd in PATH using repository_ctx.which() and fail if cannot find it. Doesn't %-escape the cmd! 158 159 Args: 160 repository_ctx: The repository context. 161 cmd: name of the executable to resolve. 162 default: Value to be returned when such executable couldn't be found. 163 Returns: 164 absolute path to the cmd or default when not found. 165 """ 166 result = repository_ctx.which(cmd) 167 if result != None: 168 return str(result) 169 path = get_env_var(repository_ctx, "PATH") 170 if default != None: 171 auto_configure_warning("Cannot find %s in PATH, using '%s' as default.\nPATH=%s" % (cmd, default, path)) 172 return default 173 auto_configure_fail("Cannot find %s in PATH, please make sure %s is installed and add its directory in PATH.\nPATH=%s" % (cmd, cmd, path)) 174 return str(result) 175 176def execute( 177 repository_ctx, 178 command, 179 environment = None, 180 expect_failure = False, 181 expect_empty_output = False): 182 """Execute a command, return stdout if succeed and throw an error if it fails. Doesn't %-escape the result! 183 184 Args: 185 repository_ctx: The repository context. 186 command: command to execute. 187 environment: dictionary with environment variables to set for the command. 188 expect_failure: True if the command is expected to fail. 189 expect_empty_output: True if the command is expected to produce no output. 190 Returns: 191 stdout of the executed command. 192 """ 193 if environment: 194 result = repository_ctx.execute(command, environment = environment) 195 else: 196 result = repository_ctx.execute(command) 197 if expect_failure != (result.return_code != 0): 198 if expect_failure: 199 auto_configure_fail( 200 "expected failure, command %s, stderr: (%s)" % ( 201 command, 202 result.stderr, 203 ), 204 ) 205 else: 206 auto_configure_fail( 207 "non-zero exit code: %d, command %s, stderr: (%s)" % ( 208 result.return_code, 209 command, 210 result.stderr, 211 ), 212 ) 213 stripped_stdout = result.stdout.strip() 214 if expect_empty_output != (not stripped_stdout): 215 if expect_empty_output: 216 auto_configure_fail( 217 "non-empty output from command %s, stdout: (%s), stderr: (%s)" % (command, result.stdout, result.stderr), 218 ) 219 else: 220 auto_configure_fail( 221 "empty output from command %s, stderr: (%s)" % (command, result.stderr), 222 ) 223 return stripped_stdout 224 225def get_cpu_value(repository_ctx): 226 """Compute the cpu_value based on the OS name. Doesn't %-escape the result! 227 228 Args: 229 repository_ctx: The repository context. 230 Returns: 231 One of (darwin, freebsd, x64_windows, ppc, s390x, arm, aarch64, k8, piii) 232 """ 233 os_name = repository_ctx.os.name 234 arch = repository_ctx.os.arch 235 if os_name.startswith("mac os"): 236 # Check if we are on x86_64 or arm64 and return the corresponding cpu value. 237 return "darwin_" + ("arm64" if arch == "aarch64" else "x86_64") 238 if os_name.find("freebsd") != -1: 239 return "freebsd" 240 if os_name.find("openbsd") != -1: 241 return "openbsd" 242 if os_name.find("windows") != -1: 243 if arch == "aarch64": 244 return "arm64_windows" 245 else: 246 return "x64_windows" 247 248 if arch in ["power", "ppc64le", "ppc", "ppc64"]: 249 return "ppc" 250 if arch in ["s390x"]: 251 return "s390x" 252 if arch in ["mips64"]: 253 return "mips64" 254 if arch in ["riscv64"]: 255 return "riscv64" 256 if arch in ["arm", "armv7l"]: 257 return "arm" 258 if arch in ["aarch64"]: 259 return "aarch64" 260 return "k8" if arch in ["amd64", "x86_64", "x64"] else "piii" 261 262def is_cc_configure_debug(repository_ctx): 263 """Returns True if CC_CONFIGURE_DEBUG is set to 1.""" 264 env = repository_ctx.os.environ 265 return "CC_CONFIGURE_DEBUG" in env and env["CC_CONFIGURE_DEBUG"] == "1" 266 267def build_flags(flags): 268 """Convert `flags` to a string of flag fields.""" 269 return "\n".join([" flag: '" + flag + "'" for flag in flags]) 270 271def get_starlark_list(values): 272 """Convert a list of string into a string that can be passed to a rule attribute.""" 273 if not values: 274 return "" 275 return "\"" + "\",\n \"".join(values) + "\"" 276 277def auto_configure_warning_maybe(repository_ctx, msg): 278 """Output warning message when CC_CONFIGURE_DEBUG is enabled.""" 279 if is_cc_configure_debug(repository_ctx): 280 auto_configure_warning(msg) 281 282def write_builtin_include_directory_paths(repository_ctx, cc, directories, file_suffix = ""): 283 """Generate output file named 'builtin_include_directory_paths' in the root of the repository.""" 284 if get_env_var(repository_ctx, "BAZEL_IGNORE_SYSTEM_HEADERS_VERSIONS", "0", False) == "1": 285 repository_ctx.file( 286 "builtin_include_directory_paths" + file_suffix, 287 """This file is generated by cc_configure and normally contains builtin include directories 288that C++ compiler reported. But because BAZEL_IGNORE_SYSTEM_HEADERS_VERSIONS was set to 1, 289header include directory paths are intentionally not put there. 290""", 291 ) 292 else: 293 repository_ctx.file( 294 "builtin_include_directory_paths" + file_suffix, 295 """This file is generated by cc_configure and contains builtin include directories 296that %s reported. This file is a dependency of every compilation action and 297changes to it will be reflected in the action cache key. When some of these 298paths change, Bazel will make sure to rerun the action, even though none of 299declared action inputs or the action commandline changes. 300 301%s 302""" % (cc, "\n".join(directories)), 303 ) 304