# Copyright (c) 2014-present PlatformIO # # 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)))) for callback, pattern in env.get("__PIO_BUILD_MIDDLEWARES", []): tmp = [] for node in sources: if pattern and not fnmatch.fnmatch(node.get_path(), pattern): tmp.append(node) continue n = callback(node) if n: tmp.append(n) sources = tmp return 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