text: # Copyright (C) 2021, QuantStack
  # SPDX-License-Identifier: BSD-3-Clause
  
  from ruamel.yaml import YAML
  import jinja2
  from boa.core.jinja_support import jinja_functions
  from conda_build.metadata import eval_selector, ns_cfg
  from collections.abc import Mapping, Iterable
  
  from boa.core.config import boa_config
  
  console = boa_config.console
  
  
  def render_recursive(dict_or_array, context_dict, jenv):
      # check if it's a dict?
      if isinstance(dict_or_array, Mapping):
          for key, value in dict_or_array.items():
              if isinstance(value, str):
                  tmpl = jenv.from_string(value)
                  dict_or_array[key] = tmpl.render(context_dict)
              elif isinstance(value, Mapping):
                  render_recursive(dict_or_array[key], context_dict, jenv)
              elif isinstance(value, Iterable):
                  render_recursive(dict_or_array[key], context_dict, jenv)
  
      elif isinstance(dict_or_array, Iterable):
          for i in range(len(dict_or_array)):
              value = dict_or_array[i]
              if isinstance(value, str):
                  tmpl = jenv.from_string(value)
                  dict_or_array[i] = tmpl.render(context_dict)
              elif isinstance(value, Mapping):
                  render_recursive(value, context_dict, jenv)
              elif isinstance(value, Iterable):
                  render_recursive(value, context_dict, jenv)
  
  
  def flatten_selectors(ydoc, namespace):
      if isinstance(ydoc, str):
          return ydoc
  
      if isinstance(ydoc, Mapping):
          has_sel = any(k.startswith("sel(") for k in ydoc.keys())
          if has_sel:
              for k, v in ydoc.items():
                  selected = eval_selector(k[3:], namespace, [])
                  if selected:
                      return v
  
              return None
  
          for k, v in ydoc.items():
              ydoc[k] = flatten_selectors(v, namespace)
  
      elif isinstance(ydoc, Iterable):
          to_delete = []
          for idx, el in enumerate(ydoc):
              res = flatten_selectors(el, namespace)
              if res is None:
                  to_delete.append(idx)
              else:
                  ydoc[idx] = res
  
          if len(to_delete):
              ydoc = [ydoc[idx] for idx in range(len(ydoc)) if idx not in to_delete]
  
          # flatten lists if necessary
          if any([isinstance(x, list) for x in ydoc]):
              final_list = []
              for x in ydoc:
                  if isinstance(x, list):
                      final_list += x
                  else:
                      final_list.append(x)
              ydoc = final_list
  
      return ydoc
  
  
  def ensure_list(x):
      if not type(x) is list:
          return [x]
      else:
          return x
  
  
  def normalize_recipe(ydoc):
      # normalizing recipe:
      # sources -> list
      # every output -> to outputs list
      if ydoc.get("context"):
          del ydoc["context"]
  
      if ydoc.get("source"):
          ydoc["source"] = ensure_list(ydoc["source"])
  
      if not ydoc.get("outputs"):
          ydoc["outputs"] = [{"package": ydoc["package"]}]
  
          toplevel_output = ydoc["outputs"][0]
      else:
          for o in ydoc["outputs"]:
              if o["package"]["name"] == ydoc["package"]["name"]:
                  toplevel_output = o
                  break
          else:
              # how do we handle no-output toplevel?!
              toplevel_output = None
              assert not ydoc.get("requirements")
  
      if ydoc.get("requirements"):
          # move these under toplevel output
          assert not toplevel_output.get("requirements")
          toplevel_output["requirements"] = ydoc["requirements"]
          del ydoc["requirements"]
  
      if ydoc.get("test"):
          # move these under toplevel output
          assert not toplevel_output.get("test")
          toplevel_output["test"] = ydoc["test"]
          del ydoc["test"]
  
      def move_to_toplevel(key):
          if ydoc.get("build", {}).get(key):
              if not toplevel_output.get("build"):
                  toplevel_output["build"] = {}
              toplevel_output["build"][key] = ydoc["build"][key]
              del ydoc["build"][key]
  
      move_to_toplevel("run_exports")
      move_to_toplevel("ignore_run_exports")
  
      return ydoc
  
  
  def default_jinja_vars(config):
      res = {}
      cfg = ns_cfg(config)
  
      res["build_platform"] = cfg["build_platform"]
      res["target_platform"] = cfg.get("target_platform", cfg["build_platform"])
  
      tgp = res["target_platform"]
  
      if tgp.startswith("win"):
          prefix = "%PREFIX%"
      else:
          prefix = "$PREFIX"
  
      # this adds PYTHON, R, RSCRIPT ... etc so that they can be used in the
      # recipe script
      for lang in ["python", "lua", "r", "rscript", "perl"]:
          res[lang.upper()] = getattr(config, "_get_" + lang)(prefix, tgp)
  
      return res
  
  
  def render(recipe_path, config=None):
      # console.print(f"\n[yellow]Rendering {recipe_path}[/yellow]\n")
      # step 1: parse YAML
      with open(recipe_path) as fi:
          loader = YAML(typ="safe")
          ydoc = loader.load(fi)
  
      # step 2: fill out context dict
      context_dict = default_jinja_vars(config)
      context_dict.update(ydoc.get("context", {}))
      jenv = jinja2.Environment()
      for key, value in context_dict.items():
          if isinstance(value, str):
              tmpl = jenv.from_string(value)
              context_dict[key] = tmpl.render(context_dict)
  
      # step 3: recursively loop over the entire recipe and render jinja with context
      jenv.globals.update(jinja_functions(config, context_dict))
      for key in ydoc:
          render_recursive(ydoc[key], context_dict, jenv)
  
      flatten_selectors(ydoc, ns_cfg(config))
  
      ydoc = normalize_recipe(ydoc)
      # console.print("\n[yellow]Normalized recipe[/yellow]\n")
      # console.print(ydoc)
      return ydoc
  