1# Copyright 2018 The Bazel Authors. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#    http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Utilities for the Android rules."""
16
17load(":providers.bzl", "FailureInfo")
18
19_CUU = "\033[A"
20_EL = "\033[K"
21_DEFAULT = "\033[0m"
22_BOLD = "\033[1m"
23_RED = "\033[31m"
24_GREEN = "\033[32m"
25_MAGENTA = "\033[35m"
26_ERASE_PREV_LINE = "\n" + _CUU + _EL
27
28_INFO = _ERASE_PREV_LINE + _GREEN + "INFO: " + _DEFAULT + "%s"
29_WARNING = _ERASE_PREV_LINE + _MAGENTA + "WARNING: " + _DEFAULT + "%s"
30_ERROR = _ERASE_PREV_LINE + _BOLD + _RED + "ERROR: " + _DEFAULT + "%s"
31
32_WORD_CHARS = {
33    "A": True,
34    "B": True,
35    "C": True,
36    "D": True,
37    "E": True,
38    "F": True,
39    "G": True,
40    "H": True,
41    "I": True,
42    "J": True,
43    "K": True,
44    "L": True,
45    "M": True,
46    "N": True,
47    "O": True,
48    "P": True,
49    "Q": True,
50    "R": True,
51    "S": True,
52    "T": True,
53    "U": True,
54    "V": True,
55    "W": True,
56    "X": True,
57    "Y": True,
58    "Z": True,
59    "a": True,
60    "b": True,
61    "c": True,
62    "d": True,
63    "e": True,
64    "f": True,
65    "g": True,
66    "h": True,
67    "i": True,
68    "j": True,
69    "k": True,
70    "l": True,
71    "m": True,
72    "n": True,
73    "o": True,
74    "p": True,
75    "q": True,
76    "r": True,
77    "s": True,
78    "t": True,
79    "u": True,
80    "v": True,
81    "w": True,
82    "x": True,
83    "y": True,
84    "z": True,
85    "0": True,
86    "1": True,
87    "2": True,
88    "3": True,
89    "4": True,
90    "5": True,
91    "6": True,
92    "7": True,
93    "8": True,
94    "9": True,
95    "_": True,
96}
97
98_HEX_CHAR = {
99    0x0: "0",
100    0x1: "1",
101    0x2: "2",
102    0x3: "3",
103    0x4: "4",
104    0x5: "5",
105    0x6: "6",
106    0x7: "7",
107    0x8: "8",
108    0x9: "9",
109    0xA: "A",
110    0xB: "B",
111    0xC: "C",
112    0xD: "D",
113    0xE: "E",
114    0xF: "F",
115}
116
117_JAVA_RESERVED = {
118    "abstract": True,
119    "assert": True,
120    "boolean": True,
121    "break": True,
122    "byte": True,
123    "case": True,
124    "catch": True,
125    "char": True,
126    "class": True,
127    "const": True,
128    "continue": True,
129    "default": True,
130    "do": True,
131    "double": True,
132    "else": True,
133    "enum": True,
134    "extends": True,
135    "final": True,
136    "finally": True,
137    "float": True,
138    "for": True,
139    "goto": True,
140    "if": True,
141    "implements": True,
142    "import": True,
143    "instanceof": True,
144    "int": True,
145    "interface": True,
146    "long": True,
147    "native": True,
148    "new": True,
149    "package": True,
150    "private": True,
151    "protected": True,
152    "public": True,
153    "return": True,
154    "short": True,
155    "static": True,
156    "strictfp": True,
157    "super": True,
158    "switch": True,
159    "synchronized": True,
160    "this": True,
161    "throw": True,
162    "throws": True,
163    "transient": True,
164    "try": True,
165    "void": True,
166    "volatile": True,
167    "while": True,
168    "true": True,
169    "false": True,
170    "null": True,
171}
172
173def _collect_providers(provider, *all_deps):
174    """Collects the requested providers from the given list of deps."""
175    providers = []
176    for deps in all_deps:
177        for dep in deps:
178            if provider in dep:
179                providers.append(dep[provider])
180    return providers
181
182def _join_depsets(providers, attr, order = "default"):
183    """Returns a merged depset using 'attr' from each provider in 'providers'."""
184    return depset(transitive = [getattr(p, attr) for p in providers], order = order)
185
186def _first(collection):
187    """Returns the first item in the collection."""
188    for i in collection:
189        return i
190    return _error("The collection is empty.")
191
192def _only(collection):
193    """Returns the only item in the collection."""
194    if len(collection) != 1:
195        _error("Expected one element, has %s." % len(collection))
196    return _first(collection)
197
198def _copy_file(ctx, src, dest):
199    if src.is_directory or dest.is_directory:
200        fail("Cannot use copy_file with directories")
201    ctx.actions.run_shell(
202        command = "cp --reflink=auto $1 $2",
203        arguments = [src.path, dest.path],
204        inputs = [src],
205        outputs = [dest],
206        mnemonic = "CopyFile",
207        progress_message = "Copy %s to %s" % (src.short_path, dest.short_path),
208    )
209
210def _copy_dir(ctx, src, dest):
211    if not src.is_directory:
212        fail("copy_dir src must be a directory")
213    ctx.actions.run_shell(
214        command = "cp -r --reflink=auto $1 $2",
215        arguments = [src.path, dest.path],
216        inputs = [src],
217        outputs = [dest],
218        mnemonic = "CopyDir",
219        progress_message = "Copy %s to %s" % (src.short_path, dest.short_path),
220    )
221
222def _info(msg):
223    """Print info."""
224    print(_INFO % msg)
225
226def _warn(msg):
227    """Print warning."""
228    print(_WARNING % msg)
229
230def _debug(msg):
231    """Print debug."""
232    print("\n%s" % msg)
233
234def _error(msg):
235    """Print error and fail."""
236    fail(_ERASE_PREV_LINE + _CUU + _ERASE_PREV_LINE + _CUU + _ERROR % msg)
237
238def _expand_var(config_vars, value):
239    """Expands make variables of the form $(SOME_VAR_NAME) for a single value.
240
241    "$$(SOME_VAR_NAME)" is escaped to a literal value of "$(SOME_VAR_NAME)" instead of being
242    expanded.
243
244    Args:
245      config_vars: String dictionary which maps config variables to their expanded values.
246      value: The string to apply substitutions to.
247
248    Returns:
249      The string value with substitutions applied.
250    """
251    parts = value.split("$(")
252    replacement = parts[0]
253    last_char = replacement[-1] if replacement else ""
254    for part in parts[1:]:
255        var_end = part.find(")")
256        if last_char == "$":
257            # If "$$(..." is found, treat it as "$(..."
258            replacement += "(" + part
259        elif var_end == -1 or part[:var_end] not in config_vars:
260            replacement += "$(" + part
261        else:
262            replacement += config_vars[part[:var_end]] + part[var_end + 1:]
263        last_char = replacement[-1] if replacement else ""
264    return replacement
265
266def _expand_make_vars(ctx, vals):
267    """Expands make variables of the form $(SOME_VAR_NAME).
268
269    Args:
270      ctx: The rules context.
271      vals: Dictionary. Values of the form $(...) will be replaced.
272
273    Returns:
274      A dictionary containing vals.keys() and the expanded values.
275    """
276    res = {}
277    for k, v in vals.items():
278        res[k] = _expand_var(ctx.var, v)
279    return res
280
281def _get_runfiles(ctx, attrs):
282    runfiles = ctx.runfiles()
283    for attr in attrs:
284        executable = attr[DefaultInfo].files_to_run.executable
285        if executable:
286            runfiles = runfiles.merge(ctx.runfiles([executable]))
287        runfiles = runfiles.merge(
288            ctx.runfiles(
289                # Wrap DefaultInfo.files in depset to strip ordering.
290                transitive_files = depset(
291                    transitive = [attr[DefaultInfo].files],
292                ),
293            ),
294        )
295        runfiles = runfiles.merge(attr[DefaultInfo].default_runfiles)
296    return runfiles
297
298def _sanitize_string(s, replacement = ""):
299    """Sanitizes a string by replacing all non-word characters.
300
301    This matches the \\w regex character class [A_Za-z0-9_].
302
303    Args:
304      s: String to sanitize.
305      replacement: Replacement for all non-word characters. Optional.
306
307    Returns:
308      The original string with all non-word characters replaced.
309    """
310    return "".join([s[i] if s[i] in _WORD_CHARS else replacement for i in range(len(s))])
311
312def _hex(n, pad = True):
313    """Convert an integer number to an uppercase hexadecimal string.
314
315    Args:
316      n: Integer number.
317      pad: Optional. Pad the result to 8 characters with leading zeroes. Default = True.
318
319    Returns:
320      Return a representation of an integer number as a hexadecimal string.
321    """
322    hex_str = ""
323    for _ in range(8):
324        r = n % 16
325        n = n // 16
326        hex_str = _HEX_CHAR[r] + hex_str
327    if pad:
328        return hex_str
329    else:
330        return hex_str.lstrip("0")
331
332def _sanitize_java_package(pkg):
333    return ".".join(["xxx" if p in _JAVA_RESERVED else p for p in pkg.split(".")])
334
335def _check_for_failures(label, *all_deps):
336    """Collects FailureInfo providers from the given list of deps and fails if there's at least one."""
337    failure_infos = _collect_providers(FailureInfo, *all_deps)
338    if failure_infos:
339        error = "in label '%s':" % label
340        for failure_info in failure_infos:
341            error += "\n\t" + failure_info.error
342        _error(error)
343
344def _run_validation(
345        ctx,
346        validation_out,
347        executable,
348        outputs = [],
349        tools = [],
350        **args):
351    """Creates an action that runs an executable as a validation.
352
353    Note: When the validation executable fails, it should return a non-zero
354    value to signify a validation failure.
355
356    Args:
357      ctx: The context.
358      validation_out: A File. The output of the executable is piped to the
359        file. This artifact should then be propagated to "validations" in the
360        OutputGroupInfo.
361      executable: See ctx.actions.run#executable.
362      outputs: See ctx.actions.run#outputs.
363      tools: See ctx.actions.run#tools.
364      **args: Remaining args are directly propagated to ctx.actions.run_shell.
365        See ctx.actions.run_shell for further documentation.
366    """
367    exec_type = type(executable)
368    exec_bin = None
369    exec_bin_path = None
370    if exec_type == "FilesToRunProvider":
371        exec_bin = executable.executable
372        exec_bin_path = exec_bin.path
373    elif exec_type == "File":
374        exec_bin = executable
375        exec_bin_path = exec_bin.path
376    elif exec_type == type(""):
377        exec_bin_path = executable
378    else:
379        fail(
380            "Error, executable should be a File, FilesToRunProvider or a " +
381            "string that represents a path to a tool, got: %s" % exec_type,
382        )
383
384    ctx.actions.run_shell(
385        command = """#!/bin/bash
386set -eu
387set -o pipefail # Returns the executables failure code, if it fails.
388
389EXECUTABLE={executable}
390VALIDATION_OUT={validation_out}
391
392"${{EXECUTABLE}}" $@ 2>&1 | tee -a "${{VALIDATION_OUT}}"
393""".format(
394            executable = exec_bin_path,
395            validation_out = validation_out.path,
396        ),
397        tools = tools + ([exec_bin] if exec_bin else []),
398        outputs = [validation_out] + outputs,
399        **args
400    )
401
402def get_android_toolchain(ctx):
403    return ctx.toolchains["@rules_android//toolchains/android:toolchain_type"]
404
405def get_android_sdk(ctx):
406    if hasattr(ctx.fragments.android, "incompatible_use_toolchain_resolution") and ctx.fragments.android.incompatible_use_toolchain_resolution:
407        return ctx.toolchains["@rules_android//toolchains/android_sdk:toolchain_type"].android_sdk_info
408    else:
409        return ctx.attr._android_sdk[AndroidSdkInfo]
410
411def _get_compilation_mode(ctx):
412    """Retrieves the compilation mode from the context.
413
414    Returns:
415      A string that represents the compilation mode.
416    """
417    return ctx.var["COMPILATION_MODE"]
418
419compilation_mode = struct(
420    DBG = "dbg",
421    FASTBUILD = "fastbuild",
422    OPT = "opt",
423    get = _get_compilation_mode,
424)
425
426utils = struct(
427    check_for_failures = _check_for_failures,
428    collect_providers = _collect_providers,
429    copy_file = _copy_file,
430    copy_dir = _copy_dir,
431    expand_make_vars = _expand_make_vars,
432    first = _first,
433    get_runfiles = _get_runfiles,
434    join_depsets = _join_depsets,
435    only = _only,
436    run_validation = _run_validation,
437    sanitize_string = _sanitize_string,
438    sanitize_java_package = _sanitize_java_package,
439    hex = _hex,
440)
441
442log = struct(
443    debug = _debug,
444    error = _error,
445    info = _info,
446    warn = _warn,
447)
448
449testing = struct(
450    expand_var = _expand_var,
451)
452