xref: /aosp_15_r20/external/bazelbuild-rules_cc/cc/private/toolchain/lib_cc_configure.bzl (revision eed53cd41c5909d05eedc7ad9720bb158fd93452)
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      Delimeter 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    return auto_configure_fail("'%s' environment variable is not set" % name)
141
142def which(repository_ctx, cmd, default = None):
143    """A wrapper around repository_ctx.which() to provide a fallback value. Doesn't %-escape the value!
144
145    Args:
146      repository_ctx: The repository context.
147      cmd: name of the executable to resolve.
148      default: Value to be returned when such executable couldn't be found.
149    Returns:
150      absolute path to the cmd or default when not found.
151    """
152    result = repository_ctx.which(cmd)
153    return default if result == None else str(result)
154
155def which_cmd(repository_ctx, cmd, default = None):
156    """Find cmd in PATH using repository_ctx.which() and fail if cannot find it. Doesn't %-escape the cmd!
157
158    Args:
159      repository_ctx: The repository context.
160      cmd: name of the executable to resolve.
161      default: Value to be returned when such executable couldn't be found.
162    Returns:
163      absolute path to the cmd or default when not found.
164    """
165    result = repository_ctx.which(cmd)
166    if result != None:
167        return str(result)
168    path = get_env_var(repository_ctx, "PATH")
169    if default != None:
170        auto_configure_warning("Cannot find %s in PATH, using '%s' as default.\nPATH=%s" % (cmd, default, path))
171        return default
172    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))
173    return str(result)
174
175def execute(
176        repository_ctx,
177        command,
178        environment = None,
179        expect_failure = False):
180    """Execute a command, return stdout if succeed and throw an error if it fails. Doesn't %-escape the result!
181
182    Args:
183      repository_ctx: The repository context.
184      command: command to execute.
185      environment: dictionary with environment variables to set for the command.
186      expect_failure: True if the command is expected to fail.
187    Returns:
188      stdout of the executed command.
189    """
190    if environment:
191        result = repository_ctx.execute(command, environment = environment)
192    else:
193        result = repository_ctx.execute(command)
194    if expect_failure != (result.return_code != 0):
195        if expect_failure:
196            auto_configure_fail(
197                "expected failure, command %s, stderr: (%s)" % (
198                    command,
199                    result.stderr,
200                ),
201            )
202        else:
203            auto_configure_fail(
204                "non-zero exit code: %d, command %s, stderr: (%s)" % (
205                    result.return_code,
206                    command,
207                    result.stderr,
208                ),
209            )
210    stripped_stdout = result.stdout.strip()
211    if not stripped_stdout:
212        auto_configure_fail(
213            "empty output from command %s, stderr: (%s)" % (command, result.stderr),
214        )
215    return stripped_stdout
216
217def get_cpu_value(repository_ctx):
218    """Compute the cpu_value based on the OS name. Doesn't %-escape the result!
219
220    Args:
221      repository_ctx: The repository context.
222    Returns:
223      One of (darwin, freebsd, x64_windows, ppc, s390x, arm, aarch64, k8, piii)
224    """
225    os_name = repository_ctx.os.name.lower()
226    if os_name.startswith("mac os"):
227        return "darwin"
228    if os_name.find("freebsd") != -1:
229        return "freebsd"
230    if os_name.find("windows") != -1:
231        return "x64_windows"
232
233    # Use uname to figure out whether we are on x86_32 or x86_64
234    result = repository_ctx.execute(["uname", "-m"])
235    if result.stdout.strip() in ["power", "ppc64le", "ppc", "ppc64"]:
236        return "ppc"
237    if result.stdout.strip() in ["s390x"]:
238        return "s390x"
239    if result.stdout.strip() in ["arm", "armv7l"]:
240        return "arm"
241    if result.stdout.strip() in ["aarch64"]:
242        return "aarch64"
243    return "k8" if result.stdout.strip() in ["amd64", "x86_64", "x64"] else "piii"
244
245def is_cc_configure_debug(repository_ctx):
246    """Returns True if CC_CONFIGURE_DEBUG is set to 1."""
247    env = repository_ctx.os.environ
248    return "CC_CONFIGURE_DEBUG" in env and env["CC_CONFIGURE_DEBUG"] == "1"
249
250def build_flags(flags):
251    """Convert `flags` to a string of flag fields."""
252    return "\n".join(["        flag: '" + flag + "'" for flag in flags])
253
254def get_starlark_list(values):
255    """Convert a list of string into a string that can be passed to a rule attribute."""
256    if not values:
257        return ""
258    return "\"" + "\",\n    \"".join(values) + "\""
259
260def auto_configure_warning_maybe(repository_ctx, msg):
261    """Output warning message when CC_CONFIGURE_DEBUG is enabled."""
262    if is_cc_configure_debug(repository_ctx):
263        auto_configure_warning(msg)
264
265def write_builtin_include_directory_paths(repository_ctx, cc, directories, file_suffix = ""):
266    """Generate output file named 'builtin_include_directory_paths' in the root of the repository."""
267    if get_env_var(repository_ctx, "BAZEL_IGNORE_SYSTEM_HEADERS_VERSIONS", "0", False) == "1":
268        repository_ctx.file(
269            "builtin_include_directory_paths" + file_suffix,
270            """This file is generated by cc_configure and normally contains builtin include directories
271that C++ compiler reported. But because BAZEL_IGNORE_SYSTEM_HEADERS_VERSIONS was set to 1,
272header include directory paths are intentionally not put there.
273""",
274        )
275    else:
276        repository_ctx.file(
277            "builtin_include_directory_paths" + file_suffix,
278            """This file is generated by cc_configure and contains builtin include directories
279that %s reported. This file is a dependency of every compilation action and
280changes to it will be reflected in the action cache key. When some of these
281paths change, Bazel will make sure to rerun the action, even though none of
282declared action inputs or the action commandline changes.
283
284%s
285""" % (cc, "\n".join(directories)),
286        )
287