text: import os
  import re
  import textwrap
  import jinja2
  
  from collections import OrderedDict
  from collections.abc import Mapping
  
  from nmigen import tracer
  from nmigen.utils import log2_int
  from nmigen.build.run import *
  
  from nmigen_soc.memory import MemoryMap
  from nmigen_soc.periph import ConstantMap, ConstantBool, ConstantInt
  
  from .. import __version__, software
  from ..periph import Peripheral
  
  
  __all__ = ["socproperty", "ConstantAddr", "ConstantMapCollection", "SoC", "ConfigBuilder"]
  
  
  def socproperty(cls, *, weak=False, src_loc_at=0):
      name   = tracer.get_var_name(depth=2 + src_loc_at)
      __name = "__{}".format(name)
  
      def getter(self):
          assert isinstance(self, SoC)
          attr = getattr(self, __name, None)
          if attr is None and not weak:
              raise NotImplementedError("SoC {!r} does not have a {}"
                                        .format(self, name))
          return attr
  
      def setter(self, value):
          assert isinstance(self, SoC)
          if not isinstance(value, cls):
              raise TypeError("{} must be an instance of {}, not {!r}"
                              .format(name, cls.__name__, value))
          setattr(self, __name, value)
  
      return property(getter, setter)
  
  
  class ConstantAddr(ConstantInt):
      def __init__(self, value, *, width=None):
          return super().__init__(value, width=width, signed=False)
  
      def __repr__(self):
          return "ConstantAddr({}, width={})".format(self.value, self.width)
  
  
  class ConstantMapCollection(Mapping):
      def __init__(self, **constant_maps):
          self._storage = OrderedDict()
          for key, value in constant_maps.items():
              if value is None:
                  pass
              elif not isinstance(value, (ConstantMap, ConstantMapCollection)):
                  raise TypeError("Constant map must be an instance of ConstantMap or "
                                  "ConstantMapCollection, not {!r}"
                                  .format(value))
              self._storage[key] = value
  
      def flatten(self, *, prefix="", separator="_"):
          if not isinstance(prefix, str):
              raise TypeError("Prefix must be a string, not {!r}".format(prefix))
          if not isinstance(separator, str):
              raise TypeError("Separator must be a string, not {!r}".format(separator))
          for key, value in self.items():
              if isinstance(value, ConstantMap):
                  for const_key, const_value in value.items():
                      yield f"{prefix}{key}{separator}{const_key}", const_value
              elif isinstance(value, ConstantMapCollection):
                  yield from value.flatten(prefix=f"{prefix}{key}{separator}", separator=separator)
  
      def union(self, **other):
          union = OrderedDict()
          for key in self.keys() | other.keys():
              self_value  = self .get(key, None)
              other_value = other.get(key, None)
              if self_value is None or other_value is None:
                  union[key] = self_value or other_value
              elif isinstance(self_value, ConstantMap):
                  if not isinstance(other_value, ConstantMap):
                      raise TypeError # TODO
                  union[key] = ConstantMap(**self_value, **other_value)
              elif isinstance(self_value, ConstantMapCollection):
                  if not isinstance(other_value, ConstantMapCollection):
                      raise TypeError # TODO
                  union[key] = self_value.merge(**{key: other_value})
              else:
                  assert False
          return ConstantMapCollection(**union)
  
      def __getitem__(self, prefix):
          return self._storage[prefix]
  
      def __iter__(self):
          yield from self._storage
  
      def __len__(self):
          return len(self._storage)
  
      def __repr__(self):
          return "ConstantMapCollection({})".format(list(self._storage.items()))
  
  
  class SoC:
      memory_map = socproperty(MemoryMap)
      constants  = socproperty(ConstantMapCollection, weak=True)
  
      def build(self, build_dir="build/soc", do_build=True, name=None):
          plan = ConfigBuilder().prepare(self, build_dir, name)
          if not do_build:
              return plan
  
          products = plan.execute_local(build_dir)
          return products
  
  
  class ConfigBuilder:
      file_templates = {
          "build_{{name}}.sh": r"""
              # {{autogenerated}}
              set -e
              {{emit_commands()}}
          """,
          "{{name}}_resources.csv": r"""
              # {{autogenerated}}
              # <resource name>, <start address>, <end address>, <access width>
              {% for res_info in soc.memory_map.all_resources() -%}
                  {{"_".join(res_info.name)}}, {{hex(res_info.start)}}, {{hex(res_info.end)}}, {{res_info.width}}
              {% endfor %}
          """,
      }
      command_templates = []
  
      def prepare(self, soc, build_dir, name, **render_params):
          name = name or type(soc).__name__.lower()
  
          autogenerated = "Automatically generated by LambdaSoC {}. Do not edit.".format(__version__)
  
          def periph_addr(periph):
              assert isinstance(periph, Peripheral)
              periph_map = periph.bus.memory_map
              for window, (start, end, ratio) in soc.memory_map.windows():
                  if periph_map is window:
                      return start
              raise KeyError(periph)
  
          def periph_size(periph):
              assert isinstance(periph, Peripheral)
              granularity_bits = log2_int(periph.bus.data_width // periph.bus.granularity)
              return 2**(periph.bus.addr_width + granularity_bits)
  
          def emit_commands():
              commands = []
              for index, command_tpl in enumerate(self.command_templates):
                  command = render(command_tpl, origin="<command#{}>".format(index + 1))
                  command = re.sub(r"\s+", " ", command)
                  commands.append(command)
              return "\n".join(commands)
  
          def render(source, origin):
              try:
                  source = textwrap.dedent(source).strip()
                  compiled = jinja2.Template(source, trim_blocks=True, lstrip_blocks=True)
              except jinja2.TemplateSyntaxError as e:
                  e.args = ("{} (at {}:{})".format(e.message, origin, e.lineno),)
                  raise
              return compiled.render({
                  "autogenerated": autogenerated,
                  "build_dir": os.path.abspath(build_dir),
                  "emit_commands": emit_commands,
                  "hex": hex,
                  "name": name,
                  "periph_addr": periph_addr,
                  "periph_size": periph_size,
                  "soc": soc,
                  "software_dir": os.path.dirname(software.__file__),
                  "constants": soc.constants,
                  "ConstantAddr": ConstantAddr,
                  **render_params,
              })
  
          plan = BuildPlan(script="build_{}".format(name))
          for filename_tpl, content_tpl in self.file_templates.items():
              plan.add_file(render(filename_tpl, origin=filename_tpl),
                            render(content_tpl, origin=content_tpl))
          return plan
  