feat: add diff option support to idf.py size commands

This adds a possibility to specify --diff option to idf.py size,
size-components and size-files commands. This can be map file directly,
project directory or build directory.

Usage example:
idf.py size-components --diff ../hello_world2/build/hello_world.map
idf.py size-components --diff ../hello_world2/build
idf.py size-components --diff ../hello_world2

Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
This commit is contained in:
Frantisek Hrbata 2024-05-06 12:13:16 +02:00 committed by BOT
parent 2edf936984
commit 6f41dee0cd
3 changed files with 99 additions and 16 deletions

View File

@ -32,6 +32,10 @@ if(DEFINED IDF_SIZE_MODE)
list(APPEND IDF_SIZE_CMD ${IDF_SIZE_MODE}) list(APPEND IDF_SIZE_CMD ${IDF_SIZE_MODE})
endif() endif()
if(DEFINED ENV{SIZE_DIFF_FILE})
list(APPEND IDF_SIZE_CMD "--diff=$ENV{SIZE_DIFF_FILE}")
endif()
list(APPEND IDF_SIZE_CMD ${MAP_FILE}) list(APPEND IDF_SIZE_CMD ${MAP_FILE})
execute_process(COMMAND ${IDF_SIZE_CMD} execute_process(COMMAND ${IDF_SIZE_CMD}

View File

@ -1,6 +1,7 @@
# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD # SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
import fnmatch import fnmatch
import glob
import json import json
import locale import locale
import os import os
@ -8,18 +9,33 @@ import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
from typing import Any, Dict, List, Optional from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from urllib.error import URLError from urllib.error import URLError
from urllib.request import Request, urlopen from urllib.request import Request
from urllib.request import urlopen
from webbrowser import open_new_tab from webbrowser import open_new_tab
import click import click
from click.core import Context from click.core import Context
from idf_py_actions.constants import GENERATORS, PREVIEW_TARGETS, SUPPORTED_TARGETS, URL_TO_DOC from idf_py_actions.constants import GENERATORS
from idf_py_actions.constants import PREVIEW_TARGETS
from idf_py_actions.constants import SUPPORTED_TARGETS
from idf_py_actions.constants import URL_TO_DOC
from idf_py_actions.errors import FatalError from idf_py_actions.errors import FatalError
from idf_py_actions.global_options import global_options from idf_py_actions.global_options import global_options
from idf_py_actions.tools import (PropertyDict, TargetChoice, ensure_build_directory, generate_hints, get_target, from idf_py_actions.tools import ensure_build_directory
idf_version, merge_action_lists, print_warning, run_target, yellow_print) from idf_py_actions.tools import generate_hints
from idf_py_actions.tools import get_target
from idf_py_actions.tools import idf_version
from idf_py_actions.tools import merge_action_lists
from idf_py_actions.tools import print_warning
from idf_py_actions.tools import PropertyDict
from idf_py_actions.tools import run_target
from idf_py_actions.tools import TargetChoice
from idf_py_actions.tools import yellow_print
def action_extensions(base_actions: Dict, project_path: str) -> Any: def action_extensions(base_actions: Dict, project_path: str) -> Any:
@ -34,7 +50,7 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any:
run_target(target_name, args, force_progression=GENERATORS[args.generator].get('force_progression', False)) run_target(target_name, args, force_progression=GENERATORS[args.generator].get('force_progression', False))
def size_target(target_name: str, ctx: Context, args: PropertyDict, output_format: str, def size_target(target_name: str, ctx: Context, args: PropertyDict, output_format: str,
output_file: str, legacy: bool) -> None: output_file: str, diff_map_file: str, legacy: bool) -> None:
""" """
Builds the app and then executes a size-related target passed in 'target_name'. Builds the app and then executes a size-related target passed in 'target_name'.
`tool_error_handler` handler is used to suppress errors during the build, `tool_error_handler` handler is used to suppress errors during the build,
@ -45,6 +61,8 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any:
for hint in generate_hints(stdout, stderr): for hint in generate_hints(stdout, stderr):
yellow_print(hint) yellow_print(hint)
env: Dict[str, Any] = {}
if not legacy and output_format != 'json': if not legacy and output_format != 'json':
try: try:
import esp_idf_size.ng # noqa: F401 import esp_idf_size.ng # noqa: F401
@ -55,28 +73,43 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any:
# Legacy mode is used only when explicitly requested with --legacy option # Legacy mode is used only when explicitly requested with --legacy option
# or when "--format json" option is specified. Here we enable the # or when "--format json" option is specified. Here we enable the
# esp-idf-size refactored version with ESP_IDF_SIZE_NG env. variable. # esp-idf-size refactored version with ESP_IDF_SIZE_NG env. variable.
os.environ['ESP_IDF_SIZE_NG'] = '1' env['ESP_IDF_SIZE_NG'] = '1'
# ESP_IDF_SIZE_FORCE_TERMINAL is set to force terminal control codes even # ESP_IDF_SIZE_FORCE_TERMINAL is set to force terminal control codes even
# if stdout is not attached to terminal. This is set to pass color codes # if stdout is not attached to terminal. This is set to pass color codes
# from esp-idf-size to idf.py. # from esp-idf-size to idf.py.
os.environ['ESP_IDF_SIZE_FORCE_TERMINAL'] = '1' env['ESP_IDF_SIZE_FORCE_TERMINAL'] = '1'
if legacy and output_format in ['json2', 'raw', 'tree']: if legacy and output_format in ['json2', 'raw', 'tree']:
# These formats are supported in new version only. # These formats are supported in new version only.
# We would get error from the esp-idf-size anyway, so print error early. # We would get error from the esp-idf-size anyway, so print error early.
raise FatalError(f'Legacy esp-idf-size does not support {output_format} format') raise FatalError(f'Legacy esp-idf-size does not support {output_format} format')
os.environ['SIZE_OUTPUT_FORMAT'] = output_format env['SIZE_OUTPUT_FORMAT'] = output_format
if output_file: if output_file:
os.environ['SIZE_OUTPUT_FILE'] = os.path.abspath(output_file) env['SIZE_OUTPUT_FILE'] = os.path.abspath(output_file)
if diff_map_file:
diff_map_file = os.path.abspath(diff_map_file)
if os.path.isdir(diff_map_file):
# The diff_map_file argument is a directory. Try to look for the map
# file directly in it, in case it's a build directory or in one level below
# if it's a project directory.
files = glob.glob(os.path.join(diff_map_file, '*.map')) or glob.glob(os.path.join(diff_map_file, '*/*.map'))
if not files:
raise FatalError(f'No diff map file found in {diff_map_file} directory')
if len(files) > 1:
map_files = ', '.join(files)
raise FatalError(f'Two or more diff map files {map_files} found in {diff_map_file} directory')
diff_map_file = files[0]
env['SIZE_DIFF_FILE'] = diff_map_file
ensure_build_directory(args, ctx.info_name) ensure_build_directory(args, ctx.info_name)
run_target('all', args, force_progression=GENERATORS[args.generator].get('force_progression', False), run_target('all', args, force_progression=GENERATORS[args.generator].get('force_progression', False),
custom_error_handler=tool_error_handler) custom_error_handler=tool_error_handler)
run_target(target_name, args) run_target(target_name, args, env=env)
def list_build_system_targets(target_name: str, ctx: Context, args: PropertyDict) -> None: def list_build_system_targets(target_name: str, ctx: Context, args: PropertyDict) -> None:
"""Shows list of targets known to build sytem (make/ninja)""" """Shows list of targets known to build system (make/ninja)"""
build_target('help', ctx, args) build_target('help', ctx, args)
def menuconfig(target_name: str, ctx: Context, args: PropertyDict, style: str) -> None: def menuconfig(target_name: str, ctx: Context, args: PropertyDict, style: str) -> None:
@ -383,6 +416,9 @@ def action_extensions(base_actions: Dict, project_path: str) -> Any:
'is_flag': True, 'is_flag': True,
'default': os.environ.get('ESP_IDF_SIZE_LEGACY', '0') == '1', 'default': os.environ.get('ESP_IDF_SIZE_LEGACY', '0') == '1',
'help': 'Use legacy esp-idf-size version'}, 'help': 'Use legacy esp-idf-size version'},
{'names': ['--diff', 'diff_map_file'],
'help': ('Show the differences in comparison with another project. '
'Argument can be map file or project directory.')},
{'names': ['--output-file', 'output_file'], {'names': ['--output-file', 'output_file'],
'help': 'Print output to the specified file instead of to the standard output'}] 'help': 'Print output to the specified file instead of to the standard output'}]

View File

@ -5,13 +5,56 @@ import logging
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from subprocess import DEVNULL from shutil import copytree
from subprocess import PIPE
from subprocess import run from subprocess import run
from subprocess import STDOUT
from tempfile import TemporaryDirectory
from typing import Any
from typing import Optional
from typing import Tuple
from typing import Union
IDF_PATH = Path(os.environ['IDF_PATH'])
IDF_PY_PATH = IDF_PATH / 'tools' / 'idf.py'
IDF_SIZE_PY_PATH = IDF_PATH / 'tools' / 'idf_size.py'
HELLO_WORLD_PATH = IDF_PATH / 'examples' / 'get-started' / 'hello_world'
PathLike = Union[str, Path]
def run_cmd(*cmd: PathLike, cwd: Optional[PathLike]=None, check: bool=True, text: bool=True) -> Tuple[int, str]:
logging.info('running: {}'.format(' '.join([str(arg) for arg in cmd])))
p = run(cmd, stdout=PIPE, stderr=STDOUT, cwd=cwd, check=check, text=text)
return p.returncode, p.stdout
def run_idf_py(*args: PathLike, **kwargs: Any) -> Tuple[int, str]:
return run_cmd(sys.executable, IDF_PY_PATH, *args, **kwargs)
def run_idf_size_py(*args: PathLike, **kwargs: Any) -> Tuple[int, str]:
return run_cmd(sys.executable, IDF_SIZE_PY_PATH, *args, **kwargs)
def test_idf_size() -> None: def test_idf_size() -> None:
# Simple test to make sure that the idf_size.py wrapper is compatible # Simple test to make sure that the idf_size.py wrapper is compatible
# with idf.py minimum required python version. # with idf.py minimum required python version.
logging.info('idf_size.py python compatibility check') logging.info('idf_size.py python compatibility check')
idf_size_path = Path(os.environ['IDF_PATH']) / 'tools' / 'idf_size.py' run_idf_size_py('--help')
run([sys.executable, idf_size_path, '--help'], stdout=DEVNULL, stderr=DEVNULL, check=True)
def test_idf_py_size_diff() -> None:
# Test idf.py size with diff option, utilizing the same map file, as the focus
# of the test lies solely on the option, not on the resulting output.
logging.info('idf.py size --diff option test')
tmpdir = TemporaryDirectory()
app_path = Path(tmpdir.name) / 'app'
copytree(HELLO_WORLD_PATH, app_path, symlinks=True)
run_idf_py('fullclean', cwd=app_path)
run_idf_py('build', cwd=app_path)
run_idf_py('size', '--diff', '.', cwd=app_path)
# The diff map file should be found automatically in project or project's build directory
run_idf_py('size', '--diff', 'build', cwd=app_path)
run_idf_py('size', '--diff', Path('build') / 'hello_world.map', cwd=app_path)