Refactor INO to CPP converter

This commit is contained in:
Ivan Kravets
2016-08-31 00:16:23 +03:00
parent 34a860cfa6
commit 8a379d2db2
12 changed files with 179 additions and 106 deletions

View File

@ -73,6 +73,7 @@ PlatformIO 3.0
* Print human-readable information when processing environments without * Print human-readable information when processing environments without
``-v, --verbose`` option ``-v, --verbose`` option
(`issue #721 <https://github.com/platformio/platformio/issues/721>`_) (`issue #721 <https://github.com/platformio/platformio/issues/721>`_)
* Improved INO to CPP converter
* Added ``license`` field to `library.json <http://docs.platformio.org/en/latest/librarymanager/config.html>`__ * Added ``license`` field to `library.json <http://docs.platformio.org/en/latest/librarymanager/config.html>`__
(`issue #522 <https://github.com/platformio/platformio/issues/522>`_) (`issue #522 <https://github.com/platformio/platformio/issues/522>`_)
* Warn about unknown options in project configuration file ``platformio.ini`` * Warn about unknown options in project configuration file ``platformio.ini``

View File

@ -198,10 +198,7 @@ Manifest File ``platform.json``
"description": "My custom development platform", "description": "My custom development platform",
"url": "http://example.com", "url": "http://example.com",
"homepage": "http://platformio.org/platforms/myplatform", "homepage": "http://platformio.org/platforms/myplatform",
"license": { "license": "Apache-2.0",
"type": "Apache-2.0",
"url": "http://opensource.org/licenses/apache2.0.php"
},
"engines": { "engines": {
"platformio": "~3.0.0", "platformio": "~3.0.0",
"scons": ">=2.3.0,<2.6.0" "scons": ">=2.3.0,<2.6.0"

View File

@ -14,7 +14,7 @@
import sys import sys
VERSION = (3, 0, "0a13") VERSION = (3, 0, "0b1")
__version__ = ".".join([str(s) for s in VERSION]) __version__ = ".".join([str(s) for s in VERSION])
__title__ = "platformio" __title__ = "platformio"

View File

@ -134,8 +134,8 @@ class LibBuilderBase(object): # pylint: disable=too-many-instance-attributes
def build_dir(self): def build_dir(self):
return join("$BUILD_DIR", "lib", basename(self.path)) return join("$BUILD_DIR", "lib", basename(self.path))
def get_inc_dirs(self, use_build_dir=False): def get_inc_dirs(self):
return [self.build_dir if use_build_dir else self.src_dir] return [self.src_dir]
@property @property
def build_flags(self): def build_flags(self):
@ -290,8 +290,7 @@ class LibBuilderBase(object): # pylint: disable=too-many-instance-attributes
self.env.AppendUnique(**{key: lb.env.get(key)}) self.env.AppendUnique(**{key: lb.env.get(key)})
if not self._built_node: if not self._built_node:
self.env.AppendUnique(CPPPATH=self.get_inc_dirs( self.env.AppendUnique(CPPPATH=self.get_inc_dirs())
use_build_dir=True))
if self.lib_archive: if self.lib_archive:
self._built_node = self.env.BuildLibrary( self._built_node = self.env.BuildLibrary(
self.build_dir, self.src_dir, self.src_filter) self.build_dir, self.src_dir, self.src_filter)
@ -345,12 +344,11 @@ class ArduinoLibBuilder(LibBuilderBase):
manifest[key.strip()] = value.strip() manifest[key.strip()] = value.strip()
return manifest return manifest
def get_inc_dirs(self, use_build_dir=False): def get_inc_dirs(self):
inc_dirs = LibBuilderBase.get_inc_dirs(self, use_build_dir) inc_dirs = LibBuilderBase.get_inc_dirs(self)
if not isdir(join(self.path, "utility")): if not isdir(join(self.path, "utility")):
return inc_dirs return inc_dirs
inc_dirs.append( inc_dirs.append(join(self.path, "utility"))
join(self.build_dir if use_build_dir else self.path, "utility"))
return inc_dirs return inc_dirs
@property @property
@ -382,8 +380,8 @@ class MbedLibBuilder(LibBuilderBase):
return join(self.path, "source") return join(self.path, "source")
return LibBuilderBase.src_dir.fget(self) return LibBuilderBase.src_dir.fget(self)
def get_inc_dirs(self, use_build_dir=False): def get_inc_dirs(self):
inc_dirs = LibBuilderBase.get_inc_dirs(self, use_build_dir) inc_dirs = LibBuilderBase.get_inc_dirs(self)
if self.path not in inc_dirs: if self.path not in inc_dirs:
inc_dirs.append(self.path) inc_dirs.append(self.path)
for p in self._manifest.get("extraIncludes", []): for p in self._manifest.get("extraIncludes", []):
@ -451,12 +449,11 @@ class PlatformIOLibBuilder(LibBuilderBase):
ilist = [i.strip() for i in ilist.split(",")] ilist = [i.strip() for i in ilist.split(",")]
return item.lower() in [i.lower() for i in ilist] return item.lower() in [i.lower() for i in ilist]
def get_inc_dirs(self, use_build_dir=False): def get_inc_dirs(self):
inc_dirs = LibBuilderBase.get_inc_dirs(self, use_build_dir) inc_dirs = LibBuilderBase.get_inc_dirs(self)
for path in self.env['CPPPATH']: for path in self.env['CPPPATH']:
if path not in self.envorigin['CPPPATH']: if path not in self.envorigin['CPPPATH']:
inc_dirs.append( inc_dirs.append(self.env.subst(path))
path if use_build_dir else self.env.subst(path))
return inc_dirs return inc_dirs
@ -499,8 +496,9 @@ def GetLibBuilders(env):
try: try:
lb = LibBuilderFactory.new(env, join(libs_dir, item)) lb = LibBuilderFactory.new(env, join(libs_dir, item))
except ValueError: except ValueError:
sys.stderr.write("Skip library with broken manifest: %s\n" % if verbose:
join(libs_dir, item)) sys.stderr.write("Skip library with broken manifest: %s\n"
% join(libs_dir, item))
continue continue
if _check_lib_builder(lb): if _check_lib_builder(lb):
items += (lb, ) items += (lb, )

View File

@ -19,7 +19,8 @@ import re
import sys import sys
from glob import glob from glob import glob
from os import environ, remove from os import environ, remove
from os.path import isfile, join from os.path import basename, isfile, join
from tempfile import mkstemp
from SCons.Action import Action from SCons.Action import Action
from SCons.Script import ARGUMENTS from SCons.Script import ARGUMENTS
@ -38,106 +39,118 @@ class InoToCPPConverter(object):
DETECTMAIN_RE = re.compile(r"void\s+(setup|loop)\s*\(", re.M | re.I) DETECTMAIN_RE = re.compile(r"void\s+(setup|loop)\s*\(", re.M | re.I)
PROTOPTRS_TPLRE = r"\([^&\(]*&(%s)[^\)]*\)" PROTOPTRS_TPLRE = r"\([^&\(]*&(%s)[^\)]*\)"
def __init__(self, nodes): def __init__(self, env):
self.nodes = nodes self.env = env
self._main_ino = None
def is_main_node(self, contents): def is_main_node(self, contents):
return self.DETECTMAIN_RE.search(contents) return self.DETECTMAIN_RE.search(contents)
def _parse_prototypes(self, file_path, contents): def convert(self, nodes):
contents = self.merge(nodes)
if not contents:
return
return self.process(contents)
def merge(self, 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)
return "\n".join(["#include <Arduino.h>"] + 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()
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)
fp.close()
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])))
remove(tmp_path)
return isfile(out_file)
def _parse_prototypes(self, contents):
prototypes = [] prototypes = []
reserved_keywords = set(["if", "else", "while"]) reserved_keywords = set(["if", "else", "while"])
for match in self.PROTOTYPE_RE.finditer(contents): for match in self.PROTOTYPE_RE.finditer(contents):
if (set([match.group(2).strip(), match.group(3).strip()]) & if (set([match.group(2).strip(), match.group(3).strip()]) &
reserved_keywords): reserved_keywords):
continue continue
prototypes.append({"path": file_path, "match": match}) prototypes.append(match)
return prototypes return prototypes
def append_prototypes(self, file_path, contents, prototypes): @staticmethod
result = [] def _get_total_lines(contents):
total = 0
for line in contents.split("\n")[::-1]:
if line.startswith("#"):
tokens = line.split(" ", 3)
if len(tokens) > 2 and tokens[1].isdigit():
return int(tokens[1]) + total
total += 1
return total
def append_prototypes(self, contents):
prototypes = self._parse_prototypes(contents)
if not prototypes: if not prototypes:
return result return contents
prototype_names = set(
[p['match'].group(3).strip() for p in prototypes])
split_pos = prototypes[0]['match'].start()
for item in prototypes:
if item['path'] == file_path:
split_pos = item['match'].start()
break
prototype_names = set([m.group(3).strip() for m in prototypes])
split_pos = prototypes[0].start()
match_ptrs = re.search(self.PROTOPTRS_TPLRE % match_ptrs = re.search(self.PROTOPTRS_TPLRE %
("|".join(prototype_names)), ("|".join(prototype_names)),
contents[:split_pos], re.M) contents[:split_pos], re.M)
if match_ptrs: if match_ptrs:
split_pos = contents.rfind("\n", 0, match_ptrs.start()) split_pos = contents.rfind("\n", 0, match_ptrs.start())
result = []
result.append(contents[:split_pos].strip()) result.append(contents[:split_pos].strip())
result.append("%s;" % result.append("%s;" % ";\n".join([m.group(1) for m in prototypes]))
";\n".join([p['match'].group(1) for p in prototypes]))
result.append('#line %d "%s"' % result.append('#line %d "%s"' %
(contents.count("\n", 0, split_pos) + 2, (self._get_total_lines(contents[:split_pos]),
file_path.replace("\\", "/"))) self._main_ino.replace("\\", "/")))
result.append(contents[split_pos:].strip()) result.append(contents[split_pos:].strip())
return result
def convert(self):
prototypes = []
data = []
for node in self.nodes:
ino_contents = node.get_text_contents()
prototypes += self._parse_prototypes(node.get_path(), ino_contents)
item = (node.get_path(), ino_contents)
if self.is_main_node(ino_contents):
data = [item] + data
else:
data.append(item)
if not data:
return None
result = ["#include <Arduino.h>"]
is_first = True
for file_path, contents in data:
result.append('#line 1 "%s"' % file_path.replace("\\", "/"))
if is_first and prototypes:
result += self.append_prototypes(file_path, contents,
prototypes)
else:
result.append(contents)
is_first = False
return "\n".join(result) return "\n".join(result)
def ConvertInoToCpp(env): def ConvertInoToCpp(env):
def delete_tmpcpp_file(file_): def _delete_file(path):
try: try:
remove(file_) if isfile(path):
remove(path)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
if isfile(file_): if path and isfile(path):
print("Warning: Could not remove temporary file '%s'. " sys.stderr.write(
"Please remove it manually." % file_) "Warning: Could not remove temporary file '%s'. "
"Please remove it manually.\n" % path)
ino_nodes = (env.Glob(join("$PROJECTSRC_DIR", "*.ino")) + ino_nodes = (env.Glob(join("$PROJECTSRC_DIR", "*.ino")) +
env.Glob(join("$PROJECTSRC_DIR", "*.pde"))) env.Glob(join("$PROJECTSRC_DIR", "*.pde")))
c = InoToCPPConverter(env)
out_file = c.convert(ino_nodes)
c = InoToCPPConverter(ino_nodes) atexit.register(_delete_file, out_file)
data = c.convert()
if not data:
return
tmpcpp_file = join(env.subst("$PROJECTSRC_DIR"), "tmp_ino_to.cpp")
with open(tmpcpp_file, "w") as f:
f.write(data)
atexit.register(delete_tmpcpp_file, tmpcpp_file)
def DumpIDEData(env): def DumpIDEData(env):
@ -146,14 +159,7 @@ def DumpIDEData(env):
includes = [] includes = []
for item in env_.get("CPPPATH", []): for item in env_.get("CPPPATH", []):
invardir = False includes.append(env_.subst(item))
for vardiritem in env_.get("VARIANT_DIRS", []):
if item == vardiritem[0]:
includes.append(env_.subst(vardiritem[1]))
invardir = True
break
if not invardir:
includes.append(env_.subst(item))
# installed libs # installed libs
for lb in env.GetLibBuilders(): for lb in env.GetLibBuilders():

View File

@ -198,11 +198,6 @@ def MatchSourceFiles(env, src_dir, src_filter=None):
return sorted(list(matches)) return sorted(list(matches))
def VariantDirWrap(env, variant_dir, src_dir, duplicate=False):
DefaultEnvironment().Append(VARIANT_DIRS=[(variant_dir, src_dir)])
env.VariantDir(variant_dir, src_dir, duplicate)
def CollectBuildFiles(env, def CollectBuildFiles(env,
variant_dir, variant_dir,
src_dir, src_dir,
@ -222,7 +217,7 @@ def CollectBuildFiles(env,
if _var_dir not in variants: if _var_dir not in variants:
variants.append(_var_dir) variants.append(_var_dir)
env.VariantDirWrap(_var_dir, _src_dir, duplicate) env.VariantDir(_var_dir, _src_dir, duplicate)
if env.IsFileWithExt(item, SRC_BUILD_EXT): if env.IsFileWithExt(item, SRC_BUILD_EXT):
sources.append(env.File(join(_var_dir, basename(item)))) sources.append(env.File(join(_var_dir, basename(item))))
@ -283,7 +278,6 @@ def generate(env):
env.AddMethod(ProcessUnFlags) env.AddMethod(ProcessUnFlags)
env.AddMethod(IsFileWithExt) env.AddMethod(IsFileWithExt)
env.AddMethod(MatchSourceFiles) env.AddMethod(MatchSourceFiles)
env.AddMethod(VariantDirWrap)
env.AddMethod(CollectBuildFiles) env.AddMethod(CollectBuildFiles)
env.AddMethod(BuildFrameworks) env.AddMethod(BuildFrameworks)
env.AddMethod(BuildLibrary) env.AddMethod(BuildLibrary)

View File

@ -165,7 +165,7 @@ def platform_show(platform):
if p.homepage: if p.homepage:
click.echo("Home: %s" % p.homepage) click.echo("Home: %s" % p.homepage)
if p.license: if p.license:
click.echo("License: %s" % p.license.get("type")) click.echo("License: %s" % p.license)
if p.frameworks: if p.frameworks:
click.echo("Frameworks: %s" % ", ".join(p.frameworks.keys())) click.echo("Frameworks: %s" % ", ".join(p.frameworks.keys()))

View File

@ -27,7 +27,6 @@ from platformio.downloader import FileDownloader
from platformio.unpacker import FileUnpacker from platformio.unpacker import FileUnpacker
from platformio.vcsclient import VCSClientFactory from platformio.vcsclient import VCSClientFactory
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
@ -504,7 +503,10 @@ class BasePkgManager(PkgRepoMixin, PkgInstallerMixin):
label=manifest['name']) label=manifest['name'])
return True return True
def update(self, name, requirements=None, only_check=False): def update(self, # pylint: disable=too-many-return-statements
name,
requirements=None,
only_check=False):
name, requirements, url = self.parse_pkg_name(name, requirements) name, requirements, url = self.parse_pkg_name(name, requirements)
package_dir = self.get_package_dir(name, requirements, url) package_dir = self.get_package_dir(name, requirements, url)
if not package_dir: if not package_dir:

View File

@ -0,0 +1,18 @@
struct MyItem {
byte foo[50];
int bar;
};
void setup() {
struct MyItem item1;
myFunction(&item1);
}
void loop() {
}
void myFunction(struct MyItem *item) {
}

View File

@ -0,0 +1,3 @@
void barFunc () { // my comment
}

View File

@ -0,0 +1,13 @@
void setup() {
barFunc();
fooFunc();
}
void loop() {
}
char* fooFunc() {
}

41
tests/test_ino2cpp.py Normal file
View File

@ -0,0 +1,41 @@
# Copyright 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 os import listdir
from os.path import dirname, isdir, join, normpath
import pytest
from platformio import util
def pytest_generate_tests(metafunc):
if "piotest_dir" not in metafunc.fixturenames:
return
test_dir = normpath(join(dirname(__file__), "ino2cpp"))
test_dirs = []
for name in listdir(test_dir):
if isdir(join(test_dir, name)):
test_dirs.append(join(test_dir, name))
test_dirs.sort()
metafunc.parametrize("piotest_dir", test_dirs)
@pytest.mark.examples
def test_ci(platformio_setup, piotest_dir):
result = util.exec_command(
["platformio", "--force", "ci", piotest_dir, "-b", "uno"]
)
if result['returncode'] != 0:
pytest.fail(result)