idf_tools.py 72 KB

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