| # Copyright 2021 Google LLC |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| load("//build/make/core:envsetup.rbc", _envsetup_init = "init") |
| |
| """Runtime functions.""" |
| |
| def _global_init(): |
| """Returns dict created from the runtime environment.""" |
| globals = dict() |
| |
| # Environment variables |
| for k in dir(rblf_env): |
| globals[k] = getattr(rblf_env, k) |
| |
| # Variables set as var=value command line arguments |
| for k in dir(rblf_cli): |
| globals[k] = getattr(rblf_cli, k) |
| |
| globals.setdefault("PRODUCT_SOONG_NAMESPACES", []) |
| _envsetup_init(globals) |
| |
| # Variables that should be defined. |
| mandatory_vars = [ |
| "PLATFORM_VERSION_CODENAME", |
| "PLATFORM_VERSION", |
| "PRODUCT_SOONG_NAMESPACES", |
| # TODO(asmundak): do we need TARGET_ARCH? AOSP does not reference it |
| "TARGET_BUILD_TYPE", |
| "TARGET_BUILD_VARIANT", |
| "TARGET_PRODUCT", |
| ] |
| for bv in mandatory_vars: |
| if not bv in globals: |
| fail(bv, " is not defined") |
| |
| return globals |
| |
| _globals_base = _global_init() |
| |
| def __print_attr(attr, value): |
| if not value: |
| return |
| if type(value) == "list": |
| if _options.rearrange: |
| value = __printvars_rearrange_list(value) |
| if _options.format == "pretty": |
| print(attr, "=", repr(value)) |
| elif _options.format == "make": |
| print(attr, ":=", " ".join(value)) |
| elif _options.format == "pretty": |
| print(attr, "=", repr(value)) |
| elif _options.format == "make": |
| print(attr, ":=", value) |
| else: |
| fail("bad output format", _options.format) |
| |
| def _printvars(globals, cfg): |
| """Prints known configuration variables.""" |
| for attr, val in sorted(cfg.items()): |
| __print_attr(attr, val) |
| if _options.print_globals: |
| print() |
| for attr, val in sorted(globals.items()): |
| if attr not in _globals_base: |
| __print_attr(attr, val) |
| |
| def __printvars_rearrange_list(value_list): |
| """Rearrange value list: return only distinct elements, maybe sorted.""" |
| seen = {item: 0 for item in value_list} |
| return sorted(seen.keys()) if _options.rearrange == "sort" else seen.keys() |
| |
| def _product_configuration(top_pcm_name, top_pcm): |
| """Creates configuration.""" |
| |
| # Product configuration is created by traversing product's inheritance |
| # tree. It is traversed twice. |
| # First, beginning with top-level module we execute a module and find |
| # its ancestors, repeating this recursively. At the end of this phase |
| # we get the full inheritance tree. |
| # Second, we traverse the tree in the postfix order (i.e., visiting a |
| # node after its ancestors) to calculate the product configuration. |
| # |
| # PCM means "Product Configuration Module", i.e., a Starlark file |
| # whose body consists of a single init function. |
| |
| globals = dict(**_globals_base) |
| |
| config_postfix = [] # Configs in postfix order |
| |
| # Each PCM is represented by a quadruple of function, config, children names |
| # and readyness (that is, the configurations from inherited PCMs have been |
| # substituted). |
| configs = {top_pcm_name: (top_pcm, None, [], False)} # All known PCMs |
| |
| stash = [] # Configs to push once their descendants are done |
| |
| # Stack containing PCMs to be processed. An item in the stack |
| # is a pair of PCMs name and its height in the product inheritance tree. |
| pcm_stack = [(top_pcm_name, 0)] |
| pcm_count = 0 |
| |
| # Run it until pcm_stack is exhausted, but no more than N times |
| for n in range(1000): |
| if not pcm_stack: |
| break |
| (name, height) = pcm_stack.pop() |
| pcm, cfg, c, _ = configs[name] |
| |
| # cfg is set only after PCM has been called, leverage this |
| # to prevent calling the same PCM twice |
| if cfg != None: |
| continue |
| |
| # Push ancestors until we reach this node's height |
| config_postfix.extend([stash.pop() for i in range(len(stash) - height)]) |
| |
| # Run this one, obtaining its configuration and child PCMs. |
| if _options.trace_modules: |
| print("%d:" % n) |
| |
| # Run PCM. |
| handle = __h_new() |
| pcm(globals, handle) |
| |
| # Now we know everything about this PCM, record it in 'configs'. |
| children = __h_inherited_modules(handle) |
| if _options.trace_modules: |
| print(" ", " ".join(children.keys())) |
| configs[name] = (pcm, __h_cfg(handle), children.keys(), False) |
| pcm_count = pcm_count + 1 |
| |
| if len(children) == 0: |
| # Leaf PCM goes straight to the config_postfix |
| config_postfix.append(name) |
| continue |
| |
| # Stash this PCM, process children in the sorted order |
| stash.append(name) |
| for child_name in sorted(children, reverse = True): |
| if child_name not in configs: |
| configs[child_name] = (children[child_name], None, [], False) |
| pcm_stack.append((child_name, len(stash))) |
| if pcm_stack: |
| fail("Inheritance processing took too many iterations") |
| |
| # Flush the stash |
| config_postfix.extend([stash.pop() for i in range(len(stash))]) |
| if len(config_postfix) != pcm_count: |
| fail("Ran %d modules but postfix tree has only %d entries" % (pcm_count, len(config_postfix))) |
| |
| if _options.trace_modules: |
| print("\n---Postfix---") |
| for x in config_postfix: |
| print(" ", x) |
| |
| # Traverse the tree from the bottom, evaluating inherited values |
| for pcm_name in config_postfix: |
| pcm, cfg, children_names, ready = configs[pcm_name] |
| |
| # Should run |
| if cfg == None: |
| fail("%s: has not been run" % pcm_name) |
| |
| # Ready once |
| if ready: |
| continue |
| |
| # Children should be ready |
| for child_name in children_names: |
| if not configs[child_name][3]: |
| fail("%s: child is not ready" % child_name) |
| |
| _substitute_inherited(configs, pcm_name, cfg) |
| _percolate_inherited(configs, pcm_name, cfg, children_names) |
| configs[pcm_name] = pcm, cfg, children_names, True |
| |
| return globals, configs[top_pcm_name][1] |
| |
| def _substitute_inherited(configs, pcm_name, cfg): |
| """Substitutes inherited values in all the attributes. |
| |
| When a value of an attribute is a list, some of its items may be |
| references to a value of a same attribute in an inherited product, |
| e.g., for a given module PRODUCT_PACKAGES can be |
| ["foo", (submodule), "bar"] |
| and for 'submodule' PRODUCT_PACKAGES may be ["baz"] |
| (we use a tuple to distinguish submodule references). |
| After the substitution the value of PRODUCT_PACKAGES for the module |
| will become ["foo", "baz", "bar"] |
| """ |
| for attr, val in cfg.items(): |
| # TODO(asmundak): should we handle single vars? |
| if type(val) != "list": |
| continue |
| |
| if attr not in _options.trace_variables: |
| cfg[attr] = _value_expand(configs, attr, val) |
| else: |
| old_val = val |
| new_val = _value_expand(configs, attr, val) |
| if new_val != old_val: |
| print("%s(i): %s=%s (was %s)" % (pcm_name, attr, new_val, old_val)) |
| cfg[attr] = new_val |
| |
| def _value_expand(configs, attr, values_list): |
| """Expands references to inherited values in a given list.""" |
| result = [] |
| expanded = {} |
| for item in values_list: |
| # Inherited values are 1-tuples |
| if type(item) != "tuple": |
| result.append(item) |
| continue |
| child_name = item[0] |
| if child_name in expanded: |
| continue |
| expanded[child_name] = True |
| child = configs[child_name] |
| if not child[3]: |
| fail("%s should be ready" % child_name) |
| __move_items(result, child[1], attr) |
| |
| return result |
| |
| def _percolate_inherited(configs, cfg_name, cfg, children_names): |
| """Percolates the settings that are present only in children.""" |
| percolated_attrs = {} |
| for child_name in children_names: |
| child_cfg = configs[child_name][1] |
| for attr, value in child_cfg.items(): |
| if type(value) != "list": |
| if attr in percolated_attrs or not attr in cfg: |
| cfg[attr] = value |
| percolated_attrs[attr] = True |
| continue |
| if attr in percolated_attrs: |
| # We already are percolating this one, just add this list |
| __move_items(cfg[attr], child_cfg, attr) |
| elif not attr in cfg: |
| percolated_attrs[attr] = True |
| cfg[attr] = [] |
| __move_items(cfg[attr], child_cfg, attr) |
| |
| for attr in _options.trace_variables: |
| if attr in percolated_attrs: |
| print("%s: %s^=%s" % (cfg_name, attr, cfg[attr])) |
| |
| def __move_items(to_list, from_cfg, attr): |
| value = from_cfg.get(attr, []) |
| if value: |
| to_list.extend(value) |
| from_cfg[attr] = [] |
| |
| def _indirect(pcm_name): |
| """Returns configuration item for the inherited module.""" |
| return (pcm_name,) |
| |
| def _addprefix(prefix, string_or_list): |
| """Adds prefix and returns a list. |
| |
| If string_or_list is a list, prepends prefix to each element. |
| Otherwise, string_or_list is considered to be a string which |
| is split into words and then prefix is prepended to each one. |
| |
| Args: |
| prefix |
| string_or_list |
| |
| """ |
| return [prefix + x for x in __words(string_or_list)] |
| |
| def _addsuffix(suffix, string_or_list): |
| """Adds suffix and returns a list. |
| |
| If string_or_list is a list, appends suffix to each element. |
| Otherwise, string_or_list is considered to be a string which |
| is split into words and then suffix is appended to each one. |
| |
| Args: |
| suffix |
| string_or_list |
| """ |
| return [x + suffix for x in __words(string_or_list)] |
| |
| def __words(string_or_list): |
| if type(string_or_list) == "list": |
| return string_or_list |
| return string_or_list.split() |
| |
| # Handle manipulation functions. |
| # A handle passed to a PCM consists of: |
| # product attributes dict ("cfg") |
| # inherited modules dict (maps module name to PCM) |
| # default value list (initially empty, modified by inheriting) |
| def __h_new(): |
| """Constructs a handle which is passed to PCM.""" |
| return (dict(), dict(), list()) |
| |
| def __h_inherited_modules(handle): |
| """Returns PCM's inherited modules dict.""" |
| return handle[1] |
| |
| def __h_cfg(handle): |
| """Returns PCM's product configuration attributes dict. |
| |
| This function is also exported as rblf.cfg, and every PCM |
| calls it at the beginning. |
| """ |
| return handle[0] |
| |
| def _setdefault(handle, attr): |
| """If attribute has not been set, assigns default value to it. |
| |
| This function is exported as rblf.setdefault(). |
| Only list attributes are initialized this way. The default |
| value is kept in the PCM's handle. Calling inherit() updates it. |
| """ |
| cfg = handle[0] |
| if cfg.get(attr) == None: |
| cfg[attr] = list(handle[2]) |
| return cfg[attr] |
| |
| def _inherit(handle, pcm_name, pcm): |
| """Records inheritance. |
| |
| This function is exported as rblf.inherit, PCM calls it when |
| a module is inherited. |
| """ |
| cfg, inherited, default_lv = handle |
| inherited[pcm_name] = pcm |
| default_lv.append(_indirect(pcm_name)) |
| |
| # Add inherited module reference to all configuration values |
| for attr, val in cfg.items(): |
| if type(val) == "list": |
| val.append(_indirect(pcm_name)) |
| |
| def _copy_if_exists(path_pair): |
| """If from file exists, returns [from:to] pair.""" |
| value = path_pair.split(":", 2) |
| |
| # Check that l[0] exists |
| return [":".join(value)] if rblf_file_exists(value[0]) else [] |
| |
| def _enforce_product_packages_exist(pkg_string_or_list): |
| """Makes including non-existent modules in PRODUCT_PACKAGES an error.""" |
| |
| #TODO(asmundak) |
| pass |
| |
| def _file_wildcard_exists(file_pattern): |
| """Return True if there are files matching given bash pattern.""" |
| return len(rblf_wildcard(file_pattern)) > 0 |
| |
| def _find_and_copy(pattern, from_dir, to_dir): |
| """Return a copy list for the files matching the pattern.""" |
| return ["%s/%s:%s/%s" % (from_dir, f, to_dir, f) for f in rblf_wildcard(pattern, from_dir)] |
| |
| def _filter_out(pattern, text): |
| """Return all the words from `text' that do not match any word in `pattern'. |
| |
| Args: |
| pattern: string or list of words. '%' stands for wildcard (in regex terms, '.*') |
| text: string or list of words |
| Return: |
| list of words |
| """ |
| rex = __mk2regex(__words(pattern)) |
| res = [] |
| for w in __words(text): |
| if not _regex_match(rex, w): |
| res.append(w) |
| return res |
| |
| def _filter(pattern, text): |
| """Return all the words in `text` that match `pattern`. |
| |
| Args: |
| pattern: strings of words or a list. A word can contain '%', |
| which stands for any sequence of characters. |
| text: string or list of words. |
| """ |
| rex = __mk2regex(__words(pattern)) |
| res = [] |
| for w in __words(text): |
| if _regex_match(rex, w): |
| res.append(w) |
| return res |
| |
| def __mk2regex(words): |
| """Returns regular expression equivalent to Make pattern.""" |
| |
| # TODO(asmundak): this will mishandle '\%' |
| return "^(" + "|".join([w.replace("%", ".*", 1) for w in words]) + ")" |
| |
| def _regex_match(regex, w): |
| return rblf_regex(regex, w) |
| |
| def _require_artifacts_in_path(paths, allowed_paths): |
| """TODO.""" |
| pass |
| |
| def _require_artifacts_in_path_relaxed(paths, allowed_paths): |
| """TODO.""" |
| pass |
| |
| def _expand_wildcard(pattern): |
| """Expands shell wildcard pattern.""" |
| return rblf_wildcard(pattern) |
| |
| def _mkerror(file, message = ""): |
| """Prints error and stops.""" |
| fail("%s: %s. Stop" % (file, message)) |
| |
| def _mkwarning(file, message = ""): |
| """Prints warning.""" |
| print("%s: warning: %s" % (file, message)) |
| |
| def _mkinfo(file, message = ""): |
| """Prints info.""" |
| print(message) |
| |
| def __get_options(): |
| """Returns struct containing runtime global settings.""" |
| settings = dict( |
| format = "pretty", |
| print_globals = False, |
| rearrange = "", |
| trace_modules = False, |
| trace_variables = [], |
| ) |
| for x in getattr(rblf_cli, "RBC_OUT", "").split(","): |
| if x == "sort" or x == "unique": |
| if settings["rearrange"]: |
| fail("RBC_OUT: either sort or unique is allowed (and sort implies unique)") |
| settings["rearrange"] = x |
| elif x == "pretty" or x == "make": |
| settings["format"] = x |
| elif x == "global": |
| settings["print_globals"] = True |
| elif x != "": |
| fail("RBC_OUT: got %s, should be one of: [pretty|make] [sort|unique]" % x) |
| for x in getattr(rblf_cli, "RBC_DEBUG", "").split(","): |
| if x == "!trace": |
| settings["trace_modules"] = True |
| elif x != "": |
| settings["trace_variables"].append(x) |
| return struct(**settings) |
| |
| # Settings used during debugging. |
| _options = __get_options() |
| rblf = struct( |
| addprefix = _addprefix, |
| addsuffix = _addsuffix, |
| copy_if_exists = _copy_if_exists, |
| cfg = __h_cfg, |
| enforce_product_packages_exist = _enforce_product_packages_exist, |
| expand_wildcard = _expand_wildcard, |
| file_exists = rblf_file_exists, |
| file_wildcard_exists = _file_wildcard_exists, |
| filter = _filter, |
| filter_out = _filter_out, |
| find_and_copy = _find_and_copy, |
| global_init = _global_init, |
| inherit = _inherit, |
| indirect = _indirect, |
| mkinfo = _mkinfo, |
| mkerror = _mkerror, |
| mkwarning = _mkwarning, |
| printvars = _printvars, |
| product_configuration = _product_configuration, |
| require_artifacts_in_path = _require_artifacts_in_path, |
| require_artifacts_in_path_relaxed = _require_artifacts_in_path_relaxed, |
| setdefault = _setdefault, |
| shell = rblf_shell, |
| warning = _mkwarning, |
| ) |