text: # SPDX-License-Identifier: Apache-2.0
  # Copyright 2021 EPAM Systems
  """
  Yocto builder module
  """
  
  import os.path
  import shlex
  from typing import List, Tuple, cast
  from moulin.utils import create_stamp_name, construct_fetcher_dep_cmd
  from moulin import ninja_syntax
  from moulin.yaml_wrapper import YamlValue
  from moulin.yaml_helpers import YAMLProcessingError
  
  
  def get_builder(conf: YamlValue, name: str, build_dir: str, src_stamps: List[str],
                  generator: ninja_syntax.Writer):
      """
      Return configured YoctoBuilder class
      """
      return YoctoBuilder(conf, name, build_dir, src_stamps, generator)
  
  
  def gen_build_rules(generator: ninja_syntax.Writer):
      """
      Generate yocto build rules for ninja
      """
      # Create build dir by calling poky/oe-init-build-env script
      cmd = " && ".join([
          "cd $yocto_dir",
          "source poky/oe-init-build-env $work_dir",
      ])
      generator.rule("yocto_init_env",
                     command=f'bash -c "{cmd}"',
                     description="Initialize Yocto build environment",
                     restat=True)
      generator.newline()
  
      # Add bitbake layers by calling bitbake-layers script
      cmd = " && ".join([
          "cd $yocto_dir",
          "source poky/oe-init-build-env $work_dir",
          "bitbake-layers add-layer $layers",
          "touch $out",
      ])
      generator.rule("yocto_add_layers",
                     command=f'bash -c "{cmd}"',
                     description="Add yocto layers",
                     pool="console",
                     restat=True)
      generator.newline()
  
      # Write own configuration to moulin.conf. Include it in local.conf
      cmd = " && ".join([
          "cd $yocto_dir",
          "echo '# Code generated by moulin. All manual changes will be lost' > $work_dir/conf/moulin.conf",
          "for x in $conf; do echo $$x >> $work_dir/conf/moulin.conf; done",
          "sed \"/require moulin\\.conf/d\" -i $work_dir/conf/local.conf",
          "echo 'require moulin.conf' >> $work_dir/conf/local.conf",
          "touch -r $work_dir/conf/bblayers.conf $work_dir/conf/local.conf",
      ])
      generator.rule(
          "yocto_update_conf",
          command=cmd,
          description="Update local.conf",
      )
      generator.newline()
  
      # Invoke bitbake. This rule uses "console" pool so we can see the bitbake output.
      cmd = " && ".join([
          # Generate fetcher dependency file
          construct_fetcher_dep_cmd(),
          "cd $yocto_dir",
          "source poky/oe-init-build-env $work_dir",
          "bitbake $target",
      ])
      generator.rule("yocto_build",
                     command=f'bash -c "{cmd}"',
                     description="Yocto Build: $name",
                     pool="console",
                     deps="gcc",
                     depfile=".moulin_$name.d",
                     restat=True)
  
  
  def _flatten_yocto_conf(conf: YamlValue) -> List[Tuple[str, str]]:
      """
      Flatten conf entries. While using YAML *entries syntax, we will get list of conf
      entries inside of other list. To overcome this, we need to move inner list 'up'
      """
  
      # Problem is conf entries that it is list itself
      result: List[Tuple[str, str]] = []
      for entry in conf:
          if not entry.is_list:
              raise YAMLProcessingError("Exptected array on 'conf' node", entry.mark)
          if entry[0].is_list:
              result.extend([(x[0].as_str, x[1].as_str) for x in entry])
          else:
              result.append((entry[0].as_str, entry[1].as_str))
      return result
  
  
  class YoctoBuilder:
      """
      YoctoBuilder class generates Ninja rules for given build configuration
      """
      def __init__(self, conf: YamlValue, name: str, build_dir: str, src_stamps: List[str],
                   generator: ninja_syntax.Writer):
          self.conf = conf
          self.name = name
          self.generator = generator
          self.src_stamps = src_stamps
          # With yocto builder it is possible to have multiple builds with the same set of
          # layers. Thus, we have two variables - build_dir and work_dir
          # - yocto_dir is the upper directory where layers are stored. Basically, we should
          #   have "poky" in our yocto_dir
          # - work_dir is the build directory where we can find conf/local.conf, tmp and other
          #   directories. It is called "build" by default
          self.yocto_dir = build_dir
          self.work_dir: str = conf.get("work_dir", "build").as_str
  
      def _get_external_src(self) -> List[Tuple[str, str]]:
          external_src_node = self.conf.get("external_src", None)
          if not external_src_node:
              return []
  
          ret: List[Tuple[str, str]] = []
          for key, val_node in cast(YamlValue, external_src_node).items():
              if val_node.is_list:
                  path = os.path.join(*[x.as_str for x in val_node])
              else:
                  path = val_node.as_str
              path = os.path.abspath(path)
              ret.append((f"EXTERNALSRC_pn-{key}", path))
  
          return ret
  
      def gen_build(self):
          """Generate ninja rules to build yocto/poky"""
          common_variables = {
              "yocto_dir": self.yocto_dir,
              "work_dir": self.work_dir,
          }
  
          # First we need to ensure that "conf" dir exists
          env_target = os.path.join(self.yocto_dir, self.work_dir, "conf", "local.conf")
          self.generator.build(env_target,
                               "yocto_init_env",
                               self.src_stamps,
                               variables=common_variables)
  
          # Then we need to add layers
          layers_node = self.conf.get("layers", None)
          if layers_node:
              layers = " ".join([x.as_str for x in layers_node])
          else:
              layers = ""
          layers_stamp = create_stamp_name(self.yocto_dir, self.work_dir, "yocto", "layers")
          self.generator.build(layers_stamp,
                               "yocto_add_layers",
                               env_target,
                               variables=dict(common_variables, layers=layers))
  
          # Next - update local.conf
          local_conf_target = os.path.join(self.yocto_dir, self.work_dir, "conf", "moulin.conf")
          local_conf_node = self.conf.get("conf", None)
          if local_conf_node:
              local_conf = _flatten_yocto_conf(local_conf_node)
          else:
              local_conf = []
  
          # Handle external sources (like build artifacts from some other build)
          local_conf.extend(self._get_external_src())
  
          # '$' is a ninja escape character so we need to quote it
          local_conf_lines = [
              shlex.quote(f'{k.replace("$", "$$")} = "{v.replace("$", "$$")}"') for k, v in local_conf
          ]
  
          self.generator.build(local_conf_target,
                               "yocto_update_conf",
                               layers_stamp,
                               variables=dict(common_variables, conf=" ".join(local_conf_lines)))
          self.generator.newline()
  
          self.generator.build(f"conf-{self.name}", "phony", local_conf_target)
          self.generator.newline()
  
          # Next step - invoke bitbake. At last :)
          targets = self.get_targets()
          additional_deps_node = self.conf.get("additional_deps", None)
          if additional_deps_node:
              deps = [os.path.join(self.yocto_dir, d.as_str) for d in additional_deps_node]
          else:
              deps = []
          deps.append(local_conf_target)
          self.generator.build(targets,
                               "yocto_build",
                               deps,
                               variables=dict(common_variables,
                                              target=self.conf["build_target"].as_str,
                                              name=self.name))
  
          return targets
  
      def get_targets(self):
          "Return list of targets that are generated by this build"
          return [
              os.path.join(self.yocto_dir, self.work_dir, t.as_str)
              for t in self.conf["target_images"]
          ]
  
      def capture_state(self):
          """
          Update stored local conf with actual SRCREVs for VCS-based recipes.
          This should ensure that we can reproduce this exact build later
          """
  