change(tools): Unified outputs of various standalone scripts (esp-pylib)

This commit is contained in:
Jakub Kocka
2026-09-09 09:43:59 +02:00
parent 22ade8b843
commit d6771a0608
20 changed files with 780 additions and 601 deletions
+64 -66
View File
@@ -1,53 +1,22 @@
# SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2023-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import os
import sys
from types import SimpleNamespace
from typing import Any
from typing import Dict
from console_output import CONSOLE_STDERR
from console_output import CONSOLE_STDOUT
from console_output import debug
from console_output import die
from console_output import eprint
from console_output import oprint
import rich_click as click
from console_output import configure_output
from console_output import status_message
from console_output import warn
from esp_pylib.excepthook import install_exception_reporting
from esp_pylib.logger import log
from rich.markup import escape
from shell_types import SHELL_CLASSES
from shell_types import SUPPORTED_SHELLS
from utils import conf
from utils import run_cmd
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(
prog='activate',
description='Activate ESP-IDF environment',
epilog='On Windows, run `python activate.py` to execute this script in the current terminal window.',
)
parser.add_argument(
'-s',
'--shell',
metavar='SHELL',
default=os.environ.get('ESP_IDF_SHELL', 'detect'),
help='Explicitly specify shell to start. For example bash, zsh, powershell.exe, cmd.exe',
)
parser.add_argument('-l', '--list', action='store_true', help=('List supported shells.'))
parser.add_argument('-e', '--export', action='store_true', help=('Generate commands to run in the terminal.'))
parser.add_argument('-n', '--no-color', action='store_true', help=('Disable ANSI color escape sequences.'))
parser.add_argument(
'-d',
'--debug',
action='store_true',
default=bool(os.environ.get('ESP_IDF_EXPORT_DEBUG')),
help=('Enable debug information.'),
)
parser.add_argument('-q', '--quiet', action='store_true', help=('Suppress all output.'))
return parser.parse_args()
@status_message('Checking python version', rv_on_ok=True)
def check_python_version() -> str:
# Check the Python version within a virtual environment
@@ -72,7 +41,7 @@ def get_deactivate_cmd() -> str:
@status_message('Establishing a new ESP-IDF environment')
def get_idf_env() -> Dict[str, str]:
def get_idf_env() -> dict[str, str]:
# Get ESP-IDF system environment variables
extra_paths_list = [
os.path.join('components', 'espcoredump'),
@@ -84,7 +53,7 @@ def get_idf_env() -> Dict[str, str]:
stdout = run_cmd(cmd)
# idf_tools.py might not export certain environment variables if they are already set
idf_env: Dict[str, Any] = {
idf_env: dict[str, Any] = {
'IDF_PATH': os.environ['IDF_PATH'],
'ESP_IDF_VERSION': os.environ['ESP_IDF_VERSION'],
'IDF_PYTHON_ENV_PATH': os.environ['IDF_PYTHON_ENV_PATH'],
@@ -95,7 +64,7 @@ def get_idf_env() -> Dict[str, str]:
var, val = line.split('=')
idf_env[var] = val
except ValueError as e:
debug('\n'.join(['Output from `./tools/idf_tools.py export --format key-value`:', f'{stdout}']))
log.debug('\n'.join(['Output from `./tools/idf_tools.py export --format key-value`:', f'{stdout}']))
raise ValueError(
'\n'.join(
[
@@ -117,7 +86,7 @@ def detect_shell(args: Any) -> str:
import psutil
if args.shell != 'detect':
debug(f'Shell explicitly stated: "{args.shell}"')
log.debug(f'Shell explicitly stated: "{args.shell}"')
return str(args.shell)
current_pid = os.getpid()
@@ -128,9 +97,9 @@ def detect_shell(args: Any) -> str:
parent_cmdline = parent.cmdline()
parent_exe = parent_cmdline[0].lstrip('-')
parent_name = os.path.basename(parent_exe)
debug(f'Parent: pid: {parent_pid}, cmdline: {parent_cmdline}, exe: {parent_exe}, name: {parent_name}')
log.debug(f'Parent: pid: {parent_pid}, cmdline: {parent_cmdline}, exe: {parent_exe}, name: {parent_name}')
if not parent_name.lower().startswith('python'):
detected_shell_name = parent_name
detected_shell_name = str(parent_name)
break
current_pid = parent_pid
@@ -155,29 +124,56 @@ def print_uninstall_msg() -> Any:
return msg
def main() -> None:
args = parse_arguments()
@click.command(
context_settings={'help_option_names': ['-h', '--help']},
epilog='On Windows, run `python activate.py` to execute this script in the current terminal window.',
)
@click.option(
'-s',
'--shell',
metavar='SHELL',
default=os.environ.get('ESP_IDF_SHELL', 'detect'),
show_default=True,
help='Explicitly specify shell to start. For example bash, zsh, powershell.exe, cmd.exe',
)
@click.option('-l', '--list', 'list_shells', is_flag=True, help='List supported shells.')
@click.option('-e', '--export', is_flag=True, help='Generate commands to run in the terminal.')
@click.option('-n', '--no-color', is_flag=True, help='Disable ANSI color escape sequences.')
@click.option(
'-d',
'--debug',
'debug_flag',
is_flag=True,
default=bool(os.environ.get('ESP_IDF_EXPORT_DEBUG')),
help='Enable debug information.',
)
@click.option('-q', '--quiet', is_flag=True, help='Suppress all output.')
def main(shell: str, list_shells: bool, export: bool, no_color: bool, debug_flag: bool, quiet: bool) -> None:
install_exception_reporting()
# Setup parsed arguments
CONSOLE_STDERR.no_color = args.no_color
CONSOLE_STDOUT.no_color = args.no_color
CONSOLE_STDERR.quiet = args.quiet
CONSOLE_STDOUT.quiet = args.quiet
# Fill config global holder
conf.ARGS = args
# Fill config global holder before configure_output
conf.ARGS = SimpleNamespace(
shell=shell,
list=list_shells,
export=export,
no_color=no_color,
debug=debug_flag,
quiet=quiet,
)
configure_output(no_color=no_color, quiet=quiet, debug=debug_flag)
debug(f'command line: {sys.argv}')
log.debug(f'command line: {sys.argv}')
if conf.ARGS.list:
oprint(SUPPORTED_SHELLS)
log.print(SUPPORTED_SHELLS)
sys.exit()
eprint(f'[dark_orange]Activating ESP-IDF {conf.IDF_VERSION}')
log.print(f'[dark_orange]Activating ESP-IDF {conf.IDF_VERSION}', file=sys.stderr)
if conf.IDF_PATH_OLD and conf.IDF_PATH != conf.IDF_PATH_OLD:
warn(f"IDF_PATH is changed from '{conf.IDF_PATH_OLD}' to '{conf.IDF_PATH}'.")
log.warn(f"IDF_PATH is changed from '{escape(conf.IDF_PATH_OLD)}' to '{escape(conf.IDF_PATH)}'.")
else:
eprint(f"Setting IDF_PATH to '{conf.IDF_PATH}'.")
log.print(f"Setting IDF_PATH to '{escape(conf.IDF_PATH)}'.", file=sys.stderr)
debug(f'IDF_PYTHON_ENV_PATH {conf.IDF_PYTHON_ENV_PATH}')
log.debug(f'IDF_PYTHON_ENV_PATH {conf.IDF_PYTHON_ENV_PATH}')
check_python_version()
check_python_dependencies()
@@ -188,19 +184,21 @@ def main() -> None:
print_uninstall_msg()
if detected_shell not in SHELL_CLASSES:
die(f'"{detected_shell}" shell is not among the supported options: "{SUPPORTED_SHELLS}"')
log.die(f'"{escape(str(detected_shell))}" shell is not among the supported options: "{SUPPORTED_SHELLS}"')
shell = SHELL_CLASSES[detected_shell](detected_shell, deactivate_cmd, new_esp_idf_env)
shell_obj = SHELL_CLASSES[detected_shell](detected_shell, deactivate_cmd, new_esp_idf_env)
if conf.ARGS.export:
shell.export()
shell_obj.export()
sys.exit()
eprint(
f'[dark_orange]Starting new \'{shell.shell}\' shell with ESP-IDF environment... (use "exit" command to quit)'
log.print(
f"[dark_orange]Starting new '{escape(str(shell_obj.shell))}' shell with ESP-IDF environment..."
' (use "exit" command to quit)',
file=sys.stderr,
)
shell.spawn()
eprint('[dark_orange]ESP-IDF environment exited.')
shell_obj.spawn()
log.print('[dark_orange]ESP-IDF environment exited.', file=sys.stderr)
if __name__ == '__main__':
+34 -43
View File
@@ -1,71 +1,62 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import sys
from collections.abc import Callable
from typing import Any
from typing import Callable
from typing import cast
from rich.markup import escape
from utils import conf
try:
# The ESP-IDF virtual environment hasn't been verified yet, so see if the rich library
# The ESP-IDF virtual environment hasn't been verified yet, so see if esp-pylib
# can be imported to display error and status messages nicely.
from rich.console import Console
from esp_pylib.logger import EspLog
from esp_pylib.logger import Verbosity
from esp_pylib.logger import log
except ImportError as e:
sys.exit(f'error: Unable to import the rich module: {e}. Please execute the install script.')
CONSOLE_STDERR = Console(stderr=True, width=255)
CONSOLE_STDOUT = Console(width=255)
sys.exit(f'error: Unable to import esp-pylib: {e}. Please execute the install script.')
def status_message(msg: str, msg_result: str='', rv_on_ok: bool=False, die_on_err: bool=True) -> Callable:
def configure_output(*, no_color: bool = False, quiet: bool = False, debug: bool = False) -> None:
# log is typed as EspLogBase; console options live on the default EspLog singleton.
# Only pass no_color=True when requested. False would override Rich's NO_COLOR detection.
options: dict[str, Any] = {'quiet': quiet}
if no_color:
options['no_color'] = True
cast(EspLog, log).set_console_options(**options)
if quiet:
log.set_verbosity(Verbosity.SILENT)
elif debug:
log.set_verbosity(Verbosity.VERBOSE)
else:
log.set_verbosity(Verbosity.NORMAL)
def status_message(msg: str, msg_result: str = '', rv_on_ok: bool = False, die_on_err: bool = True) -> Callable:
def inner(func: Callable) -> Callable:
def wrapper(*args: Any, **kwargs: Any) -> Any:
eprint(f'[dark_orange]*[/dark_orange] {msg} ... ', end='')
log.print(f'[dark_orange]*[/dark_orange] {escape(msg)} ... ', file=sys.stderr, end='')
try:
rv = func(*args, **kwargs)
except Exception as e:
eprint('[red]FAILED[/red]')
if conf.ARGS.debug:
log.print('[red]FAILED[/red]', file=sys.stderr)
if conf.ARGS and conf.ARGS.debug:
raise
if not die_on_err:
return None
die(str(e))
log.die(escape(str(e)))
if rv_on_ok:
eprint(f'[green]{rv}[/green]')
log.print(f'[green]{escape(str(rv))}[/green]', file=sys.stderr)
elif msg_result:
eprint(f'[green]{msg_result}[/green]')
log.print(f'[green]{escape(msg_result)}[/green]', file=sys.stderr)
else:
eprint('[green]OK[/green]')
log.print('[green]OK[/green]', file=sys.stderr)
return rv
return wrapper
return inner
def err(*args: Any, **kwargs: Any) -> None:
CONSOLE_STDERR.print('[red]error[/red]: ', *args, **kwargs) # type: ignore
def warn(*args: Any, **kwargs: Any) -> None:
CONSOLE_STDERR.print('[yellow]warning[/yellow]: ', *args, **kwargs) # type: ignore
def debug(*args: Any, **kwargs: Any) -> None:
if not conf.ARGS.debug:
return
CONSOLE_STDERR.print('[green_yellow]debug[/green_yellow]: ', *args, **kwargs) # type: ignore
def die(*args: Any, **kwargs: Any) -> None:
err(*args, **kwargs)
sys.exit(1)
def eprint(*args: Any, **kwargs: Any) -> None:
CONSOLE_STDERR.print(*args, **kwargs) # type: ignore
def oprint(*args: Any, **kwargs: Any) -> None:
CONSOLE_STDOUT.print(*args, **kwargs) # type: ignore
+8 -9
View File
@@ -1,4 +1,4 @@
# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import base64
import getpass
@@ -17,9 +17,8 @@ from tempfile import TemporaryDirectory
from tempfile import gettempdir
from typing import TextIO
from console_output import debug
from console_output import status_message
from console_output import warn
from esp_pylib.logger import log
from utils import conf
from utils import run_cmd
@@ -42,7 +41,7 @@ class Shell:
self.tmp_dir_path = Path(gettempdir()) / f'esp_idf_activate_{username_safe}'
except Exception as e:
self.tmp_dir_path = Path(gettempdir()) / 'esp_idf_activate'
warn(f'Failed to get username with error: {e}. Using default temporary directory {self.tmp_dir_path}.')
log.warn(f'Failed to get username with error: {e}. Using default temporary directory {self.tmp_dir_path}.')
if not conf.ARGS.debug and os.path.exists(self.tmp_dir_path):
# Do not cleanup temporary directory when debugging
@@ -55,7 +54,7 @@ class Shell:
if current_time - file_creation_time > timedelta(hours=1):
item.unlink()
except Exception as e:
warn(f'Failed to clean temp activation directory with file {item}: {e}')
log.warn(f'Failed to clean temp activation directory with file {item}: {e}')
self.tmp_dir_path.mkdir(parents=True, exist_ok=True)
@@ -90,7 +89,7 @@ class UnixShell(Shell):
with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_') as fd:
self.script_file_path = Path(fd.name)
debug(f'Temporary script file path: {self.script_file_path}')
log.debug(f'Temporary script file path: {self.script_file_path}')
self.new_esp_idf_env['IDF_TOOLS_INSTALL_CMD'] = os.path.join(conf.IDF_PATH, 'install.sh')
self.new_esp_idf_env['IDF_TOOLS_EXPORT_CMD'] = os.path.join(conf.IDF_PATH, 'export.sh')
@@ -189,7 +188,7 @@ class ZshShell(UnixShell):
# Create a temporary directory to use as ZDOTDIR
tmpdir = TemporaryDirectory()
tmpdir_path = Path(tmpdir.name)
debug(f'Temporary ZDOTDIR {tmpdir_path} with .zshrc file')
log.debug(f'Temporary ZDOTDIR {tmpdir_path} with .zshrc file')
# Copy init script to the custom ZDOTDIR
zshrc_path = tmpdir_path / '.zshrc'
@@ -232,7 +231,7 @@ class PowerShell(Shell):
with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_', suffix='.ps1') as fd:
self.script_file_path = Path(fd.name)
debug(f'Temporary script file path: {self.script_file_path}')
log.debug(f'Temporary script file path: {self.script_file_path}')
self.new_esp_idf_env['IDF_TOOLS_INSTALL_CMD'] = os.path.join(conf.IDF_PATH, 'install.ps1')
self.new_esp_idf_env['IDF_TOOLS_EXPORT_CMD'] = os.path.join(conf.IDF_PATH, 'export.ps1')
@@ -284,7 +283,7 @@ class WinCmd(Shell):
with NamedTemporaryFile(dir=self.tmp_dir_path, delete=False, prefix='activate_', suffix='.bat') as fd:
self.script_file_path = Path(fd.name)
debug(f'Temporary script file path: {self.script_file_path}')
log.debug(f'Temporary script file path: {self.script_file_path}')
self.new_esp_idf_env['IDF_TOOLS_INSTALL_CMD'] = os.path.join(conf.IDF_PATH, 'install.bat')
self.new_esp_idf_env['IDF_TOOLS_EXPORT_CMD'] = os.path.join(conf.IDF_PATH, 'export.bat')
+5 -8
View File
@@ -1,13 +1,9 @@
# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
# SPDX-FileCopyrightText: 2024-2026 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0
import argparse
import os
from subprocess import run
from subprocess import SubprocessError
from subprocess import run
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
class Config:
@@ -15,6 +11,7 @@ class Config:
Config serves as global hodler for variables used across modules
It holds also arguments from command line
"""
def __init__(self) -> None:
self.IDF_PATH = os.environ['IDF_PATH']
self.IDF_PATH_OLD = os.environ['IDF_PATH_OLD']
@@ -22,14 +19,14 @@ class Config:
self.IDF_PYTHON_ENV_PATH = os.environ['IDF_PYTHON_ENV_PATH']
self.IDF_TOOLS_PY = os.path.join(self.IDF_PATH, 'tools', 'idf_tools.py')
self.IDF_PY = os.path.join(self.IDF_PATH, 'tools', 'idf.py')
self.ARGS: Optional[argparse.Namespace] = None
self.ARGS: Any | None = None
# Global variable instance
conf = Config()
def run_cmd(cmd: List[str], env: Optional[Dict[str, Any]]=None) -> str:
def run_cmd(cmd: list[str], env: dict[str, Any] | None = None) -> str:
new_env = os.environ.copy()
if env is not None:
new_env.update(env)