# Copyright 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 atexit import re import sys from os import environ, remove, walk from os.path import basename, isdir, isfile, join, relpath from tempfile import mkstemp from SCons.Action import Action from SCons.Script import ARGUMENTS from platformio import util class InoToCPPConverter(object): PROTOTYPE_RE = re.compile(r"""^( (?:template\<.*\>\s*)? # template ([a-z_\d]+\*?\s+){1,2} # return type ([a-z_\d]+\s*) # name of prototype \([a-z_,\.\*\&\[\]\s\d]*\) # arguments )\s*\{ # must end with { """, re.X | re.M | re.I) DETECTMAIN_RE = re.compile(r"void\s+(setup|loop)\s*\(", re.M | re.I) PROTOPTRS_TPLRE = r"\([^&\(]*&(%s)[^\)]*\)" def __init__(self, env): self.env = env self._main_ino = None def is_main_node(self, contents): return self.DETECTMAIN_RE.search(contents) def convert(self, nodes): contents = self.merge(nodes) if not contents: return return self.process(contents) def merge(self, nodes): assert nodes lines = [] for node in nodes: contents = node.get_text_contents() _lines = [ '# 1 "%s"' % node.get_path().replace("\\", "/"), contents ] if self.is_main_node(contents): lines = _lines + lines self._main_ino = node.get_path() else: lines.extend(_lines) if not self._main_ino: self._main_ino = nodes[0].get_path() return "\n".join(["#include "] + lines) if lines else None def process(self, contents): out_file = self._main_ino + ".cpp" assert self._gcc_preprocess(contents, out_file) with open(out_file) as fp: contents = fp.read() contents = self._join_multiline_strings(contents) with open(out_file, "w") as fp: fp.write(self.append_prototypes(contents)) return out_file def _gcc_preprocess(self, contents, out_file): tmp_path = mkstemp()[1] with open(tmp_path, "w") as fp: fp.write(contents) self.env.Execute( self.env.VerboseAction( '$CXX -o "{0}" -x c++ -fpreprocessed -dD -E "{1}"'.format( out_file, tmp_path), "Converting " + basename( out_file[:-4]))) atexit.register(_delete_file, tmp_path) return isfile(out_file) def _join_multiline_strings(self, contents): if "\\\n" not in contents: return contents newlines = [] linenum = 0 stropen = False for line in contents.split("\n"): _linenum = self._parse_preproc_line_num(line) if _linenum is not None: linenum = _linenum else: linenum += 1 if line.endswith("\\"): if line.startswith('"'): stropen = True newlines.append(line[:-1]) continue elif stropen: newlines[len(newlines) - 1] += line[:-1] continue elif stropen and line.endswith('";'): newlines[len(newlines) - 1] += line stropen = False newlines.append('#line %d "%s"' % (linenum, self._main_ino.replace("\\", "/"))) continue newlines.append(line) return "\n".join(newlines) @staticmethod def _parse_preproc_line_num(line): if not line.startswith("#"): return None tokens = line.split(" ", 3) if len(tokens) > 2 and tokens[1].isdigit(): return int(tokens[1]) return None def _parse_prototypes(self, contents): prototypes = [] reserved_keywords = set(["if", "else", "while"]) for match in self.PROTOTYPE_RE.finditer(contents): if (set([match.group(2).strip(), match.group(3).strip()]) & reserved_keywords): continue prototypes.append(match) return prototypes def _get_total_lines(self, contents): total = 0 if contents.endswith("\n"): contents = contents[:-1] for line in contents.split("\n")[::-1]: linenum = self._parse_preproc_line_num(line) if linenum is not None: return total + linenum total += 1 return total def append_prototypes(self, contents): prototypes = self._parse_prototypes(contents) if not prototypes: return contents prototype_names = set([m.group(3).strip() for m in prototypes]) split_pos = prototypes[0].start() match_ptrs = re.search(self.PROTOPTRS_TPLRE % ("|".join(prototype_names)), contents[:split_pos], re.M) if match_ptrs: split_pos = contents.rfind("\n", 0, match_ptrs.start()) + 1 result = [] result.append(contents[:split_pos].strip()) result.append("%s;" % ";\n".join([m.group(1) for m in prototypes])) result.append('#line %d "%s"' % (self._get_total_lines(contents[:split_pos]), self._main_ino.replace("\\", "/"))) result.append(contents[split_pos:].strip()) return "\n".join(result) def ConvertInoToCpp(env): src_dir = util.glob_escape(env.subst("$PROJECTSRC_DIR")) ino_nodes = ( env.Glob(join(src_dir, "*.ino")) + env.Glob(join(src_dir, "*.pde"))) if not ino_nodes: return c = InoToCPPConverter(env) out_file = c.convert(ino_nodes) atexit.register(_delete_file, out_file) def _delete_file(path): try: if isfile(path): remove(path) except: # pylint: disable=bare-except pass def GetCompilerType(env): try: sysenv = environ.copy() sysenv['PATH'] = str(env['ENV']['PATH']) result = util.exec_command([env.subst("$CC"), "-v"], env=sysenv) except OSError: return None if result['returncode'] != 0: return None output = "".join([result['out'], result['err']]).lower() if "clang" in output and "LLVM" in output: return "clang" elif "gcc" in output: return "gcc" return None def GetActualLDScript(env): def _lookup_in_ldpath(script): for d in env.get("LIBPATH", []): path = join(env.subst(d), script) if isfile(path): return path return None script = None for f in env.get("LINKFLAGS", []): if f.startswith("-Wl,-T"): script = env.subst(f[6:].replace('"', "").strip()) if isfile(script): return script path = _lookup_in_ldpath(script) if path: return path if script: sys.stderr.write( "Error: Could not find '%s' LD script in LDPATH '%s'\n" % (script, env.subst("$LIBPATH"))) env.Exit(1) if not script and "LDSCRIPT_PATH" in env: path = _lookup_in_ldpath(env['LDSCRIPT_PATH']) if path: return path sys.stderr.write("Error: Could not find LD script\n") env.Exit(1) def VerboseAction(_, act, actstr): if int(ARGUMENTS.get("PIOVERBOSE", 0)): return act else: return Action(act, actstr) def PioClean(env, clean_dir): if not isdir(clean_dir): print "Build environment is clean" env.Exit(0) for root, _, files in walk(clean_dir): for file_ in files: remove(join(root, file_)) print "Removed %s" % relpath(join(root, file_)) print "Done cleaning" util.rmtree_(clean_dir) env.Exit(0) def exists(_): return True def generate(env): env.AddMethod(ConvertInoToCpp) env.AddMethod(GetCompilerType) env.AddMethod(GetActualLDScript) env.AddMethod(VerboseAction) env.AddMethod(PioClean) return env