• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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