1#!/usr/bin/env python 2# coding=utf-8 3# 4# SPDX-FileCopyrightText: 2019-2022 Espressif Systems (Shanghai) CO LTD 5# 6# SPDX-License-Identifier: Apache-2.0 7# 8# This script helps installing tools required to use the ESP-IDF, and updating PATH 9# to use the installed tools. It can also create a Python virtual environment, 10# and install Python requirements into it. 11# It does not install OS dependencies. It does install tools such as the Xtensa 12# GCC toolchain and ESP32 ULP coprocessor toolchain. 13# 14# By default, downloaded tools will be installed under $HOME/.espressif directory 15# (%USERPROFILE%/.espressif on Windows). This path can be modified by setting 16# IDF_TOOLS_PATH variable prior to running this tool. 17# 18# Users do not need to interact with this script directly. In IDF root directory, 19# install.sh (.bat) and export.sh (.bat) scripts are provided to invoke this script. 20# 21# Usage: 22# 23# * To install the tools, run `idf_tools.py install`. 24# 25# * To install the Python environment, run `idf_tools.py install-python-env`. 26# 27# * To start using the tools, run `eval "$(idf_tools.py export)"` — this will update 28# the PATH to point to the installed tools and set up other environment variables 29# needed by the tools. 30 31import argparse 32import contextlib 33import copy 34import errno 35import functools 36import hashlib 37import json 38import os 39import platform 40import re 41import shutil 42import ssl 43import subprocess 44import sys 45import tarfile 46import time 47from collections import OrderedDict, namedtuple 48from ssl import SSLContext # noqa: F401 49from tarfile import TarFile # noqa: F401 50from zipfile import ZipFile 51 52# Important notice: Please keep the lines above compatible with old Pythons so it won't fail with ImportError but with 53# a nice message printed by python_version_checker.check() 54try: 55 import python_version_checker 56 57 # check the Python version before it will fail with an exception on syntax or package incompatibility. 58 python_version_checker.check() 59except RuntimeError as e: 60 print(e) 61 raise SystemExit(1) 62 63from typing import IO, Any, Callable, Optional, Tuple, Union # noqa: F401 64from urllib.error import ContentTooShortError 65from urllib.request import urlopen 66# the following is only for typing annotation 67from urllib.response import addinfourl # noqa: F401 68 69try: 70 from exceptions import WindowsError 71except ImportError: 72 # Unix 73 class WindowsError(OSError): # type: ignore 74 pass 75 76 77TOOLS_FILE = 'tools/zephyr_tools.json' 78TOOLS_SCHEMA_FILE = 'tools/tools_schema.json' 79TOOLS_FILE_NEW = 'tools/tools.new.json' 80IDF_ENV_FILE = 'idf-env.json' 81TOOLS_FILE_VERSION = 1 82IDF_TOOLS_PATH_DEFAULT = os.path.join('~', '.espressif') 83UNKNOWN_VERSION = 'unknown' 84SUBST_TOOL_PATH_REGEX = re.compile(r'\${TOOL_PATH}') 85VERSION_REGEX_REPLACE_DEFAULT = r'\1' 86IDF_MAINTAINER = os.environ.get('IDF_MAINTAINER') or False 87TODO_MESSAGE = 'TODO' 88DOWNLOAD_RETRY_COUNT = 3 89URL_PREFIX_MAP_SEPARATOR = ',' 90IDF_TOOLS_INSTALL_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD') 91IDF_TOOLS_EXPORT_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD') 92 93PYTHON_PLATFORM = platform.system() + '-' + platform.machine() 94 95# Identifiers used in tools.json for different platforms. 96PLATFORM_WIN32 = 'win32' 97PLATFORM_WIN64 = 'win64' 98PLATFORM_MACOS = 'macos' 99PLATFORM_MACOS_ARM64 = 'macos-arm64' 100PLATFORM_LINUX32 = 'linux-i686' 101PLATFORM_LINUX64 = 'linux-amd64' 102PLATFORM_LINUX_ARM32 = 'linux-armel' 103PLATFORM_LINUX_ARMHF = 'linux-armhf' 104PLATFORM_LINUX_ARM64 = 'linux-arm64' 105 106 107# Mappings from various other names these platforms are known as, to the identifiers above. 108# This includes strings produced from "platform.system() + '-' + platform.machine()", see PYTHON_PLATFORM 109# definition above. 110# This list also includes various strings used in release archives of xtensa-esp32-elf-gcc, OpenOCD, etc. 111PLATFORM_FROM_NAME = { 112 # Windows 113 PLATFORM_WIN32: PLATFORM_WIN32, 114 'Windows-i686': PLATFORM_WIN32, 115 'Windows-x86': PLATFORM_WIN32, 116 PLATFORM_WIN64: PLATFORM_WIN64, 117 'Windows-x86_64': PLATFORM_WIN64, 118 'Windows-AMD64': PLATFORM_WIN64, 119 # macOS 120 PLATFORM_MACOS: PLATFORM_MACOS, 121 'osx': PLATFORM_MACOS, 122 'darwin': PLATFORM_MACOS, 123 'Darwin-x86_64': PLATFORM_MACOS, 124 PLATFORM_MACOS_ARM64: PLATFORM_MACOS_ARM64, 125 'Darwin-arm64': PLATFORM_MACOS_ARM64, 126 # Linux 127 PLATFORM_LINUX64: PLATFORM_LINUX64, 128 'linux64': PLATFORM_LINUX64, 129 'Linux-x86_64': PLATFORM_LINUX64, 130 'FreeBSD-amd64': PLATFORM_LINUX64, 131 PLATFORM_LINUX32: PLATFORM_LINUX32, 132 'linux32': PLATFORM_LINUX32, 133 'Linux-i686': PLATFORM_LINUX32, 134 'FreeBSD-i386': PLATFORM_LINUX32, 135 PLATFORM_LINUX_ARM32: PLATFORM_LINUX_ARM32, 136 'Linux-arm': PLATFORM_LINUX_ARM32, 137 'Linux-armv7l': PLATFORM_LINUX_ARM32, 138 PLATFORM_LINUX_ARMHF: PLATFORM_LINUX_ARMHF, 139 PLATFORM_LINUX_ARM64: PLATFORM_LINUX_ARM64, 140 'Linux-arm64': PLATFORM_LINUX_ARM64, 141 'Linux-aarch64': PLATFORM_LINUX_ARM64, 142 'Linux-armv8l': PLATFORM_LINUX_ARM64, 143} 144 145UNKNOWN_PLATFORM = 'unknown' 146CURRENT_PLATFORM = PLATFORM_FROM_NAME.get(PYTHON_PLATFORM, UNKNOWN_PLATFORM) 147 148EXPORT_SHELL = 'shell' 149EXPORT_KEY_VALUE = 'key-value' 150 151ISRG_X1_ROOT_CERT = u""" 152-----BEGIN CERTIFICATE----- 153MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw 154TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh 155cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 156WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu 157ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY 158MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc 159h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ 1600TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U 161A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW 162T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH 163B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC 164B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv 165KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn 166OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn 167jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw 168qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI 169rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV 170HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq 171hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL 172ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ 1733BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK 174NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 175ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur 176TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC 177jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc 178oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq 1794RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA 180mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d 181emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= 182-----END CERTIFICATE----- 183""" 184 185 186global_quiet = False 187global_non_interactive = False 188global_idf_path = None # type: Optional[str] 189global_idf_tools_path = None # type: Optional[str] 190global_tools_json = None # type: Optional[str] 191 192 193def fatal(text, *args): # type: (str, str) -> None 194 if not global_quiet: 195 sys.stderr.write('ERROR: ' + text + '\n', *args) 196 197 198def warn(text, *args): # type: (str, str) -> None 199 if not global_quiet: 200 sys.stderr.write('WARNING: ' + text + '\n', *args) 201 202 203def info(text, f=None, *args): # type: (str, Optional[IO[str]], str) -> None 204 if not global_quiet: 205 if f is None: 206 f = sys.stdout 207 f.write(text + '\n', *args) 208 209 210def run_cmd_check_output(cmd, input_text=None, extra_paths=None): 211 # type: (list[str], Optional[str], Optional[list[str]]) -> bytes 212 # If extra_paths is given, locate the executable in one of these directories. 213 # Note: it would seem logical to add extra_paths to env[PATH], instead, and let OS do the job of finding the 214 # executable for us. However this does not work on Windows: https://bugs.python.org/issue8557. 215 if extra_paths: 216 found = False 217 extensions = [''] 218 if sys.platform == 'win32': 219 extensions.append('.exe') 220 for path in extra_paths: 221 for ext in extensions: 222 fullpath = os.path.join(path, cmd[0] + ext) 223 if os.path.exists(fullpath): 224 cmd[0] = fullpath 225 found = True 226 break 227 if found: 228 break 229 230 try: 231 input_bytes = None 232 if input_text: 233 input_bytes = input_text.encode() 234 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, input=input_bytes) 235 return result.stdout + result.stderr 236 except (AttributeError, TypeError): 237 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE) 238 stdout, stderr = p.communicate(input_bytes) 239 if p.returncode != 0: 240 try: 241 raise subprocess.CalledProcessError(p.returncode, cmd, stdout, stderr) 242 except TypeError: 243 raise subprocess.CalledProcessError(p.returncode, cmd, stdout) 244 return stdout + stderr 245 246 247def to_shell_specific_paths(paths_list): # type: (list[str]) -> list[str] 248 if sys.platform == 'win32': 249 paths_list = [p.replace('/', os.path.sep) if os.path.sep in p else p for p in paths_list] 250 251 if 'MSYSTEM' in os.environ: 252 paths_msys = run_cmd_check_output(['cygpath', '-u', '-f', '-'], 253 input_text='\n'.join(paths_list)) 254 paths_list = paths_msys.decode().strip().split('\n') 255 256 return paths_list 257 258 259def get_env_for_extra_paths(extra_paths): # type: (list[str]) -> dict[str, str] 260 """ 261 Return a copy of environment variables dict, prepending paths listed in extra_paths 262 to the PATH environment variable. 263 """ 264 env_arg = os.environ.copy() 265 new_path = os.pathsep.join(extra_paths) + os.pathsep + env_arg['PATH'] 266 if sys.version_info.major == 2: 267 env_arg['PATH'] = new_path.encode('utf8') # type: ignore 268 else: 269 env_arg['PATH'] = new_path 270 return env_arg 271 272 273def get_file_size_sha256(filename, block_size=65536): # type: (str, int) -> Tuple[int, str] 274 sha256 = hashlib.sha256() 275 size = 0 276 with open(filename, 'rb') as f: 277 for block in iter(lambda: f.read(block_size), b''): 278 sha256.update(block) 279 size += len(block) 280 return size, sha256.hexdigest() 281 282 283def report_progress(count, block_size, total_size): # type: (int, int, int) -> None 284 percent = int(count * block_size * 100 / total_size) 285 percent = min(100, percent) 286 sys.stdout.write('\r%d%%' % percent) 287 sys.stdout.flush() 288 289 290def mkdir_p(path): # type: (str) -> None 291 try: 292 os.makedirs(path) 293 except OSError as exc: 294 if exc.errno != errno.EEXIST or not os.path.isdir(path): 295 raise 296 297 298def unpack(filename, destination): # type: (str, str) -> None 299 info('Extracting {0} to {1}'.format(filename, destination)) 300 if filename.endswith(('.tar.gz', '.tgz')): 301 archive_obj = tarfile.open(filename, 'r:gz') # type: Union[TarFile, ZipFile] 302 elif filename.endswith(('.tar.xz')): 303 archive_obj = tarfile.open(filename, 'r:xz') 304 elif filename.endswith('zip'): 305 archive_obj = ZipFile(filename) 306 else: 307 raise NotImplementedError('Unsupported archive type') 308 if sys.version_info.major == 2: 309 # This is a workaround for the issue that unicode destination is not handled: 310 # https://bugs.python.org/issue17153 311 destination = str(destination) 312 archive_obj.extractall(destination) 313 314 315def splittype(url): # type: (str) -> Tuple[Optional[str], str] 316 match = re.match('([^/:]+):(.*)', url, re.DOTALL) 317 if match: 318 scheme, data = match.groups() 319 return scheme.lower(), data 320 return None, url 321 322 323# An alternative version of urlretrieve which takes SSL context as an argument 324def urlretrieve_ctx(url, filename, reporthook=None, data=None, context=None): 325 # type: (str, str, Optional[Callable[[int, int, int], None]], Optional[bytes], Optional[SSLContext]) -> Tuple[str, addinfourl] 326 url_type, path = splittype(url) 327 328 # urlopen doesn't have context argument in Python <=2.7.9 329 extra_urlopen_args = {} 330 if context: 331 extra_urlopen_args['context'] = context 332 with contextlib.closing(urlopen(url, data, **extra_urlopen_args)) as fp: # type: ignore 333 headers = fp.info() 334 335 # Just return the local path and the "headers" for file:// 336 # URLs. No sense in performing a copy unless requested. 337 if url_type == 'file' and not filename: 338 return os.path.normpath(path), headers 339 340 # Handle temporary file setup. 341 tfp = open(filename, 'wb') 342 343 with tfp: 344 result = filename, headers 345 bs = 1024 * 8 346 size = int(headers.get('content-length', -1)) 347 read = 0 348 blocknum = 0 349 350 if reporthook: 351 reporthook(blocknum, bs, size) 352 353 while True: 354 block = fp.read(bs) 355 if not block: 356 break 357 read += len(block) 358 tfp.write(block) 359 blocknum += 1 360 if reporthook: 361 reporthook(blocknum, bs, size) 362 363 if size >= 0 and read < size: 364 raise ContentTooShortError( 365 'retrieval incomplete: got only %i out of %i bytes' 366 % (read, size), result) 367 368 return result 369 370 371# Sometimes renaming a directory on Windows (randomly?) causes a PermissionError. 372# This is confirmed to be a workaround: 373# https://github.com/espressif/esp-idf/issues/3819#issuecomment-515167118 374# https://github.com/espressif/esp-idf/issues/4063#issuecomment-531490140 375# https://stackoverflow.com/a/43046729 376def rename_with_retry(path_from, path_to): # type: (str, str) -> None 377 retry_count = 20 if sys.platform.startswith('win') else 1 378 for retry in range(retry_count): 379 try: 380 os.rename(path_from, path_to) 381 return 382 except OSError: 383 msg = f'Rename {path_from} to {path_to} failed' 384 if retry == retry_count - 1: 385 fatal(msg + '. Antivirus software might be causing this. Disabling it temporarily could solve the issue.') 386 raise 387 warn(msg + ', retrying...') 388 # Sleep before the next try in order to pass the antivirus check on Windows 389 time.sleep(0.5) 390 391 392def strip_container_dirs(path, levels): # type: (str, int) -> None 393 assert levels > 0 394 # move the original directory out of the way (add a .tmp suffix) 395 tmp_path = path + '.tmp' 396 if os.path.exists(tmp_path): 397 shutil.rmtree(tmp_path) 398 rename_with_retry(path, tmp_path) 399 os.mkdir(path) 400 base_path = tmp_path 401 # walk given number of levels down 402 for level in range(levels): 403 contents = os.listdir(base_path) 404 if len(contents) > 1: 405 raise RuntimeError('at level {}, expected 1 entry, got {}'.format(level, contents)) 406 base_path = os.path.join(base_path, contents[0]) 407 if not os.path.isdir(base_path): 408 raise RuntimeError('at level {}, {} is not a directory'.format(level, contents[0])) 409 # get the list of directories/files to move 410 contents = os.listdir(base_path) 411 for name in contents: 412 move_from = os.path.join(base_path, name) 413 move_to = os.path.join(path, name) 414 rename_with_retry(move_from, move_to) 415 shutil.rmtree(tmp_path) 416 417 418class ToolNotFound(RuntimeError): 419 pass 420 421 422class ToolExecError(RuntimeError): 423 pass 424 425 426class DownloadError(RuntimeError): 427 pass 428 429 430class IDFToolDownload(object): 431 def __init__(self, platform_name, url, size, sha256): # type: (str, str, int, str) -> None 432 self.platform_name = platform_name 433 self.url = url 434 self.size = size 435 self.sha256 = sha256 436 self.platform_name = platform_name 437 438 439@functools.total_ordering 440class IDFToolVersion(object): 441 STATUS_RECOMMENDED = 'recommended' 442 STATUS_SUPPORTED = 'supported' 443 STATUS_DEPRECATED = 'deprecated' 444 445 STATUS_VALUES = [STATUS_RECOMMENDED, STATUS_SUPPORTED, STATUS_DEPRECATED] 446 447 def __init__(self, version, status): # type: (str, str) -> None 448 self.version = version 449 self.status = status 450 self.downloads = OrderedDict() # type: OrderedDict[str, IDFToolDownload] 451 self.latest = False 452 453 def __lt__(self, other): # type: (IDFToolVersion) -> bool 454 if self.status != other.status: 455 return self.status > other.status 456 else: 457 assert not (self.status == IDFToolVersion.STATUS_RECOMMENDED 458 and other.status == IDFToolVersion.STATUS_RECOMMENDED) 459 return self.version < other.version 460 461 def __eq__(self, other): # type: (object) -> bool 462 if not isinstance(other, IDFToolVersion): 463 return NotImplemented 464 return self.status == other.status and self.version == other.version 465 466 def add_download(self, platform_name, url, size, sha256): # type: (str, str, int, str) -> None 467 self.downloads[platform_name] = IDFToolDownload(platform_name, url, size, sha256) 468 469 def get_download_for_platform(self, platform_name): # type: (str) -> Optional[IDFToolDownload] 470 if platform_name in PLATFORM_FROM_NAME.keys(): 471 platform_name = PLATFORM_FROM_NAME[platform_name] 472 if platform_name in self.downloads.keys(): 473 return self.downloads[platform_name] 474 if 'any' in self.downloads.keys(): 475 return self.downloads['any'] 476 return None 477 478 def compatible_with_platform(self, platform_name=PYTHON_PLATFORM): 479 # type: (str) -> bool 480 return self.get_download_for_platform(platform_name) is not None 481 482 def get_supported_platforms(self): # type: () -> set[str] 483 return set(self.downloads.keys()) 484 485 486OPTIONS_LIST = ['version_cmd', 487 'version_regex', 488 'version_regex_replace', 489 'export_paths', 490 'export_vars', 491 'install', 492 'info_url', 493 'license', 494 'strip_container_dirs', 495 'supported_targets'] 496 497IDFToolOptions = namedtuple('IDFToolOptions', OPTIONS_LIST) # type: ignore 498 499 500class IDFTool(object): 501 # possible values of 'install' field 502 INSTALL_ALWAYS = 'always' 503 INSTALL_ON_REQUEST = 'on_request' 504 INSTALL_NEVER = 'never' 505 506 def __init__(self, name, description, install, info_url, license, version_cmd, version_regex, supported_targets, version_regex_replace=None, 507 strip_container_dirs=0): 508 # type: (str, str, str, str, str, list[str], str, list[str], Optional[str], int) -> None 509 self.name = name 510 self.description = description 511 self.versions = OrderedDict() # type: dict[str, IDFToolVersion] 512 self.version_in_path = None # type: Optional[str] 513 self.versions_installed = [] # type: list[str] 514 if version_regex_replace is None: 515 version_regex_replace = VERSION_REGEX_REPLACE_DEFAULT 516 self.options = IDFToolOptions(version_cmd, version_regex, version_regex_replace, 517 [], OrderedDict(), install, info_url, license, strip_container_dirs, supported_targets) # type: ignore 518 self.platform_overrides = [] # type: list[dict[str, str]] 519 self._platform = CURRENT_PLATFORM 520 self._update_current_options() 521 522 def copy_for_platform(self, platform): # type: (str) -> IDFTool 523 result = copy.deepcopy(self) 524 result._platform = platform 525 result._update_current_options() 526 return result 527 528 def _update_current_options(self): # type: () -> None 529 self._current_options = IDFToolOptions(*self.options) 530 for override in self.platform_overrides: 531 if self._platform not in override['platforms']: 532 continue 533 override_dict = override.copy() 534 del override_dict['platforms'] 535 self._current_options = self._current_options._replace(**override_dict) # type: ignore 536 537 def add_version(self, version): # type: (IDFToolVersion) -> None 538 assert(type(version) is IDFToolVersion) 539 self.versions[version.version] = version 540 541 def get_path(self): # type: () -> str 542 return os.path.join(global_idf_tools_path, 'tools', self.name) # type: ignore 543 544 def get_path_for_version(self, version): # type: (str) -> str 545 assert(version in self.versions) 546 return os.path.join(self.get_path(), version) 547 548 def get_export_paths(self, version): # type: (str) -> list[str] 549 tool_path = self.get_path_for_version(version) 550 return [os.path.join(tool_path, *p) for p in self._current_options.export_paths] # type: ignore 551 552 def get_export_vars(self, version): # type: (str) -> dict[str, str] 553 """ 554 Get the dictionary of environment variables to be exported, for the given version. 555 Expands: 556 - ${TOOL_PATH} => the actual path where the version is installed 557 """ 558 result = {} 559 for k, v in self._current_options.export_vars.items(): # type: ignore 560 replace_path = self.get_path_for_version(version).replace('\\', '\\\\') 561 v_repl = re.sub(SUBST_TOOL_PATH_REGEX, replace_path, v) 562 if v_repl != v: 563 v_repl = to_shell_specific_paths([v_repl])[0] 564 result[k] = v_repl 565 return result 566 567 def check_version(self, extra_paths=None): # type: (Optional[list[str]]) -> str 568 """ 569 Execute the tool, optionally prepending extra_paths to PATH, 570 extract the version string and return it as a result. 571 Raises ToolNotFound if the tool is not found (not present in the paths). 572 Raises ToolExecError if the tool returns with a non-zero exit code. 573 Returns 'unknown' if tool returns something from which version string 574 can not be extracted. 575 """ 576 # this function can not be called for a different platform 577 assert self._platform == CURRENT_PLATFORM 578 cmd = self._current_options.version_cmd # type: ignore 579 try: 580 version_cmd_result = run_cmd_check_output(cmd, None, extra_paths) 581 except OSError: 582 # tool is not on the path 583 raise ToolNotFound('Tool {} not found'.format(self.name)) 584 except subprocess.CalledProcessError as e: 585 raise ToolExecError('returned non-zero exit code ({}) with error message:\n{}'.format( 586 e.returncode, e.stderr.decode('utf-8',errors='ignore'))) # type: ignore 587 588 in_str = version_cmd_result.decode('utf-8') 589 match = re.search(self._current_options.version_regex, in_str) # type: ignore 590 if not match: 591 return UNKNOWN_VERSION 592 return re.sub(self._current_options.version_regex, self._current_options.version_regex_replace, match.group(0)) # type: ignore 593 594 def get_install_type(self): # type: () -> Callable[[str], None] 595 return self._current_options.install # type: ignore 596 597 def get_supported_targets(self): # type: () -> list[str] 598 return self._current_options.supported_targets # type: ignore 599 600 def compatible_with_platform(self): # type: () -> bool 601 return any([v.compatible_with_platform() for v in self.versions.values()]) 602 603 def get_supported_platforms(self): # type: () -> set[str] 604 result = set() 605 for v in self.versions.values(): 606 result.update(v.get_supported_platforms()) 607 return result 608 609 def get_recommended_version(self): # type: () -> Optional[str] 610 recommended_versions = [k for k, v in self.versions.items() 611 if v.status == IDFToolVersion.STATUS_RECOMMENDED 612 and v.compatible_with_platform(self._platform)] 613 assert len(recommended_versions) <= 1 614 if recommended_versions: 615 return recommended_versions[0] 616 return None 617 618 def get_preferred_installed_version(self): # type: () -> Optional[str] 619 recommended_versions = [k for k in self.versions_installed 620 if self.versions[k].status == IDFToolVersion.STATUS_RECOMMENDED 621 and self.versions[k].compatible_with_platform(self._platform)] 622 assert len(recommended_versions) <= 1 623 if recommended_versions: 624 return recommended_versions[0] 625 return None 626 627 def find_installed_versions(self): # type: () -> None 628 """ 629 Checks whether the tool can be found in PATH and in global_idf_tools_path. 630 Writes results to self.version_in_path and self.versions_installed. 631 """ 632 # this function can not be called for a different platform 633 assert self._platform == CURRENT_PLATFORM 634 # First check if the tool is in system PATH 635 try: 636 ver_str = self.check_version() 637 except ToolNotFound: 638 # not in PATH 639 pass 640 except ToolExecError as e: 641 warn('tool {} found in path, but {}'.format( 642 self.name, e)) 643 else: 644 self.version_in_path = ver_str 645 646 # Now check all the versions installed in global_idf_tools_path 647 self.versions_installed = [] 648 for version, version_obj in self.versions.items(): 649 if not version_obj.compatible_with_platform(): 650 continue 651 tool_path = self.get_path_for_version(version) 652 if not os.path.exists(tool_path): 653 # version not installed 654 continue 655 try: 656 ver_str = self.check_version(self.get_export_paths(version)) 657 except ToolNotFound: 658 warn('directory for tool {} version {} is present, but tool was not found'.format( 659 self.name, version)) 660 except ToolExecError as e: 661 warn('tool {} version {} is installed, but {}'.format( 662 self.name, version, e)) 663 else: 664 if ver_str != version: 665 warn('tool {} version {} is installed, but has reported version {}'.format( 666 self.name, version, ver_str)) 667 else: 668 self.versions_installed.append(version) 669 670 def download(self, version): # type: (str) -> None 671 assert(version in self.versions) 672 download_obj = self.versions[version].get_download_for_platform(self._platform) 673 if not download_obj: 674 fatal('No packages for tool {} platform {}!'.format(self.name, self._platform)) 675 raise DownloadError() 676 677 url = download_obj.url 678 archive_name = os.path.basename(url) 679 local_path = os.path.join(global_idf_tools_path, 'dist', archive_name) # type: ignore 680 mkdir_p(os.path.dirname(local_path)) 681 682 if os.path.isfile(local_path): 683 if not self.check_download_file(download_obj, local_path): 684 warn('removing downloaded file {0} and downloading again'.format(archive_name)) 685 os.unlink(local_path) 686 else: 687 info('file {0} is already downloaded'.format(archive_name)) 688 return 689 690 downloaded = False 691 for retry in range(DOWNLOAD_RETRY_COUNT): 692 local_temp_path = local_path + '.tmp' 693 info('Downloading {} to {}'.format(archive_name, local_temp_path)) 694 try: 695 ctx = None 696 # For dl.espressif.com, add the ISRG x1 root certificate. 697 # This works around the issue with outdated certificate stores in some installations. 698 if 'dl.espressif.com' in url: 699 try: 700 ctx = ssl.create_default_context() 701 ctx.load_verify_locations(cadata=ISRG_X1_ROOT_CERT) 702 except AttributeError: 703 # no ssl.create_default_context or load_verify_locations cadata argument 704 # in Python <=2.7.8 705 pass 706 707 urlretrieve_ctx(url, local_temp_path, report_progress if not global_non_interactive else None, context=ctx) 708 sys.stdout.write('\rDone\n') 709 except Exception as e: 710 # urlretrieve could throw different exceptions, e.g. IOError when the server is down 711 # Errors are ignored because the downloaded file is checked a couple of lines later. 712 warn('Download failure {}'.format(e)) 713 sys.stdout.flush() 714 if not os.path.isfile(local_temp_path) or not self.check_download_file(download_obj, local_temp_path): 715 warn('Failed to download {} to {}'.format(url, local_temp_path)) 716 continue 717 rename_with_retry(local_temp_path, local_path) 718 downloaded = True 719 break 720 if not downloaded: 721 fatal('Failed to download, and retry count has expired') 722 raise DownloadError() 723 724 def install(self, version): # type: (str) -> None 725 # Currently this is called after calling 'download' method, so here are a few asserts 726 # for the conditions which should be true once that method is done. 727 assert (version in self.versions) 728 download_obj = self.versions[version].get_download_for_platform(self._platform) 729 assert (download_obj is not None) 730 archive_name = os.path.basename(download_obj.url) 731 archive_path = os.path.join(global_idf_tools_path, 'dist', archive_name) # type: ignore 732 assert (os.path.isfile(archive_path)) 733 dest_dir = self.get_path_for_version(version) 734 if os.path.exists(dest_dir): 735 warn('destination path already exists, removing') 736 shutil.rmtree(dest_dir) 737 mkdir_p(dest_dir) 738 unpack(archive_path, dest_dir) 739 if self._current_options.strip_container_dirs: # type: ignore 740 strip_container_dirs(dest_dir, self._current_options.strip_container_dirs) # type: ignore 741 742 @staticmethod 743 def check_download_file(download_obj, local_path): # type: (IDFToolDownload, str) -> bool 744 expected_sha256 = download_obj.sha256 745 expected_size = download_obj.size 746 file_size, file_sha256 = get_file_size_sha256(local_path) 747 if file_size != expected_size: 748 warn('file size mismatch for {}, expected {}, got {}'.format(local_path, expected_size, file_size)) 749 return False 750 if file_sha256 != expected_sha256: 751 warn('hash mismatch for {}, expected {}, got {}'.format(local_path, expected_sha256, file_sha256)) 752 return False 753 return True 754 755 @classmethod 756 def from_json(cls, tool_dict): # type: (dict[str, Union[str, list[str], dict[str, str]]]) -> IDFTool 757 # json.load will return 'str' types in Python 3 and 'unicode' in Python 2 758 expected_str_type = type(u'') 759 760 # Validate json fields 761 tool_name = tool_dict.get('name') # type: ignore 762 if type(tool_name) is not expected_str_type: 763 raise RuntimeError('tool_name is not a string') 764 765 description = tool_dict.get('description') # type: ignore 766 if type(description) is not expected_str_type: 767 raise RuntimeError('description is not a string') 768 769 version_cmd = tool_dict.get('version_cmd') 770 if type(version_cmd) is not list: 771 raise RuntimeError('version_cmd for tool %s is not a list of strings' % tool_name) 772 773 version_regex = tool_dict.get('version_regex') 774 if type(version_regex) is not expected_str_type or not version_regex: 775 raise RuntimeError('version_regex for tool %s is not a non-empty string' % tool_name) 776 777 version_regex_replace = tool_dict.get('version_regex_replace') 778 if version_regex_replace and type(version_regex_replace) is not expected_str_type: 779 raise RuntimeError('version_regex_replace for tool %s is not a string' % tool_name) 780 781 export_paths = tool_dict.get('export_paths') 782 if type(export_paths) is not list: 783 raise RuntimeError('export_paths for tool %s is not a list' % tool_name) 784 785 export_vars = tool_dict.get('export_vars', {}) # type: ignore 786 if type(export_vars) is not dict: 787 raise RuntimeError('export_vars for tool %s is not a mapping' % tool_name) 788 789 versions = tool_dict.get('versions') 790 if type(versions) is not list: 791 raise RuntimeError('versions for tool %s is not an array' % tool_name) 792 793 install = tool_dict.get('install', False) # type: ignore 794 if type(install) is not expected_str_type: 795 raise RuntimeError('install for tool %s is not a string' % tool_name) 796 797 info_url = tool_dict.get('info_url', False) # type: ignore 798 if type(info_url) is not expected_str_type: 799 raise RuntimeError('info_url for tool %s is not a string' % tool_name) 800 801 license = tool_dict.get('license', False) # type: ignore 802 if type(license) is not expected_str_type: 803 raise RuntimeError('license for tool %s is not a string' % tool_name) 804 805 strip_container_dirs = tool_dict.get('strip_container_dirs', 0) 806 if strip_container_dirs and type(strip_container_dirs) is not int: 807 raise RuntimeError('strip_container_dirs for tool %s is not an int' % tool_name) 808 809 overrides_list = tool_dict.get('platform_overrides', []) # type: ignore 810 if type(overrides_list) is not list: 811 raise RuntimeError('platform_overrides for tool %s is not a list' % tool_name) 812 813 supported_targets = tool_dict.get('supported_targets') 814 if not isinstance(supported_targets, list): 815 raise RuntimeError('supported_targets for tool %s is not a list of strings' % tool_name) 816 817 # Create the object 818 tool_obj = cls(tool_name, description, install, info_url, license, # type: ignore 819 version_cmd, version_regex, supported_targets, version_regex_replace, # type: ignore 820 strip_container_dirs) # type: ignore 821 822 for path in export_paths: # type: ignore 823 tool_obj.options.export_paths.append(path) # type: ignore 824 825 for name, value in export_vars.items(): # type: ignore 826 tool_obj.options.export_vars[name] = value # type: ignore 827 828 for index, override in enumerate(overrides_list): 829 platforms_list = override.get('platforms') # type: ignore 830 if type(platforms_list) is not list: 831 raise RuntimeError('platforms for override %d of tool %s is not a list' % (index, tool_name)) 832 833 install = override.get('install') # type: ignore 834 if install is not None and type(install) is not expected_str_type: 835 raise RuntimeError('install for override %d of tool %s is not a string' % (index, tool_name)) 836 837 version_cmd = override.get('version_cmd') # type: ignore 838 if version_cmd is not None and type(version_cmd) is not list: 839 raise RuntimeError('version_cmd for override %d of tool %s is not a list of strings' % 840 (index, tool_name)) 841 842 version_regex = override.get('version_regex') # type: ignore 843 if version_regex is not None and (type(version_regex) is not expected_str_type or not version_regex): 844 raise RuntimeError('version_regex for override %d of tool %s is not a non-empty string' % 845 (index, tool_name)) 846 847 version_regex_replace = override.get('version_regex_replace') # type: ignore 848 if version_regex_replace is not None and type(version_regex_replace) is not expected_str_type: 849 raise RuntimeError('version_regex_replace for override %d of tool %s is not a string' % 850 (index, tool_name)) 851 852 export_paths = override.get('export_paths') # type: ignore 853 if export_paths is not None and type(export_paths) is not list: 854 raise RuntimeError('export_paths for override %d of tool %s is not a list' % (index, tool_name)) 855 856 export_vars = override.get('export_vars') # type: ignore 857 if export_vars is not None and type(export_vars) is not dict: 858 raise RuntimeError('export_vars for override %d of tool %s is not a mapping' % (index, tool_name)) 859 tool_obj.platform_overrides.append(override) # type: ignore 860 861 recommended_versions = {} # type: dict[str, list[str]] 862 for version_dict in versions: # type: ignore 863 version = version_dict.get('name') # type: ignore 864 if type(version) is not expected_str_type: 865 raise RuntimeError('version name for tool {} is not a string'.format(tool_name)) 866 867 version_status = version_dict.get('status') # type: ignore 868 if type(version_status) is not expected_str_type and version_status not in IDFToolVersion.STATUS_VALUES: 869 raise RuntimeError('tool {} version {} status is not one of {}', tool_name, version, 870 IDFToolVersion.STATUS_VALUES) 871 872 version_obj = IDFToolVersion(version, version_status) 873 for platform_id, platform_dict in version_dict.items(): # type: ignore 874 if platform_id in ['name', 'status']: 875 continue 876 if platform_id not in PLATFORM_FROM_NAME.keys(): 877 raise RuntimeError('invalid platform %s for tool %s version %s' % 878 (platform_id, tool_name, version)) 879 880 version_obj.add_download(platform_id, 881 platform_dict['url'], platform_dict['size'], platform_dict['sha256']) 882 883 if version_status == IDFToolVersion.STATUS_RECOMMENDED: 884 if platform_id not in recommended_versions: 885 recommended_versions[platform_id] = [] 886 recommended_versions[platform_id].append(version) 887 888 tool_obj.add_version(version_obj) 889 for platform_id, version_list in recommended_versions.items(): 890 if len(version_list) > 1: 891 raise RuntimeError('tool {} for platform {} has {} recommended versions'.format( 892 tool_name, platform_id, len(recommended_versions))) 893 if install != IDFTool.INSTALL_NEVER and len(recommended_versions) == 0: 894 raise RuntimeError('required/optional tool {} for platform {} has no recommended versions'.format( 895 tool_name, platform_id)) 896 897 tool_obj._update_current_options() 898 return tool_obj 899 900 def to_json(self): # type: ignore 901 versions_array = [] 902 for version, version_obj in self.versions.items(): 903 version_json = { 904 'name': version, 905 'status': version_obj.status 906 } 907 for platform_id, download in version_obj.downloads.items(): 908 version_json[platform_id] = { 909 'url': download.url, 910 'size': download.size, 911 'sha256': download.sha256 912 } 913 versions_array.append(version_json) 914 overrides_array = self.platform_overrides 915 916 tool_json = { 917 'name': self.name, 918 'description': self.description, 919 'export_paths': self.options.export_paths, 920 'export_vars': self.options.export_vars, 921 'install': self.options.install, 922 'info_url': self.options.info_url, 923 'license': self.options.license, 924 'version_cmd': self.options.version_cmd, 925 'version_regex': self.options.version_regex, 926 'supported_targets': self.options.supported_targets, 927 'versions': versions_array, 928 } 929 if self.options.version_regex_replace != VERSION_REGEX_REPLACE_DEFAULT: 930 tool_json['version_regex_replace'] = self.options.version_regex_replace 931 if overrides_array: 932 tool_json['platform_overrides'] = overrides_array 933 if self.options.strip_container_dirs: 934 tool_json['strip_container_dirs'] = self.options.strip_container_dirs 935 return tool_json 936 937 938def load_tools_info(): # type: () -> dict[str, IDFTool] 939 """ 940 Load tools metadata from tools.json, return a dictionary: tool name - tool info 941 """ 942 tool_versions_file_name = global_tools_json 943 944 with open(tool_versions_file_name, 'r') as f: # type: ignore 945 tools_info = json.load(f) 946 947 return parse_tools_info_json(tools_info) # type: ignore 948 949 950def parse_tools_info_json(tools_info): # type: ignore 951 """ 952 Parse and validate the dictionary obtained by loading the tools.json file. 953 Returns a dictionary of tools (key: tool name, value: IDFTool object). 954 """ 955 if tools_info['version'] != TOOLS_FILE_VERSION: 956 raise RuntimeError('Invalid version') 957 958 tools_dict = OrderedDict() 959 960 tools_array = tools_info.get('tools') 961 if type(tools_array) is not list: 962 raise RuntimeError('tools property is missing or not an array') 963 964 for tool_dict in tools_array: 965 tool = IDFTool.from_json(tool_dict) 966 tools_dict[tool.name] = tool 967 968 return tools_dict 969 970 971def dump_tools_json(tools_info): # type: ignore 972 tools_array = [] 973 for tool_name, tool_obj in tools_info.items(): 974 tool_json = tool_obj.to_json() 975 tools_array.append(tool_json) 976 file_json = {'version': TOOLS_FILE_VERSION, 'tools': tools_array} 977 return json.dumps(file_json, indent=2, separators=(',', ': '), sort_keys=True) 978 979 980def get_python_env_path(): # type: () -> Tuple[str, str, str] 981 python_ver_major_minor = '{}.{}'.format(sys.version_info.major, sys.version_info.minor) 982 983 version_file_path = os.path.join(global_idf_path, 'version.txt') # type: ignore 984 if os.path.exists(version_file_path): 985 with open(version_file_path, 'r') as version_file: 986 idf_version_str = version_file.read() 987 else: 988 idf_version_str = '' 989 try: 990 idf_version_str = subprocess.check_output(['git', 'describe'], 991 cwd=global_idf_path, env=os.environ).decode() 992 except OSError: 993 # OSError should cover FileNotFoundError and WindowsError 994 warn('Git was not found') 995 except subprocess.CalledProcessError as e: 996 warn('Git describe was unsuccessful: {}'.format(e.output)) 997 match = re.match(r'^v([0-9]+\.[0-9]+).*', idf_version_str) 998 if match: 999 idf_version = match.group(1) # type: Optional[str] 1000 else: 1001 idf_version = None 1002 # fallback when IDF is a shallow clone 1003 try: 1004 with open(os.path.join(global_idf_path, 'components', 'esp_common', 'include', 'esp_idf_version.h')) as f: # type: ignore 1005 m = re.search(r'^#define\s+ESP_IDF_VERSION_MAJOR\s+(\d+).+?^#define\s+ESP_IDF_VERSION_MINOR\s+(\d+)', 1006 f.read(), re.DOTALL | re.MULTILINE) 1007 if m: 1008 idf_version = '.'.join((m.group(1), m.group(2))) 1009 else: 1010 warn('Reading IDF version from C header file failed!') 1011 except Exception as e: 1012 warn('Is it not possible to determine the IDF version: {}'.format(e)) 1013 1014 if idf_version is None: 1015 fatal('IDF version cannot be determined') 1016 raise SystemExit(1) 1017 1018 idf_python_env_path = os.path.join(global_idf_tools_path, 'python_env', # type: ignore 1019 'idf{}_py{}_env'.format(idf_version, python_ver_major_minor)) 1020 1021 if sys.platform == 'win32': 1022 subdir = 'Scripts' 1023 python_exe = 'python.exe' 1024 else: 1025 subdir = 'bin' 1026 python_exe = 'python' 1027 1028 idf_python_export_path = os.path.join(idf_python_env_path, subdir) 1029 virtualenv_python = os.path.join(idf_python_export_path, python_exe) 1030 1031 return idf_python_env_path, idf_python_export_path, virtualenv_python 1032 1033 1034def get_idf_env(): # type: () -> Any 1035 try: 1036 idf_env_file_path = os.path.join(global_idf_tools_path, IDF_ENV_FILE) # type: ignore 1037 with open(idf_env_file_path, 'r') as idf_env_file: 1038 return json.load(idf_env_file) 1039 except (IOError, OSError): 1040 if not os.path.exists(idf_env_file_path): 1041 warn('File {} was not found. '.format(idf_env_file_path)) 1042 else: 1043 filename, ending = os.path.splitext(os.path.basename(idf_env_file_path)) 1044 warn('File {} can not be opened, renaming to {}'.format(idf_env_file_path,filename + '_failed' + ending)) 1045 os.rename(idf_env_file_path, os.path.join(os.path.dirname(idf_env_file_path), (filename + '_failed' + ending))) 1046 1047 info('Creating {}' .format(idf_env_file_path)) 1048 return {'idfSelectedId': 'sha', 'idfInstalled': {'sha': {'targets': {}}}} 1049 1050 1051def export_targets_to_idf_env_json(targets): # type: (list[str]) -> None 1052 idf_env_json = get_idf_env() 1053 targets = list(set(targets + get_user_defined_targets())) 1054 1055 for env in idf_env_json['idfInstalled']: 1056 if env == idf_env_json['idfSelectedId']: 1057 idf_env_json['idfInstalled'][env]['targets'] = targets 1058 break 1059 1060 try: 1061 if global_idf_tools_path: # mypy fix for Optional[str] in the next call 1062 # the directory doesn't exist if this is run on a clean system the first time 1063 mkdir_p(global_idf_tools_path) 1064 with open(os.path.join(global_idf_tools_path, IDF_ENV_FILE), 'w') as w: # type: ignore 1065 json.dump(idf_env_json, w, indent=4) 1066 except (IOError, OSError): 1067 warn('File {} can not be created. '.format(os.path.join(global_idf_tools_path, IDF_ENV_FILE))) # type: ignore 1068 1069 1070def clean_targets(targets_str): # type: (str) -> list[str] 1071 targets_from_tools_json = get_all_targets_from_tools_json() 1072 invalid_targets = [] 1073 1074 targets_str = targets_str.lower() 1075 targets = targets_str.replace('-', '').split(',') 1076 if targets != ['all']: 1077 invalid_targets = [t for t in targets if t not in targets_from_tools_json] 1078 if invalid_targets: 1079 warn('Targets: "{}" are not supported. Only allowed options are: {}.'.format(', '.join(invalid_targets), ', '.join(targets_from_tools_json))) 1080 raise SystemExit(1) 1081 # removing duplicates 1082 targets = list(set(targets)) 1083 export_targets_to_idf_env_json(targets) 1084 else: 1085 export_targets_to_idf_env_json(targets_from_tools_json) 1086 return targets 1087 1088 1089def get_user_defined_targets(): # type: () -> list[str] 1090 try: 1091 with open(os.path.join(global_idf_tools_path, IDF_ENV_FILE), 'r') as idf_env_file: # type: ignore 1092 idf_env_json = json.load(idf_env_file) 1093 except OSError: 1094 # warn('File {} was not found. Installing tools for all esp targets.'.format(os.path.join(global_idf_tools_path, IDF_ENV_FILE))) # type: ignore 1095 return [] 1096 1097 targets = [] 1098 for env in idf_env_json['idfInstalled']: 1099 if env == idf_env_json['idfSelectedId']: 1100 targets = idf_env_json['idfInstalled'][env]['targets'] 1101 break 1102 return targets 1103 1104 1105def get_all_targets_from_tools_json(): # type: () -> list[str] 1106 tools_info = load_tools_info() 1107 targets_from_tools_json = [] # type: list[str] 1108 1109 for _, v in tools_info.items(): 1110 targets_from_tools_json.extend(v.get_supported_targets()) 1111 # remove duplicates 1112 targets_from_tools_json = list(set(targets_from_tools_json)) 1113 if 'all' in targets_from_tools_json: 1114 targets_from_tools_json.remove('all') 1115 return sorted(targets_from_tools_json) 1116 1117 1118def filter_tools_info(tools_info): # type: (OrderedDict[str, IDFTool]) -> OrderedDict[str,IDFTool] 1119 targets = get_user_defined_targets() 1120 if not targets: 1121 return tools_info 1122 else: 1123 filtered_tools_spec = {k:v for k, v in tools_info.items() if 1124 (v.get_install_type() == IDFTool.INSTALL_ALWAYS or v.get_install_type() == IDFTool.INSTALL_ON_REQUEST) and 1125 (any(item in targets for item in v.get_supported_targets()) or v.get_supported_targets() == ['all'])} 1126 return OrderedDict(filtered_tools_spec) 1127 1128 1129def action_list(args): # type: ignore 1130 tools_info = load_tools_info() 1131 for name, tool in tools_info.items(): 1132 if tool.get_install_type() == IDFTool.INSTALL_NEVER: 1133 continue 1134 optional_str = ' (optional)' if tool.get_install_type() == IDFTool.INSTALL_ON_REQUEST else '' 1135 info('* {}: {}{}'.format(name, tool.description, optional_str)) 1136 tool.find_installed_versions() 1137 versions_for_platform = {k: v for k, v in tool.versions.items() if v.compatible_with_platform()} 1138 if not versions_for_platform: 1139 info(' (no versions compatible with platform {})'.format(PYTHON_PLATFORM)) 1140 continue 1141 versions_sorted = sorted(versions_for_platform.keys(), key=tool.versions.get, reverse=True) # type: ignore 1142 for version in versions_sorted: 1143 version_obj = tool.versions[version] 1144 info(' - {} ({}{})'.format(version, version_obj.status, 1145 ', installed' if version in tool.versions_installed else '')) 1146 1147 1148def action_check(args): # type: ignore 1149 tools_info = load_tools_info() 1150 tools_info = filter_tools_info(tools_info) 1151 not_found_list = [] 1152 info('Checking for installed tools...') 1153 for name, tool in tools_info.items(): 1154 if tool.get_install_type() == IDFTool.INSTALL_NEVER: 1155 continue 1156 tool_found_somewhere = False 1157 info('Checking tool %s' % name) 1158 tool.find_installed_versions() 1159 if tool.version_in_path: 1160 info(' version found in PATH: %s' % tool.version_in_path) 1161 tool_found_somewhere = True 1162 else: 1163 info(' no version found in PATH') 1164 1165 for version in tool.versions_installed: 1166 info(' version installed in tools directory: %s' % version) 1167 tool_found_somewhere = True 1168 if not tool_found_somewhere and tool.get_install_type() == IDFTool.INSTALL_ALWAYS: 1169 not_found_list.append(name) 1170 if not_found_list: 1171 fatal('The following required tools were not found: ' + ' '.join(not_found_list)) 1172 raise SystemExit(1) 1173 1174 1175def action_export(args): # type: ignore 1176 tools_info = load_tools_info() 1177 tools_info = filter_tools_info(tools_info) 1178 all_tools_found = True 1179 export_vars = {} 1180 paths_to_export = [] 1181 for name, tool in tools_info.items(): 1182 if tool.get_install_type() == IDFTool.INSTALL_NEVER: 1183 continue 1184 tool.find_installed_versions() 1185 1186 if tool.version_in_path: 1187 if tool.version_in_path not in tool.versions: 1188 # unsupported version 1189 if args.prefer_system: # type: ignore 1190 warn('using an unsupported version of tool {} found in PATH: {}'.format( 1191 tool.name, tool.version_in_path)) 1192 continue 1193 else: 1194 # unsupported version in path 1195 pass 1196 else: 1197 # supported/deprecated version in PATH, use it 1198 version_obj = tool.versions[tool.version_in_path] 1199 if version_obj.status == IDFToolVersion.STATUS_SUPPORTED: 1200 info('Using a supported version of tool {} found in PATH: {}.'.format(name, tool.version_in_path), 1201 f=sys.stderr) 1202 info('However the recommended version is {}.'.format(tool.get_recommended_version()), 1203 f=sys.stderr) 1204 elif version_obj.status == IDFToolVersion.STATUS_DEPRECATED: 1205 warn('using a deprecated version of tool {} found in PATH: {}'.format(name, tool.version_in_path)) 1206 continue 1207 1208 self_restart_cmd = '{} {}{}'.format(sys.executable, __file__, 1209 (' --tools-json ' + args.tools_json) if args.tools_json else '') 1210 self_restart_cmd = to_shell_specific_paths([self_restart_cmd])[0] 1211 1212 if IDF_TOOLS_EXPORT_CMD: 1213 prefer_system_hint = '' 1214 else: 1215 prefer_system_hint = ' To use it, run \'{} export --prefer-system\''.format(self_restart_cmd) 1216 1217 if IDF_TOOLS_INSTALL_CMD: 1218 install_cmd = to_shell_specific_paths([IDF_TOOLS_INSTALL_CMD])[0] 1219 else: 1220 install_cmd = self_restart_cmd + ' install' 1221 1222 if not tool.versions_installed: 1223 if tool.get_install_type() == IDFTool.INSTALL_ALWAYS: 1224 all_tools_found = False 1225 fatal('tool {} has no installed versions. Please run \'{}\' to install it.'.format( 1226 tool.name, install_cmd)) 1227 if tool.version_in_path and tool.version_in_path not in tool.versions: 1228 info('An unsupported version of tool {} was found in PATH: {}. '.format(name, tool.version_in_path) + 1229 prefer_system_hint, f=sys.stderr) 1230 continue 1231 else: 1232 # tool is optional, and does not have versions installed 1233 # use whatever is available in PATH 1234 continue 1235 1236 if tool.version_in_path and tool.version_in_path not in tool.versions: 1237 info('Not using an unsupported version of tool {} found in PATH: {}.'.format( 1238 tool.name, tool.version_in_path) + prefer_system_hint, f=sys.stderr) 1239 1240 version_to_use = tool.get_preferred_installed_version() 1241 export_paths = tool.get_export_paths(version_to_use) 1242 if export_paths: 1243 paths_to_export += export_paths 1244 tool_export_vars = tool.get_export_vars(version_to_use) 1245 for k, v in tool_export_vars.items(): 1246 old_v = os.environ.get(k) 1247 if old_v is None or old_v != v: 1248 export_vars[k] = v 1249 1250 current_path = os.getenv('PATH') 1251 idf_python_env_path, idf_python_export_path, virtualenv_python = get_python_env_path() 1252 if os.path.exists(virtualenv_python): 1253 idf_python_env_path = to_shell_specific_paths([idf_python_env_path])[0] 1254 if os.getenv('IDF_PYTHON_ENV_PATH') != idf_python_env_path: 1255 export_vars['IDF_PYTHON_ENV_PATH'] = to_shell_specific_paths([idf_python_env_path])[0] 1256 if idf_python_export_path not in current_path: 1257 paths_to_export.append(idf_python_export_path) 1258 1259 idf_tools_dir = os.path.join(global_idf_path, 'tools') 1260 idf_tools_dir = to_shell_specific_paths([idf_tools_dir])[0] 1261 if idf_tools_dir not in current_path: 1262 paths_to_export.append(idf_tools_dir) 1263 1264 if sys.platform == 'win32' and 'MSYSTEM' not in os.environ: 1265 old_path = '%PATH%' 1266 path_sep = ';' 1267 else: 1268 old_path = '$PATH' 1269 # can't trust os.pathsep here, since for Windows Python started from MSYS shell, 1270 # os.pathsep will be ';' 1271 path_sep = ':' 1272 1273 if args.format == EXPORT_SHELL: 1274 if sys.platform == 'win32' and 'MSYSTEM' not in os.environ: 1275 export_format = 'SET "{}={}"' 1276 export_sep = '\n' 1277 else: 1278 export_format = 'export {}="{}"' 1279 export_sep = ';' 1280 elif args.format == EXPORT_KEY_VALUE: 1281 export_format = '{}={}' 1282 export_sep = '\n' 1283 else: 1284 raise NotImplementedError('unsupported export format {}'.format(args.format)) 1285 1286 if paths_to_export: 1287 export_vars['PATH'] = path_sep.join(to_shell_specific_paths(paths_to_export) + [old_path]) 1288 1289 export_statements = export_sep.join([export_format.format(k, v) for k, v in export_vars.items()]) 1290 1291 if export_statements: 1292 print(export_statements) 1293 1294 if not all_tools_found: 1295 raise SystemExit(1) 1296 1297 1298def apply_url_mirrors(args, tool_download_obj): # type: ignore 1299 apply_mirror_prefix_map(args, tool_download_obj) 1300 apply_github_assets_option(tool_download_obj) 1301 1302 1303def apply_mirror_prefix_map(args, tool_download_obj): # type: ignore 1304 """Rewrite URL for given tool_obj, given tool_version, and current platform, 1305 if --mirror-prefix-map flag or IDF_MIRROR_PREFIX_MAP environment variable is given. 1306 """ 1307 mirror_prefix_map = None 1308 mirror_prefix_map_env = os.getenv('IDF_MIRROR_PREFIX_MAP') 1309 if mirror_prefix_map_env: 1310 mirror_prefix_map = mirror_prefix_map_env.split(';') 1311 if IDF_MAINTAINER and args.mirror_prefix_map: 1312 if mirror_prefix_map: 1313 warn('Both IDF_MIRROR_PREFIX_MAP environment variable and --mirror-prefix-map flag are specified, ' + 1314 'will use the value from the command line.') 1315 mirror_prefix_map = args.mirror_prefix_map 1316 if mirror_prefix_map and tool_download_obj: 1317 for item in mirror_prefix_map: 1318 if URL_PREFIX_MAP_SEPARATOR not in item: 1319 warn('invalid mirror-prefix-map item (missing \'{}\') {}'.format(URL_PREFIX_MAP_SEPARATOR, item)) 1320 continue 1321 search, replace = item.split(URL_PREFIX_MAP_SEPARATOR, 1) 1322 old_url = tool_download_obj.url 1323 new_url = re.sub(search, replace, old_url) 1324 if new_url != old_url: 1325 info('Changed download URL: {} => {}'.format(old_url, new_url)) 1326 tool_download_obj.url = new_url 1327 break 1328 1329 1330def apply_github_assets_option(tool_download_obj): # type: ignore 1331 """ Rewrite URL for given tool_obj if the download URL is an https://github.com/ URL and the variable 1332 IDF_GITHUB_ASSETS is set. The github.com part of the URL will be replaced. 1333 """ 1334 try: 1335 github_assets = os.environ['IDF_GITHUB_ASSETS'].strip() 1336 except KeyError: 1337 return # no IDF_GITHUB_ASSETS 1338 if not github_assets: # variable exists but is empty 1339 return 1340 1341 # check no URL qualifier in the mirror URL 1342 if '://' in github_assets: 1343 fatal("IDF_GITHUB_ASSETS shouldn't include any URL qualifier, https:// is assumed") 1344 raise SystemExit(1) 1345 1346 # Strip any trailing / from the mirror URL 1347 github_assets = github_assets.rstrip('/') 1348 1349 old_url = tool_download_obj.url 1350 new_url = re.sub(r'^https://github.com/', 'https://{}/'.format(github_assets), old_url) 1351 if new_url != old_url: 1352 info('Using GitHub assets mirror for URL: {} => {}'.format(old_url, new_url)) 1353 tool_download_obj.url = new_url 1354 1355 1356def action_download(args): # type: ignore 1357 tools_info = load_tools_info() 1358 tools_spec = args.tools 1359 targets = [] # type: list[str] 1360 # Installing only single tools, no targets are specified. 1361 if 'required' in tools_spec: 1362 targets = clean_targets(args.targets) 1363 1364 if args.platform not in PLATFORM_FROM_NAME: 1365 fatal('unknown platform: {}' % args.platform) 1366 raise SystemExit(1) 1367 platform = PLATFORM_FROM_NAME[args.platform] 1368 1369 tools_info_for_platform = OrderedDict() 1370 for name, tool_obj in tools_info.items(): 1371 tool_for_platform = tool_obj.copy_for_platform(platform) 1372 tools_info_for_platform[name] = tool_for_platform 1373 1374 if not tools_spec or 'required' in tools_spec: 1375 # Downloading tools for all ESP_targets required by the operating system. 1376 tools_spec = [k for k, v in tools_info_for_platform.items() if v.get_install_type() == IDFTool.INSTALL_ALWAYS] 1377 # Filtering tools user defined list of ESP_targets 1378 if 'all' not in targets: 1379 def is_tool_selected(tool): # type: (IDFTool) -> bool 1380 supported_targets = tool.get_supported_targets() 1381 return (any(item in targets for item in supported_targets) or supported_targets == ['all']) 1382 tools_spec = [k for k in tools_spec if is_tool_selected(tools_info[k])] 1383 info('Downloading tools for {}: {}'.format(platform, ', '.join(tools_spec))) 1384 1385 # Downloading tools for all ESP_targets (MacOS, Windows, Linux) 1386 elif 'all' in tools_spec: 1387 tools_spec = [k for k, v in tools_info_for_platform.items() if v.get_install_type() != IDFTool.INSTALL_NEVER] 1388 info('Downloading tools for {}: {}'.format(platform, ', '.join(tools_spec))) 1389 1390 for tool_spec in tools_spec: 1391 if '@' not in tool_spec: 1392 tool_name = tool_spec 1393 tool_version = None 1394 else: 1395 tool_name, tool_version = tool_spec.split('@', 1) 1396 if tool_name not in tools_info_for_platform: 1397 fatal('unknown tool name: {}'.format(tool_name)) 1398 raise SystemExit(1) 1399 tool_obj = tools_info_for_platform[tool_name] 1400 if tool_version is not None and tool_version not in tool_obj.versions: 1401 fatal('unknown version for tool {}: {}'.format(tool_name, tool_version)) 1402 raise SystemExit(1) 1403 if tool_version is None: 1404 tool_version = tool_obj.get_recommended_version() 1405 if tool_version is None: 1406 fatal('tool {} not found for {} platform'.format(tool_name, platform)) 1407 raise SystemExit(1) 1408 tool_spec = '{}@{}'.format(tool_name, tool_version) 1409 1410 info('Downloading {}'.format(tool_spec)) 1411 apply_url_mirrors(args, tool_obj.versions[tool_version].get_download_for_platform(platform)) 1412 1413 tool_obj.download(tool_version) 1414 1415 1416def action_install(args): # type: ignore 1417 tools_info = load_tools_info() 1418 tools_spec = args.tools # type: ignore 1419 targets = [] # type: list[str] 1420 # Installing only single tools, no targets are specified. 1421 if 'required' in tools_spec: 1422 targets = clean_targets(args.targets) 1423 info('Selected targets are: {}' .format(', '.join(get_user_defined_targets()))) 1424 1425 if not tools_spec or 'required' in tools_spec: 1426 # Installing tools for all ESP_targets required by the operating system. 1427 tools_spec = [k for k, v in tools_info.items() if v.get_install_type() == IDFTool.INSTALL_ALWAYS] 1428 # Filtering tools user defined list of ESP_targets 1429 if 'all' not in targets: 1430 def is_tool_selected(tool): # type: (IDFTool) -> bool 1431 supported_targets = tool.get_supported_targets() 1432 return (any(item in targets for item in supported_targets) or supported_targets == ['all']) 1433 tools_spec = [k for k in tools_spec if is_tool_selected(tools_info[k])] 1434 info('Installing tools: {}'.format(', '.join(tools_spec))) 1435 1436 # Installing tools for all ESP_targets (MacOS, Windows, Linux) 1437 elif 'all' in tools_spec: 1438 tools_spec = [k for k, v in tools_info.items() if v.get_install_type() != IDFTool.INSTALL_NEVER] 1439 info('Installing tools: {}'.format(', '.join(tools_spec))) 1440 1441 for tool_spec in tools_spec: 1442 if '@' not in tool_spec: 1443 tool_name = tool_spec 1444 tool_version = None 1445 else: 1446 tool_name, tool_version = tool_spec.split('@', 1) 1447 if tool_name not in tools_info: 1448 fatal('unknown tool name: {}'.format(tool_name)) 1449 raise SystemExit(1) 1450 tool_obj = tools_info[tool_name] 1451 if not tool_obj.compatible_with_platform(): 1452 fatal('tool {} does not have versions compatible with platform {}'.format(tool_name, CURRENT_PLATFORM)) 1453 raise SystemExit(1) 1454 if tool_version is not None and tool_version not in tool_obj.versions: 1455 fatal('unknown version for tool {}: {}'.format(tool_name, tool_version)) 1456 raise SystemExit(1) 1457 if tool_version is None: 1458 tool_version = tool_obj.get_recommended_version() 1459 assert tool_version is not None 1460 tool_obj.find_installed_versions() 1461 tool_spec = '{}@{}'.format(tool_name, tool_version) 1462 if tool_version in tool_obj.versions_installed: 1463 info('Skipping {} (already installed)'.format(tool_spec)) 1464 continue 1465 1466 info('Installing {}'.format(tool_spec)) 1467 apply_url_mirrors(args, tool_obj.versions[tool_version].get_download_for_platform(PYTHON_PLATFORM)) 1468 1469 tool_obj.download(tool_version) 1470 tool_obj.install(tool_version) 1471 1472 1473def get_wheels_dir(): # type: () -> Optional[str] 1474 tools_info = load_tools_info() 1475 wheels_package_name = 'idf-python-wheels' 1476 if wheels_package_name not in tools_info: 1477 return None 1478 wheels_package = tools_info[wheels_package_name] 1479 recommended_version = wheels_package.get_recommended_version() 1480 if recommended_version is None: 1481 return None 1482 wheels_dir = wheels_package.get_path_for_version(recommended_version) 1483 if not os.path.exists(wheels_dir): 1484 return None 1485 return wheels_dir 1486 1487 1488def action_install_python_env(args): # type: ignore 1489 reinstall = args.reinstall 1490 idf_python_env_path, _, virtualenv_python = get_python_env_path() 1491 1492 is_virtualenv = hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix) 1493 if is_virtualenv and (not os.path.exists(idf_python_env_path) or reinstall): 1494 fatal('This script was called from a virtual environment, can not create a virtual environment again') 1495 raise SystemExit(1) 1496 1497 if os.path.exists(virtualenv_python): 1498 try: 1499 subprocess.check_call([virtualenv_python, '--version'], stdout=sys.stdout, stderr=sys.stderr) 1500 except (OSError, subprocess.CalledProcessError): 1501 # At this point we can reinstall the virtual environment if it is non-functional. This can happen at least 1502 # when the Python interpreter was removed which was used to create the virtual environment. 1503 reinstall = True 1504 1505 try: 1506 subprocess.check_call([virtualenv_python, '-m', 'pip', '--version'], stdout=sys.stdout, stderr=sys.stderr) 1507 except subprocess.CalledProcessError: 1508 warn('pip is not available in the existing virtual environment, new virtual environment will be created.') 1509 # Reinstallation of the virtual environment could help if pip was installed for the main Python 1510 reinstall = True 1511 1512 if reinstall and os.path.exists(idf_python_env_path): 1513 warn('Removing the existing Python environment in {}'.format(idf_python_env_path)) 1514 shutil.rmtree(idf_python_env_path) 1515 1516 if not os.path.exists(virtualenv_python): 1517 # Before creating the virtual environment, check if pip is installed. 1518 try: 1519 subprocess.check_call([sys.executable, '-m', 'pip', '--version']) 1520 except subprocess.CalledProcessError: 1521 fatal('Python interpreter at {} doesn\'t have pip installed. ' 1522 'Please check the Getting Started Guides for the steps to install prerequisites for your OS.'.format(sys.executable)) 1523 raise SystemExit(1) 1524 1525 virtualenv_installed_via_pip = False 1526 try: 1527 import virtualenv # noqa: F401 1528 except ImportError: 1529 info('Installing virtualenv') 1530 subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user', 'virtualenv'], 1531 stdout=sys.stdout, stderr=sys.stderr) 1532 virtualenv_installed_via_pip = True 1533 # since we just installed virtualenv via pip, we know that version is recent enough 1534 # so the version check below is not necessary. 1535 1536 with_seeder_option = True 1537 if not virtualenv_installed_via_pip: 1538 # virtualenv is already present in the system and may have been installed via OS package manager 1539 # check the version to determine if we should add --seeder option 1540 try: 1541 major_ver = int(virtualenv.__version__.split('.')[0]) 1542 if major_ver < 20: 1543 warn('Virtualenv version {} is old, please consider upgrading it'.format(virtualenv.__version__)) 1544 with_seeder_option = False 1545 except (ValueError, NameError, AttributeError, IndexError): 1546 pass 1547 1548 info('Creating a new Python environment in {}'.format(idf_python_env_path)) 1549 virtualenv_options = ['--python', sys.executable] 1550 if with_seeder_option: 1551 virtualenv_options += ['--seeder', 'pip'] 1552 1553 subprocess.check_call([sys.executable, '-m', 'virtualenv', 1554 *virtualenv_options, 1555 idf_python_env_path], 1556 stdout=sys.stdout, stderr=sys.stderr) 1557 env_copy = os.environ.copy() 1558 if env_copy.get('PIP_USER') == 'yes': 1559 warn('Found PIP_USER="yes" in the environment. Disabling PIP_USER in this shell to install packages into a virtual environment.') 1560 env_copy['PIP_USER'] = 'no' 1561 run_args = [virtualenv_python, '-m', 'pip', 'install', '--no-warn-script-location'] 1562 requirements_txt = os.path.join(global_idf_path, 'requirements.txt') 1563 run_args += ['-r', requirements_txt] 1564 if args.extra_wheels_dir: 1565 run_args += ['--find-links', args.extra_wheels_dir] 1566 if args.no_index: 1567 run_args += ['--no-index'] 1568 if args.extra_wheels_url: 1569 run_args += ['--extra-index-url', args.extra_wheels_url] 1570 1571 wheels_dir = get_wheels_dir() 1572 if wheels_dir is not None: 1573 run_args += ['--find-links', wheels_dir] 1574 1575 info('Installing Python packages from {}'.format(requirements_txt)) 1576 subprocess.check_call(run_args, stdout=sys.stdout, stderr=sys.stderr, env=env_copy) 1577 1578 1579def action_add_version(args): # type: ignore 1580 tools_info = load_tools_info() 1581 tool_name = args.tool 1582 tool_obj = tools_info.get(tool_name) 1583 if not tool_obj: 1584 info('Creating new tool entry for {}'.format(tool_name)) 1585 tool_obj = IDFTool(tool_name, TODO_MESSAGE, IDFTool.INSTALL_ALWAYS, 1586 TODO_MESSAGE, TODO_MESSAGE, [TODO_MESSAGE], TODO_MESSAGE) 1587 tools_info[tool_name] = tool_obj 1588 version = args.version 1589 version_obj = tool_obj.versions.get(version) 1590 if version not in tool_obj.versions: 1591 info('Creating new version {}'.format(version)) 1592 version_obj = IDFToolVersion(version, IDFToolVersion.STATUS_SUPPORTED) 1593 tool_obj.versions[version] = version_obj 1594 url_prefix = args.url_prefix or 'https://%s/' % TODO_MESSAGE 1595 for file_path in args.files: 1596 file_name = os.path.basename(file_path) 1597 # Guess which platform this file is for 1598 found_platform = None 1599 for platform_alias, platform_id in PLATFORM_FROM_NAME.items(): 1600 if platform_alias in file_name: 1601 found_platform = platform_id 1602 break 1603 if found_platform is None: 1604 info('Could not guess platform for file {}'.format(file_name)) 1605 found_platform = TODO_MESSAGE 1606 # Get file size and calculate the SHA256 1607 file_size, file_sha256 = get_file_size_sha256(file_path) 1608 url = url_prefix + file_name 1609 info('Adding download for platform {}'.format(found_platform)) 1610 info(' size: {}'.format(file_size)) 1611 info(' SHA256: {}'.format(file_sha256)) 1612 info(' URL: {}'.format(url)) 1613 version_obj.add_download(found_platform, url, file_size, file_sha256) 1614 json_str = dump_tools_json(tools_info) 1615 if not args.output: 1616 args.output = os.path.join(global_idf_path, TOOLS_FILE_NEW) 1617 with open(args.output, 'w') as f: 1618 f.write(json_str) 1619 f.write('\n') 1620 info('Wrote output to {}'.format(args.output)) 1621 1622 1623def action_rewrite(args): # type: ignore 1624 tools_info = load_tools_info() 1625 json_str = dump_tools_json(tools_info) 1626 if not args.output: 1627 args.output = os.path.join(global_idf_path, TOOLS_FILE_NEW) 1628 with open(args.output, 'w') as f: 1629 f.write(json_str) 1630 f.write('\n') 1631 info('Wrote output to {}'.format(args.output)) 1632 1633 1634def action_validate(args): # type: ignore 1635 try: 1636 import jsonschema 1637 except ImportError: 1638 fatal('You need to install jsonschema package to use validate command') 1639 raise SystemExit(1) 1640 1641 with open(os.path.join(global_idf_path, TOOLS_FILE), 'r') as tools_file: 1642 tools_json = json.load(tools_file) 1643 1644 with open(os.path.join(global_idf_path, TOOLS_SCHEMA_FILE), 'r') as schema_file: 1645 schema_json = json.load(schema_file) 1646 jsonschema.validate(tools_json, schema_json) 1647 # on failure, this will raise an exception with a fairly verbose diagnostic message 1648 1649 1650def action_gen_doc(args): # type: ignore 1651 f = args.output 1652 tools_info = load_tools_info() 1653 1654 def print_out(text): # type: (str) -> None 1655 f.write(text + '\n') 1656 1657 print_out('.. |zwsp| unicode:: U+200B') 1658 print_out(' :trim:') 1659 print_out('') 1660 1661 idf_gh_url = 'https://github.com/espressif/esp-idf' 1662 for tool_name, tool_obj in tools_info.items(): 1663 info_url = tool_obj.options.info_url 1664 if idf_gh_url + '/tree' in info_url: 1665 info_url = re.sub(idf_gh_url + r'/tree/\w+/(.*)', r':idf:`\1`', info_url) 1666 1667 license_url = 'https://spdx.org/licenses/' + tool_obj.options.license 1668 1669 print_out(""" 1670.. _tool-{name}: 1671 1672{name} 1673{underline} 1674 1675{description} 1676 1677.. include:: idf-tools-notes.inc 1678 :start-after: tool-{name}-notes 1679 :end-before: --- 1680 1681License: `{license} <{license_url}>`_ 1682 1683More info: {info_url} 1684 1685.. list-table:: 1686 :widths: 10 10 80 1687 :header-rows: 1 1688 1689 * - Platform 1690 - Required 1691 - Download 1692""".rstrip().format(name=tool_name, 1693 underline=args.heading_underline_char * len(tool_name), 1694 description=tool_obj.description, 1695 license=tool_obj.options.license, 1696 license_url=license_url, 1697 info_url=info_url)) 1698 1699 for platform_name in sorted(tool_obj.get_supported_platforms()): 1700 platform_tool = tool_obj.copy_for_platform(platform_name) 1701 install_type = platform_tool.get_install_type() 1702 if install_type == IDFTool.INSTALL_NEVER: 1703 continue 1704 elif install_type == IDFTool.INSTALL_ALWAYS: 1705 install_type_str = 'required' 1706 elif install_type == IDFTool.INSTALL_ON_REQUEST: 1707 install_type_str = 'optional' 1708 else: 1709 raise NotImplementedError() 1710 1711 version = platform_tool.get_recommended_version() 1712 version_obj = platform_tool.versions[version] 1713 download_obj = version_obj.get_download_for_platform(platform_name) 1714 1715 # Note: keep the list entries indented to the same number of columns 1716 # as the list header above. 1717 print_out(""" 1718 * - {} 1719 - {} 1720 - {} 1721 1722 .. rst-class:: tool-sha256 1723 1724 SHA256: {} 1725""".strip('\n').format(platform_name, install_type_str, download_obj.url, download_obj.sha256)) 1726 1727 print_out('') 1728 print_out('') 1729 1730 1731def main(argv): # type: (list[str]) -> None 1732 parser = argparse.ArgumentParser() 1733 1734 parser.add_argument('--quiet', help='Don\'t output diagnostic messages to stdout/stderr', action='store_true') 1735 parser.add_argument('--non-interactive', help='Don\'t output interactive messages and questions', action='store_true') 1736 parser.add_argument('--tools-json', help='Path to the tools.json file to use') 1737 parser.add_argument('--idf-path', help='ESP-IDF path to use') 1738 1739 subparsers = parser.add_subparsers(dest='action') 1740 subparsers.add_parser('list', help='List tools and versions available') 1741 subparsers.add_parser('check', help='Print summary of tools installed or found in PATH') 1742 export = subparsers.add_parser('export', help='Output command for setting tool paths, suitable for shell') 1743 export.add_argument('--format', choices=[EXPORT_SHELL, EXPORT_KEY_VALUE], default=EXPORT_SHELL, 1744 help='Format of the output: shell (suitable for printing into shell), ' + 1745 'or key-value (suitable for parsing by other tools') 1746 export.add_argument('--prefer-system', help='Normally, if the tool is already present in PATH, ' + 1747 'but has an unsupported version, a version from the tools directory ' + 1748 'will be used instead. If this flag is given, the version in PATH ' + 1749 'will be used.', action='store_true') 1750 install = subparsers.add_parser('install', help='Download and install tools into the tools directory') 1751 install.add_argument('tools', metavar='TOOL', nargs='*', default=['required'], 1752 help='Tools to install. ' + 1753 'To install a specific version use <tool_name>@<version> syntax. ' + 1754 'Use empty or \'required\' to install required tools, not optional ones. ' + 1755 'Use \'all\' to install all tools, including the optional ones.') 1756 install.add_argument('--targets', default='all', help='A comma separated list of desired chip targets for installing.' + 1757 ' It defaults to installing all supported targets.') 1758 1759 download = subparsers.add_parser('download', help='Download the tools into the dist directory') 1760 download.add_argument('--platform', help='Platform to download the tools for') 1761 download.add_argument('tools', metavar='TOOL', nargs='*', default=['required'], 1762 help='Tools to download. ' + 1763 'To download a specific version use <tool_name>@<version> syntax. ' + 1764 'Use empty or \'required\' to download required tools, not optional ones. ' + 1765 'Use \'all\' to download all tools, including the optional ones.') 1766 download.add_argument('--targets', default='all', help='A comma separated list of desired chip targets for installing.' + 1767 ' It defaults to installing all supported targets.') 1768 1769 if IDF_MAINTAINER: 1770 for subparser in [download, install]: 1771 subparser.add_argument('--mirror-prefix-map', nargs='*', 1772 help='Pattern to rewrite download URLs, with source and replacement separated by comma.' + 1773 ' E.g. http://foo.com,http://test.foo.com') 1774 1775 install_python_env = subparsers.add_parser('install-python-env', 1776 help='Create Python virtual environment and install the ' + 1777 'required Python packages') 1778 install_python_env.add_argument('--reinstall', help='Discard the previously installed environment', 1779 action='store_true') 1780 install_python_env.add_argument('--extra-wheels-dir', help='Additional directories with wheels ' + 1781 'to use during installation') 1782 install_python_env.add_argument('--extra-wheels-url', help='Additional URL with wheels', default='https://dl.espressif.com/pypi') 1783 install_python_env.add_argument('--no-index', help='Work offline without retrieving wheels index') 1784 1785 if IDF_MAINTAINER: 1786 add_version = subparsers.add_parser('add-version', help='Add or update download info for a version') 1787 add_version.add_argument('--output', help='Save new tools.json into this file') 1788 add_version.add_argument('--tool', help='Tool name to set add a version for', required=True) 1789 add_version.add_argument('--version', help='Version identifier', required=True) 1790 add_version.add_argument('--url-prefix', help='String to prepend to file names to obtain download URLs') 1791 add_version.add_argument('files', help='File names of the download artifacts', nargs='*') 1792 1793 rewrite = subparsers.add_parser('rewrite', help='Load tools.json, validate, and save the result back into JSON') 1794 rewrite.add_argument('--output', help='Save new tools.json into this file') 1795 1796 subparsers.add_parser('validate', help='Validate tools.json against schema file') 1797 1798 gen_doc = subparsers.add_parser('gen-doc', help='Write the list of tools as a documentation page') 1799 gen_doc.add_argument('--output', type=argparse.FileType('w'), default=sys.stdout, 1800 help='Output file name') 1801 gen_doc.add_argument('--heading-underline-char', help='Character to use when generating RST sections', default='~') 1802 1803 args = parser.parse_args(argv) 1804 1805 if args.action is None: 1806 parser.print_help() 1807 parser.exit(1) 1808 1809 if args.quiet: 1810 global global_quiet 1811 global_quiet = True 1812 1813 if args.non_interactive: 1814 global global_non_interactive 1815 global_non_interactive = True 1816 1817 global global_idf_path 1818 global_idf_path = os.environ.get('IDF_PATH') 1819 if args.idf_path: 1820 global_idf_path = args.idf_path 1821 if not global_idf_path: 1822 global_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) 1823 os.environ['IDF_PATH'] = global_idf_path 1824 1825 global global_idf_tools_path 1826 global_idf_tools_path = os.environ.get('IDF_TOOLS_PATH') or os.path.expanduser(IDF_TOOLS_PATH_DEFAULT) 1827 1828 # On macOS, unset __PYVENV_LAUNCHER__ variable if it is set. 1829 # Otherwise sys.executable keeps pointing to the system Python, even when a python binary from a virtualenv is invoked. 1830 # See https://bugs.python.org/issue22490#msg283859. 1831 os.environ.pop('__PYVENV_LAUNCHER__', None) 1832 1833 if sys.version_info.major == 2: 1834 try: 1835 global_idf_tools_path.decode('ascii') # type: ignore 1836 except UnicodeDecodeError: 1837 fatal('IDF_TOOLS_PATH contains non-ASCII characters: {}'.format(global_idf_tools_path) + 1838 '\nThis is not supported yet with Python 2. ' + 1839 'Please set IDF_TOOLS_PATH to a directory with an ASCII name, or switch to Python 3.') 1840 raise SystemExit(1) 1841 1842 if CURRENT_PLATFORM == UNKNOWN_PLATFORM: 1843 fatal('Platform {} appears to be unsupported'.format(PYTHON_PLATFORM)) 1844 raise SystemExit(1) 1845 1846 global global_tools_json 1847 if args.tools_json: 1848 global_tools_json = args.tools_json 1849 else: 1850 global_tools_json = os.path.join(global_idf_path, TOOLS_FILE) 1851 1852 action_func_name = 'action_' + args.action.replace('-', '_') 1853 action_func = globals()[action_func_name] 1854 1855 action_func(args) 1856 1857 1858if __name__ == '__main__': 1859 main(sys.argv[1:]) 1860