Merge branch 'break/drop_gdbgui' into 'master'

Tools: Drop official support for gdbgui

Closes IDF-4627

See merge request espressif/esp-idf!39326
This commit is contained in:
Roland Dobai
2025-06-06 10:23:44 +02:00
10 changed files with 172 additions and 113 deletions

View File

@ -232,7 +232,7 @@ It is also possible to execute the described debugging tools conveniently from `
4. ``idf.py gdbgui``
Starts `gdbgui <https://www.gdbgui.com>`_ debugger frontend enabling out-of-the-box debugging in a browser window. To enable this option, run the install script with the "--enable-gdbgui" argument, e.g., ``install.sh --enable-gdbgui``.
Starts `gdbgui <https://www.gdbgui.com>`_ debugger frontend, which enables out-of-the-box debugging in a browser window. To enable this option, follow the `installation instructions <https://www.gdbgui.com/installation/>`_ to set up the tool using the ``pipx`` method. For system dependencies, restrictions, and other limitations, please refer to the installation page and the `issue tracker <https://github.com/cs01/gdbgui/issues>`_.
You can combine these debugging actions on a single command line, allowing for convenient setup of blocking and non-blocking actions in one step. ``idf.py`` implements a simple logic to move the background actions (such as openocd) to the beginning and the interactive ones (such as gdb, monitor) to the end of the action list.

View File

@ -7,3 +7,4 @@ Migration from 5.5 to 6.0
:maxdepth: 1
peripherals
tools

View File

@ -0,0 +1,25 @@
Tools
=====
:link_to_translation:`zh_CN:[中文]`
GDBGUI installation
-------------------
The support for the ``--enable-gdbgui`` argument has been removed from the install scripts and `gdbgui <https://www.gdbgui.com>`_ cannot be installed this way anymore. Please use the ``pipx`` method from the `gdbgui installation guide <https://www.gdbgui.com/installation/>`_ in order to set up this tool.
Depending on your operating system and Python version, you may need to consult the `list of known issues <https://github.com/cs01/gdbgui/issues>`_.
For example, `Python 3.13 is not supported <https://github.com/cs01/gdbgui/issues/494>`_ on any operating systems at the moment.
The Windows operating system is not supported since gdbgui version 0.14. Because of the existence of other issues, you will need Python 3.10 with some specific versions of the dependencies. The last know working gdbgui and dependency versions can be installed with the following command:
```bash
pipx install "gdbgui==0.13.2.0" "pygdbmi<=0.9.0.2" "python-socketio<5" "jinja2<3.1" "itsdangerous<2.1"
```
On Linux or MacOS, you can use Python 3.11 or 3.12 and gdbgui version 0.15.2.0.
Please be aware that these recommendations may change over time and for an up-to-date list of issues refer to `the official issue tracker <https://github.com/cs01/gdbgui/issues>`_.
We recommend to use ``idf.py gdb`` instead of ``idf.py gdbgui``, or debug in Eclipse/Vscode if you encounter an issue with the installation.

View File

@ -232,7 +232,7 @@
4. ``idf.py gdbgui``
启动 `gdbgui <https://www.gdbgui.com>`_,在浏览器中打开调试器的前端界面。请在运行安装脚本时添加 "--enable-gdbgui" 参数,即运行 ``install.sh --enable-gdbgui``,从而确保支持 ``gdbgui`` 选项
启动 `gdbgui <https://www.gdbgui.com>`_,在浏览器中打开调试器的前端界面。要启用此选项,请参照 `安装说明 <https://www.gdbgui.com/installation/>`_,使用 ``pipx`` 方法设置该工具。关于系统依赖项、限制及其他注意事项,请参考安装页面和 `问题追踪 <https://github.com/cs01/gdbgui/issues>`_
上述这些命令也可以合并到一起使用,``idf.py`` 会自动将后台进程(比如 openocd最先运行交互式进程比如 GDBmonitor最后运行。

View File

@ -7,3 +7,4 @@
:maxdepth: 1
peripherals
tools

View File

@ -0,0 +1 @@
.. include:: ../../../../en/migration-guides/release-6.x/6.0/tools.rst

View File

@ -19,14 +19,15 @@ from typing import Union
from click import INT
from click.core import Context
from esp_coredump import CoreDump
from idf_py_actions.errors import FatalError
from idf_py_actions.serial_ext import BAUD_RATE
from idf_py_actions.serial_ext import PORT
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import ensure_build_directory
from idf_py_actions.tools import generate_hints
from idf_py_actions.tools import get_default_serial_port
from idf_py_actions.tools import get_sdkconfig_value
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import yellow_print
@ -105,17 +106,19 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
print('Failed to close/kill {}'.format(target))
processes[target] = None # to indicate this has ended
def _get_espcoredump_instance(ctx: Context,
args: PropertyDict,
gdb_timeout_sec: Optional[int] = None,
core: Optional[str] = None,
chip_rev: Optional[str] = None,
save_core: Optional[str] = None) -> CoreDump:
def _get_espcoredump_instance(
ctx: Context,
args: PropertyDict,
gdb_timeout_sec: Optional[int] = None,
core: Optional[str] = None,
chip_rev: Optional[str] = None,
save_core: Optional[str] = None,
) -> CoreDump:
ensure_build_directory(args, ctx.info_name)
project_desc = get_project_desc(args, ctx)
coredump_to_flash_config = get_sdkconfig_value(project_desc['config_file'],
'CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH')
coredump_to_flash_config = get_sdkconfig_value(
project_desc['config_file'], 'CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH'
)
coredump_to_flash = coredump_to_flash_config.rstrip().endswith('y') if coredump_to_flash_config else False
prog = os.path.join(project_desc['build_dir'], project_desc['app_elf'])
@ -141,13 +144,16 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
# The format will be determined automatically
args.port = args.port or get_default_serial_port()
else:
print('Path to core dump file is not provided. '
"Core dump can't be read from flash since this option is not enabled in menuconfig")
print(
'Path to core dump file is not provided. '
"Core dump can't be read from flash since this option is not enabled in menuconfig"
)
sys.exit(1)
espcoredump_kwargs['port'] = args.port
espcoredump_kwargs['parttable_off'] = get_sdkconfig_value(project_desc['config_file'],
'CONFIG_PARTITION_TABLE_OFFSET')
espcoredump_kwargs['parttable_off'] = get_sdkconfig_value(
project_desc['config_file'], 'CONFIG_PARTITION_TABLE_OFFSET'
)
if save_core:
espcoredump_kwargs['save_core'] = save_core
@ -169,8 +175,10 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
def is_gdb_with_python(gdb: str) -> bool:
# execute simple python command to check is it supported
return subprocess.run([gdb, '--batch-silent', '--ex', 'python import os'],
stderr=subprocess.DEVNULL).returncode == 0
return (
subprocess.run([gdb, '--batch-silent', '--ex', 'python import os'], stderr=subprocess.DEVNULL).returncode
== 0
)
def debug_cleanup() -> None:
print('cleaning up debug targets')
@ -182,7 +190,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
_terminate_async_target('gdb')
def post_debug(action: str, ctx: Context, args: PropertyDict, **kwargs: str) -> None:
""" Deal with asynchronous targets, such as openocd running in background """
"""Deal with asynchronous targets, such as openocd running in background"""
if kwargs['block'] == 1:
for target in ['openocd', 'gdbgui']:
if target in processes and processes[target] is not None:
@ -216,8 +224,9 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
project_desc = json.load(f)
return project_desc
def openocd(action: str, ctx: Context, args: PropertyDict, openocd_scripts: Optional[str],
openocd_commands: str) -> None:
def openocd(
action: str, ctx: Context, args: PropertyDict, openocd_scripts: Optional[str], openocd_commands: str
) -> None:
"""
Execute openocd as external tool
"""
@ -230,7 +239,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
openocd_arguments = project_desc.get('debug_arguments_openocd', '')
print(
'Note: OpenOCD cfg not found (via env variable OPENOCD_COMMANDS nor as a --openocd-commands argument)\n'
'OpenOCD arguments default to: "{}"'.format(openocd_arguments))
'OpenOCD arguments default to: "{}"'.format(openocd_arguments)
)
# script directory is taken from the environment by OpenOCD, update only if command line arguments to override
if openocd_scripts is not None:
openocd_arguments += ' -s {}'.format(openocd_scripts)
@ -243,14 +253,17 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
except Exception as e:
print(e)
raise FatalError(
'Error starting openocd. Please make sure it is installed and is present in executable paths', ctx)
'Error starting openocd. Please make sure it is installed and is present in executable paths', ctx
)
processes['openocd'] = process
processes['openocd_outfile'] = openocd_out
processes['openocd_outfile_name'] = openocd_out_name
print('OpenOCD started as a background task {}'.format(process.pid))
def get_gdb_args(project_desc: Dict[str, Any], gdb_x: Tuple, gdb_ex: Tuple, gdb_commands: Optional[str]) -> List[str]:
def get_gdb_args(
project_desc: Dict[str, Any], gdb_x: Tuple, gdb_ex: Tuple, gdb_commands: Optional[str]
) -> List[str]:
# check if the application was built and ELF file is in place.
app_elf = os.path.join(project_desc.get('build_dir', ''), project_desc.get('app_elf', ''))
if not os.path.exists(app_elf):
@ -260,13 +273,16 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
gdb_args = [gdb_name]
gdbinit_files = project_desc.get('gdbinit_files')
if not gdbinit_files:
raise FatalError('Please check if the project was configured correctly ("gdbinit_files" not found in "project_description.json").')
raise FatalError(
'Please check if the project was configured correctly ("gdbinit_files" not found in '
'"project_description.json").'
)
gdbinit_files = sorted(gdbinit_files.items())
gdb_x_list = list(gdb_x)
gdb_x_names = [os.path.basename(x) for x in gdb_x_list]
# compile predefined gdbinit files options.
for name, path in gdbinit_files:
name = name[len('xx_'):]
name = name[len('xx_') :]
if name == 'py_extensions':
if not is_gdb_with_python(gdb_name):
continue
@ -296,7 +312,9 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
def _get_gdbgui_version(ctx: Context) -> Tuple[int, ...]:
subprocess_success = False
try:
completed_process = subprocess.run(['gdbgui', '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
completed_process = subprocess.run(
['gdbgui', '--version'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
captured_output = completed_process.stdout.decode('utf-8', 'ignore')
subprocess_success = True
except FileNotFoundError:
@ -304,25 +322,27 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
pass
if not subprocess_success or completed_process.returncode != 0:
if sys.version_info[:2] >= (3, 11) and sys.platform == 'win32':
raise SystemExit('Unfortunately, gdbgui is supported only with Python 3.10 or older. '
'See: https://github.com/espressif/esp-idf/issues/10116. '
'Please use "idf.py gdb" or debug in Eclipse/Vscode instead.')
if sys.version_info[:2] >= (3, 13) and sys.platform != 'win32':
raise SystemExit('Unfortunately, gdbgui is supported only with Python 3.12 or older. '
'See: https://github.com/cs01/gdbgui/issues/494. '
'Please use "idf.py gdb" or debug in Eclipse/Vscode instead.')
raise FatalError('Error starting gdbgui. Please make sure gdbgui has been installed with '
'"install.{sh,bat,ps1,fish} --enable-gdbgui" and can be started. '
f'Error: {captured_output if subprocess_success else "Unknown"}', ctx)
raise SystemExit(
'Error occurred while starting gdbgui. Please make sure gdbgui has been installed with '
'pipx based on https://www.gdbgui.com/installation/ and "gdbgui --version" can be run '
'successfully. Gdbgui issues can be reported at https://github.com/cs01/gdbgui/issues.'
)
v = re.search(r'(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?', captured_output)
if not v:
raise SystemExit(f'Error: "gdbgui --version" returned "{captured_output}"')
return tuple(int(i) if i else 0 for i in (v[1], v[2], v[3], v[4]))
def gdbui(action: str, ctx: Context, args: PropertyDict, gdbgui_port: Optional[str], gdbinit: Tuple,
ex: Tuple, gdb_commands: Optional[str], require_openocd: bool) -> None:
def gdbui(
action: str,
ctx: Context,
args: PropertyDict,
gdbgui_port: Optional[str],
gdbinit: Tuple,
ex: Tuple,
gdb_commands: Optional[str],
require_openocd: bool,
) -> None:
"""
Asynchronous GDB-UI target
"""
@ -344,7 +364,9 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
# so for one item, use extra double quotes. for more items, use no extra double quotes.
gdb = gdb_args[0]
gdb_args_list = gdb_args[1:]
gdb_args_str = '"{}"'.format(' '.join(gdb_args_list)) if len(gdb_args_list) == 1 else ' '.join(gdb_args_list)
gdb_args_str = (
'"{}"'.format(' '.join(gdb_args_list)) if len(gdb_args_list) == 1 else ' '.join(gdb_args_list)
)
gdbgui_args = ['gdbgui', '-g', gdb, '--gdb-args', gdb_args_str]
if gdbgui_port is not None:
@ -396,18 +418,42 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
if task.name in ('gdb', 'gdbgui', 'gdbtui'):
task.action_args['require_openocd'] = True
def gdbtui(action: str, ctx: Context, args: PropertyDict, gdbinit: Tuple, ex: Tuple, gdb_commands: str, require_openocd: bool) -> None:
def gdbtui(
action: str,
ctx: Context,
args: PropertyDict,
gdbinit: Tuple,
ex: Tuple,
gdb_commands: str,
require_openocd: bool,
) -> None:
"""
Synchronous GDB target with text ui mode
"""
gdb(action, ctx, args, False, 1, gdbinit, ex, gdb_commands, require_openocd)
def gdb(action: str, ctx: Context, args: PropertyDict, batch: bool, gdb_tui: Optional[int], gdbinit: Tuple,
ex: Tuple, gdb_commands: Optional[str], require_openocd: bool) -> None:
def gdb(
action: str,
ctx: Context,
args: PropertyDict,
batch: bool,
gdb_tui: Optional[int],
gdbinit: Tuple,
ex: Tuple,
gdb_commands: Optional[str],
require_openocd: bool,
) -> None:
"""
Synchronous GDB target
"""
watch_openocd = Thread(target=_check_openocd_errors, args=(fail_if_openocd_failed, action, ctx,))
watch_openocd = Thread(
target=_check_openocd_errors,
args=(
fail_if_openocd_failed,
action,
ctx,
),
)
watch_openocd.start()
processes['threads_to_join'].append(watch_openocd)
project_desc = get_project_desc(args, ctx)
@ -433,25 +479,29 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
# Valid scenario: watch_openocd task won't be in the list if openocd not started from idf.py
pass
def coredump_info(action: str,
ctx: Context,
args: PropertyDict,
gdb_timeout_sec: int,
core: Optional[str] = None,
chip_rev: Optional[str] = None,
save_core: Optional[str] = None) -> None:
espcoredump = _get_espcoredump_instance(ctx=ctx, args=args, gdb_timeout_sec=gdb_timeout_sec, core=core,
chip_rev=chip_rev,
save_core=save_core)
def coredump_info(
action: str,
ctx: Context,
args: PropertyDict,
gdb_timeout_sec: int,
core: Optional[str] = None,
chip_rev: Optional[str] = None,
save_core: Optional[str] = None,
) -> None:
espcoredump = _get_espcoredump_instance(
ctx=ctx, args=args, gdb_timeout_sec=gdb_timeout_sec, core=core, chip_rev=chip_rev, save_core=save_core
)
espcoredump.info_corefile()
def coredump_debug(action: str,
ctx: Context,
args: PropertyDict,
core: Optional[str] = None,
chip_rev: Optional[str] = None,
save_core: Optional[str] = None) -> None:
def coredump_debug(
action: str,
ctx: Context,
args: PropertyDict,
core: Optional[str] = None,
chip_rev: Optional[str] = None,
save_core: Optional[str] = None,
) -> None:
espcoredump = _get_espcoredump_instance(ctx=ctx, args=args, core=core, chip_rev=chip_rev, save_core=save_core)
espcoredump.dbg_corefile()
@ -464,8 +514,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
{
'names': ['--chip-rev'],
'help': 'Specify the chip revision (e.g., 0.1). If provided, the corresponding ROM ELF file will be used '
'for decoding the core dump, improving stack traces. This is only needed for core dumps from '
'ESP-IDF older than v5.2. Newer versions already contain chip revision information.',
'for decoding the core dump, improving stack traces. This is only needed for core dumps from '
'ESP-IDF older than v5.2. Newer versions already contain chip revision information.',
'hidden': True,
},
{
@ -493,15 +543,13 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
}
ex = {
'names': ['--ex', '-ex'],
'help':
('Execute given GDB command.\n'),
'help': ('Execute given GDB command.\n'),
'default': None,
'multiple': True,
}
gdb_commands = {
'names': ['--gdb-commands', '--gdb_commands'],
'help':
('Command line arguments for gdb.\n'),
'help': ('Command line arguments for gdb.\n'),
'default': None,
}
debug_actions = {
@ -513,17 +561,14 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'options': [
{
'names': ['--openocd-scripts', '--openocd_scripts'],
'help':
('Script directory for openocd cfg files.\n'),
'default':
None,
'help': ('Script directory for openocd cfg files.\n'),
'default': None,
},
{
'names': ['--openocd-commands', '--openocd_commands'],
'help':
('Command line arguments for openocd.\n'),
'help': ('Command line arguments for openocd.\n'),
'default': None,
}
},
],
'order_dependencies': ['all', 'flash'],
},
@ -542,7 +587,11 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'names': ['--gdb-tui', '--gdb_tui'],
'help': ('run gdb in TUI mode\n'),
'default': None,
}, gdbinit, ex, gdb_commands, fail_if_openocd_failed
},
gdbinit,
ex,
gdb_commands,
fail_if_openocd_failed,
],
'order_dependencies': ['all', 'flash'],
},
@ -552,11 +601,13 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'options': [
{
'names': ['--gdbgui-port', '--gdbgui_port'],
'help':
('The port on which gdbgui will be hosted. Default: 5000\n'),
'default':
None,
}, gdbinit, ex, gdb_commands, fail_if_openocd_failed
'help': ('The port on which gdbgui will be hosted. Default: 5000\n'),
'default': None,
},
gdbinit,
ex,
gdb_commands,
fail_if_openocd_failed,
],
'order_dependencies': ['all', 'flash'],
},
@ -569,7 +620,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'coredump-info': {
'callback': coredump_info,
'help': 'Print crashed tasks registers, callstack, list of available tasks in the system, '
'memory regions and contents of memory stored in core dump (TCBs and stacks)',
'memory regions and contents of memory stored in core dump (TCBs and stacks)',
'options': coredump_base + [PORT, BAUD_RATE, gdb_timeout_sec_opt], # type: ignore
'order_dependencies': ['all', 'flash'],
},
@ -585,8 +636,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'options': [
{
'names': ['--block', '--block'],
'help':
('Set to 1 for blocking the console on the outputs of async debug actions\n'),
'help': ('Set to 1 for blocking the console on the outputs of async debug actions\n'),
'default': 0,
},
],
@ -605,8 +655,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Dict:
'options': [
{
'names': ['--block', '--block'],
'help':
('Set to 1 for blocking the console on the outputs of async debug actions\n'),
'help': ('Set to 1 for blocking the console on the outputs of async debug actions\n'),
'default': 0,
},
],

View File

@ -7,12 +7,6 @@
"optional": false,
"requirement_path": "tools/requirements/requirements.core.txt"
},
{
"name": "gdbgui",
"description": "Packages for supporting debugging from web browser",
"optional": true,
"requirement_path": "tools/requirements/requirements.gdbgui.txt"
},
{
"name": "pytest",
"description": "Packages for CI with pytest",

View File

@ -1,12 +0,0 @@
# Python package requirements for gdbgui support ESP-IDF.
# This feature can be enabled by running "install.{sh,bat,ps1,fish} --enable-gdbgui"
#
# This file lists Python packages without version specifiers. Version details
# are stored in a separate constraints file. For more information, visit:
# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/tools/idf-tools.html
# gdbgui Python 3.11 issue https://github.com/cs01/gdbgui/issues/447 was fixed in 0.15.2.0. Windows users need an
# older Python to use since new gdbgui versions don't support Windows anymore.
# Python 3.13 is not supported: https://github.com/cs01/gdbgui/issues/494
gdbgui; sys_platform != 'win32' and python_version < "3.13"
gdbgui; sys_platform == 'win32' and python_version < "3.11"

View File

@ -23,7 +23,7 @@ except ImportError:
sys.path.append('..')
import idf_tools
IDF_PATH = os.environ.get('IDF_PATH', '../..')
IDF_PATH = os.path.abspath(os.environ.get('IDF_PATH', '../..'))
TOOLS_DIR = os.environ.get('IDF_TOOLS_PATH') or os.path.expanduser(idf_tools.IDF_TOOLS_PATH_DEFAULT)
PYTHON_DIR = os.path.join(TOOLS_DIR, 'python_env')
PYTHON_DIR_BACKUP = tempfile.mkdtemp()
@ -32,7 +32,7 @@ REQ_SATISFIED = 'Python requirements are satisfied'
# Python 3.8 and 3.9 has a different error message that does not include the "No package metadata was found for" part
REQ_MISSING = r'Package was not found and is required by the application: (No package metadata was found for )?{}'
REQ_CORE = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.core.txt'))
REQ_GDBGUI = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.gdbgui.txt'))
REQ_DOCS = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.docs.txt'))
CONSTR = 'Constraint file: {}'.format(os.path.join(TOOLS_DIR, 'espidf.constraints'))
# Set default global paths for idf_tools. If some test needs to
@ -191,29 +191,29 @@ class TestPythonInstall(BasePythonInstall):
output = self.run_idf_tools(['install-python-env'])
self.assertIn(CONSTR, output)
self.assertIn(REQ_CORE, output)
self.assertNotIn(REQ_GDBGUI, output)
self.assertNotIn(REQ_DOCS, output)
output = self.run_idf_tools(['check-python-dependencies'])
self.assertIn(REQ_SATISFIED, output)
def test_opt_argument(self): # type: () -> None
output = self.run_idf_tools(['install-python-env', '--features', 'gdbgui'])
output = self.run_idf_tools(['install-python-env', '--features', 'docs'])
self.assertIn(CONSTR, output)
self.assertIn(REQ_CORE, output)
self.assertIn(REQ_GDBGUI, output)
self.assertIn(REQ_DOCS, output)
output = self.run_idf_tools(['install-python-env'])
# The gdbgui should be installed as well because the feature is is stored in the JSON file
# The docs should be installed as well because the feature is is stored in the JSON file
self.assertIn(CONSTR, output)
self.assertIn(REQ_CORE, output)
self.assertIn(REQ_GDBGUI, output)
self.assertIn(REQ_DOCS, output)
# Argument that begins with '-' can't stand alone to be parsed as value
output = self.run_idf_tools(['install-python-env', '--features=-gdbgui'])
# After removing the gdbgui should not be present
output = self.run_idf_tools(['install-python-env', '--features=-docs'])
# After removing the docs should not be present
self.assertIn(CONSTR, output)
self.assertIn(REQ_CORE, output)
self.assertNotIn(REQ_GDBGUI, output)
self.assertNotIn(REQ_DOCS, output)
def test_no_constraints(self): # type: () -> None
output = self.run_idf_tools(['install-python-env', '--no-constraints'])