idf_tools.py 91 KB

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