text: # Copyright (c) 2014-present PlatformIO <contact@platformio.org>
  #
  # 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
  #
  #    http://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.
  
  from __future__ import absolute_import
  
  import fnmatch
  import os
  import sys
  
  from SCons import Builder, Util  # pylint: disable=import-error
  from SCons.Node import FS  # pylint: disable=import-error
  from SCons.Script import COMMAND_LINE_TARGETS  # pylint: disable=import-error
  from SCons.Script import AlwaysBuild  # pylint: disable=import-error
  from SCons.Script import DefaultEnvironment  # pylint: disable=import-error
  from SCons.Script import Export  # pylint: disable=import-error
  from SCons.Script import SConscript  # pylint: disable=import-error
  
  from platformio import fs
  from platformio.compat import string_types
  from platformio.util import pioversion_to_intstr
  
  SRC_HEADER_EXT = ["h", "hpp"]
  SRC_ASM_EXT = ["S", "spp", "SPP", "sx", "s", "asm", "ASM"]
  SRC_C_EXT = ["c"]
  SRC_CXX_EXT = ["cc", "cpp", "cxx", "c++"]
  SRC_BUILD_EXT = SRC_C_EXT + SRC_CXX_EXT + SRC_ASM_EXT
  SRC_FILTER_DEFAULT = ["+<*>", "-<.git%s>" % os.sep, "-<.svn%s>" % os.sep]
  
  
  def scons_patched_match_splitext(path, suffixes=None):
      """Patch SCons Builder, append $OBJSUFFIX to the end of each target"""
      tokens = Util.splitext(path)
      if suffixes and tokens[1] and tokens[1] in suffixes:
          return (path, tokens[1])
      return tokens
  
  
  def GetBuildType(env):
      return (
          "debug"
          if (
              set(["debug", "sizedata"]) & set(COMMAND_LINE_TARGETS)
              or env.GetProjectOption("build_type") == "debug"
          )
          else "release"
      )
  
  
  def BuildProgram(env):
      env.ProcessProgramDeps()
      env.ProcessProjectDeps()
  
      # append into the beginning a main LD script
      if env.get("LDSCRIPT_PATH") and not any("-Wl,-T" in f for f in env["LINKFLAGS"]):
          env.Prepend(LINKFLAGS=["-T", env.subst("$LDSCRIPT_PATH")])
  
      # enable "cyclic reference" for linker
      if env.get("LIBS") and env.GetCompilerType() == "gcc":
          env.Prepend(_LIBFLAGS="-Wl,--start-group ")
          env.Append(_LIBFLAGS=" -Wl,--end-group")
  
      program = env.Program(
          os.path.join("$BUILD_DIR", env.subst("$PROGNAME")), env["PIOBUILDFILES"]
      )
      env.Replace(PIOMAINPROG=program)
  
      AlwaysBuild(
          env.Alias(
              "checkprogsize",
              program,
              env.VerboseAction(env.CheckUploadSize, "Checking size $PIOMAINPROG"),
          )
      )
  
      print("Building in %s mode" % env.GetBuildType())
  
      return program
  
  
  def ProcessProgramDeps(env):
      def _append_pio_macros():
          env.AppendUnique(
              CPPDEFINES=[
                  (
                      "PLATFORMIO",
                      int("{0:02d}{1:02d}{2:02d}".format(*pioversion_to_intstr())),
                  )
              ]
          )
  
      _append_pio_macros()
  
      env.PrintConfiguration()
  
      # fix ASM handling under non case-sensitive OS
      if not Util.case_sensitive_suffixes(".s", ".S"):
          env.Replace(AS="$CC", ASCOM="$ASPPCOM")
  
      # process extra flags from board
      if "BOARD" in env and "build.extra_flags" in env.BoardConfig():
          env.ProcessFlags(env.BoardConfig().get("build.extra_flags"))
  
      # apply user flags
      env.ProcessFlags(env.get("BUILD_FLAGS"))
  
      # process framework scripts
      env.BuildFrameworks(env.get("PIOFRAMEWORK"))
  
      if env.GetBuildType() == "debug":
          env.ConfigureDebugFlags()
  
      # remove specified flags
      env.ProcessUnFlags(env.get("BUILD_UNFLAGS"))
  
      if "__test" in COMMAND_LINE_TARGETS:
          env.ConfigureTestTarget()
  
  
  def ProcessProjectDeps(env):
      project_lib_builder = env.ConfigureProjectLibBuilder()
  
      # prepend project libs to the beginning of list
      env.Prepend(LIBS=project_lib_builder.build())
      # prepend extra linker related options from libs
      env.PrependUnique(
          **{
              key: project_lib_builder.env.get(key)
              for key in ("LIBS", "LIBPATH", "LINKFLAGS")
              if project_lib_builder.env.get(key)
          }
      )
  
      projenv = env.Clone()
  
      # CPPPATH from dependencies
      projenv.PrependUnique(CPPPATH=project_lib_builder.env.get("CPPPATH"))
      # extra build flags from `platformio.ini`
      projenv.ProcessFlags(env.get("SRC_BUILD_FLAGS"))
  
      is_test = "__test" in COMMAND_LINE_TARGETS
      if is_test:
          projenv.BuildSources(
              "$BUILD_TEST_DIR", "$PROJECT_TEST_DIR", "$PIOTEST_SRC_FILTER"
          )
      if not is_test or env.GetProjectOption("test_build_project_src"):
          projenv.BuildSources(
              "$BUILD_SRC_DIR", "$PROJECT_SRC_DIR", env.get("SRC_FILTER")
          )
  
      if not env.get("PIOBUILDFILES") and not COMMAND_LINE_TARGETS:
          sys.stderr.write(
              "Error: Nothing to build. Please put your source code files "
              "to '%s' folder\n" % env.subst("$PROJECT_SRC_DIR")
          )
          env.Exit(1)
  
      Export("projenv")
  
  
  def ParseFlagsExtended(env, flags):  # pylint: disable=too-many-branches
      if not isinstance(flags, list):
          flags = [flags]
      result = {}
      for raw in flags:
          for key, value in env.ParseFlags(str(raw)).items():
              if key not in result:
                  result[key] = []
              result[key].extend(value)
  
      cppdefines = []
      for item in result["CPPDEFINES"]:
          if not Util.is_Sequence(item):
              cppdefines.append(item)
              continue
          name, value = item[:2]
          if '"' in value:
              value = value.replace('"', '\\"')
          elif value.isdigit():
              value = int(value)
          elif value.replace(".", "", 1).isdigit():
              value = float(value)
          cppdefines.append((name, value))
      result["CPPDEFINES"] = cppdefines
  
      # fix relative CPPPATH & LIBPATH
      for k in ("CPPPATH", "LIBPATH"):
          for i, p in enumerate(result.get(k, [])):
              if os.path.isdir(p):
                  result[k][i] = os.path.realpath(p)
  
      # fix relative path for "-include"
      for i, f in enumerate(result.get("CCFLAGS", [])):
          if isinstance(f, tuple) and f[0] == "-include":
              result["CCFLAGS"][i] = (f[0], env.File(os.path.realpath(f[1].get_path())))
  
      return result
  
  
  def ProcessFlags(env, flags):  # pylint: disable=too-many-branches
      if not flags:
          return
      env.Append(**env.ParseFlagsExtended(flags))
  
      # Cancel any previous definition of name, either built in or
      # provided with a -U option // Issue #191
      undefines = [
          u
          for u in env.get("CCFLAGS", [])
          if isinstance(u, string_types) and u.startswith("-U")
      ]
      if undefines:
          for undef in undefines:
              env["CCFLAGS"].remove(undef)
              if undef[2:] in env["CPPDEFINES"]:
                  env["CPPDEFINES"].remove(undef[2:])
          env.Append(_CPPDEFFLAGS=" %s" % " ".join(undefines))
  
  
  def ProcessUnFlags(env, flags):
      if not flags:
          return
      parsed = env.ParseFlagsExtended(flags)
  
      # get all flags and copy them to each "*FLAGS" variable
      all_flags = []
      for key, unflags in parsed.items():
          if key.endswith("FLAGS"):
              all_flags.extend(unflags)
      for key, unflags in parsed.items():
          if key.endswith("FLAGS"):
              parsed[key].extend(all_flags)
  
      for key, unflags in parsed.items():
          for unflag in unflags:
              for current in env.get(key, []):
                  conditions = [
                      unflag == current,
                      isinstance(current, (tuple, list)) and unflag[0] == current[0],
                  ]
                  if any(conditions):
                      env[key].remove(current)
  
  
  def MatchSourceFiles(env, src_dir, src_filter=None):
      src_filter = env.subst(src_filter) if src_filter else None
      src_filter = src_filter or SRC_FILTER_DEFAULT
      return fs.match_src_files(
          env.subst(src_dir), src_filter, SRC_BUILD_EXT + SRC_HEADER_EXT
      )
  
  
  def CollectBuildFiles(
      env, variant_dir, src_dir, src_filter=None, duplicate=False
  ):  # pylint: disable=too-many-locals
      sources = []
      variants = []
  
      src_dir = env.subst(src_dir)
      if src_dir.endswith(os.sep):
          src_dir = src_dir[:-1]
  
      for item in env.MatchSourceFiles(src_dir, src_filter):
          _reldir = os.path.dirname(item)
          _src_dir = os.path.join(src_dir, _reldir) if _reldir else src_dir
          _var_dir = os.path.join(variant_dir, _reldir) if _reldir else variant_dir
  
          if _var_dir not in variants:
              variants.append(_var_dir)
              env.VariantDir(_var_dir, _src_dir, duplicate)
  
          if fs.path_endswith_ext(item, SRC_BUILD_EXT):
              sources.append(env.File(os.path.join(_var_dir, os.path.basename(item))))
  
      middlewares = env.get("__PIO_BUILD_MIDDLEWARES")
      if not middlewares:
          return sources
  
      new_sources = []
      for node in sources:
          new_node = node
          for callback, pattern in middlewares:
              if pattern and not fnmatch.fnmatch(node.srcnode().get_path(), pattern):
                  continue
              new_node = callback(new_node)
          if new_node:
              new_sources.append(new_node)
  
      return new_sources
  
  
  def AddBuildMiddleware(env, callback, pattern=None):
      env.Append(__PIO_BUILD_MIDDLEWARES=[(callback, pattern)])
  
  
  def BuildFrameworks(env, frameworks):
      if not frameworks:
          return
  
      if "BOARD" not in env:
          sys.stderr.write(
              "Please specify `board` in `platformio.ini` to use "
              "with '%s' framework\n" % ", ".join(frameworks)
          )
          env.Exit(1)
  
      board_frameworks = env.BoardConfig().get("frameworks", [])
      if frameworks == ["platformio"]:
          if board_frameworks:
              frameworks.insert(0, board_frameworks[0])
          else:
              sys.stderr.write("Error: Please specify `board` in `platformio.ini`\n")
              env.Exit(1)
  
      for f in frameworks:
          if f == "arduino":
              # Arduino IDE appends .o the end of filename
              Builder.match_splitext = scons_patched_match_splitext
              if "nobuild" not in COMMAND_LINE_TARGETS:
                  env.ConvertInoToCpp()
  
          if f in board_frameworks:
              SConscript(env.GetFrameworkScript(f), exports="env")
          else:
              sys.stderr.write("Error: This board doesn't support %s framework!\n" % f)
              env.Exit(1)
  
  
  def BuildLibrary(env, variant_dir, src_dir, src_filter=None):
      env.ProcessUnFlags(env.get("BUILD_UNFLAGS"))
      return env.StaticLibrary(
          env.subst(variant_dir), env.CollectBuildFiles(variant_dir, src_dir, src_filter)
      )
  
  
  def BuildSources(env, variant_dir, src_dir, src_filter=None):
      nodes = env.CollectBuildFiles(variant_dir, src_dir, src_filter)
      DefaultEnvironment().Append(
          PIOBUILDFILES=[
              env.Object(node) if isinstance(node, FS.File) else node for node in nodes
          ]
      )
  
  
  def exists(_):
      return True
  
  
  def generate(env):
      env.AddMethod(GetBuildType)
      env.AddMethod(BuildProgram)
      env.AddMethod(ProcessProgramDeps)
      env.AddMethod(ProcessProjectDeps)
      env.AddMethod(ParseFlagsExtended)
      env.AddMethod(ProcessFlags)
      env.AddMethod(ProcessUnFlags)
      env.AddMethod(MatchSourceFiles)
      env.AddMethod(CollectBuildFiles)
      env.AddMethod(AddBuildMiddleware)
      env.AddMethod(BuildFrameworks)
      env.AddMethod(BuildLibrary)
      env.AddMethod(BuildSources)
      return env
  