forked from platformio/platformio-core
268 lines
8.5 KiB
Python
268 lines
8.5 KiB
Python
# 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.
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import click
|
|
|
|
from platformio import fs, proc
|
|
from platformio.check.defect import DefectItem
|
|
from platformio.package.manager.core import get_core_package_dir
|
|
from platformio.package.meta import PackageSpec
|
|
from platformio.project.helpers import load_build_metadata
|
|
|
|
|
|
class CheckToolBase: # pylint: disable=too-many-instance-attributes
|
|
def __init__(self, project_dir, config, envname, options):
|
|
self.config = config
|
|
self.envname = envname
|
|
self.options = options
|
|
self.project_dir = project_dir
|
|
self.cc_flags = []
|
|
self.cxx_flags = []
|
|
self.cpp_includes = []
|
|
self.cpp_defines = []
|
|
self.toolchain_defines = []
|
|
self._tmp_files = []
|
|
self.cc_path = None
|
|
self.cxx_path = None
|
|
self._defects = []
|
|
self._on_defect_callback = None
|
|
self._bad_input = False
|
|
self._load_cpp_data()
|
|
|
|
# detect all defects by default
|
|
if not self.options.get("severity"):
|
|
self.options["severity"] = [
|
|
DefectItem.SEVERITY_LOW,
|
|
DefectItem.SEVERITY_MEDIUM,
|
|
DefectItem.SEVERITY_HIGH,
|
|
]
|
|
# cast to severity by ids
|
|
self.options["severity"] = [
|
|
s if isinstance(s, int) else DefectItem.severity_to_int(s)
|
|
for s in self.options["severity"]
|
|
]
|
|
|
|
def _load_cpp_data(self):
|
|
data = load_build_metadata(self.project_dir, self.envname)
|
|
if not data:
|
|
return
|
|
self.cc_flags = click.parser.split_arg_string(data.get("cc_flags", ""))
|
|
self.cxx_flags = click.parser.split_arg_string(data.get("cxx_flags", ""))
|
|
self.cpp_includes = self._dump_includes(data.get("includes", {}))
|
|
self.cpp_defines = data.get("defines", [])
|
|
self.cc_path = data.get("cc_path")
|
|
self.cxx_path = data.get("cxx_path")
|
|
self.toolchain_defines = self._get_toolchain_defines()
|
|
|
|
def get_tool_dir(self, pkg_name):
|
|
for spec in self.options["platform_packages"] or []:
|
|
spec = PackageSpec(spec)
|
|
if spec.name == pkg_name:
|
|
return get_core_package_dir(pkg_name, spec=spec)
|
|
return get_core_package_dir(pkg_name)
|
|
|
|
def get_flags(self, tool):
|
|
result = []
|
|
flags = self.options.get("flags") or []
|
|
for flag in flags:
|
|
if ":" not in flag or flag.startswith("-"):
|
|
result.extend([f for f in flag.split(" ") if f])
|
|
elif flag.startswith("%s:" % tool):
|
|
result.extend([f for f in flag.split(":", 1)[1].split(" ") if f])
|
|
|
|
return result
|
|
|
|
def _get_toolchain_defines(self):
|
|
def _extract_defines(language, includes_file):
|
|
build_flags = self.cxx_flags if language == "c++" else self.cc_flags
|
|
defines = []
|
|
cmd = "echo | %s -x %s %s %s -dM -E -" % (
|
|
self.cc_path,
|
|
language,
|
|
" ".join(
|
|
[f for f in build_flags if f.startswith(("-m", "-f", "-std"))]
|
|
),
|
|
includes_file,
|
|
)
|
|
result = proc.exec_command(cmd, shell=True)
|
|
|
|
if result["returncode"] != 0:
|
|
click.echo("Warning: Failed to extract toolchain defines!")
|
|
if self.options.get("verbose"):
|
|
click.echo(result["out"])
|
|
click.echo(result["err"])
|
|
|
|
for line in result["out"].split("\n"):
|
|
tokens = line.strip().split(" ", 2)
|
|
if not tokens or tokens[0] != "#define":
|
|
continue
|
|
if len(tokens) > 2:
|
|
defines.append("%s=%s" % (tokens[1], tokens[2]))
|
|
else:
|
|
defines.append(tokens[1])
|
|
|
|
return defines
|
|
|
|
incflags_file = self._long_includes_hook(self.cpp_includes)
|
|
return {lang: _extract_defines(lang, incflags_file) for lang in ("c", "c++")}
|
|
|
|
def _create_tmp_file(self, data):
|
|
with tempfile.NamedTemporaryFile("w", delete=False) as fp:
|
|
fp.write(data)
|
|
self._tmp_files.append(fp.name)
|
|
return fp.name
|
|
|
|
def _long_includes_hook(self, includes):
|
|
data = []
|
|
for inc in includes:
|
|
data.append('-I"%s"' % fs.to_unix_path(inc))
|
|
|
|
return '@"%s"' % self._create_tmp_file(" ".join(data))
|
|
|
|
@staticmethod
|
|
def _dump_includes(includes_map):
|
|
result = []
|
|
for includes in includes_map.values():
|
|
for include in includes:
|
|
if include not in result:
|
|
result.append(include)
|
|
return result
|
|
|
|
@staticmethod
|
|
def is_flag_set(flag, flags):
|
|
return any(flag in f for f in flags)
|
|
|
|
def get_defects(self):
|
|
return self._defects
|
|
|
|
def configure_command(self):
|
|
raise NotImplementedError
|
|
|
|
def on_tool_output(self, line):
|
|
line = self.tool_output_filter(line)
|
|
if not line:
|
|
return
|
|
|
|
defect = self.parse_defect(line)
|
|
|
|
if not isinstance(defect, DefectItem):
|
|
if self.options.get("verbose"):
|
|
click.echo(line)
|
|
return
|
|
|
|
if defect.severity not in self.options["severity"]:
|
|
return
|
|
|
|
self._defects.append(defect)
|
|
if self._on_defect_callback:
|
|
self._on_defect_callback(defect)
|
|
|
|
@staticmethod
|
|
def tool_output_filter(line):
|
|
return line
|
|
|
|
@staticmethod
|
|
def parse_defect(raw_line):
|
|
return raw_line
|
|
|
|
def clean_up(self):
|
|
for f in self._tmp_files:
|
|
if os.path.isfile(f):
|
|
os.remove(f)
|
|
|
|
@staticmethod
|
|
def is_check_successful(cmd_result):
|
|
return cmd_result["returncode"] == 0
|
|
|
|
def execute_check_cmd(self, cmd):
|
|
result = proc.exec_command(
|
|
cmd,
|
|
stdout=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
|
stderr=proc.LineBufferedAsyncPipe(self.on_tool_output),
|
|
)
|
|
|
|
if not self.is_check_successful(result):
|
|
click.echo(
|
|
"\nError: Failed to execute check command! Exited with code %d."
|
|
% result["returncode"]
|
|
)
|
|
if self.options.get("verbose"):
|
|
click.echo(result["out"])
|
|
click.echo(result["err"])
|
|
self._bad_input = True
|
|
|
|
return result
|
|
|
|
@staticmethod
|
|
def get_project_target_files(project_dir, src_filters):
|
|
c_extension = (".c",)
|
|
cpp_extensions = (".cc", ".cpp", ".cxx", ".ino")
|
|
header_extensions = (".h", ".hh", ".hpp", ".hxx")
|
|
|
|
result = {"c": [], "c++": [], "headers": []}
|
|
|
|
def _add_file(path):
|
|
if path.endswith(header_extensions):
|
|
result["headers"].append(os.path.abspath(path))
|
|
elif path.endswith(c_extension):
|
|
result["c"].append(os.path.abspath(path))
|
|
elif path.endswith(cpp_extensions):
|
|
result["c++"].append(os.path.abspath(path))
|
|
|
|
src_filters = normalize_src_filters(src_filters)
|
|
for f in fs.match_src_files(project_dir, src_filters):
|
|
_add_file(f)
|
|
|
|
return result
|
|
|
|
def check(self, on_defect_callback=None):
|
|
self._on_defect_callback = on_defect_callback
|
|
cmd = self.configure_command()
|
|
if cmd:
|
|
if self.options.get("verbose"):
|
|
click.echo(" ".join(cmd))
|
|
|
|
self.execute_check_cmd(cmd)
|
|
|
|
else:
|
|
if self.options.get("verbose"):
|
|
click.echo("Error: Couldn't configure command")
|
|
self._bad_input = True
|
|
|
|
self.clean_up()
|
|
|
|
return self._bad_input
|
|
|
|
|
|
#
|
|
# Helpers
|
|
#
|
|
|
|
|
|
def normalize_src_filters(src_filters):
|
|
def _normalize(src_filters):
|
|
return (
|
|
src_filters
|
|
if src_filters.startswith(("+<", "-<"))
|
|
else "+<%s>" % src_filters
|
|
)
|
|
|
|
if isinstance(src_filters, (list, tuple)):
|
|
return " ".join([_normalize(f) for f in src_filters])
|
|
|
|
return _normalize(src_filters)
|