diff --git a/docs b/docs index 3293903c..0487c24f 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 3293903cac7c050908b594a838bd5a220e47e2c6 +Subproject commit 0487c24f930e3b1f45d594b6564148dc5324f263 diff --git a/platformio/commands/debug.py b/platformio/commands/debug.py index 11e25248..b0c1ed93 100644 --- a/platformio/commands/debug.py +++ b/platformio/commands/debug.py @@ -17,6 +17,7 @@ import asyncio import os +import subprocess import click @@ -24,13 +25,14 @@ from platformio import app, exception, fs, proc from platformio.commands.platform import platform_install as cmd_platform_install from platformio.compat import WINDOWS from platformio.debug import helpers +from platformio.debug.config.factory import DebugConfigFactory from platformio.debug.exception import DebugInvalidOptionsError from platformio.debug.process.client import DebugClientProcess from platformio.platform.exception import UnknownPlatform from platformio.platform.factory import PlatformFactory from platformio.project.config import ProjectConfig from platformio.project.exception import ProjectEnvsNotAvailableError -from platformio.project.helpers import is_platformio_project, load_project_ide_data +from platformio.project.helpers import is_platformio_project @click.command( @@ -62,46 +64,40 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro app.set_session_var("custom_project_conf", project_conf) # use env variables from Eclipse or CLion - for sysenv in ("CWD", "PWD", "PLATFORMIO_PROJECT_DIR"): + for name in ("CWD", "PWD", "PLATFORMIO_PROJECT_DIR"): if is_platformio_project(project_dir): break - if os.getenv(sysenv): - project_dir = os.getenv(sysenv) + if os.getenv(name): + project_dir = os.getenv(name) with fs.cd(project_dir): - config = ProjectConfig.get_instance(project_conf) - config.validate(envs=[environment] if environment else None) - - env_name = environment or helpers.get_default_debug_env(config) - env_options = config.items(env=env_name, as_dict=True) - if not set(env_options.keys()) >= set(["platform", "board"]): - raise ProjectEnvsNotAvailableError() - - try: - platform = PlatformFactory.new(env_options["platform"]) - except UnknownPlatform: - ctx.invoke( - cmd_platform_install, - platforms=[env_options["platform"]], - skip_default_package=True, - ) - platform = PlatformFactory.new(env_options["platform"]) - - debug_options = helpers.configure_initial_debug_options(platform, env_options) - assert debug_options + project_config = ProjectConfig.get_instance(project_conf) + project_config.validate(envs=[environment] if environment else None) + env_name = environment or helpers.get_default_debug_env(project_config) if not interface: return helpers.predebug_project(ctx, project_dir, env_name, False, verbose) - ide_data = load_project_ide_data(project_dir, env_name) - if not ide_data: - raise DebugInvalidOptionsError("Could not load a build configuration") + env_options = project_config.items(env=env_name, as_dict=True) + if not set(env_options.keys()) >= set(["platform", "board"]): + raise ProjectEnvsNotAvailableError() + + try: + platform = PlatformFactory.new(env_options["platform"]) + except UnknownPlatform: + ctx.invoke( + cmd_platform_install, + platforms=[env_options["platform"]], + skip_default_package=True, + ) + platform = PlatformFactory.new(env_options["platform"]) + + debug_config = DebugConfigFactory.new(platform, project_config, env_name) if "--version" in __unprocessed: - result = proc.exec_command([ide_data["gdb_path"], "--version"]) - if result["returncode"] == 0: - return click.echo(result["out"]) - raise exception.PlatformioException("\n".join([result["out"], result["err"]])) + return subprocess.run( + [debug_config.client_executable_path, "--version"], check=True + ) try: fs.ensure_udev_rules() @@ -113,29 +109,23 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro nl=False, ) - try: - debug_options = platform.configure_debug_options(debug_options, ide_data) - except NotImplementedError: - # legacy for ESP32 dev-platform <=2.0.0 - debug_options["load_cmds"] = helpers.configure_esp32_load_cmds( - debug_options, ide_data - ) - rebuild_prog = False - preload = debug_options["load_cmds"] == ["preload"] - load_mode = debug_options["load_mode"] + preload = debug_config.load_cmds == ["preload"] + load_mode = debug_config.load_mode if load_mode == "always": - rebuild_prog = preload or not helpers.has_debug_symbols(ide_data["prog_path"]) + rebuild_prog = preload or not helpers.has_debug_symbols( + debug_config.program_path + ) elif load_mode == "modified": rebuild_prog = helpers.is_prog_obsolete( - ide_data["prog_path"] - ) or not helpers.has_debug_symbols(ide_data["prog_path"]) + debug_config.program_path + ) or not helpers.has_debug_symbols(debug_config.program_path) else: - rebuild_prog = not os.path.isfile(ide_data["prog_path"]) + rebuild_prog = not os.path.isfile(debug_config.program_path) if preload or (not rebuild_prog and load_mode != "always"): # don't load firmware through debug server - debug_options["load_cmds"] = [] + debug_config.load_cmds = [] if rebuild_prog: if helpers.is_gdbmi_mode(): @@ -155,15 +145,15 @@ def cli(ctx, project_dir, project_conf, environment, verbose, interface, __unpro # save SHA sum of newly created prog if load_mode == "modified": - helpers.is_prog_obsolete(ide_data["prog_path"]) + helpers.is_prog_obsolete(debug_config.program_path) - if not os.path.isfile(ide_data["prog_path"]): + if not os.path.isfile(debug_config.program_path): raise DebugInvalidOptionsError("Program/firmware is missed") loop = asyncio.ProactorEventLoop() if WINDOWS else asyncio.get_event_loop() asyncio.set_event_loop(loop) - client = DebugClientProcess(project_dir, __unprocessed, debug_options, env_options) - coro = client.run(ide_data["gdb_path"], ide_data["prog_path"]) + client = DebugClientProcess(project_dir, debug_config) + coro = client.run(__unprocessed) loop.run_until_complete(coro) if WINDOWS: # an issue with asyncio executor and STIDIN, it cannot be closed gracefully diff --git a/platformio/debug/config/__init__.py b/platformio/debug/config/__init__.py new file mode 100644 index 00000000..b0514903 --- /dev/null +++ b/platformio/debug/config/__init__.py @@ -0,0 +1,13 @@ +# 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. diff --git a/platformio/debug/config/base.py b/platformio/debug/config/base.py new file mode 100644 index 00000000..25c295c2 --- /dev/null +++ b/platformio/debug/config/base.py @@ -0,0 +1,227 @@ +# 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. + +import os + +from platformio import fs, proc, util +from platformio.compat import string_types +from platformio.debug.exception import DebugInvalidOptionsError +from platformio.debug.helpers import reveal_debug_port +from platformio.project.config import ProjectConfig +from platformio.project.helpers import get_project_core_dir, load_project_ide_data +from platformio.project.options import ProjectOptions + + +class DebugConfigBase: # pylint: disable=too-many-instance-attributes + def __init__(self, platform, project_config, env_name): + self.platform = platform + self.project_config = project_config + self.env_name = env_name + self.env_options = project_config.items(env=env_name, as_dict=True) + self.board_config = platform.board_config(self.env_options["board"]) + self.build_data = self._load_build_data() + self.tool_name = self.board_config.get_debug_tool_name( + self.env_options.get("debug_tool") + ) + self.tool_settings = ( + self.board_config.get("debug", {}).get("tools", {}).get(self.tool_name, {}) + ) + + self._load_cmds = None + self._port = None + + self.server = self._configure_server() + + try: + platform.configure_debug_session(self) + except NotImplementedError: + pass + + @staticmethod + def cleanup_cmds(items): + items = ProjectConfig.parse_multi_values(items) + return ["$LOAD_CMDS" if item == "$LOAD_CMD" else item for item in items] + + @property + def program_path(self): + return self.build_data["prog_path"] + + @property + def client_executable_path(self): + return self.build_data["gdb_path"] + + @property + def load_cmds(self): + if self._load_cmds is not None: + return self._load_cmds + result = self.env_options.get("debug_load_cmds") + if not result: + result = self.tool_settings.get("load_cmds") + if not result: + # legacy + result = self.tool_settings.get("load_cmd") + if not result: + result = ProjectOptions["env.debug_load_cmds"].default + return self.cleanup_cmds(result) + + @load_cmds.setter + def load_cmds(self, cmds): + self._load_cmds = cmds + + @property + def load_mode(self): + result = self.env_options.get("debug_load_mode") + if not result: + result = self.tool_settings.get("load_mode") + return result or ProjectOptions["env.debug_load_mode"].default + + @property + def init_break(self): + result = self.env_options.get("debug_init_break") + if not result: + result = self.tool_settings.get("init_break") + return result or ProjectOptions["env.debug_init_break"].default + + @property + def init_cmds(self): + return self.env_options.get( + "debug_init_cmds", self.tool_settings.get("init_cmds") + ) + + @property + def extra_cmds(self): + return self.cleanup_cmds( + self.env_options.get("debug_extra_cmds") + ) + self.cleanup_cmds(self.tool_settings.get("extra_cmds")) + + @property + def port(self): + return reveal_debug_port( + self.env_options.get("debug_port", self.tool_settings.get("port")) + or self._port, + self.tool_name, + self.tool_settings, + ) + + @port.setter + def port(self, value): + self._port = value + + @property + def upload_protocol(self): + return self.env_options.get( + "upload_protocol", self.board_config.get("upload", {}).get("protocol") + ) + + @property + def speed(self): + return self.env_options.get("debug_speed", self.tool_settings.get("speed")) + + @property + def server_ready_pattern(self): + return (self.server or {}).get("ready_pattern") + + def _load_build_data(self): + data = load_project_ide_data(self.project_config.path, self.env_name) + if data: + return data + raise DebugInvalidOptionsError("Could not load a build configuration") + + def _configure_server(self): + result = None + # specific server per a system + if isinstance(self.tool_settings.get("server", {}), list): + for item in self.tool_settings["server"][:]: + self.tool_settings["server"] = item + if util.get_systype() in item.get("system", []): + break + + # user overwrites debug server + if self.env_options.get("debug_server"): + result = { + "cwd": None, + "executable": None, + "arguments": self.env_options.get("debug_server"), + } + result["executable"] = result["arguments"][0] + result["arguments"] = result["arguments"][1:] + elif "server" in self.tool_settings: + result = self.tool_settings["server"] + server_package = result.get("package") + server_package_dir = ( + self.platform.get_package_dir(server_package) + if server_package + else None + ) + if server_package and not server_package_dir: + self.platform.install_packages( + with_packages=[server_package], + skip_default_package=True, + silent=True, + ) + server_package_dir = self.platform.get_package_dir(server_package) + result.update( + dict( + cwd=server_package_dir if server_package else None, + executable=result.get("executable"), + arguments=[ + a.replace("$PACKAGE_DIR", server_package_dir) + if server_package_dir + else a + for a in result.get("arguments", []) + ], + ) + ) + return self.reveal_patterns(result) if result else None + + def get_init_script(self, debugger): + try: + return getattr(self, "%s_INIT_SCRIPT" % debugger.upper()) + except AttributeError: + raise NotImplementedError + + def reveal_patterns(self, source, recursive=True): + patterns = { + "PLATFORMIO_CORE_DIR": get_project_core_dir(), + "PYTHONEXE": proc.get_pythonexe_path(), + "PROJECT_DIR": self.project_config.path, + "PROG_PATH": self.program_path, + "PROG_DIR": os.path.dirname(self.program_path), + "PROG_NAME": os.path.basename(os.path.splitext(self.program_path)[0]), + "DEBUG_PORT": self.port, + "UPLOAD_PROTOCOL": self.upload_protocol, + "INIT_BREAK": self.init_break or "", + "LOAD_CMDS": "\n".join(self.load_cmds or []), + } + for key, value in patterns.items(): + if key.endswith(("_DIR", "_PATH")): + patterns[key] = fs.to_unix_path(value) + + def _replace(text): + for key, value in patterns.items(): + pattern = "$%s" % key + text = text.replace(pattern, value or "") + return text + + if isinstance(source, string_types): + source = _replace(source) + elif isinstance(source, (list, dict)): + items = enumerate(source) if isinstance(source, list) else source.items() + for key, value in items: + if isinstance(value, string_types): + source[key] = _replace(value) + elif isinstance(value, (list, dict)) and recursive: + source[key] = self.reveal_patterns(value, patterns) + + return source diff --git a/platformio/debug/config/blackmagic.py b/platformio/debug/config/blackmagic.py new file mode 100644 index 00000000..bfc16246 --- /dev/null +++ b/platformio/debug/config/blackmagic.py @@ -0,0 +1,49 @@ +# 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 platformio.debug.config.base import DebugConfigBase + + +class BlackmagicDebugConfig(DebugConfigBase): + + GDB_INIT_SCRIPT = """ +define pio_reset_halt_target + set language c + set *0xE000ED0C = 0x05FA0004 + set $busy = (*0xE000ED0C & 0x4) + while ($busy) + set $busy = (*0xE000ED0C & 0x4) + end + set language auto +end + +define pio_reset_run_target + pio_reset_halt_target +end + +target extended-remote $DEBUG_PORT +monitor swdp_scan +attach 1 +set mem inaccessible-by-default off +$LOAD_CMDS +$INIT_BREAK + +set language c +set *0xE000ED0C = 0x05FA0004 +set $busy = (*0xE000ED0C & 0x4) +while ($busy) + set $busy = (*0xE000ED0C & 0x4) +end +set language auto +""" diff --git a/platformio/debug/config/factory.py b/platformio/debug/config/factory.py new file mode 100644 index 00000000..87019dfa --- /dev/null +++ b/platformio/debug/config/factory.py @@ -0,0 +1,41 @@ +# 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. + +import importlib +import re + +from platformio.debug.config.generic import GenericDebugConfig + + +class DebugConfigFactory(object): + @staticmethod + def get_clsname(name): + name = re.sub(r"[^\da-z\_\-]+", "", name, flags=re.I) + return "%s%sDebugConfig" % (name.upper()[0], name.lower()[1:]) + + @classmethod + def new(cls, platform, project_config, env_name): + board_config = platform.board_config( + project_config.get("env:" + env_name, "board") + ) + tool_name = board_config.get_debug_tool_name( + project_config.get("env:" + env_name, "debug_tool") + ) + config_cls = None + try: + mod = importlib.import_module("platformio.debug.config.%s" % tool_name) + config_cls = getattr(mod, cls.get_clsname(tool_name)) + except ModuleNotFoundError: + config_cls = GenericDebugConfig + return config_cls(platform, project_config, env_name) diff --git a/platformio/debug/config/generic.py b/platformio/debug/config/generic.py new file mode 100644 index 00000000..a8c6c410 --- /dev/null +++ b/platformio/debug/config/generic.py @@ -0,0 +1,38 @@ +# 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 platformio.debug.config.base import DebugConfigBase + + +class GenericDebugConfig(DebugConfigBase): + + GDB_INIT_SCRIPT = """ +define pio_reset_halt_target + monitor reset halt +end + +define pio_reset_run_target + monitor reset +end + +target extended-remote $DEBUG_PORT +monitor init +$LOAD_CMDS +pio_reset_halt_target +$INIT_BREAK +""" + + def __init__(self, *args, **kwargs): + super(GenericDebugConfig, self).__init__(*args, **kwargs) + self.port = ":3333" diff --git a/platformio/debug/config/jlink.py b/platformio/debug/config/jlink.py new file mode 100644 index 00000000..020decd7 --- /dev/null +++ b/platformio/debug/config/jlink.py @@ -0,0 +1,48 @@ +# 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 platformio.debug.config.base import DebugConfigBase + + +class JlinkDebugConfig(DebugConfigBase): + + GDB_INIT_SCRIPT = """ +define pio_reset_halt_target + monitor reset + monitor halt +end + +define pio_reset_run_target + monitor clrbp + monitor reset + monitor go +end + +target extended-remote $DEBUG_PORT +monitor clrbp +monitor speed auto +pio_reset_halt_target +$LOAD_CMDS +$INIT_BREAK +""" + + def __init__(self, *args, **kwargs): + super(JlinkDebugConfig, self).__init__(*args, **kwargs) + self.port = ":2331" + + @property + def server_ready_pattern(self): + return super(JlinkDebugConfig, self).server_ready_pattern or ( + "Waiting for GDB connection" + ) diff --git a/platformio/debug/config/mspdebug.py b/platformio/debug/config/mspdebug.py new file mode 100644 index 00000000..6c9b9412 --- /dev/null +++ b/platformio/debug/config/mspdebug.py @@ -0,0 +1,36 @@ +# 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 platformio.debug.config.base import DebugConfigBase + + +class MspdebugDebugConfig(DebugConfigBase): + + GDB_INIT_SCRIPT = """ +define pio_reset_halt_target +end + +define pio_reset_run_target +end + +target extended-remote $DEBUG_PORT +monitor erase +$LOAD_CMDS +pio_reset_halt_target +$INIT_BREAK +""" + + def __init__(self, *args, **kwargs): + super(MspdebugDebugConfig, self).__init__(*args, **kwargs) + self.port = ":2000" diff --git a/platformio/debug/config/qemu.py b/platformio/debug/config/qemu.py new file mode 100644 index 00000000..d32af5a2 --- /dev/null +++ b/platformio/debug/config/qemu.py @@ -0,0 +1,37 @@ +# 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 platformio.debug.config.base import DebugConfigBase + + +class QemuDebugConfig(DebugConfigBase): + + GDB_INIT_SCRIPT = """ +define pio_reset_halt_target + monitor system_reset +end + +define pio_reset_run_target + monitor system_reset +end + +target extended-remote $DEBUG_PORT +$LOAD_CMDS +pio_reset_halt_target +$INIT_BREAK +""" + + def __init__(self, *args, **kwargs): + super(QemuDebugConfig, self).__init__(*args, **kwargs) + self.port = ":1234" diff --git a/platformio/debug/config/renode.py b/platformio/debug/config/renode.py new file mode 100644 index 00000000..3aef5ef8 --- /dev/null +++ b/platformio/debug/config/renode.py @@ -0,0 +1,45 @@ +# 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 platformio.debug.config.base import DebugConfigBase + + +class RenodeDebugConfig(DebugConfigBase): + + GDB_INIT_SCRIPT = """ +define pio_reset_halt_target + monitor machine Reset + $LOAD_CMDS + monitor start +end + +define pio_reset_run_target + pio_reset_halt_target +end + +target extended-remote $DEBUG_PORT +$LOAD_CMDS +$INIT_BREAK +monitor start +""" + + def __init__(self, *args, **kwargs): + super(RenodeDebugConfig, self).__init__(*args, **kwargs) + self.port = ":3333" + + @property + def server_ready_pattern(self): + return super(RenodeDebugConfig, self).server_ready_pattern or ( + "GDB server with all CPUs started on port" + ) diff --git a/platformio/debug/helpers.py b/platformio/debug/helpers.py index 72c3ff4b..6038f000 100644 --- a/platformio/debug/helpers.py +++ b/platformio/debug/helpers.py @@ -20,13 +20,11 @@ from hashlib import sha1 from io import BytesIO from os.path import isfile -from platformio import fs, util +from platformio import util from platformio.commands import PlatformioCLI from platformio.commands.run.command import cli as cmd_run from platformio.compat import is_bytes from platformio.debug.exception import DebugInvalidOptionsError -from platformio.project.config import ProjectConfig -from platformio.project.options import ProjectOptions class GDBMIConsoleStream(BytesIO): # pylint: disable=too-few-public-methods @@ -86,130 +84,6 @@ def predebug_project(ctx, project_dir, env_name, preload, verbose): time.sleep(5) -def configure_initial_debug_options(platform, env_options): - def _cleanup_cmds(items): - items = ProjectConfig.parse_multi_values(items) - return ["$LOAD_CMDS" if item == "$LOAD_CMD" else item for item in items] - - board_config = platform.board_config(env_options["board"]) - tool_name = board_config.get_debug_tool_name(env_options.get("debug_tool")) - tool_settings = board_config.get("debug", {}).get("tools", {}).get(tool_name, {}) - server_options = None - - # specific server per a system - if isinstance(tool_settings.get("server", {}), list): - for item in tool_settings["server"][:]: - tool_settings["server"] = item - if util.get_systype() in item.get("system", []): - break - - # user overwrites debug server - if env_options.get("debug_server"): - server_options = { - "cwd": None, - "executable": None, - "arguments": env_options.get("debug_server"), - } - server_options["executable"] = server_options["arguments"][0] - server_options["arguments"] = server_options["arguments"][1:] - elif "server" in tool_settings: - server_options = tool_settings["server"] - server_package = server_options.get("package") - server_package_dir = ( - platform.get_package_dir(server_package) if server_package else None - ) - if server_package and not server_package_dir: - platform.install_packages( - with_packages=[server_package], skip_default_package=True, silent=True - ) - server_package_dir = platform.get_package_dir(server_package) - server_options.update( - dict( - cwd=server_package_dir if server_package else None, - executable=server_options.get("executable"), - arguments=[ - a.replace("$PACKAGE_DIR", server_package_dir) - if server_package_dir - else a - for a in server_options.get("arguments", []) - ], - ) - ) - - extra_cmds = _cleanup_cmds(env_options.get("debug_extra_cmds")) - extra_cmds.extend(_cleanup_cmds(tool_settings.get("extra_cmds"))) - result = dict( - tool=tool_name, - upload_protocol=env_options.get( - "upload_protocol", board_config.get("upload", {}).get("protocol") - ), - load_cmds=_cleanup_cmds( - env_options.get( - "debug_load_cmds", - tool_settings.get( - "load_cmds", - tool_settings.get( - "load_cmd", ProjectOptions["env.debug_load_cmds"].default - ), - ), - ) - ), - load_mode=env_options.get( - "debug_load_mode", - tool_settings.get( - "load_mode", ProjectOptions["env.debug_load_mode"].default - ), - ), - init_break=env_options.get( - "debug_init_break", - tool_settings.get( - "init_break", ProjectOptions["env.debug_init_break"].default - ), - ), - init_cmds=_cleanup_cmds( - env_options.get("debug_init_cmds", tool_settings.get("init_cmds")) - ), - extra_cmds=extra_cmds, - require_debug_port=tool_settings.get("require_debug_port", False), - port=reveal_debug_port( - env_options.get("debug_port", tool_settings.get("port")), - tool_name, - tool_settings, - ), - speed=env_options.get("debug_speed", tool_settings.get("speed")), - server=server_options, - ) - return result - - -def configure_esp32_load_cmds(debug_options, configuration): - """ - DEPRECATED: Moved to ESP32 dev-platform - See platform.py::configure_debug_options - """ - flash_images = configuration.get("extra", {}).get("flash_images") - ignore_conds = [ - debug_options["load_cmds"] != ["load"], - "xtensa-esp32" not in configuration.get("cc_path", ""), - not flash_images, - not all(isfile(item["path"]) for item in flash_images), - ] - if any(ignore_conds): - return debug_options["load_cmds"] - - mon_cmds = [ - 'monitor program_esp32 "{{{path}}}" {offset} verify'.format( - path=fs.to_unix_path(item["path"]), offset=item["offset"] - ) - for item in flash_images - ] - mon_cmds.append( - 'monitor program_esp32 "{%s.bin}" 0x10000 verify' - % fs.to_unix_path(configuration["prog_path"][:-4]) - ) - return mon_cmds - - def has_debug_symbols(prog_path): if not isfile(prog_path): return False diff --git a/platformio/debug/initcfgs.py b/platformio/debug/initcfgs.py deleted file mode 100644 index 55e7c34f..00000000 --- a/platformio/debug/initcfgs.py +++ /dev/null @@ -1,161 +0,0 @@ -# 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. - -GDB_DEFAULT_INIT_CONFIG = """ -define pio_reset_halt_target - monitor reset halt -end - -define pio_reset_run_target - monitor reset -end - -target extended-remote $DEBUG_PORT -monitor init -$LOAD_CMDS -pio_reset_halt_target -$INIT_BREAK -""" - -GDB_STUTIL_INIT_CONFIG = """ -define pio_reset_halt_target - monitor reset - monitor halt -end - -define pio_reset_run_target - monitor reset -end - -target extended-remote $DEBUG_PORT -$LOAD_CMDS -pio_reset_halt_target -$INIT_BREAK -""" - -GDB_JLINK_INIT_CONFIG = """ -define pio_reset_halt_target - monitor reset - monitor halt -end - -define pio_reset_run_target - monitor clrbp - monitor reset - monitor go -end - -target extended-remote $DEBUG_PORT -monitor clrbp -monitor speed auto -pio_reset_halt_target -$LOAD_CMDS -$INIT_BREAK -""" - -GDB_BLACKMAGIC_INIT_CONFIG = """ -define pio_reset_halt_target - set language c - set *0xE000ED0C = 0x05FA0004 - set $busy = (*0xE000ED0C & 0x4) - while ($busy) - set $busy = (*0xE000ED0C & 0x4) - end - set language auto -end - -define pio_reset_run_target - pio_reset_halt_target -end - -target extended-remote $DEBUG_PORT -monitor swdp_scan -attach 1 -set mem inaccessible-by-default off -$LOAD_CMDS -$INIT_BREAK - -set language c -set *0xE000ED0C = 0x05FA0004 -set $busy = (*0xE000ED0C & 0x4) -while ($busy) - set $busy = (*0xE000ED0C & 0x4) -end -set language auto -""" - -GDB_MSPDEBUG_INIT_CONFIG = """ -define pio_reset_halt_target -end - -define pio_reset_run_target -end - -target extended-remote $DEBUG_PORT -monitor erase -$LOAD_CMDS -pio_reset_halt_target -$INIT_BREAK -""" - -GDB_QEMU_INIT_CONFIG = """ -define pio_reset_halt_target - monitor system_reset -end - -define pio_reset_run_target - monitor system_reset -end - -target extended-remote $DEBUG_PORT -$LOAD_CMDS -pio_reset_halt_target -$INIT_BREAK -""" - -GDB_RENODE_INIT_CONFIG = """ -define pio_reset_halt_target - monitor machine Reset - $LOAD_CMDS - monitor start -end - -define pio_reset_run_target - pio_reset_halt_target -end - -target extended-remote $DEBUG_PORT -$LOAD_CMDS -$INIT_BREAK -monitor start -""" - - -TOOL_TO_CONFIG = { - "jlink": GDB_JLINK_INIT_CONFIG, - "mspdebug": GDB_MSPDEBUG_INIT_CONFIG, - "qemu": GDB_QEMU_INIT_CONFIG, - "blackmagic": GDB_BLACKMAGIC_INIT_CONFIG, - "renode": GDB_RENODE_INIT_CONFIG, -} - - -def get_gdb_init_config(debug_options): - tool = debug_options.get("tool") - if tool and tool in TOOL_TO_CONFIG: - return TOOL_TO_CONFIG[tool] - server_exe = (debug_options.get("server") or {}).get("executable", "").lower() - if "st-util" in server_exe: - return GDB_STUTIL_INIT_CONFIG - return GDB_DEFAULT_INIT_CONFIG diff --git a/platformio/debug/process/base.py b/platformio/debug/process/base.py index a97d5446..846fbf77 100644 --- a/platformio/debug/process/base.py +++ b/platformio/debug/process/base.py @@ -18,16 +18,12 @@ import subprocess import sys import time -from platformio import fs from platformio.compat import ( WINDOWS, aio_create_task, aio_get_running_loop, get_locale_encoding, - string_types, ) -from platformio.proc import get_pythonexe_path -from platformio.project.helpers import get_project_core_dir class DebugSubprocessProtocol(asyncio.SubprocessProtocol): @@ -61,12 +57,6 @@ class DebugBaseProcess: STDOUT_CHUNK_SIZE = 2048 LOG_FILE = None - COMMON_PATTERNS = { - "PLATFORMIO_HOME_DIR": get_project_core_dir(), - "PLATFORMIO_CORE_DIR": get_project_core_dir(), - "PYTHONEXE": get_pythonexe_path(), - } - def __init__(self): self.transport = None self._is_running = False @@ -155,32 +145,6 @@ class DebugBaseProcess: self._exit_future.set_result(True) self._exit_future = None - def apply_patterns(self, source, patterns=None): - _patterns = self.COMMON_PATTERNS.copy() - _patterns.update(patterns or {}) - - for key, value in _patterns.items(): - if key.endswith(("_DIR", "_PATH")): - _patterns[key] = fs.to_unix_path(value) - - def _replace(text): - for key, value in _patterns.items(): - pattern = "$%s" % key - text = text.replace(pattern, value or "") - return text - - if isinstance(source, string_types): - source = _replace(source) - elif isinstance(source, (list, dict)): - items = enumerate(source) if isinstance(source, list) else source.items() - for key, value in items: - if isinstance(value, string_types): - source[key] = _replace(value) - elif isinstance(value, (list, dict)): - source[key] = self.apply_patterns(value, patterns) - - return source - def terminate(self): if not self.is_running() or not self.transport: return diff --git a/platformio/debug/process/client.py b/platformio/debug/process/client.py index e8b05715..aad6c69d 100644 --- a/platformio/debug/process/client.py +++ b/platformio/debug/process/client.py @@ -23,8 +23,6 @@ from platformio import fs, proc, telemetry, util from platformio.cache import ContentCache from platformio.compat import aio_get_running_loop, hashlib_encode_data, is_bytes from platformio.debug import helpers -from platformio.debug.exception import DebugInvalidOptionsError -from platformio.debug.initcfgs import get_gdb_init_config from platformio.debug.process.base import DebugBaseProcess from platformio.debug.process.server import DebugServerProcess from platformio.project.helpers import get_project_cache_dir @@ -37,14 +35,12 @@ class DebugClientProcess( PIO_SRC_NAME = ".pioinit" INIT_COMPLETED_BANNER = "PlatformIO: Initialization completed" - def __init__(self, project_dir, args, debug_options, env_options): + def __init__(self, project_dir, debug_config): super(DebugClientProcess, self).__init__() self.project_dir = project_dir - self.args = list(args) - self.debug_options = debug_options - self.env_options = env_options + self.debug_config = debug_config - self._server_process = DebugServerProcess(debug_options, env_options) + self._server_process = DebugServerProcess(debug_config) self._session_id = None if not os.path.isdir(get_project_cache_dir()): @@ -56,27 +52,13 @@ class DebugClientProcess( self._target_is_running = False self._errors_buffer = b"" - async def run(self, gdb_path, prog_path): - session_hash = gdb_path + prog_path + async def run(self, extra_args): + gdb_path = self.debug_config.client_executable_path + session_hash = gdb_path + self.debug_config.program_path self._session_id = hashlib.sha1(hashlib_encode_data(session_hash)).hexdigest() self._kill_previous_session() - - patterns = { - "PROJECT_DIR": self.project_dir, - "PROG_PATH": prog_path, - "PROG_DIR": os.path.dirname(prog_path), - "PROG_NAME": os.path.basename(os.path.splitext(prog_path)[0]), - "DEBUG_PORT": self.debug_options["port"], - "UPLOAD_PROTOCOL": self.debug_options["upload_protocol"], - "INIT_BREAK": self.debug_options["init_break"] or "", - "LOAD_CMDS": "\n".join(self.debug_options["load_cmds"] or []), - } - - await self._server_process.run(patterns) - if not patterns["DEBUG_PORT"]: - patterns["DEBUG_PORT"] = self._server_process.get_debug_port() - - self.generate_pioinit(self._gdbsrc_dir, patterns) + self.debug_config.port = await self._server_process.run() + self.generate_init_script(os.path.join(self._gdbsrc_dir, self.PIO_SRC_NAME)) # start GDB client args = [ @@ -89,13 +71,11 @@ class DebugClientProcess( "-l", "10", ] - args.extend(self.args) - if not gdb_path: - raise DebugInvalidOptionsError("GDB client is not configured") + args.extend(list(extra_args or [])) gdb_data_dir = self._get_data_dir(gdb_path) if gdb_data_dir: args.extend(["--data-directory", gdb_data_dir]) - args.append(patterns["PROG_PATH"]) + args.append(self.debug_config.program_path) await self.spawn(*args, cwd=self.project_dir, wait_until_exit=True) @staticmethod @@ -107,13 +87,13 @@ class DebugClientProcess( ) return gdb_data_dir if os.path.isdir(gdb_data_dir) else None - def generate_pioinit(self, dst_dir, patterns): + def generate_init_script(self, dst): # default GDB init commands depending on debug tool - commands = get_gdb_init_config(self.debug_options).split("\n") + commands = self.debug_config.get_init_script("gdb").split("\n") - if self.debug_options["init_cmds"]: - commands = self.debug_options["init_cmds"] - commands.extend(self.debug_options["extra_cmds"]) + if self.debug_config.init_cmds: + commands = self.debug_config.init_cmds + commands.extend(self.debug_config.extra_cmds) if not any("define pio_reset_run_target" in cmd for cmd in commands): commands = [ @@ -134,20 +114,20 @@ class DebugClientProcess( "define pio_restart_target", " pio_reset_halt_target", " $INIT_BREAK", - " %s" % ("continue" if patterns["INIT_BREAK"] else "next"), + " %s" % ("continue" if self.debug_config.init_break else "next"), "end", ] banner = [ "echo PlatformIO Unified Debugger -> http://bit.ly/pio-debug\\n", - "echo PlatformIO: debug_tool = %s\\n" % self.debug_options["tool"], + "echo PlatformIO: debug_tool = %s\\n" % self.debug_config.tool_name, "echo PlatformIO: Initializing remote target...\\n", ] footer = ["echo %s\\n" % self.INIT_COMPLETED_BANNER] commands = banner + commands + footer - with open(os.path.join(dst_dir, self.PIO_SRC_NAME), "w") as fp: - fp.write("\n".join(self.apply_patterns(commands, patterns))) + with open(dst, "w") as fp: + fp.write("\n".join(self.debug_config.reveal_patterns(commands))) def connection_made(self, transport): super(DebugClientProcess, self).connection_made(transport) @@ -179,7 +159,9 @@ class DebugClientProcess( # go to init break automatically if self.INIT_COMPLETED_BANNER.encode() in data: telemetry.send_event( - "Debug", "Started", telemetry.dump_run_environment(self.env_options) + "Debug", + "Started", + telemetry.dump_run_environment(self.debug_config.env_options), ) self._auto_exec_continue() @@ -194,12 +176,12 @@ class DebugClientProcess( aio_get_running_loop().call_later(0.1, self._auto_exec_continue) return - if not self.debug_options["init_break"] or self._target_is_running: + if not self.debug_config.init_break or self._target_is_running: return self.console_log( "PlatformIO: Resume the execution to `debug_init_break = %s`\n" - % self.debug_options["init_break"] + % self.debug_config.init_break ) self.console_log( "PlatformIO: More configuration options -> http://bit.ly/pio-debug\n" @@ -226,7 +208,7 @@ class DebugClientProcess( last_erros = re.sub(r'((~|&)"|\\n\"|\\t)', " ", last_erros, flags=re.M) err = "%s -> %s" % ( - telemetry.dump_run_environment(self.env_options), + telemetry.dump_run_environment(self.debug_config.env_options), last_erros, ) telemetry.send_exception("DebugInitError: %s" % err) diff --git a/platformio/debug/process/server.py b/platformio/debug/process/server.py index 53717338..24684037 100644 --- a/platformio/debug/process/server.py +++ b/platformio/debug/process/server.py @@ -13,10 +13,12 @@ # limitations under the License. import asyncio +import fnmatch import os import time -from platformio import fs, util +from platformio import fs +from platformio.compat import MACOS, WINDOWS from platformio.debug.exception import DebugInvalidOptionsError from platformio.debug.helpers import escape_gdbmi_stream, is_gdbmi_mode from platformio.debug.process.base import DebugBaseProcess @@ -24,27 +26,22 @@ from platformio.proc import where_is_program class DebugServerProcess(DebugBaseProcess): - def __init__(self, debug_options, env_options): + def __init__(self, debug_config): super(DebugServerProcess, self).__init__() - self.debug_options = debug_options - self.env_options = env_options - - self._debug_port = ":3333" + self.debug_config = debug_config self._ready = False - async def run(self, patterns): # pylint: disable=too-many-branches - systype = util.get_systype() - server = self.debug_options.get("server") + async def run(self): # pylint: disable=too-many-branches + server = self.debug_config.server if not server: return None - server = self.apply_patterns(server, patterns) server_executable = server["executable"] if not server_executable: return None if server["cwd"]: server_executable = os.path.join(server["cwd"], server_executable) if ( - "windows" in systype + WINDOWS and not server_executable.endswith(".exe") and os.path.isfile(server_executable + ".exe") ): @@ -56,15 +53,18 @@ class DebugServerProcess(DebugBaseProcess): raise DebugInvalidOptionsError( "\nCould not launch Debug Server '%s'. Please check that it " "is installed and is included in a system PATH\n\n" - "See documentation or contact contact@platformio.org:\n" + "See documentation:\n" "https://docs.platformio.org/page/plus/debugging.html\n" % server_executable ) openocd_pipe_allowed = all( - [not self.debug_options["port"], "openocd" in server_executable] + [ + not self.debug_config.env_options.get("debug_port"), + "gdb" in self.debug_config.client_executable_path, + "openocd" in server_executable, + ] ) - # openocd_pipe_allowed = False if openocd_pipe_allowed: args = [] if server["cwd"]: @@ -76,18 +76,16 @@ class DebugServerProcess(DebugBaseProcess): str_args = " ".join( [arg if arg.startswith("-") else '"%s"' % arg for arg in args] ) - self._debug_port = '| "%s" %s' % (server_executable, str_args) - self._debug_port = fs.to_unix_path(self._debug_port) - return self._debug_port + return fs.to_unix_path('| "%s" %s' % (server_executable, str_args)) env = os.environ.copy() # prepend server "lib" folder to LD path if ( - "windows" not in systype + not WINDOWS and server["cwd"] and os.path.isdir(os.path.join(server["cwd"], "lib")) ): - ld_key = "DYLD_LIBRARY_PATH" if "darwin" in systype else "LD_LIBRARY_PATH" + ld_key = "DYLD_LIBRARY_PATH" if MACOS else "LD_LIBRARY_PATH" env[ld_key] = os.path.join(server["cwd"], "lib") if os.environ.get(ld_key): env[ld_key] = "%s:%s" % (env[ld_key], os.environ.get(ld_key)) @@ -102,20 +100,12 @@ class DebugServerProcess(DebugBaseProcess): await self.spawn( *([server_executable] + server["arguments"]), cwd=server["cwd"], env=env ) - - if "mspdebug" in server_executable.lower(): - self._debug_port = ":2000" - elif "jlink" in server_executable.lower(): - self._debug_port = ":2331" - elif "qemu" in server_executable.lower(): - self._debug_port = ":1234" - await self._wait_until_ready() - return self._debug_port + return self.debug_config.port async def _wait_until_ready(self): - ready_pattern = self.debug_options.get("server", {}).get("ready_pattern") + ready_pattern = self.debug_config.server_ready_pattern timeout = 60 if ready_pattern else 10 elapsed = 0 delay = 0.5 @@ -129,14 +119,11 @@ class DebugServerProcess(DebugBaseProcess): def _check_ready_by_pattern(self, data): if self._ready: return self._ready - ready_pattern = self.debug_options.get("server", {}).get("ready_pattern") + ready_pattern = self.debug_config.server_ready_pattern if ready_pattern: self._ready = ready_pattern.encode() in data return self._ready - def get_debug_port(self): - return self._debug_port - def stdout_data_received(self, data): super(DebugServerProcess, self).stdout_data_received( escape_gdbmi_stream("@", data) if is_gdbmi_mode() else data diff --git a/platformio/platform/base.py b/platformio/platform/base.py index 3cadbd73..1d7911b0 100644 --- a/platformio/platform/base.py +++ b/platformio/platform/base.py @@ -203,7 +203,7 @@ class PlatformBase( # pylint: disable=too-many-instance-attributes,too-many-pub elif "nobuild" in targets and opts.get("type") != "framework": self.packages[name]["optional"] = True - def configure_debug_options(self, initial_debug_options, ide_data): + def configure_debug_session(self, debug_config): raise NotImplementedError def get_lib_storages(self):