mirror of
				https://github.com/espressif/esp-idf.git
				synced 2025-11-04 00:51:42 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			3232 lines
		
	
	
		
			137 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			3232 lines
		
	
	
		
			137 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
#!/usr/bin/env python
 | 
						|
# coding=utf-8
 | 
						|
#
 | 
						|
# SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD
 | 
						|
#
 | 
						|
# SPDX-License-Identifier: Apache-2.0
 | 
						|
#
 | 
						|
# This script helps installing tools required to use the ESP-IDF, and updating PATH
 | 
						|
# to use the installed tools. It can also create a Python virtual environment,
 | 
						|
# and install Python requirements into it.
 | 
						|
#  It does not install OS dependencies. It does install tools such as the Xtensa
 | 
						|
#  GCC toolchain and ESP32 ULP coprocessor toolchain.
 | 
						|
#
 | 
						|
# By default, downloaded tools will be installed under $HOME/.espressif directory
 | 
						|
# (%USERPROFILE%/.espressif on Windows). This path can be modified by setting
 | 
						|
# IDF_TOOLS_PATH variable prior to running this tool.
 | 
						|
#
 | 
						|
# Users do not need to interact with this script directly. In IDF root directory,
 | 
						|
# install.sh (.bat) and export.sh (.bat) scripts are provided to invoke this script.
 | 
						|
#
 | 
						|
# Usage:
 | 
						|
#
 | 
						|
# * To install the tools, run `idf_tools.py install`.
 | 
						|
#
 | 
						|
# * To install the Python environment, run `idf_tools.py install-python-env`.
 | 
						|
#
 | 
						|
# * To start using the tools, run `eval "$(idf_tools.py export)"` — this will update
 | 
						|
#   the PATH to point to the installed tools and set up other environment variables
 | 
						|
#   needed by the tools.
 | 
						|
import argparse
 | 
						|
import contextlib
 | 
						|
import copy
 | 
						|
import datetime
 | 
						|
import errno
 | 
						|
import fnmatch
 | 
						|
import functools
 | 
						|
import hashlib
 | 
						|
import json
 | 
						|
import os
 | 
						|
import platform
 | 
						|
import re
 | 
						|
import shutil
 | 
						|
import ssl
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import tarfile
 | 
						|
import tempfile
 | 
						|
import time
 | 
						|
from collections import namedtuple
 | 
						|
from collections import OrderedDict
 | 
						|
from json import JSONEncoder
 | 
						|
from ssl import SSLContext
 | 
						|
from tarfile import TarFile
 | 
						|
from zipfile import ZipFile
 | 
						|
 | 
						|
# Important notice: Please keep the lines above compatible with old Pythons so it won't fail with ImportError but with
 | 
						|
# a nice message printed by python_version_checker.check()
 | 
						|
try:
 | 
						|
    import python_version_checker
 | 
						|
 | 
						|
    # check the Python version before it will fail with an exception on syntax or package incompatibility.
 | 
						|
    python_version_checker.check()
 | 
						|
except RuntimeError as e:
 | 
						|
    print(e)
 | 
						|
    raise SystemExit(1)
 | 
						|
 | 
						|
from typing import IO, Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Union
 | 
						|
from urllib.error import ContentTooShortError
 | 
						|
from urllib.parse import urljoin, urlparse
 | 
						|
from urllib.request import urlopen
 | 
						|
from urllib.response import addinfourl
 | 
						|
 | 
						|
try:
 | 
						|
    from exceptions import WindowsError
 | 
						|
except ImportError:
 | 
						|
    # Unix
 | 
						|
    class WindowsError(OSError):  # type: ignore
 | 
						|
        pass
 | 
						|
 | 
						|
 | 
						|
TOOLS_FILE = 'tools/tools.json'
 | 
						|
TOOLS_SCHEMA_FILE = 'tools/tools_schema.json'
 | 
						|
TOOLS_FILE_NEW = 'tools/tools.new.json'
 | 
						|
IDF_ENV_FILE = 'idf-env.json'
 | 
						|
TOOLS_FILE_VERSION = 2
 | 
						|
IDF_TOOLS_PATH_DEFAULT = os.path.join('~', '.espressif')
 | 
						|
UNKNOWN_VERSION = 'unknown'
 | 
						|
SUBST_TOOL_PATH_REGEX = re.compile(r'\${TOOL_PATH}')
 | 
						|
VERSION_REGEX_REPLACE_DEFAULT = r'\1'
 | 
						|
IDF_MAINTAINER = os.environ.get('IDF_MAINTAINER') or False
 | 
						|
TODO_MESSAGE = 'TODO'
 | 
						|
DOWNLOAD_RETRY_COUNT = 3
 | 
						|
URL_PREFIX_MAP_SEPARATOR = ','
 | 
						|
IDF_TOOLS_INSTALL_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD')
 | 
						|
IDF_TOOLS_EXPORT_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD')
 | 
						|
IDF_DL_URL = 'https://dl.espressif.com/dl/esp-idf'
 | 
						|
IDF_PIP_WHEELS_URL = os.environ.get('IDF_PIP_WHEELS_URL', 'https://dl.espressif.com/pypi')
 | 
						|
PYTHON_VENV_DIR_TEMPLATE = 'idf{}_py{}_env'
 | 
						|
PYTHON_VER_MAJOR_MINOR = f'{sys.version_info.major}.{sys.version_info.minor}'
 | 
						|
VENV_VER_FILE = 'idf_version.txt'
 | 
						|
 | 
						|
 | 
						|
class GlobalVarsStore:
 | 
						|
    """
 | 
						|
    Pythonic way how to handle global variables.
 | 
						|
    One global instance of this class is initialized and used as an entrypoint (store)
 | 
						|
    It handles string and boolean properties.
 | 
						|
    """
 | 
						|
    _instance: Optional['GlobalVarsStore'] = None
 | 
						|
    _bool_properties = ['quiet', 'non_interactive']
 | 
						|
    _string_properties = ['idf_path', 'idf_tools_path', 'tools_json']
 | 
						|
 | 
						|
    def __new__(cls, *args: Any, **kwargs: Any) -> 'GlobalVarsStore':
 | 
						|
        if not cls._instance:
 | 
						|
            cls._instance = super(GlobalVarsStore, cls).__new__(cls, *args, **kwargs)
 | 
						|
            cls._instance._initialize_properties()
 | 
						|
        return cls._instance
 | 
						|
 | 
						|
    def _initialize_properties(self) -> None:
 | 
						|
        # Initialize boolean properties to False
 | 
						|
        for prop in self._bool_properties:
 | 
						|
            setattr(self, f'_{prop}', False)
 | 
						|
        # Initialize string properties to None
 | 
						|
        for prop in self._string_properties:
 | 
						|
            setattr(self, f'_{prop}', None)
 | 
						|
 | 
						|
    def __getattr__(self, name: str) -> Any:
 | 
						|
        if name in self._bool_properties + self._string_properties:
 | 
						|
            value: Union[str, bool] = getattr(self, f'_{name}')
 | 
						|
            if value is None and name in self._string_properties:
 | 
						|
                raise ReferenceError(f'Variable {name} accessed before initialization.')
 | 
						|
            return value
 | 
						|
        raise AttributeError(f'{name} is not a valid attribute')
 | 
						|
 | 
						|
    def __setattr__(self, name: str, value: Any) -> None:
 | 
						|
        if name in self._bool_properties + self._string_properties:
 | 
						|
            super().__setattr__(f'_{name}', value)
 | 
						|
        else:
 | 
						|
            super().__setattr__(name, value)
 | 
						|
 | 
						|
 | 
						|
g = GlobalVarsStore()
 | 
						|
 | 
						|
 | 
						|
def fatal(text: str, *args: str) -> None:
 | 
						|
    """
 | 
						|
    Writes ERROR: + text to sys.stderr.
 | 
						|
    """
 | 
						|
    if not g.quiet:
 | 
						|
        sys.stderr.write(f'ERROR: {text}\n', *args)
 | 
						|
 | 
						|
 | 
						|
def warn(text: str, *args: str) -> None:
 | 
						|
    """
 | 
						|
    Writes WARNING: + text to sys.stderr.
 | 
						|
    """
 | 
						|
    if not g.quiet:
 | 
						|
        sys.stderr.write(f'WARNING: {text}\n', *args)
 | 
						|
 | 
						|
 | 
						|
def info(text: str, f: Optional[IO[str]]=None, *args: str) -> None:
 | 
						|
    """
 | 
						|
    Writes text to a stream specified by second arg, sys.stdout by default.
 | 
						|
    """
 | 
						|
    if not g.quiet:
 | 
						|
        if f is None:
 | 
						|
            f = sys.stdout
 | 
						|
        f.write(f'{text}\n', *args)
 | 
						|
 | 
						|
 | 
						|
def print_hints_on_download_error(err: str) -> None:
 | 
						|
    """
 | 
						|
    Prints hint on download error. Tries to specify the message depending on the error.
 | 
						|
    """
 | 
						|
    info('Please make sure you have a working Internet connection.')
 | 
						|
 | 
						|
    if 'CERTIFICATE' in err:
 | 
						|
        info('Certificate issues are usually caused by an outdated certificate database on your computer.')
 | 
						|
        info('Please check the documentation of your operating system for how to upgrade it.')
 | 
						|
 | 
						|
        if sys.platform == 'darwin':
 | 
						|
            info('Running "./Install\\ Certificates.command" might be able to fix this issue.')
 | 
						|
 | 
						|
        info(f'Running "{sys.executable} -m pip install --upgrade certifi" can also resolve this issue in some cases.')
 | 
						|
 | 
						|
    # Certificate issue on Windows can be hidden under different errors which might be even translated,
 | 
						|
    # e.g. "[WinError -2146881269] ASN1 valor de tag inválido encontrado"
 | 
						|
    if sys.platform == 'win32':
 | 
						|
        info('By downloading and using the offline installer from https://dl.espressif.com/dl/esp-idf '
 | 
						|
             'you might be able to work around this issue.')
 | 
						|
 | 
						|
 | 
						|
PYTHON_PLATFORM = f'{platform.system()}-{platform.machine()}'
 | 
						|
 | 
						|
# Identifiers used in tools.json for different platforms.
 | 
						|
PLATFORM_WIN32 = 'win32'
 | 
						|
PLATFORM_WIN64 = 'win64'
 | 
						|
PLATFORM_MACOS = 'macos'
 | 
						|
PLATFORM_MACOS_ARM64 = 'macos-arm64'
 | 
						|
PLATFORM_LINUX32 = 'linux-i686'
 | 
						|
PLATFORM_LINUX64 = 'linux-amd64'
 | 
						|
PLATFORM_LINUX_ARM32 = 'linux-armel'
 | 
						|
PLATFORM_LINUX_ARMHF = 'linux-armhf'
 | 
						|
PLATFORM_LINUX_ARM64 = 'linux-arm64'
 | 
						|
 | 
						|
 | 
						|
class Platforms:
 | 
						|
    """
 | 
						|
    Mappings from various other names these platforms are known as, to the identifiers above.
 | 
						|
    This includes strings produced from "platform.system() + '-' + platform.machine()", see PYTHON_PLATFORM
 | 
						|
    definition above.
 | 
						|
    """
 | 
						|
    # Mappings from various other names these platforms are known as, to the identifiers above.
 | 
						|
    # This includes strings produced from "platform.system() + '-' + platform.machine()", see PYTHON_PLATFORM
 | 
						|
    # definition above.
 | 
						|
    # This list also includes various strings used in release archives of xtensa-esp32-elf-gcc, OpenOCD, etc.
 | 
						|
    PLATFORM_FROM_NAME = {
 | 
						|
        # Windows
 | 
						|
        PLATFORM_WIN32: PLATFORM_WIN32,
 | 
						|
        'Windows-i686': PLATFORM_WIN32,
 | 
						|
        'Windows-x86': PLATFORM_WIN32,
 | 
						|
        'i686-w64-mingw32': PLATFORM_WIN32,
 | 
						|
        PLATFORM_WIN64: PLATFORM_WIN64,
 | 
						|
        'Windows-x86_64': PLATFORM_WIN64,
 | 
						|
        'Windows-AMD64': PLATFORM_WIN64,
 | 
						|
        'x86_64-w64-mingw32': PLATFORM_WIN64,
 | 
						|
        'Windows-ARM64': PLATFORM_WIN64,
 | 
						|
        # macOS
 | 
						|
        PLATFORM_MACOS: PLATFORM_MACOS,
 | 
						|
        'osx': PLATFORM_MACOS,
 | 
						|
        'darwin': PLATFORM_MACOS,
 | 
						|
        'Darwin-x86_64': PLATFORM_MACOS,
 | 
						|
        'x86_64-apple-darwin': PLATFORM_MACOS,
 | 
						|
        PLATFORM_MACOS_ARM64: PLATFORM_MACOS_ARM64,
 | 
						|
        'Darwin-arm64': PLATFORM_MACOS_ARM64,
 | 
						|
        'aarch64-apple-darwin': PLATFORM_MACOS_ARM64,
 | 
						|
        'arm64-apple-darwin': PLATFORM_MACOS_ARM64,
 | 
						|
        # Linux
 | 
						|
        PLATFORM_LINUX64: PLATFORM_LINUX64,
 | 
						|
        'linux64': PLATFORM_LINUX64,
 | 
						|
        'Linux-x86_64': PLATFORM_LINUX64,
 | 
						|
        'FreeBSD-amd64': PLATFORM_LINUX64,
 | 
						|
        'x86_64-linux-gnu': PLATFORM_LINUX64,
 | 
						|
        PLATFORM_LINUX32: PLATFORM_LINUX32,
 | 
						|
        'linux32': PLATFORM_LINUX32,
 | 
						|
        'Linux-i686': PLATFORM_LINUX32,
 | 
						|
        'FreeBSD-i386': PLATFORM_LINUX32,
 | 
						|
        'i586-linux-gnu': PLATFORM_LINUX32,
 | 
						|
        'i686-linux-gnu': PLATFORM_LINUX32,
 | 
						|
        PLATFORM_LINUX_ARM64: PLATFORM_LINUX_ARM64,
 | 
						|
        'Linux-arm64': PLATFORM_LINUX_ARM64,
 | 
						|
        'Linux-aarch64': PLATFORM_LINUX_ARM64,
 | 
						|
        'Linux-armv8l': PLATFORM_LINUX_ARM64,
 | 
						|
        'aarch64': PLATFORM_LINUX_ARM64,
 | 
						|
        PLATFORM_LINUX_ARMHF: PLATFORM_LINUX_ARMHF,
 | 
						|
        'arm-linux-gnueabihf': PLATFORM_LINUX_ARMHF,
 | 
						|
        PLATFORM_LINUX_ARM32: PLATFORM_LINUX_ARM32,
 | 
						|
        'arm-linux-gnueabi': PLATFORM_LINUX_ARM32,
 | 
						|
        'Linux-armv7l': PLATFORM_LINUX_ARM32,
 | 
						|
        'Linux-arm': PLATFORM_LINUX_ARM32,
 | 
						|
    }
 | 
						|
 | 
						|
    # List of platforms that are not supported by ESP-IDF
 | 
						|
    UNSUPPORTED_PLATFORMS = [
 | 
						|
        'Linux-armv6l'
 | 
						|
    ]
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def detect_linux_arm_platform(supposed_platform: Optional[str]) -> Optional[str]:
 | 
						|
        """
 | 
						|
        We probe the python binary to check exactly what environment the script is running in.
 | 
						|
 | 
						|
        ARM platform may run on armhf hardware but having armel installed packages.
 | 
						|
        To avoid possible armel/armhf libraries mixing need to define user's
 | 
						|
        packages architecture to use the same
 | 
						|
        See note section in https://gcc.gnu.org/onlinedocs/gcc/ARM-Options.html#index-mfloat-abi
 | 
						|
 | 
						|
        ARM platform may run on aarch64 hardware but having armhf installed packages
 | 
						|
        (it happens if a docker container is running on arm64 hardware, but using an armhf image).
 | 
						|
        """
 | 
						|
        if supposed_platform not in (PLATFORM_LINUX_ARM32, PLATFORM_LINUX_ARMHF, PLATFORM_LINUX_ARM64):
 | 
						|
            return supposed_platform
 | 
						|
 | 
						|
        # suppose that installed python was built with the right ABI
 | 
						|
        with open(sys.executable, 'rb') as f:
 | 
						|
            # see ELF header description in https://man7.org/linux/man-pages/man5/elf.5.html, offsets depend on ElfN size
 | 
						|
            if int.from_bytes(f.read(4), sys.byteorder) != int.from_bytes(b'\x7fELF', sys.byteorder):
 | 
						|
                return supposed_platform  # ELF magic not found. Use the default platform name from PLATFORM_FROM_NAME
 | 
						|
            f.seek(18)  # seek to e_machine
 | 
						|
            e_machine = int.from_bytes(f.read(2), sys.byteorder)
 | 
						|
            if e_machine == 183:  # EM_AARCH64, https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst
 | 
						|
                supposed_platform = PLATFORM_LINUX_ARM64
 | 
						|
            elif e_machine == 40:  # EM_ARM, https://github.com/ARM-software/abi-aa/blob/main/aaelf32/aaelf32.rst
 | 
						|
                f.seek(36)  # seek to e_flags
 | 
						|
                e_flags = int.from_bytes(f.read(4), sys.byteorder)
 | 
						|
                if e_flags & 0x400:
 | 
						|
                    supposed_platform = PLATFORM_LINUX_ARMHF
 | 
						|
                else:
 | 
						|
                    supposed_platform = PLATFORM_LINUX_ARM32
 | 
						|
 | 
						|
        return supposed_platform
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get(platform_alias: Optional[str]) -> str:
 | 
						|
        """
 | 
						|
        Get a proper platform name based on PLATFORM_FROM_NAME dict.
 | 
						|
        """
 | 
						|
        if not platform_alias:
 | 
						|
            raise ValueError('System platform could not be identified.')
 | 
						|
 | 
						|
        if platform_alias in Platforms.UNSUPPORTED_PLATFORMS:
 | 
						|
            raise ValueError(f'Platform \'{platform_alias}\' is not supported by ESP-IDF.')
 | 
						|
 | 
						|
        if platform_alias == 'any' and CURRENT_PLATFORM:
 | 
						|
            platform_alias = CURRENT_PLATFORM
 | 
						|
        platform_name = Platforms.PLATFORM_FROM_NAME.get(platform_alias, None)
 | 
						|
        if sys.platform == 'linux':
 | 
						|
            platform_name = Platforms.detect_linux_arm_platform(platform_name)
 | 
						|
 | 
						|
        if not platform_name:
 | 
						|
            raise ValueError(f'Support for platform \'{platform_alias}\' hasn\'t been added yet.')
 | 
						|
 | 
						|
        return platform_name
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_by_filename(file_name: str) -> str:
 | 
						|
        """
 | 
						|
        Guess the right platform based on the file name.
 | 
						|
        """
 | 
						|
        found_alias = ''
 | 
						|
        for platform_alias in Platforms.PLATFORM_FROM_NAME:
 | 
						|
            # Find the longest alias which matches with file name to avoid mismatching
 | 
						|
            if platform_alias in file_name and len(found_alias) < len(platform_alias):
 | 
						|
                found_alias = platform_alias
 | 
						|
        return Platforms.get(found_alias)
 | 
						|
 | 
						|
 | 
						|
def parse_platform_arg(platform_str: str) -> str:
 | 
						|
    """
 | 
						|
    Parses platform from input string and checks whether it is a valid platform.
 | 
						|
    If not, raises SystemExit exception with error message.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        platform = Platforms.get(platform_str)
 | 
						|
    except ValueError as e:
 | 
						|
        fatal(str(e))
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    return platform
 | 
						|
 | 
						|
 | 
						|
CURRENT_PLATFORM = parse_platform_arg(PYTHON_PLATFORM)
 | 
						|
 | 
						|
 | 
						|
EXPORT_SHELL = 'shell'
 | 
						|
EXPORT_KEY_VALUE = 'key-value'
 | 
						|
 | 
						|
# the older "DigiCert Global Root CA" certificate used with github.com
 | 
						|
DIGICERT_ROOT_CA_CERT = """
 | 
						|
-----BEGIN CERTIFICATE-----
 | 
						|
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
 | 
						|
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
 | 
						|
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
 | 
						|
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
 | 
						|
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
 | 
						|
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
 | 
						|
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
 | 
						|
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
 | 
						|
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
 | 
						|
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
 | 
						|
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
 | 
						|
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
 | 
						|
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
 | 
						|
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
 | 
						|
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
 | 
						|
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
 | 
						|
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
 | 
						|
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
 | 
						|
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
 | 
						|
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
 | 
						|
-----END CERTIFICATE-----
 | 
						|
"""
 | 
						|
 | 
						|
# the newer "DigiCert Global Root G2" certificate used with dl.espressif.com
 | 
						|
DIGICERT_ROOT_G2_CERT = """
 | 
						|
-----BEGIN CERTIFICATE-----
 | 
						|
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
 | 
						|
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
 | 
						|
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
 | 
						|
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
 | 
						|
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
 | 
						|
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
 | 
						|
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
 | 
						|
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
 | 
						|
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
 | 
						|
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
 | 
						|
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
 | 
						|
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
 | 
						|
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
 | 
						|
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
 | 
						|
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
 | 
						|
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
 | 
						|
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
 | 
						|
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
 | 
						|
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
 | 
						|
MrY=
 | 
						|
-----END CERTIFICATE-----
 | 
						|
"""
 | 
						|
 | 
						|
DL_CERT_DICT = {'dl.espressif.com': DIGICERT_ROOT_G2_CERT,
 | 
						|
                'github.com': DIGICERT_ROOT_CA_CERT}
 | 
						|
 | 
						|
 | 
						|
def run_cmd_check_output(cmd: List[str], input_text: Optional[str]=None, extra_paths: Optional[List[str]]=None) -> bytes:
 | 
						|
    """
 | 
						|
    Runs command and checks output for exceptions. If AttributeError or TypeError occurs, function re-runs the process.
 | 
						|
    If return code was not 0, subprocess.CalledProcessError is raised, otherwise, the original error is masked.
 | 
						|
    Returns both stdout and stderr of the run command.
 | 
						|
    """
 | 
						|
    # If extra_paths is given, locate the executable in one of these directories.
 | 
						|
    # Note: it would seem logical to add extra_paths to env[PATH], instead, and let OS do the job of finding the
 | 
						|
    # executable for us. However this does not work on Windows: https://bugs.python.org/issue8557.
 | 
						|
    if extra_paths:
 | 
						|
        found = False
 | 
						|
        extensions = ['']
 | 
						|
        if sys.platform == 'win32':
 | 
						|
            extensions.append('.exe')
 | 
						|
        for path in extra_paths:
 | 
						|
            for ext in extensions:
 | 
						|
                fullpath = os.path.join(path, cmd[0] + ext)
 | 
						|
                if os.path.exists(fullpath):
 | 
						|
                    cmd[0] = fullpath
 | 
						|
                    found = True
 | 
						|
                    break
 | 
						|
            if found:
 | 
						|
                break
 | 
						|
 | 
						|
    try:
 | 
						|
        input_bytes = None
 | 
						|
        if input_text:
 | 
						|
            input_bytes = input_text.encode()
 | 
						|
        result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, input=input_bytes)
 | 
						|
        return result.stdout + result.stderr
 | 
						|
    except (AttributeError, TypeError):
 | 
						|
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
 | 
						|
        stdout, stderr = p.communicate(input_bytes)
 | 
						|
        if p.returncode != 0:
 | 
						|
            try:
 | 
						|
                raise subprocess.CalledProcessError(p.returncode, cmd, stdout, stderr)
 | 
						|
            except TypeError:
 | 
						|
                raise subprocess.CalledProcessError(p.returncode, cmd, stdout)
 | 
						|
        return stdout + stderr
 | 
						|
 | 
						|
 | 
						|
def to_shell_specific_paths(paths_list: List[str]) -> List[str]:
 | 
						|
    """
 | 
						|
    Converts / (linux)  to \\ (Windows) if called under win32 platform.
 | 
						|
    """
 | 
						|
    if sys.platform == 'win32':
 | 
						|
        paths_list = [p.replace('/', os.path.sep) if os.path.sep in p else p for p in paths_list]
 | 
						|
 | 
						|
    return paths_list
 | 
						|
 | 
						|
 | 
						|
def get_env_for_extra_paths(extra_paths: List[str]) -> Dict[str, str]:
 | 
						|
    """
 | 
						|
    Return a copy of environment variables dict, prepending paths listed in extra_paths
 | 
						|
    to the PATH environment variable.
 | 
						|
    """
 | 
						|
    env_arg = os.environ.copy()
 | 
						|
    new_path = os.pathsep.join(extra_paths) + os.pathsep + env_arg['PATH']
 | 
						|
    if sys.version_info.major == 2:
 | 
						|
        env_arg['PATH'] = new_path.encode('utf8')  # type: ignore
 | 
						|
    else:
 | 
						|
        env_arg['PATH'] = new_path
 | 
						|
    return env_arg
 | 
						|
 | 
						|
 | 
						|
def get_file_size_sha256(filename: str, block_size: int=65536) -> Tuple[int, str]:
 | 
						|
    """
 | 
						|
    Gets file size and its sha256.
 | 
						|
    """
 | 
						|
    sha256 = hashlib.sha256()
 | 
						|
    size = 0
 | 
						|
    with open(filename, 'rb') as f:
 | 
						|
        for block in iter(lambda: f.read(block_size), b''):
 | 
						|
            sha256.update(block)
 | 
						|
            size += len(block)
 | 
						|
    return size, sha256.hexdigest()
 | 
						|
 | 
						|
 | 
						|
def report_progress(count: int, block_size: int, total_size: int) -> None:
 | 
						|
    """
 | 
						|
    Prints progress (count * block_size * 100 / total_size) to stdout.
 | 
						|
    """
 | 
						|
    percent = int(count * block_size * 100 / total_size)
 | 
						|
    percent = min(100, percent)
 | 
						|
    sys.stdout.write('\r%d%%' % percent)
 | 
						|
    sys.stdout.flush()
 | 
						|
 | 
						|
 | 
						|
def mkdir_p(path: str) -> None:
 | 
						|
    """
 | 
						|
    Makes directory in given path.
 | 
						|
    Supresses error when directory is already created or path is a path to file.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        os.makedirs(path)
 | 
						|
    except OSError as exc:
 | 
						|
        if exc.errno != errno.EEXIST or not os.path.isdir(path):
 | 
						|
            raise
 | 
						|
 | 
						|
 | 
						|
def unpack(filename: str, destination: str) -> None:
 | 
						|
    """
 | 
						|
    Extracts file specified by filename into destination depending on its type.
 | 
						|
    """
 | 
						|
    info(f'Extracting {filename} to {destination}')
 | 
						|
    if filename.endswith(('.tar.gz', '.tgz')):
 | 
						|
        archive_obj: Union[TarFile, ZipFile] = tarfile.open(filename, 'r:gz')
 | 
						|
    elif filename.endswith(('.tar.xz')):
 | 
						|
        archive_obj = tarfile.open(filename, 'r:xz')
 | 
						|
    elif filename.endswith(('.tar.bz2')):
 | 
						|
        archive_obj = tarfile.open(filename, 'r:bz2')
 | 
						|
    elif filename.endswith('zip'):
 | 
						|
        archive_obj = ZipFile(filename)
 | 
						|
    else:
 | 
						|
        raise NotImplementedError('Unsupported archive type')
 | 
						|
    if sys.version_info.major == 2:
 | 
						|
        # This is a workaround for the issue that unicode destination is not handled:
 | 
						|
        # https://bugs.python.org/issue17153
 | 
						|
        destination = str(destination)
 | 
						|
    archive_obj.extractall(destination)
 | 
						|
    # ZipFile on Unix systems does not preserve file permissions while extracting it
 | 
						|
    # We need to reset the permissions afterward
 | 
						|
    if sys.platform != 'win32' and filename.endswith('zip') and isinstance(archive_obj, ZipFile):
 | 
						|
        for file_info in archive_obj.infolist():
 | 
						|
            extracted_file = os.path.join(destination, file_info.filename)
 | 
						|
            extracted_permissions = file_info.external_attr >> 16 & 0o777  # Extract Unix permissions
 | 
						|
            if os.path.exists(extracted_file):
 | 
						|
                os.chmod(extracted_file, extracted_permissions)
 | 
						|
 | 
						|
 | 
						|
def splittype(url: str) -> Tuple[Optional[str], str]:
 | 
						|
    """
 | 
						|
    Splits given url into its type (e.g. https, file) and the rest.
 | 
						|
    """
 | 
						|
    match = re.match('([^/:]+):(.*)', url, re.DOTALL)
 | 
						|
    if match:
 | 
						|
        scheme, data = match.groups()
 | 
						|
        return scheme.lower(), data
 | 
						|
    return None, url
 | 
						|
 | 
						|
 | 
						|
def urlretrieve_ctx(url: str,
 | 
						|
                    filename: str,
 | 
						|
                    reporthook: Optional[Callable[[int, int, int], None]]=None,
 | 
						|
                    data: Optional[bytes]=None,
 | 
						|
                    context: Optional[SSLContext]=None) -> Tuple[str, addinfourl]:
 | 
						|
    """
 | 
						|
    Retrieve data from given URL. An alternative version of urlretrieve which takes SSL context as an argument.
 | 
						|
    """
 | 
						|
    url_type, path = splittype(url)
 | 
						|
 | 
						|
    # urlopen doesn't have context argument in Python <=2.7.9
 | 
						|
    extra_urlopen_args = {}
 | 
						|
    if context:
 | 
						|
        extra_urlopen_args['context'] = context
 | 
						|
    with contextlib.closing(urlopen(url, data, **extra_urlopen_args)) as fp:  # type: ignore
 | 
						|
        headers = fp.info()
 | 
						|
 | 
						|
        # Just return the local path and the "headers" for file://
 | 
						|
        # URLs. No sense in performing a copy unless requested.
 | 
						|
        if url_type == 'file' and not filename:
 | 
						|
            return os.path.normpath(path), headers
 | 
						|
 | 
						|
        # Handle temporary file setup.
 | 
						|
        tfp = open(filename, 'wb')
 | 
						|
 | 
						|
        with tfp:
 | 
						|
            result = filename, headers
 | 
						|
            bs = 1024 * 8
 | 
						|
            size = int(headers.get('content-length', -1))
 | 
						|
            read = 0
 | 
						|
            blocknum = 0
 | 
						|
 | 
						|
            if reporthook:
 | 
						|
                reporthook(blocknum, bs, size)
 | 
						|
 | 
						|
            while True:
 | 
						|
                block = fp.read(bs)
 | 
						|
                if not block:
 | 
						|
                    break
 | 
						|
                read += len(block)
 | 
						|
                tfp.write(block)
 | 
						|
                blocknum += 1
 | 
						|
                if reporthook:
 | 
						|
                    reporthook(blocknum, bs, size)
 | 
						|
 | 
						|
    if size >= 0 and read < size:
 | 
						|
        raise ContentTooShortError(
 | 
						|
            'retrieval incomplete: got only %i out of %i bytes'
 | 
						|
            % (read, size), result)
 | 
						|
 | 
						|
    return result
 | 
						|
 | 
						|
 | 
						|
def download(url: str, destination: str) -> Union[None, Exception]:
 | 
						|
    """
 | 
						|
    Download from given url and save into given destiantion.
 | 
						|
    """
 | 
						|
    info(f'Downloading {url}')
 | 
						|
    info(f'Destination: {destination}')
 | 
						|
    try:
 | 
						|
        for site, cert in DL_CERT_DICT.items():
 | 
						|
            # For dl.espressif.com and github.com, add the DigiCert root certificate.
 | 
						|
            # This works around the issue with outdated certificate stores in some installations.
 | 
						|
            if site in url:
 | 
						|
                ctx = ssl.create_default_context()
 | 
						|
                ctx.load_verify_locations(cadata=cert)
 | 
						|
                break
 | 
						|
        else:
 | 
						|
            ctx = None
 | 
						|
 | 
						|
        urlretrieve_ctx(url, destination, report_progress if not g.non_interactive else None, context=ctx)
 | 
						|
        sys.stdout.write('\rDone\n')
 | 
						|
        return None
 | 
						|
    except Exception as e:
 | 
						|
        # urlretrieve could throw different exceptions, e.g. IOError when the server is down
 | 
						|
        return e
 | 
						|
    finally:
 | 
						|
        sys.stdout.flush()
 | 
						|
 | 
						|
 | 
						|
def rename_with_retry(path_from: str, path_to: str) -> None:
 | 
						|
    """
 | 
						|
    Sometimes renaming a directory on Windows (randomly?) causes a PermissionError.
 | 
						|
    This is confirmed to be a workaround:
 | 
						|
    https://github.com/espressif/esp-idf/issues/3819#issuecomment-515167118
 | 
						|
    https://github.com/espressif/esp-idf/issues/4063#issuecomment-531490140
 | 
						|
    https://stackoverflow.com/a/43046729
 | 
						|
    """
 | 
						|
    retry_count = 20 if sys.platform.startswith('win') else 1
 | 
						|
    for retry in range(retry_count):
 | 
						|
        try:
 | 
						|
            os.rename(path_from, path_to)
 | 
						|
            return
 | 
						|
        except OSError:
 | 
						|
            msg = f'Rename {path_from} to {path_to} failed'
 | 
						|
            if retry == retry_count - 1:
 | 
						|
                fatal(f'{msg}. Antivirus software might be causing this. Disabling it temporarily could solve the issue.')
 | 
						|
                raise
 | 
						|
            warn(f'{msg}, retrying...')
 | 
						|
            # Sleep before the next try in order to pass the antivirus check on Windows
 | 
						|
            time.sleep(0.5)
 | 
						|
 | 
						|
 | 
						|
def do_strip_container_dirs(path: str, levels: int) -> None:
 | 
						|
    """
 | 
						|
    The number of top directory levels specified by levels argument will be removed when extracting.
 | 
						|
    E.g. if levels=2, archive path a/b/c/d.txt will be extracted as c/d.txt.
 | 
						|
    """
 | 
						|
    assert levels > 0
 | 
						|
    # move the original directory out of the way (add a .tmp suffix)
 | 
						|
    tmp_path = f'{path}.tmp'
 | 
						|
    if os.path.exists(tmp_path):
 | 
						|
        shutil.rmtree(tmp_path)
 | 
						|
    rename_with_retry(path, tmp_path)
 | 
						|
    os.mkdir(path)
 | 
						|
    base_path = tmp_path
 | 
						|
    # walk given number of levels down
 | 
						|
    for level in range(levels):
 | 
						|
        contents = os.listdir(base_path)
 | 
						|
        if len(contents) > 1:
 | 
						|
            raise RuntimeError(f'at level {level}, expected 1 entry, got {contents}')
 | 
						|
        base_path = os.path.join(base_path, contents[0])
 | 
						|
        if not os.path.isdir(base_path):
 | 
						|
            raise RuntimeError(f'at level {level}, {contents[0]} is not a directory')
 | 
						|
    # get the list of directories/files to move
 | 
						|
    contents = os.listdir(base_path)
 | 
						|
    for name in contents:
 | 
						|
        move_from = os.path.join(base_path, name)
 | 
						|
        move_to = os.path.join(path, name)
 | 
						|
        rename_with_retry(move_from, move_to)
 | 
						|
    shutil.rmtree(tmp_path)
 | 
						|
 | 
						|
 | 
						|
class ToolNotFoundError(RuntimeError):
 | 
						|
    """
 | 
						|
    Raise when the tool is not found (not present in the paths etc.).
 | 
						|
    """
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class ToolExecError(RuntimeError):
 | 
						|
    """
 | 
						|
    Raise when  the tool returns with a non-zero exit code.
 | 
						|
    """
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class ToolBinaryError(RuntimeError):
 | 
						|
    """"
 | 
						|
    Raise when an error occurred when running any version of the tool.
 | 
						|
    """
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class IDFToolDownload(object):
 | 
						|
    """
 | 
						|
    Structure to store all the relevant information about particular download.
 | 
						|
    """
 | 
						|
    def __init__(self, platform_name: str, url: str, size: int, sha256: str, rename_dist: str) -> None:
 | 
						|
        self.platform_name = platform_name
 | 
						|
        self.url = url
 | 
						|
        self.size = size
 | 
						|
        self.sha256 = sha256
 | 
						|
        self.rename_dist = rename_dist
 | 
						|
 | 
						|
 | 
						|
@functools.total_ordering
 | 
						|
class IDFToolVersion(object):
 | 
						|
    """
 | 
						|
    Used for storing information about version; status (recommended, supported, deprecated)
 | 
						|
    and easy way of comparing different versions. Also allows platform compatibility check
 | 
						|
    and getting right download for given platform, if available.
 | 
						|
    """
 | 
						|
    STATUS_RECOMMENDED = 'recommended'
 | 
						|
    STATUS_SUPPORTED = 'supported'
 | 
						|
    STATUS_DEPRECATED = 'deprecated'
 | 
						|
 | 
						|
    STATUS_VALUES = [STATUS_RECOMMENDED, STATUS_SUPPORTED, STATUS_DEPRECATED]
 | 
						|
 | 
						|
    def __init__(self, version: str, status: str) -> None:
 | 
						|
        self.version = version
 | 
						|
        self.status = status
 | 
						|
        self.downloads: OrderedDict[str, IDFToolDownload] = OrderedDict()
 | 
						|
        self.latest = False
 | 
						|
 | 
						|
    def __lt__(self, other: 'IDFToolVersion') -> bool:
 | 
						|
        if self.status != other.status:
 | 
						|
            return self.status > other.status
 | 
						|
        else:
 | 
						|
            assert not (self.status == IDFToolVersion.STATUS_RECOMMENDED
 | 
						|
                        and other.status == IDFToolVersion.STATUS_RECOMMENDED)
 | 
						|
            return self.version < other.version
 | 
						|
 | 
						|
    def __eq__(self, other: object) -> bool:
 | 
						|
        if not isinstance(other, IDFToolVersion):
 | 
						|
            return NotImplemented
 | 
						|
        return self.status == other.status and self.version == other.version
 | 
						|
 | 
						|
    def add_download(self, platform_name: str, url: str, size: int, sha256: str, rename_dist: str = '') -> None:
 | 
						|
        """
 | 
						|
        Add download entry of type IDFToolDownload into self.downloads.
 | 
						|
        """
 | 
						|
        self.downloads[platform_name] = IDFToolDownload(platform_name, url, size, sha256, rename_dist)
 | 
						|
 | 
						|
    def get_download_for_platform(self, platform_name: Optional[str]) -> Optional[IDFToolDownload]:
 | 
						|
        """
 | 
						|
        Get download for given platform if usable download already exists.
 | 
						|
        """
 | 
						|
        try:
 | 
						|
            platform_name = Platforms.get(platform_name)
 | 
						|
            if platform_name in self.downloads.keys():
 | 
						|
                return self.downloads[platform_name]
 | 
						|
        # exception can be ommited, as not detected platform is handled without err message
 | 
						|
        except ValueError:
 | 
						|
            pass
 | 
						|
        if 'any' in self.downloads.keys():
 | 
						|
            return self.downloads['any']
 | 
						|
        return None
 | 
						|
 | 
						|
    def compatible_with_platform(self, platform_name: Optional[str] = PYTHON_PLATFORM) -> bool:
 | 
						|
        """
 | 
						|
        Check whether this version is compatible with given platform name.
 | 
						|
        """
 | 
						|
        return self.get_download_for_platform(platform_name) is not None
 | 
						|
 | 
						|
    def get_supported_platforms(self) -> Set[str]:
 | 
						|
        """
 | 
						|
        Get all platforms for which this version has a valid download record.
 | 
						|
        """
 | 
						|
        return set(self.downloads.keys())
 | 
						|
 | 
						|
 | 
						|
IDFToolOptions = namedtuple('IDFToolOptions', [
 | 
						|
    'version_cmd',
 | 
						|
    'version_regex',
 | 
						|
    'version_regex_replace',
 | 
						|
    'is_executable',
 | 
						|
    'export_paths',
 | 
						|
    'export_vars',
 | 
						|
    'install',
 | 
						|
    'info_url',
 | 
						|
    'license',
 | 
						|
    'strip_container_dirs',
 | 
						|
    'supported_targets'])
 | 
						|
 | 
						|
 | 
						|
class IDFTool(object):
 | 
						|
    """
 | 
						|
    Used to store info about IDF tools from tools.json file in a Python-accesible form.
 | 
						|
    The term "IDF tool" is used for e.g. CMake, ninja, QUEMU and toolchains.
 | 
						|
    """
 | 
						|
    # possible values of 'install' field
 | 
						|
    INSTALL_ALWAYS = 'always'
 | 
						|
    INSTALL_ON_REQUEST = 'on_request'
 | 
						|
    INSTALL_NEVER = 'never'
 | 
						|
 | 
						|
    def __init__(self, name: str,
 | 
						|
                 description: str,
 | 
						|
                 install: str,
 | 
						|
                 info_url: str,
 | 
						|
                 license: str,
 | 
						|
                 version_cmd: List[str],
 | 
						|
                 version_regex: str,
 | 
						|
                 supported_targets: List[str],
 | 
						|
                 version_regex_replace: Optional[str] = None,
 | 
						|
                 strip_container_dirs: int = 0,
 | 
						|
                 is_executable: bool = True) -> None:
 | 
						|
        self.name = name
 | 
						|
        self.description = description
 | 
						|
        self.drop_versions()
 | 
						|
        self.version_in_path: Optional[str] = None
 | 
						|
        self.versions_installed: List[str] = []
 | 
						|
        if version_regex_replace is None:
 | 
						|
            version_regex_replace = VERSION_REGEX_REPLACE_DEFAULT
 | 
						|
        self.options = IDFToolOptions(version_cmd, version_regex, version_regex_replace, is_executable,
 | 
						|
                                      [], OrderedDict(), install, info_url, license, strip_container_dirs, supported_targets)  # type: ignore
 | 
						|
        self.platform_overrides: List[Dict[str, str]] = []
 | 
						|
        self._platform = CURRENT_PLATFORM
 | 
						|
        self._update_current_options()
 | 
						|
        self.is_executable = is_executable
 | 
						|
 | 
						|
    def copy_for_platform(self, platform: str) -> 'IDFTool':
 | 
						|
        """
 | 
						|
        Copy the IDFTool record in respect to given platform (e.g. apply platform overrides).
 | 
						|
        """
 | 
						|
        result = copy.deepcopy(self)
 | 
						|
        result._platform = platform
 | 
						|
        result._update_current_options()
 | 
						|
        return result
 | 
						|
 | 
						|
    def _update_current_options(self) -> None:
 | 
						|
        """
 | 
						|
        Update current options by platform overrides, if applicable for current platform.
 | 
						|
        """
 | 
						|
        self._current_options = IDFToolOptions(*self.options)
 | 
						|
        for override in self.platform_overrides:
 | 
						|
            if self._platform and self._platform not in override['platforms']:
 | 
						|
                continue
 | 
						|
            override_dict = override.copy()
 | 
						|
            del override_dict['platforms']
 | 
						|
            self._current_options = self._current_options._replace(**override_dict)  # type: ignore
 | 
						|
 | 
						|
    def drop_versions(self) -> None:
 | 
						|
        """
 | 
						|
        Clear self.versions dictionary.
 | 
						|
        """
 | 
						|
        self.versions: Dict[str, IDFToolVersion] = OrderedDict()
 | 
						|
 | 
						|
    def add_version(self, version: IDFToolVersion) -> None:
 | 
						|
        """
 | 
						|
        Add new IDFVersion to self.versions.
 | 
						|
        """
 | 
						|
        assert type(version) is IDFToolVersion
 | 
						|
        self.versions[version.version] = version
 | 
						|
 | 
						|
    def get_path(self) -> str:
 | 
						|
        """
 | 
						|
        Returns path where the tool is installed.
 | 
						|
        """
 | 
						|
        return os.path.join(g.idf_tools_path, 'tools', self.name)
 | 
						|
 | 
						|
    def get_path_for_version(self, version: str) -> str:
 | 
						|
        """
 | 
						|
        Returns path for the tool of given version.
 | 
						|
        """
 | 
						|
        assert version in self.versions
 | 
						|
        return os.path.join(self.get_path(), version)
 | 
						|
 | 
						|
    def get_export_paths(self, version: str) -> List[str]:
 | 
						|
        """
 | 
						|
        Returns a list of paths that need to be exported.
 | 
						|
        """
 | 
						|
        tool_path = self.get_path_for_version(version)
 | 
						|
        return [os.path.join(tool_path, *p) for p in self._current_options.export_paths]  # type: ignore
 | 
						|
 | 
						|
    def get_export_vars(self, version: str) -> Dict[str, str]:
 | 
						|
        """
 | 
						|
        Get the dictionary of environment variables to be exported, for the given version.
 | 
						|
        Expands:
 | 
						|
        - ${TOOL_PATH} => the actual path where the version is installed.
 | 
						|
        """
 | 
						|
        result = {}
 | 
						|
        for k, v in self._current_options.export_vars.items():  # type: ignore
 | 
						|
            replace_path = self.get_path_for_version(version).replace('\\', '\\\\')
 | 
						|
            v_repl = re.sub(SUBST_TOOL_PATH_REGEX, replace_path, v)
 | 
						|
            if v_repl != v:
 | 
						|
                v_repl = to_shell_specific_paths([v_repl])[0]
 | 
						|
            old_v = os.environ.get(k)
 | 
						|
            if old_v is None or old_v != v_repl:
 | 
						|
                result[k] = v_repl
 | 
						|
        return result
 | 
						|
 | 
						|
    def get_version(self, extra_paths: Optional[List[str]] = None, executable_path: Optional[str] = None) -> str:
 | 
						|
        """
 | 
						|
        Execute the tool, optionally prepending extra_paths to PATH,
 | 
						|
        extract the version string and return it as a result.
 | 
						|
        Raises ToolNotFoundError if the tool is not found (not present in the paths).
 | 
						|
        Raises ToolExecError if the tool returns with a non-zero exit code.
 | 
						|
        Returns 'unknown' if tool returns something from which version string
 | 
						|
        can not be extracted.
 | 
						|
        """
 | 
						|
        # this function can not be called for a different platform
 | 
						|
        assert self._platform == CURRENT_PLATFORM
 | 
						|
        cmd = self._current_options.version_cmd  # type: ignore
 | 
						|
        if executable_path:
 | 
						|
            cmd[0] = executable_path
 | 
						|
 | 
						|
        if not cmd[0]:
 | 
						|
            # There is no command available, so return early. It seems that
 | 
						|
            # within some very strange context empty [''] may actually execute
 | 
						|
            # something https://github.com/espressif/esp-idf/issues/11880
 | 
						|
            raise ToolNotFoundError(f'Tool {self.name} not found')
 | 
						|
 | 
						|
        try:
 | 
						|
            version_cmd_result = run_cmd_check_output(cmd, None, extra_paths)
 | 
						|
        except OSError:
 | 
						|
            # tool is not on the path
 | 
						|
            raise ToolNotFoundError(f'Tool {self.name} not found')
 | 
						|
        except subprocess.CalledProcessError as e:
 | 
						|
            raise ToolExecError(f'returned non-zero exit code ({e.returncode}) with error message:\n{e.stderr.decode("utf-8",errors="ignore")}')  # type: ignore
 | 
						|
 | 
						|
        in_str = version_cmd_result.decode('utf-8')
 | 
						|
        match = re.search(self._current_options.version_regex, in_str)  # type: ignore
 | 
						|
        if not match:
 | 
						|
            return UNKNOWN_VERSION
 | 
						|
        return re.sub(self._current_options.version_regex, self._current_options.version_regex_replace, match.group(0))  # type: ignore
 | 
						|
 | 
						|
    def check_version(self, executable_path: Optional[str]) -> bool:
 | 
						|
        """
 | 
						|
        Check if tool's version from executable path is in self.version dictionary.
 | 
						|
        """
 | 
						|
        version = self.get_version(executable_path=executable_path)
 | 
						|
        return version in self.versions
 | 
						|
 | 
						|
    def get_install_type(self) -> Callable[[str], None]:
 | 
						|
        """
 | 
						|
        Returns whether the tools are installed always, on request or never.
 | 
						|
        """
 | 
						|
        return self._current_options.install  # type: ignore
 | 
						|
 | 
						|
    def get_supported_targets(self) -> List[str]:
 | 
						|
        """
 | 
						|
        Returns list of supported targets with current options.
 | 
						|
        """
 | 
						|
        return self._current_options.supported_targets  # type: ignore
 | 
						|
 | 
						|
    def is_supported_for_any_of_targets(self, targets: List[str]) -> bool:
 | 
						|
        """
 | 
						|
        Checks whether the tool is suitable for at least one of the specified targets.
 | 
						|
        """
 | 
						|
        supported_targets = self.get_supported_targets()
 | 
						|
        return (any(item in targets for item in supported_targets) or supported_targets == ['all'])
 | 
						|
 | 
						|
    def compatible_with_platform(self) -> bool:
 | 
						|
        """
 | 
						|
        Checks whether this tool (any version) is compatible  with the platform.
 | 
						|
        """
 | 
						|
        return any([v.compatible_with_platform() for v in self.versions.values()])
 | 
						|
 | 
						|
    def get_supported_platforms(self) -> Set[str]:
 | 
						|
        """
 | 
						|
        Return set of platforms that are supported by at least one version of the tool.
 | 
						|
        """
 | 
						|
        result = set()
 | 
						|
        for v in self.versions.values():
 | 
						|
            result.update(v.get_supported_platforms())
 | 
						|
        return result
 | 
						|
 | 
						|
    def get_recommended_version(self) -> Optional[str]:
 | 
						|
        """
 | 
						|
        Get all reccomended versions of the tool. If more versions are recommended, highest version is returned.
 | 
						|
        """
 | 
						|
        recommended_versions = [k for k, v in self.versions.items()
 | 
						|
                                if v.status == IDFToolVersion.STATUS_RECOMMENDED
 | 
						|
                                and v.compatible_with_platform(self._platform)]
 | 
						|
        assert len(recommended_versions) <= 1
 | 
						|
        if recommended_versions:
 | 
						|
            return recommended_versions[0]
 | 
						|
        return None
 | 
						|
 | 
						|
    def get_preferred_installed_version(self) -> Optional[str]:
 | 
						|
        """
 | 
						|
        Get the preferred installed version of the tool. If more versions installed, return the highest.
 | 
						|
        """
 | 
						|
        recommended_versions = [k for k in self.versions_installed
 | 
						|
                                if self.versions[k].status == IDFToolVersion.STATUS_RECOMMENDED
 | 
						|
                                and self.versions[k].compatible_with_platform(self._platform)]
 | 
						|
        assert len(recommended_versions) <= 1
 | 
						|
        if recommended_versions:
 | 
						|
            return recommended_versions[0]
 | 
						|
        return None
 | 
						|
 | 
						|
    def find_installed_versions(self) -> None:
 | 
						|
        """
 | 
						|
        Checks whether the tool can be found in PATH and in GlobalVarsStore.idf_tools_path.
 | 
						|
        Writes results to self.version_in_path and self.versions_installed.
 | 
						|
 | 
						|
        Raises ToolBinaryError if an error occurred when running any version of the tool.
 | 
						|
        """
 | 
						|
        # this function can not be called for a different platform
 | 
						|
        assert self._platform == CURRENT_PLATFORM
 | 
						|
        tool_error = False
 | 
						|
        # First check if the tool is in system PATH
 | 
						|
        try:
 | 
						|
            ver_str = self.get_version()
 | 
						|
        except ToolNotFoundError:
 | 
						|
            # not in PATH
 | 
						|
            pass
 | 
						|
        except ToolExecError as e:
 | 
						|
            fatal(f'tool {self.name} found in path, but {e}')
 | 
						|
            tool_error = True
 | 
						|
        else:
 | 
						|
            self.version_in_path = ver_str
 | 
						|
 | 
						|
        # Now check all the versions installed in GlobalVarsStore.idf_tools_path
 | 
						|
        self.versions_installed = []
 | 
						|
        for version, version_obj in self.versions.items():
 | 
						|
            if not version_obj.compatible_with_platform():
 | 
						|
                continue
 | 
						|
            tool_path = self.get_path_for_version(version)
 | 
						|
            if not os.path.exists(tool_path):
 | 
						|
                # version not installed
 | 
						|
                continue
 | 
						|
            if not self.is_executable:
 | 
						|
                self.versions_installed.append(version)
 | 
						|
                continue
 | 
						|
            try:
 | 
						|
                ver_str = self.get_version(self.get_export_paths(version))
 | 
						|
            except ToolNotFoundError:
 | 
						|
                warn(f'directory for tool {self.name} version {version} is present, but tool was not found')
 | 
						|
            except ToolExecError as e:
 | 
						|
                fatal(f'tool {self.name} version {version} is installed, but {e}')
 | 
						|
                tool_error = True
 | 
						|
            else:
 | 
						|
                if ver_str != version:
 | 
						|
                    warn(f'tool {self.name} version {version} is installed, but has reported version {ver_str}')
 | 
						|
                else:
 | 
						|
                    self.versions_installed.append(version)
 | 
						|
        if tool_error:
 | 
						|
            raise ToolBinaryError
 | 
						|
 | 
						|
    def latest_installed_version(self) -> Optional[str]:
 | 
						|
        """
 | 
						|
        Get the latest installed tool version by directly checking the
 | 
						|
        tool's version directories.
 | 
						|
        """
 | 
						|
        tool_path = self.get_path()
 | 
						|
        if not os.path.exists(tool_path):
 | 
						|
            return None
 | 
						|
        dentries = os.listdir(tool_path)
 | 
						|
        dirs = [d for d in dentries if os.path.isdir(os.path.join(tool_path, d))]
 | 
						|
        for version in sorted(dirs, reverse=True):
 | 
						|
            # get_path_for_version() has assert to check if version is in versions
 | 
						|
            # dict, so get_export_paths() cannot be used. Let's just create the
 | 
						|
            # export paths list directly here.
 | 
						|
            paths = [os.path.join(tool_path, version, *p) for p in self._current_options.export_paths]
 | 
						|
            try:
 | 
						|
                ver_str = self.get_version(paths)
 | 
						|
            except (ToolNotFoundError, ToolExecError):
 | 
						|
                continue
 | 
						|
            if ver_str != version:
 | 
						|
                continue
 | 
						|
            return version
 | 
						|
 | 
						|
        return None
 | 
						|
 | 
						|
    def download(self, version: str) -> None:
 | 
						|
        """
 | 
						|
        Download archive of the tool for platform given by self._platform.
 | 
						|
        """
 | 
						|
        assert version in self.versions
 | 
						|
        download_obj = self.versions[version].get_download_for_platform(self._platform)
 | 
						|
        if not download_obj:
 | 
						|
            fatal(f'No packages for tool {self.name} platform {self._platform}!')
 | 
						|
            raise SystemExit(1)
 | 
						|
 | 
						|
        url = download_obj.url
 | 
						|
        archive_name = download_obj.rename_dist if download_obj.rename_dist else os.path.basename(url)
 | 
						|
        local_path = os.path.join(g.idf_tools_path, 'dist', archive_name)
 | 
						|
        mkdir_p(os.path.dirname(local_path))
 | 
						|
 | 
						|
        if os.path.isfile(local_path):
 | 
						|
            if not self.check_download_file(download_obj, local_path):
 | 
						|
                warn(f'removing downloaded file {archive_name} and downloading again')
 | 
						|
                os.unlink(local_path)
 | 
						|
            else:
 | 
						|
                info(f'file {archive_name} is already downloaded')
 | 
						|
                return
 | 
						|
 | 
						|
        downloaded = False
 | 
						|
        local_temp_path = f'{local_path}.tmp'
 | 
						|
        for retry in range(DOWNLOAD_RETRY_COUNT):
 | 
						|
            err = download(url, local_temp_path)
 | 
						|
            if not os.path.isfile(local_temp_path) or not self.check_download_file(download_obj, local_temp_path):
 | 
						|
                warn(f'Download failure: {err}')
 | 
						|
                warn(f'Failed to download {url} to {local_temp_path}')
 | 
						|
                continue
 | 
						|
            rename_with_retry(local_temp_path, local_path)
 | 
						|
            downloaded = True
 | 
						|
            break
 | 
						|
        if not downloaded:
 | 
						|
            fatal('Failed to download, and retry count has expired')
 | 
						|
            print_hints_on_download_error(str(err))
 | 
						|
            raise SystemExit(1)
 | 
						|
 | 
						|
    def install(self, version: str) -> None:
 | 
						|
        """
 | 
						|
        Unpack archive to destination directory and remove given number of top-level folder if specified.
 | 
						|
        Should always be called after IDFTool.download().
 | 
						|
        """
 | 
						|
        # Currently this is called after calling 'download' method, so here are a few asserts
 | 
						|
        # for the conditions which should be true once that method is done.
 | 
						|
        assert version in self.versions
 | 
						|
        download_obj = self.versions[version].get_download_for_platform(self._platform)
 | 
						|
        assert download_obj is not None
 | 
						|
        archive_name = download_obj.rename_dist if download_obj.rename_dist else os.path.basename(download_obj.url)
 | 
						|
        archive_path = os.path.join(g.idf_tools_path, 'dist', archive_name)
 | 
						|
        assert os.path.isfile(archive_path)
 | 
						|
        dest_dir = self.get_path_for_version(version)
 | 
						|
        if os.path.exists(dest_dir):
 | 
						|
            warn('destination path already exists, removing')
 | 
						|
            shutil.rmtree(dest_dir)
 | 
						|
        mkdir_p(dest_dir)
 | 
						|
        unpack(archive_path, dest_dir)
 | 
						|
        if self._current_options.strip_container_dirs:  # type: ignore
 | 
						|
            do_strip_container_dirs(dest_dir, self._current_options.strip_container_dirs)  # type: ignore
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def check_download_file(download_obj: IDFToolDownload, local_path: str) -> bool:
 | 
						|
        """
 | 
						|
        Compare the computed sha256 to sha256 specified by downloaded archive.
 | 
						|
        """
 | 
						|
        expected_sha256 = download_obj.sha256
 | 
						|
        expected_size = download_obj.size
 | 
						|
        file_size, file_sha256 = get_file_size_sha256(local_path)
 | 
						|
        if file_size != expected_size:
 | 
						|
            warn(f'file size mismatch for {local_path}, expected {expected_size}, got {file_size}')
 | 
						|
            return False
 | 
						|
        if file_sha256 != expected_sha256:
 | 
						|
            warn(f'hash mismatch for {local_path}, expected {expected_sha256}, got {file_sha256}')
 | 
						|
            return False
 | 
						|
        return True
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def from_json(cls, tool_dict: Dict[str, Union[str, List[str], Dict[str, str]]]) -> 'IDFTool':
 | 
						|
        """
 | 
						|
        Create IDFTool class instance form its JSON dump.
 | 
						|
        """
 | 
						|
        # Validate json fields
 | 
						|
        tool_name = tool_dict.get('name')  # type: ignore
 | 
						|
        if not isinstance(tool_name, str):
 | 
						|
            raise RuntimeError('tool_name is not a string')
 | 
						|
 | 
						|
        description = tool_dict.get('description')  # type: ignore
 | 
						|
        if not isinstance(description, str):
 | 
						|
            raise RuntimeError('description is not a string')
 | 
						|
 | 
						|
        is_executable = tool_dict.get('is_executable', True)  # type: ignore
 | 
						|
        if not isinstance(is_executable, bool):
 | 
						|
            raise RuntimeError(f'is_executable for tool {tool_name} is not a bool')
 | 
						|
 | 
						|
        version_cmd = tool_dict.get('version_cmd')
 | 
						|
        if type(version_cmd) is not list:
 | 
						|
            raise RuntimeError(f'version_cmd for tool {tool_name} is not a list of strings')
 | 
						|
 | 
						|
        version_regex = tool_dict.get('version_regex')
 | 
						|
        if not isinstance(version_regex, str) or (not version_regex and is_executable):
 | 
						|
            raise RuntimeError(f'version_regex for tool {tool_name} is not a non-empty string')
 | 
						|
 | 
						|
        version_regex_replace = tool_dict.get('version_regex_replace')
 | 
						|
        if version_regex_replace and not isinstance(version_regex_replace, str):
 | 
						|
            raise RuntimeError(f'version_regex_replace for tool {tool_name} is not a string')
 | 
						|
 | 
						|
        export_paths = tool_dict.get('export_paths')
 | 
						|
        if type(export_paths) is not list:
 | 
						|
            raise RuntimeError(f'export_paths for tool {tool_name} is not a list')
 | 
						|
 | 
						|
        export_vars = tool_dict.get('export_vars', {})  # type: ignore
 | 
						|
        if type(export_vars) is not dict:
 | 
						|
            raise RuntimeError(f'export_vars for tool {tool_name} is not a mapping')
 | 
						|
 | 
						|
        versions = tool_dict.get('versions')
 | 
						|
        if type(versions) is not list:
 | 
						|
            raise RuntimeError(f'versions for tool {tool_name} is not an array')
 | 
						|
 | 
						|
        install = tool_dict.get('install', False)  # type: ignore
 | 
						|
        if not isinstance(install, str):
 | 
						|
            raise RuntimeError(f'install for tool {tool_name} is not a string')
 | 
						|
 | 
						|
        info_url = tool_dict.get('info_url', False)  # type: ignore
 | 
						|
        if not isinstance(info_url, str):
 | 
						|
            raise RuntimeError(f'info_url for tool {tool_name} is not a string')
 | 
						|
 | 
						|
        license = tool_dict.get('license', False)  # type: ignore
 | 
						|
        if not isinstance(license, str):
 | 
						|
            raise RuntimeError(f'license for tool {tool_name} is not a string')
 | 
						|
 | 
						|
        strip_container_dirs = tool_dict.get('strip_container_dirs', 0)
 | 
						|
        if strip_container_dirs and type(strip_container_dirs) is not int:
 | 
						|
            raise RuntimeError(f'strip_container_dirs for tool {tool_name} is not an int')
 | 
						|
 | 
						|
        overrides_list = tool_dict.get('platform_overrides', [])  # type: ignore
 | 
						|
        if type(overrides_list) is not list:
 | 
						|
            raise RuntimeError(f'platform_overrides for tool {tool_name} is not a list')
 | 
						|
 | 
						|
        supported_targets = tool_dict.get('supported_targets')
 | 
						|
        if not isinstance(supported_targets, list):
 | 
						|
            raise RuntimeError(f'supported_targets for tool {tool_name} is not a list of strings')
 | 
						|
 | 
						|
        # Create the object
 | 
						|
        tool_obj: 'IDFTool' = cls(tool_name, description, install, info_url, license,  # type: ignore
 | 
						|
                                  version_cmd, version_regex, supported_targets, version_regex_replace,  # type: ignore
 | 
						|
                                  strip_container_dirs, is_executable)  # type: ignore
 | 
						|
 | 
						|
        for path in export_paths:  # type: ignore
 | 
						|
            tool_obj.options.export_paths.append(path)  # type: ignore
 | 
						|
 | 
						|
        for name, value in export_vars.items():  # type: ignore
 | 
						|
            tool_obj.options.export_vars[name] = value  # type: ignore
 | 
						|
 | 
						|
        for index, override in enumerate(overrides_list):
 | 
						|
            platforms_list = override.get('platforms')  # type: ignore
 | 
						|
            if type(platforms_list) is not list:
 | 
						|
                raise RuntimeError('platforms for override %d of tool %s is not a list' % (index, tool_name))
 | 
						|
 | 
						|
            install = override.get('install')  # type: ignore
 | 
						|
            if install is not None and not isinstance(install, str):
 | 
						|
                raise RuntimeError('install for override %d of tool %s is not a string' % (index, tool_name))
 | 
						|
 | 
						|
            version_cmd = override.get('version_cmd')  # type: ignore
 | 
						|
            if version_cmd is not None and type(version_cmd) is not list:
 | 
						|
                raise RuntimeError('version_cmd for override %d of tool %s is not a list of strings' %
 | 
						|
                                   (index, tool_name))
 | 
						|
 | 
						|
            version_regex = override.get('version_regex')  # type: ignore
 | 
						|
            if version_regex is not None and (not isinstance(version_regex, str) or not version_regex):
 | 
						|
                raise RuntimeError('version_regex for override %d of tool %s is not a non-empty string' %
 | 
						|
                                   (index, tool_name))
 | 
						|
 | 
						|
            version_regex_replace = override.get('version_regex_replace')  # type: ignore
 | 
						|
            if version_regex_replace is not None and not isinstance(version_regex_replace, str):
 | 
						|
                raise RuntimeError('version_regex_replace for override %d of tool %s is not a string' %
 | 
						|
                                   (index, tool_name))
 | 
						|
 | 
						|
            export_paths = override.get('export_paths')  # type: ignore
 | 
						|
            if export_paths is not None and type(export_paths) is not list:
 | 
						|
                raise RuntimeError('export_paths for override %d of tool %s is not a list' % (index, tool_name))
 | 
						|
 | 
						|
            export_vars = override.get('export_vars')  # type: ignore
 | 
						|
            if export_vars is not None and type(export_vars) is not dict:
 | 
						|
                raise RuntimeError('export_vars for override %d of tool %s is not a mapping' % (index, tool_name))
 | 
						|
            tool_obj.platform_overrides.append(override)  # type: ignore
 | 
						|
 | 
						|
        recommended_versions:Dict[str, List[str]] = {}
 | 
						|
        for version_dict in versions:  # type: ignore
 | 
						|
            version = version_dict.get('name')  # type: ignore
 | 
						|
            if not isinstance(version, str):
 | 
						|
                raise RuntimeError(f'version name for tool {tool_name} is not a string')
 | 
						|
 | 
						|
            version_status = version_dict.get('status')  # type: ignore
 | 
						|
            if not isinstance(version_status, str) and version_status not in IDFToolVersion.STATUS_VALUES:
 | 
						|
                raise RuntimeError('tool {} version {} status is not one of {}', tool_name, version,
 | 
						|
                                   IDFToolVersion.STATUS_VALUES)
 | 
						|
 | 
						|
            version_obj = IDFToolVersion(version, version_status)
 | 
						|
            for platform_id, platform_dict in version_dict.items():  # type: ignore
 | 
						|
                if platform_id in ['name', 'status']:
 | 
						|
                    continue
 | 
						|
                try:
 | 
						|
                    Platforms.get(platform_id)
 | 
						|
                except ValueError:
 | 
						|
                    raise RuntimeError('invalid platform %s for tool %s version %s' %
 | 
						|
                                       (platform_id, tool_name, version))
 | 
						|
 | 
						|
                version_obj.add_download(platform_id,
 | 
						|
                                         platform_dict['url'], platform_dict['size'],
 | 
						|
                                         platform_dict['sha256'], platform_dict.get('rename_dist', ''))
 | 
						|
 | 
						|
                if version_status == IDFToolVersion.STATUS_RECOMMENDED:
 | 
						|
                    if platform_id not in recommended_versions:
 | 
						|
                        recommended_versions[platform_id] = []
 | 
						|
                    recommended_versions[platform_id].append(version)
 | 
						|
 | 
						|
            tool_obj.add_version(version_obj)
 | 
						|
        for platform_id, version_list in recommended_versions.items():
 | 
						|
            if len(version_list) > 1:
 | 
						|
                raise RuntimeError(f'tool {tool_name} for platform {platform_id} has {len(recommended_versions)} recommended versions')
 | 
						|
            if install != IDFTool.INSTALL_NEVER and len(recommended_versions) == 0:
 | 
						|
                raise RuntimeError(f'required/optional tool {tool_name} for platform {platform_id} has no recommended versions')
 | 
						|
 | 
						|
        tool_obj._update_current_options()
 | 
						|
        return tool_obj
 | 
						|
 | 
						|
    def to_json(self):  # type: ignore
 | 
						|
        """
 | 
						|
        Make a JSON dump of self.
 | 
						|
        """
 | 
						|
        versions_array = []
 | 
						|
        for version, version_obj in self.versions.items():
 | 
						|
            version_json = {
 | 
						|
                'name': version,
 | 
						|
                'status': version_obj.status
 | 
						|
            }
 | 
						|
            for platform_id, download in version_obj.downloads.items():
 | 
						|
                if download.rename_dist:
 | 
						|
                    version_json[platform_id] = {
 | 
						|
                        'url': download.url,
 | 
						|
                        'size': download.size,
 | 
						|
                        'sha256': download.sha256,
 | 
						|
                        'rename_dist': download.rename_dist
 | 
						|
                    }
 | 
						|
                else:
 | 
						|
                    version_json[platform_id] = {
 | 
						|
                        'url': download.url,
 | 
						|
                        'size': download.size,
 | 
						|
                        'sha256': download.sha256
 | 
						|
                    }
 | 
						|
            versions_array.append(version_json)
 | 
						|
        overrides_array = self.platform_overrides
 | 
						|
 | 
						|
        tool_json = {
 | 
						|
            'name': self.name,
 | 
						|
            'description': self.description,
 | 
						|
            'export_paths': self.options.export_paths,
 | 
						|
            'export_vars': self.options.export_vars,
 | 
						|
            'install': self.options.install,
 | 
						|
            'info_url': self.options.info_url,
 | 
						|
            'license': self.options.license,
 | 
						|
            'version_cmd': self.options.version_cmd,
 | 
						|
            'version_regex': self.options.version_regex,
 | 
						|
            'supported_targets': self.options.supported_targets,
 | 
						|
            'versions': versions_array,
 | 
						|
        }
 | 
						|
        if self.options.version_regex_replace != VERSION_REGEX_REPLACE_DEFAULT:
 | 
						|
            tool_json['version_regex_replace'] = self.options.version_regex_replace
 | 
						|
        if overrides_array:
 | 
						|
            tool_json['platform_overrides'] = overrides_array
 | 
						|
        if self.options.strip_container_dirs:
 | 
						|
            tool_json['strip_container_dirs'] = self.options.strip_container_dirs
 | 
						|
        if self.options.is_executable is False:
 | 
						|
            tool_json['is_executable'] = self.options.is_executable
 | 
						|
        return tool_json
 | 
						|
 | 
						|
 | 
						|
class IDFEnvEncoder(JSONEncoder):
 | 
						|
    """
 | 
						|
    IDFEnvEncoder is used for encoding IDFEnv, IDFRecord, SelectedIDFRecord classes to JSON in readable format. Not as (__main__.IDFRecord object at '0x7fcxx').
 | 
						|
    Additionally remove first underscore with private properties when processing.
 | 
						|
    """
 | 
						|
    def default(self, obj):  # type: ignore
 | 
						|
        return {k.lstrip('_'): v for k, v in vars(obj).items()}
 | 
						|
 | 
						|
 | 
						|
class IDFRecord:
 | 
						|
    """
 | 
						|
    IDFRecord represents one record of installed ESP-IDF on system.
 | 
						|
    Contains:
 | 
						|
        * version - actual version of ESP-IDF (example '5.0')
 | 
						|
        * path - absolute path to the ESP-IDF
 | 
						|
        * features - features using ESP-IDF
 | 
						|
        * targets - ESP chips for which are installed needed toolchains (example ['esp32' , 'esp32s2'])
 | 
						|
                  - Default value is [], since user didn't define any targets yet
 | 
						|
    """
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self.version: str = ''
 | 
						|
        self.path: str = ''
 | 
						|
        self._features: List[str] = ['core']
 | 
						|
        self._targets: List[str] = []
 | 
						|
 | 
						|
    def __iter__(self):  # type: ignore
 | 
						|
        yield from {
 | 
						|
            'version': self.version,
 | 
						|
            'path': self.path,
 | 
						|
            'features': self._features,
 | 
						|
            'targets': self._targets
 | 
						|
        }.items()
 | 
						|
 | 
						|
    def __str__(self) -> str:
 | 
						|
        return json.dumps(dict(self), ensure_ascii=False, indent=4)  # type: ignore
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return self.__str__()
 | 
						|
 | 
						|
    def __eq__(self, other: object) -> bool:
 | 
						|
        if not isinstance(other, IDFRecord):
 | 
						|
            return False
 | 
						|
        return all(getattr(self, x) == getattr(other, x) for x in ('version', 'path', 'features', 'targets'))
 | 
						|
 | 
						|
    def __ne__(self, other: object) -> bool:
 | 
						|
        if not isinstance(other, IDFRecord):
 | 
						|
            return False
 | 
						|
        return not self.__eq__(other)
 | 
						|
 | 
						|
    @property
 | 
						|
    def features(self) -> List[str]:
 | 
						|
        return self._features
 | 
						|
 | 
						|
    def update_features(self, add: Tuple[str, ...] = (), remove: Tuple[str, ...] = ()) -> None:
 | 
						|
        # Update features, but maintain required feature 'core'
 | 
						|
        # If the same feature is present in both argument's tuples, do not update this feature
 | 
						|
        add_set = set(add)
 | 
						|
        remove_set = set(remove)
 | 
						|
        # Remove duplicates
 | 
						|
        features_to_add = add_set.difference(remove_set)
 | 
						|
        features_to_remove = remove_set.difference(add_set)
 | 
						|
 | 
						|
        features = set(self._features)
 | 
						|
        features.update(features_to_add)
 | 
						|
        features.difference_update(features_to_remove)
 | 
						|
        features.add('core')
 | 
						|
        self._features = list(features)
 | 
						|
 | 
						|
    @property
 | 
						|
    def targets(self) -> List[str]:
 | 
						|
        return self._targets
 | 
						|
 | 
						|
    def extend_targets(self, targets: List[str]) -> None:
 | 
						|
        # Targets can be only updated, but always maintain existing targets.
 | 
						|
        self._targets = list(set(targets + self._targets))
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def get_active_idf_record(cls) -> 'IDFRecord':
 | 
						|
        idf_record_obj = cls()
 | 
						|
        idf_record_obj.version = get_idf_version()
 | 
						|
        idf_record_obj.path = g.idf_path
 | 
						|
        return idf_record_obj
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def get_idf_record_from_dict(cls, record_dict: Dict[str, Any]) -> 'IDFRecord':
 | 
						|
        idf_record_obj = cls()
 | 
						|
        try:
 | 
						|
            idf_record_obj.version = record_dict['version']
 | 
						|
            idf_record_obj.path = record_dict['path']
 | 
						|
        except KeyError:
 | 
						|
            # When some of these key attributes, which are irreplaceable with default values, are not found, raise VallueError
 | 
						|
            raise ValueError('Inconsistent record')
 | 
						|
 | 
						|
        idf_record_obj.update_features(record_dict.get('features', []))
 | 
						|
        idf_record_obj.extend_targets(record_dict.get('targets', []))
 | 
						|
 | 
						|
        return idf_record_obj
 | 
						|
 | 
						|
 | 
						|
class IDFEnv:
 | 
						|
    """
 | 
						|
    IDFEnv represents ESP-IDF Environments installed on system and is responsible for loading and saving structured data.
 | 
						|
    All information is saved and loaded from IDF_ENV_FILE.
 | 
						|
    Contains:
 | 
						|
        * idf_installed - all installed environments of ESP-IDF on system.
 | 
						|
    """
 | 
						|
    def __init__(self) -> None:
 | 
						|
        active_idf_id = active_repo_id()
 | 
						|
        self.idf_installed: Dict[str, IDFRecord] = {active_idf_id: IDFRecord.get_active_idf_record()}
 | 
						|
 | 
						|
    def __iter__(self):  # type: ignore
 | 
						|
        yield from {
 | 
						|
            'idfInstalled': self.idf_installed,
 | 
						|
        }.items()
 | 
						|
 | 
						|
    def __str__(self) -> str:
 | 
						|
        return json.dumps(dict(self), cls=IDFEnvEncoder, ensure_ascii=False, indent=4)  # type: ignore
 | 
						|
 | 
						|
    def __repr__(self) -> str:
 | 
						|
        return self.__str__()
 | 
						|
 | 
						|
    def save(self) -> None:
 | 
						|
        """
 | 
						|
        Diff current class instance with instance loaded from IDF_ENV_FILE and save only if are different.
 | 
						|
        """
 | 
						|
        # It is enough to compare just active records because others can't be touched by the running script
 | 
						|
        if self.get_active_idf_record() != self.get_idf_env().get_active_idf_record():
 | 
						|
            idf_env_file_path = os.path.join(g.idf_tools_path, IDF_ENV_FILE)
 | 
						|
            try:
 | 
						|
                if g.idf_tools_path:  # mypy fix for Optional[str] in the next call
 | 
						|
                    # the directory doesn't exist if this is run on a clean system the first time
 | 
						|
                    mkdir_p(g.idf_tools_path)
 | 
						|
                with open(idf_env_file_path, 'w', encoding='utf-8') as w:
 | 
						|
                    info(f'Updating {idf_env_file_path}')
 | 
						|
                    json.dump(dict(self), w, cls=IDFEnvEncoder, ensure_ascii=False, indent=4)  # type: ignore
 | 
						|
            except (IOError, OSError):
 | 
						|
                if not os.access(g.idf_tools_path, os.W_OK):
 | 
						|
                    raise OSError(f'IDF_TOOLS_PATH {g.idf_tools_path} is not accessible to write. '
 | 
						|
                                  'Required changes have not been saved')
 | 
						|
                raise OSError(f'File {idf_env_file_path} is not accessible to write or corrupted. Required changes have not been saved')
 | 
						|
 | 
						|
    def get_active_idf_record(self) -> IDFRecord:
 | 
						|
        return self.idf_installed[active_repo_id()]
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def get_idf_env(cls) -> 'IDFEnv':
 | 
						|
        """
 | 
						|
        IDFEnv class is used to process IDF_ENV_FILE file. The constructor is therefore called only in this method that loads the file and checks its contents.
 | 
						|
        """
 | 
						|
        idf_env_obj = cls()
 | 
						|
        try:
 | 
						|
            idf_env_file_path = os.path.join(g.idf_tools_path, IDF_ENV_FILE)
 | 
						|
            with open(idf_env_file_path, 'r', encoding='utf-8') as idf_env_file:
 | 
						|
                idf_env_json = json.load(idf_env_file)
 | 
						|
 | 
						|
                try:
 | 
						|
                    idf_installed = idf_env_json['idfInstalled']
 | 
						|
                except KeyError:
 | 
						|
                    # If no ESP-IDF record is found in loaded file, do not update and keep default value from constructor
 | 
						|
                    pass
 | 
						|
                else:
 | 
						|
                    # Load and verify ESP-IDF records found in IDF_ENV_FILE
 | 
						|
                    idf_installed.pop('sha', None)
 | 
						|
                    idf_installed_verified:Dict[str, IDFRecord] = {}
 | 
						|
                    for idf in idf_installed:
 | 
						|
                        try:
 | 
						|
                            idf_installed_verified[idf] = IDFRecord.get_idf_record_from_dict(idf_installed[idf])
 | 
						|
                        except ValueError as err:
 | 
						|
                            warn(f'{err} "{idf}" found in {idf_env_file_path}, removing this record.')
 | 
						|
                    # Combine ESP-IDF loaded records with the one in constructor, to be sure that there is an active ESP-IDF record in the idf_installed
 | 
						|
                    # If the active record is already in idf_installed, it is not overwritten
 | 
						|
                    idf_env_obj.idf_installed = dict(idf_env_obj.idf_installed, **idf_installed_verified)
 | 
						|
 | 
						|
        except (IOError, OSError, ValueError):
 | 
						|
            # If no, empty or not-accessible to read IDF_ENV_FILE found, use default values from constructor
 | 
						|
            pass
 | 
						|
 | 
						|
        return idf_env_obj
 | 
						|
 | 
						|
 | 
						|
class ENVState:
 | 
						|
    """
 | 
						|
    ENVState is used to handle IDF global variables that are set in environment and need to be removed when switching between ESP-IDF versions in opened shell.
 | 
						|
    Every opened shell/terminal has it's own temporary file to store these variables.
 | 
						|
    The temporary file's name is generated automatically with suffix 'idf_ + opened shell ID'. Path to this tmp file is stored as env global variable (env_key).
 | 
						|
    The shell ID is crucial, since in one terminal can be opened more shells.
 | 
						|
    * env_key - global variable name/key
 | 
						|
    * deactivate_file_path - global variable value (generated tmp file name)
 | 
						|
    * idf_variables - loaded IDF variables from file
 | 
						|
    """
 | 
						|
    env_key = 'IDF_DEACTIVATE_FILE_PATH'
 | 
						|
    deactivate_file_path = os.environ.get(env_key, '')
 | 
						|
 | 
						|
    def __init__(self) -> None:
 | 
						|
        self.idf_variables: Dict[str, Any] = {}
 | 
						|
 | 
						|
    @classmethod
 | 
						|
    def get_env_state(cls) -> 'ENVState':
 | 
						|
        env_state_obj = cls()
 | 
						|
 | 
						|
        if cls.deactivate_file_path:
 | 
						|
            try:
 | 
						|
                with open(cls.deactivate_file_path, 'r') as fp:
 | 
						|
                    env_state_obj.idf_variables = json.load(fp)
 | 
						|
            except (IOError, OSError, ValueError):
 | 
						|
                pass
 | 
						|
        return env_state_obj
 | 
						|
 | 
						|
    def save(self) -> str:
 | 
						|
        try:
 | 
						|
            if self.deactivate_file_path and os.path.basename(self.deactivate_file_path).endswith(f'idf_{str(os.getppid())}'):
 | 
						|
                # If exported file path/name exists and belongs to actual opened shell
 | 
						|
                with open(self.deactivate_file_path, 'w') as w:
 | 
						|
                    json.dump(self.idf_variables, w, ensure_ascii=False, indent=4)  # type: ignore
 | 
						|
            else:
 | 
						|
                with tempfile.NamedTemporaryFile(delete=False, suffix=f'idf_{str(os.getppid())}') as fp:
 | 
						|
                    self.deactivate_file_path = fp.name
 | 
						|
                    fp.write(json.dumps(self.idf_variables, ensure_ascii=False, indent=4).encode('utf-8'))
 | 
						|
        except (IOError, OSError):
 | 
						|
            warn(f'File storing IDF env variables {self.deactivate_file_path} is not accessible to write. '
 | 
						|
                 'Potentional switching ESP-IDF versions may cause problems')
 | 
						|
        return self.deactivate_file_path
 | 
						|
 | 
						|
 | 
						|
def load_tools_info() -> Dict[str, IDFTool]:
 | 
						|
    """
 | 
						|
    Load tools metadata from tools.json, return a dictionary: tool name - tool info.
 | 
						|
    """
 | 
						|
 | 
						|
    tool_versions_file_name = g.tools_json
 | 
						|
 | 
						|
    with open(tool_versions_file_name, 'r') as f:  # type: ignore
 | 
						|
        tools_info = json.load(f)
 | 
						|
 | 
						|
    return parse_tools_info_json(tools_info)  # type: ignore
 | 
						|
 | 
						|
 | 
						|
def parse_tools_info_json(tools_info):  # type: ignore
 | 
						|
    """
 | 
						|
    Parse and validate the dictionary obtained by loading the tools.json file.
 | 
						|
    Returns a dictionary of tools (key: tool name, value: IDFTool object).
 | 
						|
    """
 | 
						|
    tools_dict = OrderedDict()
 | 
						|
 | 
						|
    tools_array = tools_info.get('tools')
 | 
						|
    if type(tools_array) is not list:
 | 
						|
        raise RuntimeError('tools property is missing or not an array')
 | 
						|
 | 
						|
    for tool_dict in tools_array:
 | 
						|
        tool = IDFTool.from_json(tool_dict)
 | 
						|
        tools_dict[tool.name] = tool
 | 
						|
 | 
						|
    return tools_dict
 | 
						|
 | 
						|
 | 
						|
def dump_tools_json(tools_info: Optional[Dict[str, IDFTool]]):  # type: ignore
 | 
						|
    """
 | 
						|
    Dump all the tools into JSON.
 | 
						|
    """
 | 
						|
    tools_array = []
 | 
						|
    if tools_info:
 | 
						|
        for _, tool_obj in tools_info.items():
 | 
						|
            tool_json = tool_obj.to_json()
 | 
						|
            tools_array.append(tool_json)
 | 
						|
        file_json = {'version': TOOLS_FILE_VERSION, 'tools': tools_array}
 | 
						|
        return json.dumps(file_json, indent=2, separators=(',', ': '), sort_keys=True)
 | 
						|
    else:
 | 
						|
        return json.dumps({}, indent=2, separators=(',', ': '), sort_keys=True)
 | 
						|
 | 
						|
 | 
						|
def get_python_exe_and_subdir() -> Tuple[str, str]:
 | 
						|
    """
 | 
						|
    Returns:
 | 
						|
    * python executable name (python on Unix, python.exe on Win)
 | 
						|
    * subdir in which the executable is (bin for Unix, Scripts for Win)
 | 
						|
    """
 | 
						|
    if sys.platform == 'win32':
 | 
						|
        subdir = 'Scripts'
 | 
						|
        python_exe = 'python.exe'
 | 
						|
    else:
 | 
						|
        subdir = 'bin'
 | 
						|
        python_exe = 'python'
 | 
						|
    return python_exe, subdir
 | 
						|
 | 
						|
 | 
						|
def get_idf_version() -> str:
 | 
						|
    """
 | 
						|
    Return ESP-IDF version.
 | 
						|
    """
 | 
						|
    idf_version: Optional[str] = None
 | 
						|
 | 
						|
    version_file_path = os.path.join(g.idf_path, 'version.txt')
 | 
						|
    if os.path.exists(version_file_path):
 | 
						|
        with open(version_file_path, 'r') as version_file:
 | 
						|
            idf_version_str = version_file.read()
 | 
						|
 | 
						|
        match = re.match(r'^v([0-9]+\.[0-9]+).*', idf_version_str)
 | 
						|
        if match:
 | 
						|
            idf_version = match.group(1)
 | 
						|
 | 
						|
    if idf_version is None:
 | 
						|
        try:
 | 
						|
            with open(os.path.join(g.idf_path, 'components', 'esp_common', 'include', 'esp_idf_version.h')) as f:
 | 
						|
                m = re.search(r'^#define\s+ESP_IDF_VERSION_MAJOR\s+(\d+).+?^#define\s+ESP_IDF_VERSION_MINOR\s+(\d+)',
 | 
						|
                              f.read(), re.DOTALL | re.MULTILINE)
 | 
						|
                if m:
 | 
						|
                    idf_version = '.'.join((m.group(1), m.group(2)))
 | 
						|
                else:
 | 
						|
                    fatal('Reading IDF version from C header file failed!')
 | 
						|
                    raise SystemExit(1)
 | 
						|
        except Exception as e:
 | 
						|
            fatal(f'It is not possible to determine the IDF version: {e}')
 | 
						|
            raise SystemExit(1)
 | 
						|
    return idf_version
 | 
						|
 | 
						|
 | 
						|
def get_python_env_path() -> Tuple[str, str, str, str]:
 | 
						|
    """
 | 
						|
    Returns tuple of Python environment path, Python env. path with subdir and full path from Python (i.e. with executable).
 | 
						|
    """
 | 
						|
    idf_version = get_idf_version()
 | 
						|
    idf_python_env_path = os.getenv('IDF_PYTHON_ENV_PATH') or os.path.join(g.idf_tools_path,
 | 
						|
                                                                           'python_env',
 | 
						|
                                                                           PYTHON_VENV_DIR_TEMPLATE.format(idf_version,
 | 
						|
                                                                                                           PYTHON_VER_MAJOR_MINOR))
 | 
						|
 | 
						|
    python_exe, subdir = get_python_exe_and_subdir()
 | 
						|
    idf_python_export_path = os.path.join(idf_python_env_path, subdir)
 | 
						|
    virtualenv_python = os.path.join(idf_python_export_path, python_exe)
 | 
						|
 | 
						|
    return idf_python_env_path, idf_python_export_path, virtualenv_python, idf_version
 | 
						|
 | 
						|
 | 
						|
def parse_tools_arg(tools_str: List[str]) -> List[str]:
 | 
						|
    """
 | 
						|
    Base parsing "tools" argumets: all, required, etc.
 | 
						|
    """
 | 
						|
    if not tools_str:
 | 
						|
        return ['required']
 | 
						|
    else:
 | 
						|
        return tools_str
 | 
						|
 | 
						|
 | 
						|
def expand_tools_arg(tools_spec: List[str], overall_tools: OrderedDict, targets: List[str]) -> List[str]:
 | 
						|
    """
 | 
						|
    Expand list of tools 'tools_spec' in according:
 | 
						|
        - a tool is in the 'overall_tools' list
 | 
						|
        - consider metapackages like "required" and "all"
 | 
						|
        - process wildcards in tool names
 | 
						|
        - a tool supports chips from 'targets'
 | 
						|
    """
 | 
						|
    tools = []
 | 
						|
    # Filtering tools if they are in overall_tools
 | 
						|
    # Processing wildcards if possible
 | 
						|
    for tool_pattern in tools_spec:
 | 
						|
        tools.extend([k for k, _ in overall_tools.items() if fnmatch.fnmatch(k,tool_pattern) and k not in tools])
 | 
						|
 | 
						|
    # Processing "metapackage"
 | 
						|
    if 'required' in tools_spec:
 | 
						|
        tools.extend([k for k, v in overall_tools.items() if v.get_install_type() == IDFTool.INSTALL_ALWAYS and k not in tools])
 | 
						|
 | 
						|
    elif 'all' in tools_spec:
 | 
						|
        tools.extend([k for k, v in overall_tools.items() if v.get_install_type() != IDFTool.INSTALL_NEVER and k not in tools])
 | 
						|
 | 
						|
    # Filtering by ESP_targets
 | 
						|
    tools = [k for k in tools if overall_tools[k].is_supported_for_any_of_targets(targets)]
 | 
						|
    return tools
 | 
						|
 | 
						|
 | 
						|
def parse_targets_arg(targets_str: str) -> List[str]:
 | 
						|
    """
 | 
						|
    Parse and check if targets_str is a valid list of targets and return a target list.
 | 
						|
    """
 | 
						|
    targets_from_tools_json = get_all_targets_from_tools_json()
 | 
						|
    invalid_targets = []
 | 
						|
 | 
						|
    targets_str = targets_str.lower()
 | 
						|
    targets = targets_str.replace('-', '').split(',')
 | 
						|
    if targets == ['all']:
 | 
						|
        return targets_from_tools_json
 | 
						|
    else:
 | 
						|
        invalid_targets = [t for t in targets if t not in targets_from_tools_json]
 | 
						|
        if invalid_targets:
 | 
						|
            warn(f'Targets: \"{", ".join(invalid_targets)}\" are not supported. Only allowed options are: {", ".join(targets_from_tools_json)}.')
 | 
						|
            raise SystemExit(1)
 | 
						|
        return targets
 | 
						|
 | 
						|
 | 
						|
def add_and_check_targets(idf_env_obj: IDFEnv, targets_str: str) -> List[str]:
 | 
						|
    """
 | 
						|
    Define targets from targets_str, check that the target names are valid and add them to idf_env_obj.
 | 
						|
    """
 | 
						|
    targets = parse_targets_arg(targets_str)
 | 
						|
    idf_env_obj.get_active_idf_record().extend_targets(targets)
 | 
						|
    return idf_env_obj.get_active_idf_record().targets
 | 
						|
 | 
						|
 | 
						|
def feature_to_requirements_path(feature: str) -> str:
 | 
						|
    """
 | 
						|
    Convert feature (ci, core, docs, gdbgui, pytest, ...) to the path to its requirements.txt.
 | 
						|
    """
 | 
						|
    return os.path.join(g.idf_path, 'tools', 'requirements', f'requirements.{feature}.txt')
 | 
						|
 | 
						|
 | 
						|
def process_and_check_features(idf_env_obj: IDFEnv, features_str: str) -> List[str]:
 | 
						|
    """
 | 
						|
    Check whether new feature is valid. If yes, update features in active IDF record.
 | 
						|
    """
 | 
						|
    new_features = []
 | 
						|
    remove_features = []
 | 
						|
    for new_feature_candidate in features_str.split(','):
 | 
						|
        if new_feature_candidate.startswith('-'):
 | 
						|
            remove_features += [new_feature_candidate.lstrip('-')]
 | 
						|
        else:
 | 
						|
            new_feature_candidate = new_feature_candidate.lstrip('+')
 | 
						|
            # Feature to be added needs to be checked if is valid
 | 
						|
            if os.path.isfile(feature_to_requirements_path(new_feature_candidate)):
 | 
						|
                new_features += [new_feature_candidate]
 | 
						|
    idf_env_obj.get_active_idf_record().update_features(tuple(new_features), tuple(remove_features))
 | 
						|
    return idf_env_obj.get_active_idf_record().features
 | 
						|
 | 
						|
 | 
						|
def get_all_targets_from_tools_json() -> List[str]:
 | 
						|
    """
 | 
						|
    Returns list of all targets from tools.json file.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    targets_from_tools_json: List[str] = []
 | 
						|
 | 
						|
    for _, v in tools_info.items():
 | 
						|
        targets_from_tools_json.extend(v.get_supported_targets())
 | 
						|
    # remove duplicates
 | 
						|
    targets_from_tools_json = list(set(targets_from_tools_json))
 | 
						|
    if 'all' in targets_from_tools_json:
 | 
						|
        targets_from_tools_json.remove('all')
 | 
						|
    return sorted(targets_from_tools_json)
 | 
						|
 | 
						|
 | 
						|
def filter_tools_info(idf_env_obj: IDFEnv, tools_info: Dict[str, IDFTool]) -> Dict[str,IDFTool]:
 | 
						|
    """
 | 
						|
    Filter tools info; return only those targets which:
 | 
						|
        * are installable (install type is INSTALL_ALWAYS or INSTALL_ON_REQUEST)
 | 
						|
        * support at least one target from active IDF record
 | 
						|
    """
 | 
						|
    targets = idf_env_obj.get_active_idf_record().targets
 | 
						|
    if not targets:
 | 
						|
        return tools_info
 | 
						|
    else:
 | 
						|
        filtered_tools_spec = {k:v for k, v in tools_info.items() if
 | 
						|
                               (v.get_install_type() == IDFTool.INSTALL_ALWAYS or v.get_install_type() == IDFTool.INSTALL_ON_REQUEST) and
 | 
						|
                               (any(item in targets for item in v.get_supported_targets()) or v.get_supported_targets() == ['all'])}
 | 
						|
        return OrderedDict(filtered_tools_spec)
 | 
						|
 | 
						|
 | 
						|
def add_variables_to_deactivate_file(args: List[str], new_idf_vars:Dict[str, Any]) -> str:
 | 
						|
    """
 | 
						|
    Add IDF global variables that need to be removed when the active esp-idf environment is deactivated.
 | 
						|
    """
 | 
						|
    if 'PATH' in new_idf_vars:
 | 
						|
        new_idf_vars['PATH'] = new_idf_vars['PATH'].split(':')[:-1]  # PATH is stored as list of sub-paths without '$PATH'
 | 
						|
 | 
						|
    new_idf_vars['PATH'] = new_idf_vars.get('PATH', [])
 | 
						|
    args_add_paths_extras = vars(args).get('add_paths_extras')  # remove mypy error with args
 | 
						|
    new_idf_vars['PATH'] = new_idf_vars['PATH'] + args_add_paths_extras.split(':') if args_add_paths_extras else new_idf_vars['PATH']
 | 
						|
 | 
						|
    env_state_obj = ENVState.get_env_state()
 | 
						|
 | 
						|
    if env_state_obj.idf_variables:
 | 
						|
        exported_idf_vars = env_state_obj.idf_variables
 | 
						|
        new_idf_vars['PATH'] = list(set(new_idf_vars['PATH'] + exported_idf_vars.get('PATH', [])))  # remove duplicates
 | 
						|
        env_state_obj.idf_variables = dict(exported_idf_vars, **new_idf_vars)  # merge two dicts
 | 
						|
    else:
 | 
						|
        env_state_obj.idf_variables = new_idf_vars
 | 
						|
    deactivate_file_path = env_state_obj.save()
 | 
						|
 | 
						|
    return deactivate_file_path
 | 
						|
 | 
						|
 | 
						|
def deactivate_statement(args: List[str]) -> None:
 | 
						|
    """
 | 
						|
    Deactivate statement is sequence of commands, that remove IDF global variables from enviroment,
 | 
						|
    so the environment gets to the state it was before calling export.{sh/fish} script.
 | 
						|
    """
 | 
						|
    env_state_obj = ENVState.get_env_state()
 | 
						|
    if not env_state_obj.idf_variables:
 | 
						|
        warn('No IDF variables to remove from environment found. Deactivation of previous esp-idf version was not successful.')
 | 
						|
        return
 | 
						|
    unset_vars = env_state_obj.idf_variables
 | 
						|
    env_path: Optional[str] = os.getenv('PATH')
 | 
						|
    if env_path:
 | 
						|
        cleared_env_path = ':'.join([k for k in env_path.split(':') if k not in unset_vars['PATH']])
 | 
						|
 | 
						|
    unset_list = [k for k in unset_vars.keys() if k != 'PATH']
 | 
						|
    unset_format, sep = get_unset_format_and_separator(args)
 | 
						|
    unset_statement = sep.join([unset_format.format(k) for k in unset_list])
 | 
						|
 | 
						|
    export_format, sep = get_export_format_and_separator(args)
 | 
						|
    export_statement = export_format.format('PATH', cleared_env_path)
 | 
						|
 | 
						|
    deactivate_statement_str = sep.join([unset_statement, export_statement])
 | 
						|
 | 
						|
    print(deactivate_statement_str)
 | 
						|
    # After deactivation clear old variables
 | 
						|
    env_state_obj.idf_variables.clear()
 | 
						|
    env_state_obj.save()
 | 
						|
    return
 | 
						|
 | 
						|
 | 
						|
def get_export_format_and_separator(args: List[str]) -> Tuple[str, str]:
 | 
						|
    """
 | 
						|
    Returns export pattern (formatted string) either for exporting in shell or as a key-value pair.
 | 
						|
    """
 | 
						|
    return {EXPORT_SHELL: ('export {}="{}"', ';'), EXPORT_KEY_VALUE: ('{}={}', '\n')}[args.format]  # type: ignore
 | 
						|
 | 
						|
 | 
						|
def get_unset_format_and_separator(args: List[str]) -> Tuple[str, str]:
 | 
						|
    """
 | 
						|
    Returns pattern to unset a variable (formatted string) either for shell or for key-value pair.
 | 
						|
    """
 | 
						|
    return {EXPORT_SHELL: ('unset {}', ';'), EXPORT_KEY_VALUE: ('{}', '\n')}[args.format]  # type: ignore
 | 
						|
 | 
						|
 | 
						|
def different_idf_detected() -> bool:
 | 
						|
    """
 | 
						|
    Checks if new IDF detected.
 | 
						|
    """
 | 
						|
    # If IDF global variable found, test if belong to different ESP-IDF version
 | 
						|
    if 'IDF_TOOLS_EXPORT_CMD' in os.environ:
 | 
						|
        if g.idf_path != os.path.dirname(os.environ['IDF_TOOLS_EXPORT_CMD']):
 | 
						|
            return True
 | 
						|
 | 
						|
    # No previous ESP-IDF export detected, nothing to be unset
 | 
						|
    if all(s not in os.environ for s in ['IDF_PYTHON_ENV_PATH', 'OPENOCD_SCRIPTS', 'ESP_IDF_VERSION']):
 | 
						|
        return False
 | 
						|
 | 
						|
    # User is exporting the same version as is in env
 | 
						|
    if os.getenv('ESP_IDF_VERSION') == get_idf_version():
 | 
						|
        return False
 | 
						|
 | 
						|
    # Different version detected
 | 
						|
    return True
 | 
						|
 | 
						|
 | 
						|
def active_repo_id() -> str:
 | 
						|
    """
 | 
						|
    Function returns unique id of running ESP-IDF combining current idfpath with version.
 | 
						|
    The id is unique with same version & different path or same path & different version.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        # g.idf_path is forcefully casted to str just to make type linters happy
 | 
						|
        return f'{str(g.idf_path)}-v{get_idf_version()}'
 | 
						|
    except ReferenceError:
 | 
						|
        return f'UNKNOWN_PATH-v{get_idf_version()}'
 | 
						|
 | 
						|
 | 
						|
def list_default(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Prints currently installed versions of all tools compatible with current platform.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    tool_error = False
 | 
						|
    for name, tool in tools_info.items():
 | 
						|
        if tool.get_install_type() == IDFTool.INSTALL_NEVER:
 | 
						|
            continue
 | 
						|
        optional_str = ' (optional)' if tool.get_install_type() == IDFTool.INSTALL_ON_REQUEST else ''
 | 
						|
        info(f'* {name}: {tool.description}{optional_str}')
 | 
						|
        try:
 | 
						|
            tool.find_installed_versions()
 | 
						|
        except ToolBinaryError:
 | 
						|
            tool_error = True
 | 
						|
        versions_for_platform = {k: v for k, v in tool.versions.items() if v.compatible_with_platform()}
 | 
						|
        if not versions_for_platform:
 | 
						|
            info(f'  (no versions compatible with platform {PYTHON_PLATFORM})')
 | 
						|
            continue
 | 
						|
        versions_sorted = sorted(versions_for_platform.keys(), key=tool.versions.get, reverse=True)  # type: ignore
 | 
						|
        for version in versions_sorted:
 | 
						|
            version_obj = tool.versions[version]
 | 
						|
            info('  - {} ({}{})'.format(version, version_obj.status,
 | 
						|
                                        ', installed' if version in tool.versions_installed else ''))
 | 
						|
    if tool_error:
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def list_outdated(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Prints info if currently installed version can be replaced by newer one for every tool.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    for name, tool in tools_info.items():
 | 
						|
        if tool.get_install_type() == IDFTool.INSTALL_NEVER:
 | 
						|
            continue
 | 
						|
        versions_for_platform = {k: v for k, v in tool.versions.items() if v.compatible_with_platform()}
 | 
						|
        if not versions_for_platform:
 | 
						|
            continue
 | 
						|
        version_installed = tool.latest_installed_version()
 | 
						|
        if not version_installed:
 | 
						|
            continue
 | 
						|
        version_available = sorted(versions_for_platform.keys(), key=tool.versions.get, reverse=True)[0]
 | 
						|
        if version_installed < version_available:
 | 
						|
            info(f'{name}: version {version_installed} is outdated by {version_available}')
 | 
						|
 | 
						|
 | 
						|
def action_list(args):  # type: ignore
 | 
						|
    """
 | 
						|
    If args.outdated flag is set, prints if currently installed tools can be replaced by their newer version.
 | 
						|
    If not, prints currently installed tools with their version.
 | 
						|
    """
 | 
						|
    if args.outdated:
 | 
						|
        list_outdated(args)
 | 
						|
    else:
 | 
						|
        list_default(args)
 | 
						|
 | 
						|
 | 
						|
def action_check(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Checks what tools are installed and if some mandatory tool is missing, exits with return code 1.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    tools_info = filter_tools_info(IDFEnv.get_idf_env(), tools_info)
 | 
						|
    not_found_list = []
 | 
						|
    tool_error = False
 | 
						|
    info('Checking for installed tools...')
 | 
						|
    for name, tool in tools_info.items():
 | 
						|
        if tool.get_install_type() == IDFTool.INSTALL_NEVER:
 | 
						|
            continue
 | 
						|
        tool_found_somewhere = False
 | 
						|
        info(f'Checking tool {name}')
 | 
						|
        try:
 | 
						|
            tool.find_installed_versions()
 | 
						|
        except ToolBinaryError:
 | 
						|
            tool_error = True
 | 
						|
        if tool.version_in_path:
 | 
						|
            info(f'    version found in PATH: {tool.version_in_path}')
 | 
						|
            tool_found_somewhere = True
 | 
						|
        else:
 | 
						|
            info('    no version found in PATH')
 | 
						|
 | 
						|
        for version in tool.versions_installed:
 | 
						|
            info(f'    version installed in tools directory: {version}')
 | 
						|
            tool_found_somewhere = True
 | 
						|
        if not tool_found_somewhere and tool.get_install_type() == IDFTool.INSTALL_ALWAYS:
 | 
						|
            not_found_list.append(name)
 | 
						|
    if not_found_list:
 | 
						|
        fatal(f'The following required tools were not found: {" ".join(not_found_list)}')
 | 
						|
        raise SystemExit(1)
 | 
						|
    if tool_error:
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
# The following function is used in process_tool which is a part of the action_export.
 | 
						|
def handle_recommended_version_to_use(
 | 
						|
    tool: IDFTool,
 | 
						|
    tool_name: str,
 | 
						|
    version_to_use: str,
 | 
						|
    prefer_system_hint: str,
 | 
						|
) -> Tuple[list, dict]:
 | 
						|
    """
 | 
						|
    If there is unsupported tools version in PATH, prints info about that.
 | 
						|
    """
 | 
						|
    tool_export_paths = tool.get_export_paths(version_to_use)
 | 
						|
    tool_export_vars = tool.get_export_vars(version_to_use)
 | 
						|
    if tool.version_in_path and tool.version_in_path not in tool.versions:
 | 
						|
        info(f'Not using an unsupported version of tool {tool.name} found in PATH: {tool.version_in_path}.' + prefer_system_hint, f=sys.stderr)
 | 
						|
    return tool_export_paths, tool_export_vars
 | 
						|
 | 
						|
 | 
						|
# The following function is used in process_tool which is a part of the action_export.
 | 
						|
def handle_supported_or_deprecated_version(tool: IDFTool, tool_name: str) -> None:
 | 
						|
    """
 | 
						|
    Prints info if supported, but not recommended or deprecated version of the tool is used.
 | 
						|
    """
 | 
						|
    version_obj: IDFToolVersion = tool.versions[tool.version_in_path]  # type: ignore
 | 
						|
    if version_obj.status == IDFToolVersion.STATUS_SUPPORTED:
 | 
						|
        info(f'Using a supported version of tool {tool_name} found in PATH: {tool.version_in_path}.',
 | 
						|
             f=sys.stderr)
 | 
						|
        info(f'However the recommended version is {tool.get_recommended_version()}.',
 | 
						|
             f=sys.stderr)
 | 
						|
    elif version_obj.status == IDFToolVersion.STATUS_DEPRECATED:
 | 
						|
        warn(f'using a deprecated version of tool {tool_name} found in PATH: {tool.version_in_path}')
 | 
						|
 | 
						|
 | 
						|
# The following function is used in process_tool which is a part of the action_export.
 | 
						|
def handle_missing_versions(
 | 
						|
    tool: IDFTool,
 | 
						|
    tool_name: str,
 | 
						|
    install_cmd: str,
 | 
						|
    prefer_system_hint: str
 | 
						|
) -> None:
 | 
						|
    """
 | 
						|
    Prints the info about missing tool to stderr if tool has no supported versions installed.
 | 
						|
    """
 | 
						|
    fatal(f'tool {tool.name} has no installed versions. Please run \'{install_cmd}\' to install it.')
 | 
						|
    if tool.version_in_path and tool.version_in_path not in tool.versions:
 | 
						|
        info(f'An unsupported version of tool {tool_name} was found in PATH: {tool.version_in_path}. ' +
 | 
						|
             prefer_system_hint, f=sys.stderr)
 | 
						|
 | 
						|
 | 
						|
def process_tool(
 | 
						|
    tool: IDFTool,
 | 
						|
    tool_name: str,
 | 
						|
    args: argparse.Namespace,
 | 
						|
    install_cmd: str,
 | 
						|
    prefer_system_hint: str
 | 
						|
) -> Tuple[list, dict, bool]:
 | 
						|
    """
 | 
						|
    Helper function used only in action export.
 | 
						|
    Returns:
 | 
						|
        * Paths that need to be exported.
 | 
						|
        * Dictionary of environment variables that need to be exported for the tool.
 | 
						|
        * Flag if any tool was found.
 | 
						|
    """
 | 
						|
    tool_found: bool = True
 | 
						|
    tool_export_paths: List[str] = []
 | 
						|
    tool_export_vars: Dict[str, str] = {}
 | 
						|
 | 
						|
    try:
 | 
						|
        tool.find_installed_versions()
 | 
						|
    except ToolBinaryError:
 | 
						|
        tool_found = False
 | 
						|
    recommended_version_to_use = tool.get_preferred_installed_version()
 | 
						|
 | 
						|
    if not tool.is_executable and recommended_version_to_use:
 | 
						|
        tool_export_vars = tool.get_export_vars(recommended_version_to_use)
 | 
						|
        return tool_export_paths, tool_export_vars, tool_found
 | 
						|
 | 
						|
    if recommended_version_to_use and not args.prefer_system:
 | 
						|
        tool_export_paths, tool_export_vars = handle_recommended_version_to_use(
 | 
						|
            tool, tool_name, recommended_version_to_use, prefer_system_hint
 | 
						|
        )
 | 
						|
        return tool_export_paths, tool_export_vars, tool_found
 | 
						|
 | 
						|
    if tool.version_in_path:
 | 
						|
        if tool.version_in_path not in tool.versions:
 | 
						|
            # unsupported version
 | 
						|
            if args.prefer_system:  # type: ignore
 | 
						|
                warn(f'using an unsupported version of tool {tool.name} found in PATH: {tool.version_in_path}')
 | 
						|
                return tool_export_paths, tool_export_vars, tool_found
 | 
						|
            else:
 | 
						|
                # unsupported version in path
 | 
						|
                pass
 | 
						|
        else:
 | 
						|
            # supported/deprecated version in PATH, use it
 | 
						|
            handle_supported_or_deprecated_version(tool, tool_name)
 | 
						|
            return tool_export_paths, tool_export_vars, tool_found
 | 
						|
 | 
						|
    if not tool.versions_installed:
 | 
						|
        if tool.get_install_type() == IDFTool.INSTALL_ALWAYS:
 | 
						|
            handle_missing_versions(tool, tool_name, install_cmd, prefer_system_hint)
 | 
						|
            tool_found = False
 | 
						|
        # If a tool found, but it is optional and does not have versions installed, use whatever is in PATH.
 | 
						|
        return tool_export_paths, tool_export_vars, tool_found
 | 
						|
 | 
						|
    return tool_export_paths, tool_export_vars, tool_found
 | 
						|
 | 
						|
 | 
						|
def check_python_venv_compatibility(idf_python_env_path: str, idf_version: str) -> None:
 | 
						|
    try:
 | 
						|
        with open(os.path.join(idf_python_env_path, VENV_VER_FILE), 'r') as f:
 | 
						|
            read_idf_version = f.read().strip()
 | 
						|
        if read_idf_version != idf_version:
 | 
						|
            fatal(f'Python environment is set to {idf_python_env_path} which was generated for '
 | 
						|
                  f'ESP-IDF {read_idf_version} instead of the current {idf_version}. '
 | 
						|
                  'The issue can be solved by (1) removing the directory and re-running the install script, '
 | 
						|
                  'or (2) unsetting the IDF_PYTHON_ENV_PATH environment variable, or (3) '
 | 
						|
                  're-runing the install script from a clean shell where an ESP-IDF environment is '
 | 
						|
                  'not active.')
 | 
						|
            raise SystemExit(1)
 | 
						|
    except OSError as e:
 | 
						|
        # perhaps the environment was generated before the support for VENV_VER_FILE was added
 | 
						|
        warn(f'Error while accessing the ESP-IDF version file in the Python environment: {e}')
 | 
						|
 | 
						|
 | 
						|
def action_export(args: Any) -> None:
 | 
						|
    """
 | 
						|
    Exports all necessary environment variables and paths needed for tools used.
 | 
						|
    """
 | 
						|
    if args.deactivate and different_idf_detected():
 | 
						|
        deactivate_statement(args)
 | 
						|
        return
 | 
						|
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    tools_info = filter_tools_info(IDFEnv.get_idf_env(), tools_info)
 | 
						|
    all_tools_found = True
 | 
						|
    export_vars: Dict[str, str] = {}
 | 
						|
    paths_to_export = []
 | 
						|
 | 
						|
    self_restart_cmd = f'{sys.executable} {__file__}{(" --tools-json {args.tools_json}") if args.tools_json else ""}'
 | 
						|
    self_restart_cmd = to_shell_specific_paths([self_restart_cmd])[0]
 | 
						|
    prefer_system_hint = '' if IDF_TOOLS_EXPORT_CMD else f' To use it, run \'{self_restart_cmd} export --prefer-system\''
 | 
						|
    install_cmd = to_shell_specific_paths([IDF_TOOLS_INSTALL_CMD])[0] if IDF_TOOLS_INSTALL_CMD else f'{self_restart_cmd} install'
 | 
						|
 | 
						|
    for name, tool in tools_info.items():
 | 
						|
        if tool.get_install_type() == IDFTool.INSTALL_NEVER:
 | 
						|
            continue
 | 
						|
        tool_export_paths, tool_export_vars, tool_found = process_tool(tool, name, args, install_cmd, prefer_system_hint)
 | 
						|
        if not tool_found:
 | 
						|
            all_tools_found = False
 | 
						|
        paths_to_export += tool_export_paths
 | 
						|
        export_vars = {**export_vars, **tool_export_vars}
 | 
						|
 | 
						|
    if not all_tools_found:
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    current_path = os.getenv('PATH')
 | 
						|
    idf_python_env_path, idf_python_export_path, virtualenv_python, _ = get_python_env_path()
 | 
						|
    if os.path.exists(virtualenv_python):
 | 
						|
        idf_python_env_path = to_shell_specific_paths([idf_python_env_path])[0]
 | 
						|
        if os.getenv('IDF_PYTHON_ENV_PATH') != idf_python_env_path:
 | 
						|
            export_vars['IDF_PYTHON_ENV_PATH'] = to_shell_specific_paths([idf_python_env_path])[0]
 | 
						|
        if current_path and idf_python_export_path not in current_path:  # getenv can return None
 | 
						|
            paths_to_export.append(idf_python_export_path)
 | 
						|
 | 
						|
    idf_version = get_idf_version()
 | 
						|
    if os.getenv('ESP_IDF_VERSION') != idf_version:
 | 
						|
        export_vars['ESP_IDF_VERSION'] = idf_version
 | 
						|
 | 
						|
    check_python_venv_compatibility(idf_python_env_path, idf_version)
 | 
						|
 | 
						|
    idf_tools_dir = os.path.join(g.idf_path, 'tools')  # type: ignore
 | 
						|
    idf_tools_dir = to_shell_specific_paths([idf_tools_dir])[0]
 | 
						|
    if current_path and idf_tools_dir not in current_path:
 | 
						|
        paths_to_export.append(idf_tools_dir)
 | 
						|
 | 
						|
    if sys.platform == 'win32':
 | 
						|
        old_path = '%PATH%'
 | 
						|
        path_sep = ';'
 | 
						|
    else:
 | 
						|
        old_path = '$PATH'
 | 
						|
        path_sep = ':'
 | 
						|
 | 
						|
    export_format, export_sep = get_export_format_and_separator(args)
 | 
						|
 | 
						|
    if paths_to_export:
 | 
						|
        export_vars['PATH'] = path_sep.join(to_shell_specific_paths(paths_to_export) + [old_path])
 | 
						|
        # Correct PATH order check for Windows platform
 | 
						|
        # idf-exe has to be before \tools in PATH
 | 
						|
        if sys.platform == 'win32':
 | 
						|
            paths_to_check = rf'{export_vars["PATH"]}{os.environ["PATH"]}'
 | 
						|
            try:
 | 
						|
                if paths_to_check.index(r'\tools;') < paths_to_check.index(r'\idf-exe'):
 | 
						|
                    warn('The PATH is not in correct order (idf-exe should be before esp-idf\\tools)')
 | 
						|
            except ValueError:
 | 
						|
                fatal(f'Both of the directories (..\\idf-exe\\.. and ..\\tools) has to be in the PATH:\n\n{paths_to_check}\n')
 | 
						|
 | 
						|
    if export_vars:
 | 
						|
        # if not copy of export_vars is given to function, it brekas the formatting string for 'export_statements'
 | 
						|
        deactivate_file_path = add_variables_to_deactivate_file(args, export_vars.copy())
 | 
						|
        export_vars[ENVState.env_key] = deactivate_file_path
 | 
						|
        export_statements = export_sep.join([export_format.format(k, v) for k, v in export_vars.items()])
 | 
						|
        print(export_statements)
 | 
						|
 | 
						|
 | 
						|
def get_idf_download_url_apply_mirrors(args: Any = None, download_url: str = IDF_DL_URL) -> str:
 | 
						|
    """
 | 
						|
    Returns URL for ESP-IDF download with applied mirrors if available.
 | 
						|
    If original URL pointed to Github and IDF_GITHUB_ASSETS is set, change the source to Espressif's download servers.
 | 
						|
    """
 | 
						|
    url = apply_mirror_prefix_map(args, download_url)
 | 
						|
    url = apply_github_assets_option(url)
 | 
						|
    return url
 | 
						|
 | 
						|
 | 
						|
def apply_mirror_prefix_map(args: Any, idf_download_url: str) -> str:
 | 
						|
    """
 | 
						|
    Rewrite URL for given idf_download_url.
 | 
						|
    If --mirror-prefix-map flag or IDF_MIRROR_PREFIX_MAP environment variable is given.
 | 
						|
    """
 | 
						|
    new_url = idf_download_url
 | 
						|
    mirror_prefix_map = None
 | 
						|
    mirror_prefix_map_env = os.getenv('IDF_MIRROR_PREFIX_MAP')
 | 
						|
    if mirror_prefix_map_env:
 | 
						|
        mirror_prefix_map = mirror_prefix_map_env.split(';')
 | 
						|
    if IDF_MAINTAINER and args and args.mirror_prefix_map:
 | 
						|
        if mirror_prefix_map:
 | 
						|
            warn('Both IDF_MIRROR_PREFIX_MAP environment variable and --mirror-prefix-map flag are specified, '
 | 
						|
                 'will use the value from the command line.')
 | 
						|
        mirror_prefix_map = args.mirror_prefix_map
 | 
						|
    if mirror_prefix_map:
 | 
						|
        for item in mirror_prefix_map:
 | 
						|
            if URL_PREFIX_MAP_SEPARATOR not in item:
 | 
						|
                warn(f'invalid mirror-prefix-map item (missing \'{URL_PREFIX_MAP_SEPARATOR}\') {item}')
 | 
						|
                continue
 | 
						|
            search, replace = item.split(URL_PREFIX_MAP_SEPARATOR, 1)
 | 
						|
            replace = replace.replace('\\', '\\\\')  # On windows replace single \ with double \\
 | 
						|
            new_url = re.sub(search, replace, idf_download_url)
 | 
						|
            if new_url != idf_download_url:
 | 
						|
                info(f'Changed download URL: {idf_download_url} => {new_url}')
 | 
						|
                break
 | 
						|
    return new_url
 | 
						|
 | 
						|
 | 
						|
def apply_github_assets_option(idf_download_url: str) -> str:
 | 
						|
    """
 | 
						|
    Rewrite URL for given idf_download_url if the download URL is an https://github.com/ URL and the variable
 | 
						|
    IDF_GITHUB_ASSETS is set. The github.com part of the URL will be replaced.
 | 
						|
    """
 | 
						|
    new_url = idf_download_url
 | 
						|
    github_assets = os.environ.get('IDF_GITHUB_ASSETS', '').strip()
 | 
						|
    if not github_assets:
 | 
						|
        # no IDF_GITHUB_ASSETS or variable exists but is empty
 | 
						|
        return new_url
 | 
						|
 | 
						|
    # check no URL qualifier in the mirror URL
 | 
						|
    if '://' in github_assets:
 | 
						|
        fatal("IDF_GITHUB_ASSETS shouldn't include any URL qualifier, https:// is assumed")
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    # Strip any trailing / from the mirror URL
 | 
						|
    github_assets = github_assets.rstrip('/')
 | 
						|
 | 
						|
    new_url = re.sub(r'^https://github.com/', f'https://{github_assets}/', idf_download_url)
 | 
						|
    if new_url != idf_download_url:
 | 
						|
        info(f'Using GitHub assets mirror for URL: {idf_download_url} => {new_url}')
 | 
						|
    return new_url
 | 
						|
 | 
						|
 | 
						|
def get_tools_spec_and_platform_info(selected_platform: str, targets: List[str], tools_spec: List[str],
 | 
						|
                                     quiet: bool = False) -> Tuple[List[str], Dict[str, IDFTool]]:
 | 
						|
    """
 | 
						|
    Returns tools_spec list and dict of tools for selected platform in form tool_name : IDFTool object.
 | 
						|
    NOTE: If this function is not called from action_download, but is used just for detecting active tools, info about downloading is unwanted.
 | 
						|
    """
 | 
						|
    global global_quiet
 | 
						|
    try:
 | 
						|
        old_global_quiet = g.quiet
 | 
						|
        g.quiet = quiet
 | 
						|
        tools_info = load_tools_info()
 | 
						|
        tools_info_for_platform = OrderedDict()
 | 
						|
        for name, tool_obj in tools_info.items():
 | 
						|
            tool_for_platform = tool_obj.copy_for_platform(selected_platform)
 | 
						|
            tools_info_for_platform[name] = tool_for_platform
 | 
						|
 | 
						|
        tools_spec = expand_tools_arg(tools_spec, tools_info_for_platform, targets)
 | 
						|
        info(f'Downloading tools for {selected_platform}: {", ".join(tools_spec)}')
 | 
						|
    finally:
 | 
						|
        g.quiet = old_global_quiet
 | 
						|
 | 
						|
    return tools_spec, tools_info_for_platform
 | 
						|
 | 
						|
 | 
						|
def action_download(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Saves current IDF environment and for every tools in tools_spec, downloads the right archive for tools version and target platform, if possible.
 | 
						|
    If not, prints apropriate message to stderr  and raise SystemExit() expception.
 | 
						|
    """
 | 
						|
    tools_spec = parse_tools_arg(args.tools)
 | 
						|
 | 
						|
    targets: List[str] = []
 | 
						|
    # Saving IDFEnv::targets for selected ESP_targets if all tools have been specified
 | 
						|
    if 'required' in tools_spec or 'all' in tools_spec:
 | 
						|
        idf_env_obj = IDFEnv.get_idf_env()
 | 
						|
        targets = add_and_check_targets(idf_env_obj, args.targets)
 | 
						|
        try:
 | 
						|
            idf_env_obj.save()
 | 
						|
        except OSError as err:
 | 
						|
            if args.targets in targets:
 | 
						|
                targets.remove(args.targets)
 | 
						|
            warn(f'Downloading tools for targets was not successful with error: {err}')
 | 
						|
    # Taking into account ESP_targets but not saving them for individual tools (specified list of tools)
 | 
						|
    else:
 | 
						|
        targets = parse_targets_arg(args.targets)
 | 
						|
 | 
						|
    platform = parse_platform_arg(args.platform)
 | 
						|
 | 
						|
    tools_spec, tools_info_for_platform = get_tools_spec_and_platform_info(platform, targets, tools_spec)
 | 
						|
    for tool_spec in tools_spec:
 | 
						|
        if '@' not in tool_spec:
 | 
						|
            tool_name = tool_spec
 | 
						|
            tool_version = None
 | 
						|
        else:
 | 
						|
            tool_name, tool_version = tool_spec.split('@', 1)
 | 
						|
        if tool_name not in tools_info_for_platform:
 | 
						|
            fatal(f'unknown tool name: {tool_name}')
 | 
						|
            raise SystemExit(1)
 | 
						|
        tool_obj = tools_info_for_platform[tool_name]
 | 
						|
        if tool_version is not None and tool_version not in tool_obj.versions:
 | 
						|
            fatal(f'unknown version for tool {tool_name}: {tool_version}')
 | 
						|
            raise SystemExit(1)
 | 
						|
        if tool_version is None:
 | 
						|
            tool_version = tool_obj.get_recommended_version()
 | 
						|
        if tool_version is None:
 | 
						|
            fatal(f'tool {tool_name} not found for {platform} platform')
 | 
						|
            raise SystemExit(1)
 | 
						|
        tool_spec = f'{tool_name}@{tool_version}'
 | 
						|
 | 
						|
        info(f'Downloading {tool_spec}')
 | 
						|
        _idf_tool_obj = tool_obj.versions[tool_version].get_download_for_platform(platform)
 | 
						|
        _idf_tool_obj.url = get_idf_download_url_apply_mirrors(args, _idf_tool_obj.url)
 | 
						|
 | 
						|
        tool_obj.download(tool_version)
 | 
						|
 | 
						|
 | 
						|
def action_install(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Saves current IDF environment and for every tool in tools_spec, installs the tool from the archive downloaded before, if possible.
 | 
						|
    If not, raises SystemExit.
 | 
						|
    """
 | 
						|
    tools_spec = parse_tools_arg(args.tools)
 | 
						|
 | 
						|
    targets: List[str] = []
 | 
						|
    # Saving IDFEnv::targets for selected ESP_targets if all tools have been specified
 | 
						|
    if 'required' in tools_spec or 'all' in tools_spec:
 | 
						|
        idf_env_obj = IDFEnv.get_idf_env()
 | 
						|
        targets = add_and_check_targets(idf_env_obj, args.targets)
 | 
						|
        try:
 | 
						|
            idf_env_obj.save()
 | 
						|
        except OSError as err:
 | 
						|
            if args.targets in targets:
 | 
						|
                targets.remove(args.targets)
 | 
						|
            warn(f'Installing targets was not successful with error: {err}')
 | 
						|
        info(f'Selected targets are: {", ".join(targets)}')
 | 
						|
    # Taking into account ESP_targets but not saving them for individual tools (specified list of tools)
 | 
						|
    else:
 | 
						|
        targets = parse_targets_arg(args.targets)
 | 
						|
 | 
						|
    info(f'Current system platform: {CURRENT_PLATFORM}')
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    tools_spec = expand_tools_arg(tools_spec, tools_info, targets)
 | 
						|
    info(f'Installing tools: {", ".join(tools_spec)}')
 | 
						|
    tool_error = False
 | 
						|
    for tool_spec in tools_spec:
 | 
						|
        if '@' not in tool_spec:
 | 
						|
            tool_name = tool_spec
 | 
						|
            tool_version = None
 | 
						|
        else:
 | 
						|
            tool_name, tool_version = tool_spec.split('@', 1)
 | 
						|
        if tool_name not in tools_info:
 | 
						|
            fatal(f'unknown tool name: {tool_name}')
 | 
						|
            raise SystemExit(1)
 | 
						|
        tool_obj = tools_info[tool_name]
 | 
						|
        if not tool_obj.compatible_with_platform():
 | 
						|
            fatal(f'tool {tool_name} does not have versions compatible with platform {CURRENT_PLATFORM}')
 | 
						|
            raise SystemExit(1)
 | 
						|
        if tool_version is not None and tool_version not in tool_obj.versions:
 | 
						|
            fatal(f'unknown version for tool {tool_name}: {tool_version}')
 | 
						|
            raise SystemExit(1)
 | 
						|
        if tool_version is None:
 | 
						|
            tool_version = tool_obj.get_recommended_version()
 | 
						|
        assert tool_version is not None
 | 
						|
        try:
 | 
						|
            tool_obj.find_installed_versions()
 | 
						|
        except ToolBinaryError:
 | 
						|
            tool_error = True
 | 
						|
        tool_spec = f'{tool_name}@{tool_version}'
 | 
						|
        if tool_version in tool_obj.versions_installed:
 | 
						|
            info(f'Skipping {tool_spec} (already installed)')
 | 
						|
            continue
 | 
						|
 | 
						|
        info(f'Installing {tool_spec}')
 | 
						|
        _idf_tool_obj = tool_obj.versions[tool_version].get_download_for_platform(PYTHON_PLATFORM)
 | 
						|
        _idf_tool_obj.url = get_idf_download_url_apply_mirrors(args, _idf_tool_obj.url)
 | 
						|
 | 
						|
        tool_obj.download(tool_version)
 | 
						|
        tool_obj.install(tool_version)
 | 
						|
 | 
						|
    if tool_error:
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def get_wheels_dir() -> Optional[str]:
 | 
						|
    """
 | 
						|
    Gets path for idf-python-wheels package.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    wheels_package_name = 'idf-python-wheels'
 | 
						|
    if wheels_package_name not in tools_info:
 | 
						|
        return None
 | 
						|
    wheels_package = tools_info[wheels_package_name]
 | 
						|
    recommended_version = wheels_package.get_recommended_version()
 | 
						|
    if recommended_version is None:
 | 
						|
        return None
 | 
						|
    wheels_dir = wheels_package.get_path_for_version(recommended_version)
 | 
						|
    if not os.path.exists(wheels_dir):
 | 
						|
        return None
 | 
						|
    return wheels_dir
 | 
						|
 | 
						|
 | 
						|
def get_requirements(new_features: str) -> List[str]:
 | 
						|
    """
 | 
						|
    Returns list of path for requirements.txt for given feature list.
 | 
						|
    """
 | 
						|
    idf_env_obj = IDFEnv.get_idf_env()
 | 
						|
    features = process_and_check_features(idf_env_obj, new_features)
 | 
						|
    try:
 | 
						|
        idf_env_obj.save()
 | 
						|
    except OSError as err:
 | 
						|
        if new_features in features:
 | 
						|
            features.remove(new_features)
 | 
						|
        warn(f'Updating features was not successful with error: {err}')
 | 
						|
    return [feature_to_requirements_path(feature) for feature in features]
 | 
						|
 | 
						|
 | 
						|
def get_constraints(idf_version: str, online: bool = True) -> str:
 | 
						|
    """
 | 
						|
    Download constraints file for specified IDF vversion if it was not downloaded recently (1 day),
 | 
						|
    check success and place it in constraints file location.
 | 
						|
    """
 | 
						|
    idf_download_url = get_idf_download_url_apply_mirrors()
 | 
						|
    constraint_file = f'espidf.constraints.v{idf_version}.txt'
 | 
						|
    constraint_path = os.path.join(g.idf_tools_path, constraint_file)
 | 
						|
    constraint_url = '/'.join([idf_download_url, constraint_file])
 | 
						|
    temp_path = f'{constraint_path}.tmp'
 | 
						|
 | 
						|
    if not online:
 | 
						|
        if os.path.isfile(constraint_path):
 | 
						|
            return constraint_path
 | 
						|
        else:
 | 
						|
            fatal(f'{constraint_path} doesn\'t exist. Perhaps you\'ve forgotten to run the install scripts. '
 | 
						|
                  f'Please check the installation guide for more information.')
 | 
						|
            raise SystemExit(1)
 | 
						|
 | 
						|
    mkdir_p(os.path.dirname(temp_path))
 | 
						|
 | 
						|
    try:
 | 
						|
        age = datetime.date.today() - datetime.date.fromtimestamp(os.path.getmtime(constraint_path))
 | 
						|
        if age < datetime.timedelta(days=1):
 | 
						|
            info(f'Skipping the download of {constraint_path} because it was downloaded recently.')
 | 
						|
            return constraint_path
 | 
						|
    except OSError:
 | 
						|
        # doesn't exist or inaccessible
 | 
						|
        pass
 | 
						|
 | 
						|
    for _ in range(DOWNLOAD_RETRY_COUNT):
 | 
						|
        err = download(constraint_url, temp_path)
 | 
						|
        if not os.path.isfile(temp_path):
 | 
						|
            warn(f'Download failure: {err}')
 | 
						|
            warn(f'Failed to download {constraint_url} to {temp_path}')
 | 
						|
            continue
 | 
						|
        if os.path.isfile(constraint_path):
 | 
						|
            # Windows cannot rename to existing file. It needs to be deleted.
 | 
						|
            os.remove(constraint_path)
 | 
						|
        rename_with_retry(temp_path, constraint_path)
 | 
						|
        return constraint_path
 | 
						|
 | 
						|
    if os.path.isfile(constraint_path):
 | 
						|
        warn('Failed to download, retry count has expired, using a previously downloaded version')
 | 
						|
        return constraint_path
 | 
						|
    else:
 | 
						|
        fatal('Failed to download, and retry count has expired')
 | 
						|
        print_hints_on_download_error(str(err))
 | 
						|
        info('See the help on how to disable constraints in order to work around this issue.')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def install_legacy_python_virtualenv(path: str) -> None:
 | 
						|
    """
 | 
						|
    Checks if pip is installed (and installs it if not), checks whether virtualenv is already installed (and in which version),
 | 
						|
    and finally creates virtual environment with python -m virtualenv <virtualenv_options>.
 | 
						|
    """
 | 
						|
    # Before creating the virtual environment, check if pip is installed.
 | 
						|
    try:
 | 
						|
        subprocess.check_call([sys.executable, '-m', 'pip', '--version'])
 | 
						|
    except subprocess.CalledProcessError:
 | 
						|
        fatal(f'Python interpreter at {sys.executable} doesn\'t have pip installed. '
 | 
						|
              'Please check the Getting Started Guides for the steps to install prerequisites for your OS.')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    virtualenv_installed_via_pip = False
 | 
						|
    try:
 | 
						|
        import virtualenv  # noqa: F401
 | 
						|
    except ImportError:
 | 
						|
        info('Installing virtualenv')
 | 
						|
        subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user', 'virtualenv'],
 | 
						|
                              stdout=sys.stdout, stderr=sys.stderr)
 | 
						|
        virtualenv_installed_via_pip = True
 | 
						|
        # since we just installed virtualenv via pip, we know that version is recent enough
 | 
						|
        # so the version check below is not necessary.
 | 
						|
 | 
						|
    with_seeder_option = True
 | 
						|
    if not virtualenv_installed_via_pip:
 | 
						|
        # virtualenv is already present in the system and may have been installed via OS package manager
 | 
						|
        # check the version to determine if we should add --seeder option
 | 
						|
        try:
 | 
						|
            major_ver = int(virtualenv.__version__.split('.')[0])
 | 
						|
            if major_ver < 20:
 | 
						|
                warn(f'Virtualenv version {virtualenv.__version__} is old, please consider upgrading it')
 | 
						|
                with_seeder_option = False
 | 
						|
        except (ValueError, NameError, AttributeError, IndexError):
 | 
						|
            pass
 | 
						|
 | 
						|
    info(f'Creating a new Python environment using virtualenv in {path}')
 | 
						|
    virtualenv_options = ['--python', sys.executable]
 | 
						|
    if with_seeder_option:
 | 
						|
        virtualenv_options += ['--seeder', 'pip']
 | 
						|
 | 
						|
    subprocess.check_call([sys.executable, '-m', 'virtualenv',
 | 
						|
                           *virtualenv_options,
 | 
						|
                           path],
 | 
						|
                          stdout=sys.stdout, stderr=sys.stderr)
 | 
						|
 | 
						|
 | 
						|
def action_install_python_env(args):  # type: ignore
 | 
						|
    """
 | 
						|
    (Re)installs python virtual environment.
 | 
						|
    If Python virtualenv is already installed, checks for errors (missing/corrupted python interpreter, pip...)
 | 
						|
    and reinstalls if needed. Removes current virtualenv before reinstalling.
 | 
						|
    """
 | 
						|
    use_constraints = not args.no_constraints
 | 
						|
    reinstall = args.reinstall
 | 
						|
    idf_python_env_path, _, virtualenv_python, idf_version = get_python_env_path()
 | 
						|
 | 
						|
    is_virtualenv = hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
 | 
						|
    if is_virtualenv and (not os.path.exists(idf_python_env_path) or reinstall):
 | 
						|
        fatal('This script was called from a virtual environment, can not create a virtual environment again')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    if os.path.exists(virtualenv_python):
 | 
						|
        try:
 | 
						|
            subprocess.check_call([virtualenv_python, '--version'], stdout=sys.stdout, stderr=sys.stderr)
 | 
						|
        except (OSError, subprocess.CalledProcessError):
 | 
						|
            # At this point we can reinstall the virtual environment if it is non-functional. This can happen at least
 | 
						|
            # when the Python interpreter which was used to create the virtual environment was removed.
 | 
						|
            reinstall = True
 | 
						|
 | 
						|
        try:
 | 
						|
            subprocess.check_call([virtualenv_python, '-m', 'pip', '--version'], stdout=sys.stdout, stderr=sys.stderr)
 | 
						|
        except subprocess.CalledProcessError:
 | 
						|
            warn('pip is not available in the existing virtual environment, new virtual environment will be created.')
 | 
						|
            # Reinstallation of the virtual environment could help if pip was installed for the main Python
 | 
						|
            reinstall = True
 | 
						|
 | 
						|
        if sys.platform != 'win32':
 | 
						|
            try:
 | 
						|
                subprocess.check_call([virtualenv_python, '-c', 'import curses'], stdout=sys.stdout, stderr=sys.stderr)
 | 
						|
            except subprocess.CalledProcessError:
 | 
						|
                warn('curses can not be imported, new virtual environment will be created.')
 | 
						|
                reinstall = True
 | 
						|
 | 
						|
    if reinstall and os.path.exists(idf_python_env_path):
 | 
						|
        warn(f'Removing the existing Python environment in {idf_python_env_path}')
 | 
						|
        shutil.rmtree(idf_python_env_path)
 | 
						|
 | 
						|
    venv_can_upgrade = False
 | 
						|
 | 
						|
    if os.path.exists(virtualenv_python):
 | 
						|
        check_python_venv_compatibility(idf_python_env_path, idf_version)
 | 
						|
    else:
 | 
						|
        if subprocess.run([sys.executable, '-m', 'venv', '-h'], check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0:
 | 
						|
            # venv available
 | 
						|
            virtualenv_options = ['--clear']  # delete environment if already exists
 | 
						|
            if sys.version_info[:2] >= (3, 9):
 | 
						|
                # upgrade pip & setuptools
 | 
						|
                virtualenv_options += ['--upgrade-deps']
 | 
						|
                venv_can_upgrade = True
 | 
						|
 | 
						|
            info(f'Creating a new Python environment in {idf_python_env_path}')
 | 
						|
 | 
						|
            try:
 | 
						|
                environ_idf_python_env_path = os.environ['IDF_PYTHON_ENV_PATH']
 | 
						|
                correct_env_path = environ_idf_python_env_path.endswith(PYTHON_VENV_DIR_TEMPLATE.format(idf_version,
 | 
						|
                                                                                                        PYTHON_VER_MAJOR_MINOR))
 | 
						|
                if not correct_env_path and re.search(PYTHON_VENV_DIR_TEMPLATE.format(r'\d+\.\d+', r'\d+\.\d+'),
 | 
						|
                                                      environ_idf_python_env_path):
 | 
						|
                    warn(f'IDF_PYTHON_ENV_PATH is set to {environ_idf_python_env_path} but it does not match '
 | 
						|
                         f'the detected {idf_version} ESP-IDF version and/or the used {PYTHON_VER_MAJOR_MINOR} '
 | 
						|
                         'version of Python. If you have not set IDF_PYTHON_ENV_PATH intentionally then it is '
 | 
						|
                         'recommended to re-run this script from a clean shell where an ESP-IDF environment is '
 | 
						|
                         'not active.')
 | 
						|
 | 
						|
            except KeyError:
 | 
						|
                # if IDF_PYTHON_ENV_PATH not defined then the above checks can be skipped
 | 
						|
                pass
 | 
						|
 | 
						|
            subprocess.check_call([sys.executable, '-m', 'venv',
 | 
						|
                                  *virtualenv_options,
 | 
						|
                                  idf_python_env_path],
 | 
						|
                                  stdout=sys.stdout, stderr=sys.stderr)
 | 
						|
 | 
						|
            try:
 | 
						|
                with open(os.path.join(idf_python_env_path, VENV_VER_FILE), 'w') as f:
 | 
						|
                    f.write(idf_version)
 | 
						|
            except OSError as e:
 | 
						|
                warn(f'Error while generating the ESP-IDF version file in the Python environment: {e}')
 | 
						|
 | 
						|
        else:
 | 
						|
            # The embeddable Python for Windows doesn't have the built-in venv module
 | 
						|
            install_legacy_python_virtualenv(idf_python_env_path)
 | 
						|
 | 
						|
    env_copy = os.environ.copy()
 | 
						|
    if env_copy.get('PIP_USER')  == 'yes':
 | 
						|
        warn('Found PIP_USER="yes" in the environment. Disabling PIP_USER in this shell to install packages into a virtual environment.')
 | 
						|
        env_copy['PIP_USER'] = 'no'
 | 
						|
 | 
						|
    if not venv_can_upgrade:
 | 
						|
        info('Upgrading pip and setuptools...')
 | 
						|
        subprocess.check_call([virtualenv_python, '-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools'],
 | 
						|
                              stdout=sys.stdout, stderr=sys.stderr, env=env_copy)
 | 
						|
 | 
						|
    run_args = [virtualenv_python, '-m', 'pip', 'install', '--no-warn-script-location']
 | 
						|
    requirements_file_list = get_requirements(args.features)
 | 
						|
    for requirement_file in requirements_file_list:
 | 
						|
        run_args += ['-r', requirement_file]
 | 
						|
    if use_constraints:
 | 
						|
        constraint_file = get_constraints(idf_version)
 | 
						|
        run_args += ['--upgrade', '--constraint', constraint_file]
 | 
						|
    if args.extra_wheels_dir:
 | 
						|
        run_args += ['--find-links', args.extra_wheels_dir]
 | 
						|
    if args.no_index:
 | 
						|
        run_args += ['--no-index']
 | 
						|
    if args.extra_wheels_url:
 | 
						|
        run_args += ['--extra-index-url', args.extra_wheels_url]
 | 
						|
 | 
						|
    wheels_dir = get_wheels_dir()
 | 
						|
    if wheels_dir is not None:
 | 
						|
        run_args += ['--find-links', wheels_dir]
 | 
						|
 | 
						|
    info('Installing Python packages')
 | 
						|
    if use_constraints:
 | 
						|
        info(f' Constraint file: {constraint_file}')
 | 
						|
    info(' Requirement files:')
 | 
						|
    info(os.linesep.join(f'  - {path}' for path in requirements_file_list))
 | 
						|
    subprocess.check_call(run_args, stdout=sys.stdout, stderr=sys.stderr, env=env_copy)
 | 
						|
 | 
						|
 | 
						|
def action_check_python_dependencies(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Checks if all the dependencies (from requirements, constraints...) are installed properly.
 | 
						|
    Raises SystemExit if not.
 | 
						|
    """
 | 
						|
    use_constraints = not args.no_constraints
 | 
						|
    req_paths = get_requirements('')  # no new features -> just detect the existing ones
 | 
						|
 | 
						|
    _, _, virtualenv_python, idf_version = get_python_env_path()
 | 
						|
 | 
						|
    if not os.path.isfile(virtualenv_python):
 | 
						|
        fatal(f'{virtualenv_python} doesn\'t exist! Please run the install script or "idf_tools.py install-python-env" in order to create it')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    if use_constraints:
 | 
						|
        constr_path = get_constraints(idf_version, online=False)  # keep offline for checking
 | 
						|
        info(f'Constraint file: {constr_path}')
 | 
						|
 | 
						|
    info('Requirement files:')
 | 
						|
    info(os.linesep.join(f' - {path}' for path in req_paths))
 | 
						|
 | 
						|
    info(f'Python being checked: {virtualenv_python}')
 | 
						|
 | 
						|
    # The dependency checker will be invoked with virtualenv_python. idf_tools.py could have been invoked with a
 | 
						|
    # different one, therefore, importing is not a suitable option.
 | 
						|
    dep_check_cmd = [virtualenv_python,
 | 
						|
                     os.path.join(g.idf_path,
 | 
						|
                                  'tools',
 | 
						|
                                  'check_python_dependencies.py')]
 | 
						|
 | 
						|
    if use_constraints:
 | 
						|
        dep_check_cmd += ['-c', constr_path]
 | 
						|
 | 
						|
    for req_path in req_paths:
 | 
						|
        dep_check_cmd += ['-r', req_path]
 | 
						|
 | 
						|
    try:
 | 
						|
        ret = subprocess.run(dep_check_cmd)
 | 
						|
        if ret and ret.returncode:
 | 
						|
            # returncode is a negative number and system exit output is usually expected be positive.
 | 
						|
            raise SystemExit(-ret.returncode)
 | 
						|
    except FileNotFoundError:
 | 
						|
        # Python environment not yet created
 | 
						|
        fatal('Requirements are not satisfied!')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
class ChecksumCalculator():
 | 
						|
    """
 | 
						|
    A class used to get size/checksum/basename of local artifact files.
 | 
						|
    """
 | 
						|
    def __init__(self, files: List[str]) -> None:
 | 
						|
        self.files = files
 | 
						|
 | 
						|
    def __iter__(self) -> Iterator[Tuple[int, str, str]]:
 | 
						|
        for f in self.files:
 | 
						|
            yield (*get_file_size_sha256(f), os.path.basename(f))
 | 
						|
 | 
						|
 | 
						|
class ChecksumParsingError(RuntimeError):
 | 
						|
    """
 | 
						|
    Raised when checksum cannot be parsed.
 | 
						|
    """
 | 
						|
    pass
 | 
						|
 | 
						|
 | 
						|
class ChecksumFileParser():
 | 
						|
    """
 | 
						|
    A class used to get size/sha256/filename of artifact using checksum-file with format:
 | 
						|
        # <artifact-filename>: <size> bytes
 | 
						|
        <sha256sum-string> *<artifact-filename>
 | 
						|
        ... (2 lines for every artifact) ...
 | 
						|
    """
 | 
						|
    def __init__(self, tool_name: str, url: str) -> None:
 | 
						|
        self.tool_name = tool_name
 | 
						|
 | 
						|
        sha256_file_tmp = os.path.join(g.idf_tools_path, 'tools', 'add-version.sha256.tmp')
 | 
						|
        sha256_file = os.path.abspath(url)
 | 
						|
 | 
						|
        # download sha256 file if URL presented
 | 
						|
        if urlparse(url).scheme:
 | 
						|
            sha256_file = sha256_file_tmp
 | 
						|
            download(url, sha256_file)
 | 
						|
 | 
						|
        with open(sha256_file, 'r') as f:
 | 
						|
            self.checksum = f.read().splitlines()
 | 
						|
 | 
						|
        # remove temp file
 | 
						|
        if os.path.isfile(sha256_file_tmp):
 | 
						|
            os.remove(sha256_file_tmp)
 | 
						|
 | 
						|
    def parseLine(self, regex: str, line: str) -> str:
 | 
						|
        match = re.search(regex, line)
 | 
						|
        if not match:
 | 
						|
            raise ChecksumParsingError(f'Can not parse line "{line}" with regex "{regex}"')
 | 
						|
        return match.group(1)
 | 
						|
 | 
						|
    # parse checksum file with formatting used by crosstool-ng, gdb, ... releases
 | 
						|
    # e.g. https://github.com/espressif/crosstool-NG/releases/download/esp-2021r2/crosstool-NG-esp-2021r2-checksum.sha256
 | 
						|
    def __iter__(self) -> Iterator[Tuple[int, str, str]]:
 | 
						|
        try:
 | 
						|
            for bytes_str, hash_str in zip(self.checksum[0::2], self.checksum[1::2]):
 | 
						|
                bytes_filename = self.parseLine(r'^# (\S*):', bytes_str)
 | 
						|
                hash_filename = self.parseLine(r'^\S* [\* ](\S*)', hash_str)
 | 
						|
                if hash_filename != bytes_filename:
 | 
						|
                    fatal('filename in hash-line and in bytes-line are not the same')
 | 
						|
                    raise SystemExit(1)
 | 
						|
                # crosstool-ng checksum file contains info about few tools
 | 
						|
                # e.g.: "xtensa-esp32-elf", "xtensa-esp32s2-elf"
 | 
						|
                # filter records for file by tool_name to avoid mismatch
 | 
						|
                if not hash_filename.startswith(self.tool_name):
 | 
						|
                    continue
 | 
						|
                size = self.parseLine(r'^# \S*: (\d*) bytes', bytes_str)
 | 
						|
                sha256 = self.parseLine(r'^(\S*) ', hash_str)
 | 
						|
                yield int(size), sha256, hash_filename
 | 
						|
        except (TypeError, AttributeError) as err:
 | 
						|
            fatal(f'Error while parsing, check checksum file ({err})')
 | 
						|
            raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def action_add_version(args: Any) -> None:
 | 
						|
    """
 | 
						|
    Adds new version of the tool to IDFTool entry together with download entry and updating json dump.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    tool_name = args.tool
 | 
						|
    tool_obj = tools_info.get(tool_name)
 | 
						|
    if not tool_obj:
 | 
						|
        info(f'Creating new tool entry for {tool_name}')
 | 
						|
        tool_obj = IDFTool(tool_name, TODO_MESSAGE, IDFTool.INSTALL_ALWAYS,
 | 
						|
                           TODO_MESSAGE, TODO_MESSAGE, [TODO_MESSAGE], TODO_MESSAGE,
 | 
						|
                           [TODO_MESSAGE])
 | 
						|
        tools_info[tool_name] = tool_obj
 | 
						|
    version = args.version
 | 
						|
    version_status = IDFToolVersion.STATUS_SUPPORTED
 | 
						|
    if args.override and len(tool_obj.versions):
 | 
						|
        tool_obj.drop_versions()
 | 
						|
        version_status = IDFToolVersion.STATUS_RECOMMENDED
 | 
						|
    version_obj = tool_obj.versions.get(version)
 | 
						|
    if not version_obj:
 | 
						|
        info(f'Creating new version {version}')
 | 
						|
        version_obj = IDFToolVersion(version, version_status)
 | 
						|
        tool_obj.versions[version] = version_obj
 | 
						|
    url_prefix = args.url_prefix or f'https://{TODO_MESSAGE}/'
 | 
						|
    checksum_info: ChecksumFileParser = (ChecksumFileParser(tool_name, args.checksum_file)
 | 
						|
                                         if args.checksum_file
 | 
						|
                                         else ChecksumCalculator(args.artifact_file))  # type: ignore
 | 
						|
    for file_size, file_sha256, file_name in checksum_info:
 | 
						|
        # Guess which platform this file is for
 | 
						|
        try:
 | 
						|
            found_platform = Platforms.get_by_filename(file_name)
 | 
						|
        except ValueError:
 | 
						|
            info(f'Could not guess platform for file {file_name}')
 | 
						|
            found_platform = TODO_MESSAGE
 | 
						|
        url = urljoin(url_prefix, file_name)
 | 
						|
        info(f'Adding download for platform {found_platform}')
 | 
						|
        info(f'    size:   {file_size}')
 | 
						|
        info(f'    SHA256: {file_sha256}')
 | 
						|
        info(f'    URL:    {url}')
 | 
						|
        version_obj.add_download(found_platform, url, file_size, file_sha256)
 | 
						|
    json_str = dump_tools_json(tools_info)
 | 
						|
    if not args.output:
 | 
						|
        args.output = os.path.join(g.idf_path, TOOLS_FILE_NEW)  # type: ignore
 | 
						|
    with open(args.output, 'w') as f:
 | 
						|
        f.write(json_str)
 | 
						|
        f.write('\n')
 | 
						|
    info(f'Wrote output to {args.output}')
 | 
						|
 | 
						|
 | 
						|
def action_rewrite(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Write JSON dump of all tools from tools_info into file specified by args.output.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    json_str = dump_tools_json(tools_info)
 | 
						|
    if not args.output:
 | 
						|
        args.output = os.path.join(g.idf_path, TOOLS_FILE_NEW)
 | 
						|
    with open(args.output, 'w') as f:
 | 
						|
        f.write(json_str)
 | 
						|
        f.write('\n')
 | 
						|
    info(f'Wrote output to {args.output}')
 | 
						|
 | 
						|
 | 
						|
def action_uninstall(args: Any) -> None:
 | 
						|
    """
 | 
						|
    Print or remove installed tools versions, that are not used by active ESP-IDF version anymore.
 | 
						|
    Additionally remove all older versions of previously downloaded archives.
 | 
						|
    """
 | 
						|
    tools_info = load_tools_info()
 | 
						|
    tools_path = os.path.join(g.idf_tools_path, 'tools')
 | 
						|
    dist_path = os.path.join(g.idf_tools_path, 'dist')
 | 
						|
    installed_tools = os.listdir(tools_path) if os.path.isdir(tools_path) else []
 | 
						|
 | 
						|
    unused_tools_versions = {}
 | 
						|
    for tool in installed_tools:
 | 
						|
        tool_versions = os.listdir(os.path.join(tools_path, tool)) if os.path.isdir(os.path.join(tools_path, tool)) else []
 | 
						|
        try:
 | 
						|
            unused_versions = ([x for x in tool_versions if x != tools_info[tool].get_recommended_version()])
 | 
						|
        except KeyError:  # When tool that is not supported by tools_info (tools.json) anymore, remove the whole tool file
 | 
						|
            unused_versions = ['']
 | 
						|
        if unused_versions:
 | 
						|
            unused_tools_versions[tool] = unused_versions
 | 
						|
 | 
						|
    # Keeping tools added by windows installer
 | 
						|
    KEEP_WIN_TOOLS = ['idf-git', 'idf-python']
 | 
						|
    for tool in KEEP_WIN_TOOLS:
 | 
						|
        if tool in unused_tools_versions:
 | 
						|
            unused_tools_versions.pop(tool)
 | 
						|
 | 
						|
    # Print unused tools.
 | 
						|
    if args.dry_run:
 | 
						|
        if unused_tools_versions:
 | 
						|
            print('For removing old versions of {} use command \'{} {} {}\''.format(', '.join(unused_tools_versions), get_python_exe_and_subdir()[0],
 | 
						|
                  os.path.join(g.idf_path, 'tools', 'idf_tools.py'), 'uninstall'))
 | 
						|
        return
 | 
						|
 | 
						|
    # Remove installed tools that are not used by current ESP-IDF version.
 | 
						|
    for tool in unused_tools_versions:
 | 
						|
        for version in unused_tools_versions[tool]:
 | 
						|
            try:
 | 
						|
                if version:
 | 
						|
                    path_to_remove = os.path.join(tools_path, tool, version)
 | 
						|
                else:
 | 
						|
                    path_to_remove = os.path.join(tools_path, tool)
 | 
						|
                shutil.rmtree(path_to_remove)
 | 
						|
                info(f'{path_to_remove} was removed.')
 | 
						|
            except OSError as error:
 | 
						|
                warn(f'{error.filename} can not be removed because {error.strerror}.')
 | 
						|
 | 
						|
    # Remove old archives versions and archives that are not used by the current ESP-IDF version.
 | 
						|
    if args.remove_archives:
 | 
						|
        tools_spec, tools_info_for_platform = get_tools_spec_and_platform_info(CURRENT_PLATFORM, ['all'], ['all'], quiet=True)
 | 
						|
        used_archives = []
 | 
						|
 | 
						|
        # Detect used active archives
 | 
						|
        for tool_spec in tools_spec:
 | 
						|
            if '@' not in tool_spec:
 | 
						|
                tool_name = tool_spec
 | 
						|
                tool_version = None
 | 
						|
            else:
 | 
						|
                tool_name, tool_version = tool_spec.split('@', 1)
 | 
						|
            tool_obj = tools_info_for_platform[tool_name]
 | 
						|
            if tool_version is None:
 | 
						|
                tool_version = tool_obj.get_recommended_version()
 | 
						|
            # mypy-checks
 | 
						|
            if tool_version is not None:
 | 
						|
                archive_version = tool_obj.versions[tool_version].get_download_for_platform(CURRENT_PLATFORM)
 | 
						|
            if archive_version is not None:
 | 
						|
                archive_version_url = archive_version.url
 | 
						|
 | 
						|
            archive = os.path.basename(archive_version_url)
 | 
						|
            used_archives.append(archive)
 | 
						|
 | 
						|
        downloaded_archives = os.listdir(dist_path)
 | 
						|
        for archive in downloaded_archives:
 | 
						|
            if archive not in used_archives:
 | 
						|
                os.remove(os.path.join(dist_path, archive))
 | 
						|
                info(f'{os.path.join(dist_path, archive)} was removed.')
 | 
						|
 | 
						|
 | 
						|
def action_validate(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Validate whether everything is installed correctly.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        import jsonschema
 | 
						|
    except ImportError:
 | 
						|
        fatal('You need to install jsonschema package to use validate command')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    with open(os.path.join(g.idf_path, TOOLS_FILE), 'r') as tools_file:
 | 
						|
        tools_json = json.load(tools_file)
 | 
						|
 | 
						|
    with open(os.path.join(g.idf_path, TOOLS_SCHEMA_FILE), 'r') as schema_file:
 | 
						|
        schema_json = json.load(schema_file)
 | 
						|
    jsonschema.validate(tools_json, schema_json)
 | 
						|
    # on failure, this will raise an exception with a fairly verbose diagnostic message
 | 
						|
 | 
						|
 | 
						|
def action_gen_doc(args):  # type: ignore
 | 
						|
    """
 | 
						|
    Prints basic documentation and information about every tool from tool_info.
 | 
						|
    """
 | 
						|
    f = args.output
 | 
						|
    tools_info = load_tools_info()
 | 
						|
 | 
						|
    def print_out(text: str) -> None:
 | 
						|
        f.write(f'{text}\n')
 | 
						|
 | 
						|
    print_out('.. |zwsp| unicode:: U+200B')
 | 
						|
    print_out('   :trim:')
 | 
						|
    print_out('')
 | 
						|
 | 
						|
    idf_gh_url = 'https://github.com/espressif/esp-idf'
 | 
						|
    for tool_name, tool_obj in tools_info.items():
 | 
						|
        info_url = tool_obj.options.info_url
 | 
						|
        if f'{idf_gh_url}/tree' in info_url:
 | 
						|
            info_url = re.sub(f'{idf_gh_url}/tree/\\w+/(.*)', r':idf:`\1`', info_url)
 | 
						|
 | 
						|
        license_url = f'https://spdx.org/licenses/{tool_obj.options.license}'
 | 
						|
 | 
						|
        print_out("""
 | 
						|
.. _tool-{name}:
 | 
						|
 | 
						|
{name}
 | 
						|
{underline}
 | 
						|
 | 
						|
{description}
 | 
						|
 | 
						|
.. include:: idf-tools-notes.inc
 | 
						|
   :start-after: tool-{name}-notes
 | 
						|
   :end-before: ---
 | 
						|
 | 
						|
License: `{license} <{license_url}>`_
 | 
						|
 | 
						|
More info: {info_url}
 | 
						|
 | 
						|
.. list-table::
 | 
						|
   :widths: 10 10 80
 | 
						|
   :header-rows: 1
 | 
						|
 | 
						|
   * - Platform
 | 
						|
     - Required
 | 
						|
     - Download
 | 
						|
""".rstrip().format(name=tool_name,
 | 
						|
                    underline=args.heading_underline_char * len(tool_name),
 | 
						|
                    description=tool_obj.description,
 | 
						|
                    license=tool_obj.options.license,
 | 
						|
                    license_url=license_url,
 | 
						|
                    info_url=info_url))
 | 
						|
 | 
						|
        for platform_name in sorted(tool_obj.get_supported_platforms()):
 | 
						|
            platform_tool = tool_obj.copy_for_platform(platform_name)
 | 
						|
            install_type = platform_tool.get_install_type()
 | 
						|
            if install_type == IDFTool.INSTALL_NEVER:
 | 
						|
                continue
 | 
						|
            elif install_type == IDFTool.INSTALL_ALWAYS:
 | 
						|
                install_type_str = 'required'
 | 
						|
            elif install_type == IDFTool.INSTALL_ON_REQUEST:
 | 
						|
                install_type_str = 'optional'
 | 
						|
            else:
 | 
						|
                raise NotImplementedError()
 | 
						|
 | 
						|
            version = platform_tool.get_recommended_version()
 | 
						|
            version_obj = platform_tool.versions[version]
 | 
						|
            download_obj = version_obj.get_download_for_platform(platform_name)
 | 
						|
 | 
						|
            # Note: keep the list entries indented to the same number of columns
 | 
						|
            # as the list header above.
 | 
						|
            print_out("""
 | 
						|
   * - {}
 | 
						|
     - {}
 | 
						|
     - {}
 | 
						|
 | 
						|
       .. rst-class:: tool-sha256
 | 
						|
 | 
						|
          SHA256: {}
 | 
						|
""".strip('\n').format(platform_name, install_type_str, download_obj.url, download_obj.sha256))
 | 
						|
 | 
						|
        print_out('')
 | 
						|
    print_out('')
 | 
						|
 | 
						|
 | 
						|
def action_check_tool_supported(args: Any) -> None:
 | 
						|
    """
 | 
						|
    Print "True"/"False" to stdout as a result that tool is supported in IDF.
 | 
						|
    Print error message to stderr otherwise and set exit code to 1.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        tools_info = load_tools_info()
 | 
						|
        for _, v in tools_info.items():
 | 
						|
            if v.name == args.tool_name:
 | 
						|
                print(v.check_version(args.exec_path))
 | 
						|
                break
 | 
						|
    except (RuntimeError, ToolNotFoundError, ToolExecError) as err:
 | 
						|
        fatal(f'Failed to check tool support: (name: {args.tool_name}, exec: {args.exec_path})')
 | 
						|
        fatal(f'{err}')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def action_get_tool_supported_versions(args: Any) -> None:
 | 
						|
    """
 | 
						|
    Print supported versions of a tool to stdout.
 | 
						|
    Print error message to stderr otherwise and set exit code to 1.
 | 
						|
    """
 | 
						|
    try:
 | 
						|
        tools_info = load_tools_info()
 | 
						|
        for _, v in tools_info.items():
 | 
						|
            if v.name == args.tool_name:
 | 
						|
                print(list(v.versions.keys()))
 | 
						|
                break
 | 
						|
    except RuntimeError as err:
 | 
						|
        fatal(f'Failed to get tool supported versions. (tool: {args.tool_name})')
 | 
						|
        fatal(f'{err}')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
 | 
						|
def main(argv: List[str]) -> None:
 | 
						|
    parser = argparse.ArgumentParser()
 | 
						|
 | 
						|
    parser.add_argument('--quiet', help='Don\'t output diagnostic messages to stdout/stderr', action='store_true')
 | 
						|
    parser.add_argument('--non-interactive', help='Don\'t output interactive messages and questions', action='store_true')
 | 
						|
    parser.add_argument('--tools-json', help='Path to the tools.json file to use')
 | 
						|
    parser.add_argument('--idf-path', help='ESP-IDF path to use')
 | 
						|
 | 
						|
    subparsers = parser.add_subparsers(dest='action')
 | 
						|
    list_parser = subparsers.add_parser('list', help='List tools and versions available')
 | 
						|
    list_parser.add_argument('--outdated', help='Print only outdated installed tools', action='store_true')
 | 
						|
    subparsers.add_parser('check', help='Print summary of tools installed or found in PATH')
 | 
						|
    export = subparsers.add_parser('export', help='Output command for setting tool paths, suitable for shell')
 | 
						|
    export.add_argument('--format', choices=[EXPORT_SHELL, EXPORT_KEY_VALUE], default=EXPORT_SHELL,
 | 
						|
                        help=('Format of the output: shell (suitable for printing into shell), '
 | 
						|
                              'or key-value (suitable for parsing by other tools'))
 | 
						|
    export.add_argument('--prefer-system', help=('Normally, if the tool is already present in PATH, '
 | 
						|
                                                 'but has an unsupported version, a version from the tools directory '
 | 
						|
                                                 'will be used instead. If this flag is given, the version in PATH '
 | 
						|
                                                 'will be used.'), action='store_true')
 | 
						|
    export.add_argument('--deactivate', help='Output command for deactivate different ESP-IDF version, previously set with export', action='store_true')
 | 
						|
    export.add_argument('--unset', help=argparse.SUPPRESS, action='store_true')
 | 
						|
    export.add_argument('--add_paths_extras', help='Add idf-related path extras for deactivate option')
 | 
						|
    install = subparsers.add_parser('install', help='Download and install tools into the tools directory')
 | 
						|
    install.add_argument('tools', metavar='TOOL', nargs='*', default=['required'],
 | 
						|
                         help=('Tools to install.\n'
 | 
						|
                               'To install a specific version use <tool_name>@<version> syntax. '
 | 
						|
                               'To install tools by pattern use wildcards in <tool_name_pattern>. '
 | 
						|
                               'Use empty or \'required\' to install required tools, not optional ones. '
 | 
						|
                               'Use \'all\' to install all tools, including the optional ones.'))
 | 
						|
    install.add_argument('--targets', default='all', help=('A comma separated list of desired chip targets for installing. '
 | 
						|
                                                           'It defaults to installing all supported targets.'))
 | 
						|
 | 
						|
    download = subparsers.add_parser('download', help='Download the tools into the dist directory')
 | 
						|
    download.add_argument('--platform', default=CURRENT_PLATFORM, help='Platform to download the tools for')
 | 
						|
    download.add_argument('tools', metavar='TOOL', nargs='*', default=['required'],
 | 
						|
                          help=('Tools to download. '
 | 
						|
                                'To download a specific version use <tool_name>@<version> syntax. '
 | 
						|
                                'To download tools by pattern use wildcards in <tool_name_pattern> . '
 | 
						|
                                'Use empty or \'required\' to download required tools, not optional ones. '
 | 
						|
                                'Use \'all\' to download all tools, including the optional ones.'))
 | 
						|
    download.add_argument('--targets', default='all', help=('A comma separated list of desired chip targets for installing. '
 | 
						|
                                                            ' It defaults to installing all supported targets.'))
 | 
						|
 | 
						|
    uninstall = subparsers.add_parser('uninstall', help='Remove installed tools, that are not used by current version of ESP-IDF.')
 | 
						|
    uninstall.add_argument('--dry-run', help='Print unused tools.', action='store_true')
 | 
						|
    uninstall.add_argument('--remove-archives', help='Remove old archive versions and archives from unused tools.', action='store_true')
 | 
						|
 | 
						|
    no_constraints_default = os.environ.get('IDF_PYTHON_CHECK_CONSTRAINTS', '').lower() in ['0', 'n', 'no']
 | 
						|
 | 
						|
    if IDF_MAINTAINER:
 | 
						|
        for subparser in [download, install]:
 | 
						|
            subparser.add_argument('--mirror-prefix-map', nargs='*',
 | 
						|
                                   help=('Pattern to rewrite download URLs, with source and replacement separated by comma. '
 | 
						|
                                         'E.g. http://foo.com,http://test.foo.com'))
 | 
						|
 | 
						|
    install_python_env = subparsers.add_parser('install-python-env',
 | 
						|
                                               help=('Create Python virtual environment and install the '
 | 
						|
                                                     'required Python packages'))
 | 
						|
    install_python_env.add_argument('--reinstall', help='Discard the previously installed environment',
 | 
						|
                                    action='store_true')
 | 
						|
    install_python_env.add_argument('--extra-wheels-dir', help=('Additional directories with wheels '
 | 
						|
                                                                'to use during installation'))
 | 
						|
    install_python_env.add_argument('--extra-wheels-url', help='Additional URL with wheels', default=IDF_PIP_WHEELS_URL)
 | 
						|
    install_python_env.add_argument('--no-index', help='Work offline without retrieving wheels index')
 | 
						|
    install_python_env.add_argument('--features', default='core', help=('A comma separated list of desired features for installing. '
 | 
						|
                                                                        'It defaults to installing just the core funtionality.'))
 | 
						|
    install_python_env.add_argument('--no-constraints', action='store_true', default=no_constraints_default,
 | 
						|
                                    help=('Disable constraint settings. Use with care and only when you want to manage '
 | 
						|
                                          'package versions by yourself. It can be set with the IDF_PYTHON_CHECK_CONSTRAINTS '
 | 
						|
                                          'environment variable.'))
 | 
						|
 | 
						|
    if IDF_MAINTAINER:
 | 
						|
        add_version = subparsers.add_parser('add-version', help='Add or update download info for a version')
 | 
						|
        add_version.add_argument('--output', help='Save new tools.json into this file')
 | 
						|
        add_version.add_argument('--tool', help='Tool name to set add a version for', required=True)
 | 
						|
        add_version.add_argument('--version', help='Version identifier', required=True)
 | 
						|
        add_version.add_argument('--url-prefix', help='String to prepend to file names to obtain download URLs')
 | 
						|
        add_version.add_argument('--override', action='store_true', help='Override tool versions with new data')
 | 
						|
        add_version_files_group = add_version.add_mutually_exclusive_group(required=True)
 | 
						|
        add_version_files_group.add_argument('--checksum-file', help='URL or path to local file with checksum/size for artifacts')
 | 
						|
        add_version_files_group.add_argument('--artifact-file', help='File names of the download artifacts', nargs='*')
 | 
						|
 | 
						|
        rewrite = subparsers.add_parser('rewrite', help='Load tools.json, validate, and save the result back into JSON')
 | 
						|
        rewrite.add_argument('--output', help='Save new tools.json into this file')
 | 
						|
 | 
						|
        subparsers.add_parser('validate', help='Validate tools.json against schema file')
 | 
						|
 | 
						|
        gen_doc = subparsers.add_parser('gen-doc', help='Write the list of tools as a documentation page')
 | 
						|
        gen_doc.add_argument('--output', type=argparse.FileType('w'), default=sys.stdout,
 | 
						|
                             help='Output file name')
 | 
						|
        gen_doc.add_argument('--heading-underline-char', help='Character to use when generating RST sections', default='~')
 | 
						|
 | 
						|
    check_python_dependencies = subparsers.add_parser('check-python-dependencies',
 | 
						|
                                                      help='Check that all required Python packages are installed.')
 | 
						|
    check_python_dependencies.add_argument('--no-constraints', action='store_true', default=no_constraints_default,
 | 
						|
                                           help='Disable constraint settings. Use with care and only when you want '
 | 
						|
                                                'to manage package versions by yourself. It can be set with the IDF_PYTHON_CHECK_CONSTRAINTS '
 | 
						|
                                                'environment variable.')
 | 
						|
 | 
						|
    if os.environ.get('IDF_TOOLS_VERSION_HELPER'):
 | 
						|
        check_tool_supported = subparsers.add_parser('check-tool-supported',
 | 
						|
                                                     help='Check that selected tool is compatible with IDF. Writes "True"/"False" to stdout in success.')
 | 
						|
        check_tool_supported.add_argument('--tool-name', required=True, help='Tool name (from tools.json)')
 | 
						|
        check_tool_supported.add_argument('--exec-path', required=True, help='Full path to executable under the test')
 | 
						|
 | 
						|
        get_tool_supported_versions = subparsers.add_parser('get-tool-supported-versions', help='Prints a list of tool\'s supported versions')
 | 
						|
        get_tool_supported_versions.add_argument('--tool-name', required=True,  help='Tool name (from tools.json)')
 | 
						|
 | 
						|
    args = parser.parse_args(argv)
 | 
						|
 | 
						|
    if args.action is None:
 | 
						|
        parser.print_help()
 | 
						|
        parser.exit(1)
 | 
						|
 | 
						|
    g.quiet = args.quiet
 | 
						|
 | 
						|
    g.non_interactive = args.non_interactive
 | 
						|
 | 
						|
    if 'unset' in args and args.unset:
 | 
						|
        args.deactivate = True
 | 
						|
 | 
						|
    g.idf_path = args.idf_path or os.environ.get('IDF_PATH') or os.path.realpath(os.path.join(os.path.dirname(__file__), '..'))
 | 
						|
    os.environ['IDF_PATH'] = g.idf_path
 | 
						|
 | 
						|
    g.idf_tools_path = os.environ.get('IDF_TOOLS_PATH') or os.path.expanduser(IDF_TOOLS_PATH_DEFAULT)
 | 
						|
 | 
						|
    # On macOS, unset __PYVENV_LAUNCHER__ variable if it is set.
 | 
						|
    # Otherwise sys.executable keeps pointing to the system Python, even when a python binary from a virtualenv is invoked.
 | 
						|
    # See https://bugs.python.org/issue22490#msg283859.
 | 
						|
    os.environ.pop('__PYVENV_LAUNCHER__', None)
 | 
						|
 | 
						|
    if sys.version_info.major == 2:
 | 
						|
        try:
 | 
						|
            g.idf_tools_path.decode('ascii')  # type: ignore
 | 
						|
        except UnicodeDecodeError:
 | 
						|
            fatal(f'IDF_TOOLS_PATH contains non-ASCII characters: {g.idf_tools_path}'
 | 
						|
                  '\nThis is not supported yet with Python 2. '
 | 
						|
                  'Please set IDF_TOOLS_PATH to a directory with an ASCII name, or switch to Python 3.')
 | 
						|
            raise SystemExit(1)
 | 
						|
 | 
						|
    if CURRENT_PLATFORM is None:
 | 
						|
        fatal(f'Platform {PYTHON_PLATFORM} appears to be unsupported')
 | 
						|
        raise SystemExit(1)
 | 
						|
 | 
						|
    if args.tools_json:
 | 
						|
        g.tools_json = args.tools_json
 | 
						|
    else:
 | 
						|
        g.tools_json = os.path.join(g.idf_path, TOOLS_FILE)
 | 
						|
 | 
						|
    action_func_name = f'action_{args.action.replace("-", "_")}'
 | 
						|
    action_func = globals()[action_func_name]
 | 
						|
 | 
						|
    action_func(args)
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    if 'MSYSTEM' in os.environ:
 | 
						|
        fatal('MSys/Mingw is not supported. Please follow the getting started guide of the documentation to set up '
 | 
						|
              'a supported environment')
 | 
						|
        raise SystemExit(1)
 | 
						|
    main(sys.argv[1:])
 |