3337120f79
Wraps self.cc_path in quotes when building the toolchain defines extraction command. This fixes the issue where microcontroller names containing spaces (e.g., 'Feather M0') would cause the command to be split incorrectly. Fixes #4894
268 lines
8.4 KiB
Python
268 lines
8.4 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 = data.get("cc_flags", [])
|
|
self.cxx_flags = 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)
|