1# Copyright 2021 Google LLC
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#      https://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
15load("//build/make/core:envsetup.rbc", _envsetup_init = "init")
16
17"""Runtime functions."""
18
19def _global_init():
20    """Returns dict created from the runtime environment."""
21    globals = dict()
22
23    # Environment variables
24    for k in dir(rblf_env):
25        globals[k] = getattr(rblf_env, k)
26
27    # Variables set as var=value command line arguments
28    for k in dir(rblf_cli):
29        globals[k] = getattr(rblf_cli, k)
30
31    globals.setdefault("PRODUCT_SOONG_NAMESPACES", [])
32    _envsetup_init(globals)
33
34    # Variables that should be defined.
35    mandatory_vars = [
36        "PLATFORM_VERSION_CODENAME",
37        "PLATFORM_VERSION",
38        "PRODUCT_SOONG_NAMESPACES",
39        # TODO(asmundak): do we need TARGET_ARCH? AOSP does not reference it
40        "TARGET_BUILD_TYPE",
41        "TARGET_BUILD_VARIANT",
42        "TARGET_PRODUCT",
43    ]
44    for bv in mandatory_vars:
45        if not bv in globals:
46            fail(bv, " is not defined")
47
48    return globals
49
50_globals_base = _global_init()
51
52def __print_attr(attr, value):
53    if not value:
54        return
55    if type(value) == "list":
56        if _options.rearrange:
57            value = __printvars_rearrange_list(value)
58        if _options.format == "pretty":
59            print(attr, "=", repr(value))
60        elif _options.format == "make":
61            print(attr, ":=", " ".join(value))
62    elif _options.format == "pretty":
63        print(attr, "=", repr(value))
64    elif _options.format == "make":
65        print(attr, ":=", value)
66    else:
67        fail("bad output format", _options.format)
68
69def _printvars(globals, cfg):
70    """Prints known configuration variables."""
71    for attr, val in sorted(cfg.items()):
72        __print_attr(attr, val)
73    if _options.print_globals:
74        print()
75        for attr, val in sorted(globals.items()):
76            if attr not in _globals_base:
77                __print_attr(attr, val)
78
79def __printvars_rearrange_list(value_list):
80    """Rearrange value list: return only distinct elements, maybe sorted."""
81    seen = {item: 0 for item in value_list}
82    return sorted(seen.keys()) if _options.rearrange == "sort" else seen.keys()
83
84def _product_configuration(top_pcm_name, top_pcm):
85    """Creates configuration."""
86
87    # Product configuration is created by traversing product's inheritance
88    # tree. It is traversed twice.
89    # First, beginning with top-level module we execute a module and find
90    # its ancestors, repeating this recursively. At the end of this phase
91    # we get the full inheritance tree.
92    # Second, we traverse the tree in the postfix order (i.e., visiting a
93    # node after its ancestors) to calculate the product configuration.
94    #
95    # PCM means "Product Configuration Module", i.e., a Starlark file
96    # whose body consists of a single init function.
97
98    globals = dict(**_globals_base)
99
100    config_postfix = []  # Configs in postfix order
101
102    # Each PCM is represented by a quadruple of function, config, children names
103    # and readyness (that is, the configurations from inherited PCMs have been
104    # substituted).
105    configs = {top_pcm_name: (top_pcm, None, [], False)}  # All known PCMs
106
107    stash = []  # Configs to push once their descendants are done
108
109    # Stack containing PCMs to be processed. An item in the stack
110    # is a pair of PCMs name and its height in the product inheritance tree.
111    pcm_stack = [(top_pcm_name, 0)]
112    pcm_count = 0
113
114    # Run it until pcm_stack is exhausted, but no more than N times
115    for n in range(1000):
116        if not pcm_stack:
117            break
118        (name, height) = pcm_stack.pop()
119        pcm, cfg, c, _ = configs[name]
120
121        # cfg is set only after PCM has been called, leverage this
122        # to prevent calling the same PCM twice
123        if cfg != None:
124            continue
125
126        # Push ancestors until we reach this node's height
127        config_postfix.extend([stash.pop() for i in range(len(stash) - height)])
128
129        # Run this one, obtaining its configuration and child PCMs.
130        if _options.trace_modules:
131            print("%d:" % n)
132
133        # Run PCM.
134        handle = __h_new()
135        pcm(globals, handle)
136
137        # Now we know everything about this PCM, record it in 'configs'.
138        children = __h_inherited_modules(handle)
139        if _options.trace_modules:
140            print("   ", "    ".join(children.keys()))
141        configs[name] = (pcm, __h_cfg(handle), children.keys(), False)
142        pcm_count = pcm_count + 1
143
144        if len(children) == 0:
145            # Leaf PCM goes straight to the config_postfix
146            config_postfix.append(name)
147            continue
148
149        # Stash this PCM, process children in the sorted order
150        stash.append(name)
151        for child_name in sorted(children, reverse = True):
152            if child_name not in configs:
153                configs[child_name] = (children[child_name], None, [], False)
154            pcm_stack.append((child_name, len(stash)))
155    if pcm_stack:
156        fail("Inheritance processing took too many iterations")
157
158    # Flush the stash
159    config_postfix.extend([stash.pop() for i in range(len(stash))])
160    if len(config_postfix) != pcm_count:
161        fail("Ran %d modules but postfix tree has only %d entries" % (pcm_count, len(config_postfix)))
162
163    if _options.trace_modules:
164        print("\n---Postfix---")
165        for x in config_postfix:
166            print("   ", x)
167
168    # Traverse the tree from the bottom, evaluating inherited values
169    for pcm_name in config_postfix:
170        pcm, cfg, children_names, ready = configs[pcm_name]
171
172        # Should run
173        if cfg == None:
174            fail("%s: has not been run" % pcm_name)
175
176        # Ready once
177        if ready:
178            continue
179
180        # Children should be ready
181        for child_name in children_names:
182            if not configs[child_name][3]:
183                fail("%s: child is not ready" % child_name)
184
185        _substitute_inherited(configs, pcm_name, cfg)
186        _percolate_inherited(configs, pcm_name, cfg, children_names)
187        configs[pcm_name] = pcm, cfg, children_names, True
188
189    return globals, configs[top_pcm_name][1]
190
191def _substitute_inherited(configs, pcm_name, cfg):
192    """Substitutes inherited values in all the attributes.
193
194    When a value of an attribute is a list, some of its items may be
195    references to a value of a same attribute in an inherited product,
196    e.g., for a given module PRODUCT_PACKAGES can be
197      ["foo", (submodule), "bar"]
198    and for 'submodule' PRODUCT_PACKAGES may be ["baz"]
199    (we use a tuple to distinguish submodule references).
200    After the substitution the value of PRODUCT_PACKAGES for the module
201    will become ["foo", "baz", "bar"]
202    """
203    for attr, val in cfg.items():
204        # TODO(asmundak): should we handle single vars?
205        if type(val) != "list":
206            continue
207
208        if attr not in _options.trace_variables:
209            cfg[attr] = _value_expand(configs, attr, val)
210        else:
211            old_val = val
212            new_val = _value_expand(configs, attr, val)
213            if new_val != old_val:
214                print("%s(i): %s=%s (was %s)" % (pcm_name, attr, new_val, old_val))
215            cfg[attr] = new_val
216
217def _value_expand(configs, attr, values_list):
218    """Expands references to inherited values in a given list."""
219    result = []
220    expanded = {}
221    for item in values_list:
222        # Inherited values are 1-tuples
223        if type(item) != "tuple":
224            result.append(item)
225            continue
226        child_name = item[0]
227        if child_name in expanded:
228            continue
229        expanded[child_name] = True
230        child = configs[child_name]
231        if not child[3]:
232            fail("%s should be ready" % child_name)
233        __move_items(result, child[1], attr)
234
235    return result
236
237def _percolate_inherited(configs, cfg_name, cfg, children_names):
238    """Percolates the settings that are present only in children."""
239    percolated_attrs = {}
240    for child_name in children_names:
241        child_cfg = configs[child_name][1]
242        for attr, value in child_cfg.items():
243            if type(value) != "list":
244                if attr in percolated_attrs or not attr in cfg:
245                    cfg[attr] = value
246                    percolated_attrs[attr] = True
247                continue
248            if attr in percolated_attrs:
249                # We already are percolating this one, just add this list
250                __move_items(cfg[attr], child_cfg, attr)
251            elif not attr in cfg:
252                percolated_attrs[attr] = True
253                cfg[attr] = []
254                __move_items(cfg[attr], child_cfg, attr)
255
256    for attr in _options.trace_variables:
257        if attr in percolated_attrs:
258            print("%s: %s^=%s" % (cfg_name, attr, cfg[attr]))
259
260def __move_items(to_list, from_cfg, attr):
261    value = from_cfg.get(attr, [])
262    if value:
263        to_list.extend(value)
264        from_cfg[attr] = []
265
266def _indirect(pcm_name):
267    """Returns configuration item for the inherited module."""
268    return (pcm_name,)
269
270def _addprefix(prefix, string_or_list):
271    """Adds prefix and returns a list.
272
273    If string_or_list is a list, prepends prefix to each element.
274    Otherwise, string_or_list is considered to be a string which
275    is split into words and then prefix is prepended to each one.
276
277    Args:
278        prefix
279        string_or_list
280
281    """
282    return [prefix + x for x in __words(string_or_list)]
283
284def _addsuffix(suffix, string_or_list):
285    """Adds suffix and returns a list.
286
287    If string_or_list is a list, appends suffix to each element.
288    Otherwise, string_or_list is considered to be a string which
289    is split into words and then suffix is appended to each one.
290
291    Args:
292      suffix
293      string_or_list
294    """
295    return [x + suffix for x in __words(string_or_list)]
296
297def __words(string_or_list):
298    if type(string_or_list) == "list":
299        return string_or_list
300    return string_or_list.split()
301
302# Handle manipulation functions.
303# A handle passed to a PCM consists of:
304#   product attributes dict ("cfg")
305#   inherited modules dict (maps module name to PCM)
306#   default value list (initially empty, modified by inheriting)
307def __h_new():
308    """Constructs a handle which is passed to PCM."""
309    return (dict(), dict(), list())
310
311def __h_inherited_modules(handle):
312    """Returns PCM's inherited modules dict."""
313    return handle[1]
314
315def __h_cfg(handle):
316    """Returns PCM's product configuration attributes dict.
317
318    This function is also exported as rblf.cfg, and every PCM
319    calls it at the beginning.
320    """
321    return handle[0]
322
323def _setdefault(handle, attr):
324    """If attribute has not been set, assigns default value to it.
325
326    This function is exported as rblf.setdefault().
327    Only list attributes are initialized this way. The default
328    value is kept in the PCM's handle. Calling inherit() updates it.
329    """
330    cfg = handle[0]
331    if cfg.get(attr) == None:
332        cfg[attr] = list(handle[2])
333    return cfg[attr]
334
335def _inherit(handle, pcm_name, pcm):
336    """Records inheritance.
337
338    This function is exported as rblf.inherit, PCM calls it when
339    a module is inherited.
340    """
341    cfg, inherited, default_lv = handle
342    inherited[pcm_name] = pcm
343    default_lv.append(_indirect(pcm_name))
344
345    # Add inherited module reference to all configuration values
346    for attr, val in cfg.items():
347        if type(val) == "list":
348            val.append(_indirect(pcm_name))
349
350def _copy_if_exists(path_pair):
351    """If from file exists, returns [from:to] pair."""
352    value = path_pair.split(":", 2)
353
354    # Check that l[0] exists
355    return [":".join(value)] if rblf_file_exists(value[0]) else []
356
357def _enforce_product_packages_exist(pkg_string_or_list):
358    """Makes including non-existent modules in PRODUCT_PACKAGES an error."""
359
360    #TODO(asmundak)
361    pass
362
363def _file_wildcard_exists(file_pattern):
364    """Return True if there are files matching given bash pattern."""
365    return len(rblf_wildcard(file_pattern)) > 0
366
367def _find_and_copy(pattern, from_dir, to_dir):
368    """Return a copy list for the files matching the pattern."""
369    return ["%s/%s:%s/%s" % (from_dir, f, to_dir, f) for f in rblf_wildcard(pattern, from_dir)]
370
371def _filter_out(pattern, text):
372    """Return all the words from `text' that do not match any word in `pattern'.
373
374    Args:
375        pattern: string or list of words. '%' stands for wildcard (in regex terms, '.*')
376        text: string or list of words
377    Return:
378        list of words
379    """
380    rex = __mk2regex(__words(pattern))
381    res = []
382    for w in __words(text):
383        if not _regex_match(rex, w):
384            res.append(w)
385    return res
386
387def _filter(pattern, text):
388    """Return all the words in `text` that match `pattern`.
389
390    Args:
391        pattern: strings of words or a list. A word can contain '%',
392         which stands for any sequence of characters.
393        text: string or list of words.
394    """
395    rex = __mk2regex(__words(pattern))
396    res = []
397    for w in __words(text):
398        if _regex_match(rex, w):
399            res.append(w)
400    return res
401
402def __mk2regex(words):
403    """Returns regular expression equivalent to Make pattern."""
404
405    # TODO(asmundak): this will mishandle '\%'
406    return "^(" + "|".join([w.replace("%", ".*", 1) for w in words]) + ")"
407
408def _regex_match(regex, w):
409    return rblf_regex(regex, w)
410
411def _require_artifacts_in_path(paths, allowed_paths):
412    """TODO."""
413    pass
414
415def _require_artifacts_in_path_relaxed(paths, allowed_paths):
416    """TODO."""
417    pass
418
419def _expand_wildcard(pattern):
420    """Expands shell wildcard pattern."""
421    return rblf_wildcard(pattern)
422
423def _mkerror(file, message = ""):
424    """Prints error and stops."""
425    fail("%s: %s. Stop" % (file, message))
426
427def _mkwarning(file, message = ""):
428    """Prints warning."""
429    print("%s: warning: %s" % (file, message))
430
431def _mkinfo(file, message = ""):
432    """Prints info."""
433    print(message)
434
435def __get_options():
436    """Returns struct containing runtime global settings."""
437    settings = dict(
438        format = "pretty",
439        print_globals = False,
440        rearrange = "",
441        trace_modules = False,
442        trace_variables = [],
443    )
444    for x in getattr(rblf_cli, "RBC_OUT", "").split(","):
445        if x == "sort" or x == "unique":
446            if settings["rearrange"]:
447                fail("RBC_OUT: either sort or unique is allowed (and sort implies unique)")
448            settings["rearrange"] = x
449        elif x == "pretty" or x == "make":
450            settings["format"] = x
451        elif x == "global":
452            settings["print_globals"] = True
453        elif x != "":
454            fail("RBC_OUT: got %s, should be one of: [pretty|make] [sort|unique]" % x)
455    for x in getattr(rblf_cli, "RBC_DEBUG", "").split(","):
456        if x == "!trace":
457            settings["trace_modules"] = True
458        elif x != "":
459            settings["trace_variables"].append(x)
460    return struct(**settings)
461
462# Settings used during debugging.
463_options = __get_options()
464rblf = struct(
465    addprefix = _addprefix,
466    addsuffix = _addsuffix,
467    copy_if_exists = _copy_if_exists,
468    cfg = __h_cfg,
469    enforce_product_packages_exist = _enforce_product_packages_exist,
470    expand_wildcard = _expand_wildcard,
471    file_exists = rblf_file_exists,
472    file_wildcard_exists = _file_wildcard_exists,
473    filter = _filter,
474    filter_out = _filter_out,
475    find_and_copy = _find_and_copy,
476    global_init = _global_init,
477    inherit = _inherit,
478    indirect = _indirect,
479    mkinfo = _mkinfo,
480    mkerror = _mkerror,
481    mkwarning = _mkwarning,
482    printvars = _printvars,
483    product_configuration = _product_configuration,
484    require_artifacts_in_path = _require_artifacts_in_path,
485    require_artifacts_in_path_relaxed = _require_artifacts_in_path_relaxed,
486    setdefault = _setdefault,
487    shell = rblf_shell,
488    warning = _mkwarning,
489)
490