idf_tools.py 71 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693
  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. #
  4. # This script helps installing tools required to use the ESP-IDF, and updating PATH
  5. # to use the installed tools. It can also create a Python virtual environment,
  6. # and install Python requirements into it.
  7. # It does not install OS dependencies. It does install tools such as the Xtensa
  8. # GCC toolchain and ESP32 ULP coprocessor toolchain.
  9. #
  10. # By default, downloaded tools will be installed under $HOME/.espressif directory
  11. # (%USERPROFILE%/.espressif on Windows). This path can be modified by setting
  12. # IDF_TOOLS_PATH variable prior to running this tool.
  13. #
  14. # Users do not need to interact with this script directly. In IDF root directory,
  15. # install.sh (.bat) and export.sh (.bat) scripts are provided to invoke this script.
  16. #
  17. # Usage:
  18. #
  19. # * To install the tools, run `idf_tools.py install`.
  20. #
  21. # * To install the Python environment, run `idf_tools.py install-python-env`.
  22. #
  23. # * To start using the tools, run `eval "$(idf_tools.py export)"` — this will update
  24. # the PATH to point to the installed tools and set up other environment variables
  25. # needed by the tools.
  26. #
  27. ###
  28. #
  29. # Copyright 2019 Espressif Systems (Shanghai) PTE LTD
  30. #
  31. # Licensed under the Apache License, Version 2.0 (the "License");
  32. # you may not use this file except in compliance with the License.
  33. # You may obtain a copy of the License at
  34. #
  35. # http://www.apache.org/licenses/LICENSE-2.0
  36. #
  37. # Unless required by applicable law or agreed to in writing, software
  38. # distributed under the License is distributed on an "AS IS" BASIS,
  39. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  40. # See the License for the specific language governing permissions and
  41. # limitations under the License.
  42. import json
  43. import os
  44. import subprocess
  45. import sys
  46. import argparse
  47. import re
  48. import platform
  49. import hashlib
  50. import tarfile
  51. import time
  52. import zipfile
  53. import errno
  54. import shutil
  55. import functools
  56. import copy
  57. from collections import OrderedDict, namedtuple
  58. try:
  59. import typing # noqa: F401
  60. from typing import IO, Any, Callable, Optional, Tuple, Union # noqa: F401
  61. except ImportError:
  62. pass
  63. try:
  64. from urllib.request import urlretrieve
  65. except ImportError:
  66. from urllib import urlretrieve
  67. try:
  68. from exceptions import WindowsError
  69. except ImportError:
  70. class WindowsError(OSError):
  71. pass
  72. TOOLS_FILE = 'tools/tools.json'
  73. TOOLS_SCHEMA_FILE = 'tools/tools_schema.json'
  74. TOOLS_FILE_NEW = 'tools/tools.new.json'
  75. IDF_ENV_FILE = 'idf-env.json'
  76. TOOLS_FILE_VERSION = 1
  77. IDF_TOOLS_PATH_DEFAULT = os.path.join('~', '.espressif')
  78. UNKNOWN_VERSION = 'unknown'
  79. SUBST_TOOL_PATH_REGEX = re.compile(r'\${TOOL_PATH}')
  80. VERSION_REGEX_REPLACE_DEFAULT = r'\1'
  81. IDF_MAINTAINER = os.environ.get('IDF_MAINTAINER') or False
  82. TODO_MESSAGE = 'TODO'
  83. DOWNLOAD_RETRY_COUNT = 3
  84. URL_PREFIX_MAP_SEPARATOR = ','
  85. IDF_TOOLS_INSTALL_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD')
  86. IDF_TOOLS_EXPORT_CMD = os.environ.get('IDF_TOOLS_INSTALL_CMD')
  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. PLATFORM_LINUX32: PLATFORM_LINUX32,
  121. 'linux32': PLATFORM_LINUX32,
  122. 'Linux-i686': PLATFORM_LINUX32,
  123. PLATFORM_LINUX_ARM32: PLATFORM_LINUX_ARM32,
  124. 'Linux-arm': PLATFORM_LINUX_ARM32,
  125. 'Linux-armv7l': PLATFORM_LINUX_ARM32,
  126. PLATFORM_LINUX_ARMHF: PLATFORM_LINUX_ARMHF,
  127. PLATFORM_LINUX_ARM64: PLATFORM_LINUX_ARM64,
  128. 'Linux-arm64': PLATFORM_LINUX_ARM64,
  129. 'Linux-aarch64': PLATFORM_LINUX_ARM64,
  130. 'Linux-armv8l': PLATFORM_LINUX_ARM64,
  131. }
  132. UNKNOWN_PLATFORM = 'unknown'
  133. CURRENT_PLATFORM = PLATFORM_FROM_NAME.get(PYTHON_PLATFORM, UNKNOWN_PLATFORM)
  134. EXPORT_SHELL = 'shell'
  135. EXPORT_KEY_VALUE = 'key-value'
  136. global_quiet = False
  137. global_non_interactive = False
  138. global_idf_path = None # type: typing.Optional[str]
  139. global_idf_tools_path = None # type: typing.Optional[str]
  140. global_tools_json = None # type: typing.Optional[str]
  141. def fatal(text, *args):
  142. if not global_quiet:
  143. sys.stderr.write('ERROR: ' + text + '\n', *args)
  144. def warn(text, *args):
  145. if not global_quiet:
  146. sys.stderr.write('WARNING: ' + text + '\n', *args)
  147. def info(text, f=None, *args):
  148. if not global_quiet:
  149. if f is None:
  150. f = sys.stdout
  151. f.write(text + '\n', *args)
  152. def run_cmd_check_output(cmd, input_text=None, extra_paths=None):
  153. # If extra_paths is given, locate the executable in one of these directories.
  154. # Note: it would seem logical to add extra_paths to env[PATH], instead, and let OS do the job of finding the
  155. # executable for us. However this does not work on Windows: https://bugs.python.org/issue8557.
  156. if extra_paths:
  157. found = False
  158. extensions = ['']
  159. if sys.platform == 'win32':
  160. extensions.append('.exe')
  161. for path in extra_paths:
  162. for ext in extensions:
  163. fullpath = os.path.join(path, cmd[0] + ext)
  164. if os.path.exists(fullpath):
  165. cmd[0] = fullpath
  166. found = True
  167. break
  168. if found:
  169. break
  170. try:
  171. if input_text:
  172. input_text = input_text.encode()
  173. result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, input=input_text)
  174. return result.stdout + result.stderr
  175. except (AttributeError, TypeError):
  176. p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
  177. stdout, stderr = p.communicate(input_text)
  178. if p.returncode != 0:
  179. try:
  180. raise subprocess.CalledProcessError(p.returncode, cmd, stdout, stderr)
  181. except TypeError:
  182. raise subprocess.CalledProcessError(p.returncode, cmd, stdout)
  183. return stdout + stderr
  184. def to_shell_specific_paths(paths_list):
  185. if sys.platform == 'win32':
  186. paths_list = [p.replace('/', os.path.sep) if os.path.sep in p else p for p in paths_list]
  187. if 'MSYSTEM' in os.environ:
  188. paths_msys = run_cmd_check_output(['cygpath', '-u', '-f', '-'],
  189. input_text='\n'.join(paths_list))
  190. paths_list = paths_msys.decode().strip().split('\n')
  191. return paths_list
  192. def get_env_for_extra_paths(extra_paths):
  193. """
  194. Return a copy of environment variables dict, prepending paths listed in extra_paths
  195. to the PATH environment variable.
  196. """
  197. env_arg = os.environ.copy()
  198. new_path = os.pathsep.join(extra_paths) + os.pathsep + env_arg['PATH']
  199. if sys.version_info.major == 2:
  200. env_arg['PATH'] = new_path.encode('utf8')
  201. else:
  202. env_arg['PATH'] = new_path
  203. return env_arg
  204. def get_file_size_sha256(filename, block_size=65536):
  205. sha256 = hashlib.sha256()
  206. size = 0
  207. with open(filename, 'rb') as f:
  208. for block in iter(lambda: f.read(block_size), b''):
  209. sha256.update(block)
  210. size += len(block)
  211. return size, sha256.hexdigest()
  212. def report_progress(count, block_size, total_size):
  213. percent = int(count * block_size * 100 / total_size)
  214. percent = min(100, percent)
  215. sys.stdout.write("\r%d%%" % percent)
  216. sys.stdout.flush()
  217. def mkdir_p(path):
  218. try:
  219. os.makedirs(path)
  220. except OSError as exc:
  221. if exc.errno != errno.EEXIST or not os.path.isdir(path):
  222. raise
  223. def unpack(filename, destination):
  224. info('Extracting {0} to {1}'.format(filename, destination))
  225. if filename.endswith('tar.gz'):
  226. archive_obj = tarfile.open(filename, 'r:gz')
  227. elif filename.endswith('zip'):
  228. archive_obj = zipfile.ZipFile(filename)
  229. else:
  230. raise NotImplementedError('Unsupported archive type')
  231. if sys.version_info.major == 2:
  232. # This is a workaround for the issue that unicode destination is not handled:
  233. # https://bugs.python.org/issue17153
  234. destination = str(destination)
  235. archive_obj.extractall(destination)
  236. # Sometimes renaming a directory on Windows (randomly?) causes a PermissionError.
  237. # This is confirmed to be a workaround:
  238. # https://github.com/espressif/esp-idf/issues/3819#issuecomment-515167118
  239. # https://github.com/espressif/esp-idf/issues/4063#issuecomment-531490140
  240. # https://stackoverflow.com/a/43046729
  241. def rename_with_retry(path_from, path_to): # type: (str, str) -> None
  242. retry_count = 20 if sys.platform.startswith('win') else 1
  243. for retry in range(retry_count):
  244. try:
  245. os.rename(path_from, path_to)
  246. return
  247. except OSError:
  248. msg = 'Rename {} to {} failed'.format(path_from, path_to)
  249. if retry == retry_count - 1:
  250. fatal(msg + '. Antivirus software might be causing this. Disabling it temporarily could solve the issue.')
  251. raise
  252. warn(msg + ', retrying...')
  253. # Sleep before the next try in order to pass the antivirus check on Windows
  254. time.sleep(0.5)
  255. def strip_container_dirs(path, levels):
  256. assert levels > 0
  257. # move the original directory out of the way (add a .tmp suffix)
  258. tmp_path = path + '.tmp'
  259. if os.path.exists(tmp_path):
  260. shutil.rmtree(tmp_path)
  261. rename_with_retry(path, tmp_path)
  262. os.mkdir(path)
  263. base_path = tmp_path
  264. # walk given number of levels down
  265. for level in range(levels):
  266. contents = os.listdir(base_path)
  267. if len(contents) > 1:
  268. raise RuntimeError('at level {}, expected 1 entry, got {}'.format(level, contents))
  269. base_path = os.path.join(base_path, contents[0])
  270. if not os.path.isdir(base_path):
  271. raise RuntimeError('at level {}, {} is not a directory'.format(level, contents[0]))
  272. # get the list of directories/files to move
  273. contents = os.listdir(base_path)
  274. for name in contents:
  275. move_from = os.path.join(base_path, name)
  276. move_to = os.path.join(path, name)
  277. rename_with_retry(move_from, move_to)
  278. shutil.rmtree(tmp_path)
  279. class ToolNotFound(RuntimeError):
  280. pass
  281. class ToolExecError(RuntimeError):
  282. pass
  283. class DownloadError(RuntimeError):
  284. pass
  285. class IDFToolDownload(object):
  286. def __init__(self, platform_name, url, size, sha256):
  287. self.platform_name = platform_name
  288. self.url = url
  289. self.size = size
  290. self.sha256 = sha256
  291. self.platform_name = platform_name
  292. @functools.total_ordering
  293. class IDFToolVersion(object):
  294. STATUS_RECOMMENDED = 'recommended'
  295. STATUS_SUPPORTED = 'supported'
  296. STATUS_DEPRECATED = 'deprecated'
  297. STATUS_VALUES = [STATUS_RECOMMENDED, STATUS_SUPPORTED, STATUS_DEPRECATED]
  298. def __init__(self, version, status):
  299. self.version = version
  300. self.status = status
  301. self.downloads = OrderedDict()
  302. self.latest = False
  303. def __lt__(self, other):
  304. if self.status != other.status:
  305. return self.status > other.status
  306. else:
  307. assert not (self.status == IDFToolVersion.STATUS_RECOMMENDED
  308. and other.status == IDFToolVersion.STATUS_RECOMMENDED)
  309. return self.version < other.version
  310. def __eq__(self, other):
  311. return self.status == other.status and self.version == other.version
  312. def add_download(self, platform_name, url, size, sha256):
  313. self.downloads[platform_name] = IDFToolDownload(platform_name, url, size, sha256)
  314. def get_download_for_platform(self, platform_name): # type: (str) -> IDFToolDownload
  315. if platform_name in PLATFORM_FROM_NAME.keys():
  316. platform_name = PLATFORM_FROM_NAME[platform_name]
  317. if platform_name in self.downloads.keys():
  318. return self.downloads[platform_name]
  319. if 'any' in self.downloads.keys():
  320. return self.downloads['any']
  321. return None
  322. def compatible_with_platform(self, platform_name=PYTHON_PLATFORM):
  323. return self.get_download_for_platform(platform_name) is not None
  324. def get_supported_platforms(self): # type: () -> typing.Set[str]
  325. return set(self.downloads.keys())
  326. OPTIONS_LIST = ['version_cmd',
  327. 'version_regex',
  328. 'version_regex_replace',
  329. 'export_paths',
  330. 'export_vars',
  331. 'install',
  332. 'info_url',
  333. 'license',
  334. 'strip_container_dirs',
  335. 'supported_targets']
  336. IDFToolOptions = namedtuple('IDFToolOptions', OPTIONS_LIST)
  337. class IDFTool(object):
  338. # possible values of 'install' field
  339. INSTALL_ALWAYS = 'always'
  340. INSTALL_ON_REQUEST = 'on_request'
  341. INSTALL_NEVER = 'never'
  342. def __init__(self, name, description, install, info_url, license, version_cmd, version_regex, supported_targets, version_regex_replace=None,
  343. strip_container_dirs=0):
  344. # type: (str, str, str, str, str, list[str], str, list[str], Optional[str], int) -> None
  345. self.name = name
  346. self.description = description
  347. self.versions = OrderedDict() # type: typing.Dict[str, IDFToolVersion]
  348. self.version_in_path = None
  349. self.versions_installed = []
  350. if version_regex_replace is None:
  351. version_regex_replace = VERSION_REGEX_REPLACE_DEFAULT
  352. self.options = IDFToolOptions(version_cmd, version_regex, version_regex_replace,
  353. [], OrderedDict(), install, info_url, license, strip_container_dirs, supported_targets) # type: ignore
  354. self.platform_overrides = [] # type: list[dict[str, str]]
  355. self._platform = CURRENT_PLATFORM
  356. self._update_current_options()
  357. def copy_for_platform(self, platform): # type: (str) -> IDFTool
  358. result = copy.deepcopy(self)
  359. result._platform = platform
  360. result._update_current_options()
  361. return result
  362. def _update_current_options(self):
  363. self._current_options = IDFToolOptions(*self.options)
  364. for override in self.platform_overrides:
  365. if self._platform not in override['platforms']:
  366. continue
  367. override_dict = override.copy()
  368. del override_dict['platforms']
  369. self._current_options = self._current_options._replace(**override_dict)
  370. def add_version(self, version):
  371. assert(type(version) is IDFToolVersion)
  372. self.versions[version.version] = version
  373. def get_path(self): # type: () -> str
  374. return os.path.join(global_idf_tools_path, 'tools', self.name)
  375. def get_path_for_version(self, version): # type: (str) -> str
  376. assert(version in self.versions)
  377. return os.path.join(self.get_path(), version)
  378. def get_export_paths(self, version): # type: (str) -> typing.List[str]
  379. tool_path = self.get_path_for_version(version)
  380. return [os.path.join(tool_path, *p) for p in self._current_options.export_paths]
  381. def get_export_vars(self, version): # type: (str) -> typing.Dict[str]
  382. """
  383. Get the dictionary of environment variables to be exported, for the given version.
  384. Expands:
  385. - ${TOOL_PATH} => the actual path where the version is installed
  386. """
  387. result = {}
  388. for k, v in self._current_options.export_vars.items():
  389. replace_path = self.get_path_for_version(version).replace('\\', '\\\\')
  390. v_repl = re.sub(SUBST_TOOL_PATH_REGEX, replace_path, v)
  391. if v_repl != v:
  392. v_repl = to_shell_specific_paths([v_repl])[0]
  393. result[k] = v_repl
  394. return result
  395. def check_version(self, extra_paths=None): # type: (typing.Optional[typing.List[str]]) -> str
  396. """
  397. Execute the tool, optionally prepending extra_paths to PATH,
  398. extract the version string and return it as a result.
  399. Raises ToolNotFound if the tool is not found (not present in the paths).
  400. Raises ToolExecError if the tool returns with a non-zero exit code.
  401. Returns 'unknown' if tool returns something from which version string
  402. can not be extracted.
  403. """
  404. # this function can not be called for a different platform
  405. assert self._platform == CURRENT_PLATFORM
  406. cmd = self._current_options.version_cmd
  407. try:
  408. version_cmd_result = run_cmd_check_output(cmd, None, extra_paths)
  409. except OSError:
  410. # tool is not on the path
  411. raise ToolNotFound('Tool {} not found'.format(self.name))
  412. except subprocess.CalledProcessError as e:
  413. raise ToolExecError('returned non-zero exit code ({}) with error message:\n{}'.format(
  414. e.returncode, e.stderr.decode('utf-8',errors='ignore'))) # type: ignore
  415. in_str = version_cmd_result.decode("utf-8")
  416. match = re.search(self._current_options.version_regex, in_str)
  417. if not match:
  418. return UNKNOWN_VERSION
  419. return re.sub(self._current_options.version_regex, self._current_options.version_regex_replace, match.group(0))
  420. def get_install_type(self):
  421. return self._current_options.install
  422. def get_supported_targets(self): # type: () -> list[str]
  423. return self._current_options.supported_targets # type: ignore
  424. def compatible_with_platform(self): # type: () -> bool
  425. return any([v.compatible_with_platform() for v in self.versions.values()])
  426. def get_supported_platforms(self): # type: () -> typing.Set[str]
  427. result = set()
  428. for v in self.versions.values():
  429. result.update(v.get_supported_platforms())
  430. return result
  431. def get_recommended_version(self):
  432. recommended_versions = [k for k, v in self.versions.items()
  433. if v.status == IDFToolVersion.STATUS_RECOMMENDED
  434. and v.compatible_with_platform(self._platform)]
  435. assert len(recommended_versions) <= 1
  436. if recommended_versions:
  437. return recommended_versions[0]
  438. return None
  439. def get_preferred_installed_version(self):
  440. recommended_versions = [k for k in self.versions_installed
  441. if self.versions[k].status == IDFToolVersion.STATUS_RECOMMENDED
  442. and self.versions[k].compatible_with_platform(self._platform)]
  443. assert len(recommended_versions) <= 1
  444. if recommended_versions:
  445. return recommended_versions[0]
  446. return None
  447. def find_installed_versions(self):
  448. """
  449. Checks whether the tool can be found in PATH and in global_idf_tools_path.
  450. Writes results to self.version_in_path and self.versions_installed.
  451. """
  452. # this function can not be called for a different platform
  453. assert self._platform == CURRENT_PLATFORM
  454. # First check if the tool is in system PATH
  455. try:
  456. ver_str = self.check_version()
  457. except ToolNotFound:
  458. # not in PATH
  459. pass
  460. except ToolExecError as e:
  461. warn('tool {} found in path, but {}'.format(
  462. self.name, e))
  463. else:
  464. self.version_in_path = ver_str
  465. # Now check all the versions installed in global_idf_tools_path
  466. self.versions_installed = []
  467. for version, version_obj in self.versions.items():
  468. if not version_obj.compatible_with_platform():
  469. continue
  470. tool_path = self.get_path_for_version(version)
  471. if not os.path.exists(tool_path):
  472. # version not installed
  473. continue
  474. try:
  475. ver_str = self.check_version(self.get_export_paths(version))
  476. except ToolNotFound:
  477. warn('directory for tool {} version {} is present, but tool was not found'.format(
  478. self.name, version))
  479. except ToolExecError as e:
  480. warn('tool {} version {} is installed, but {}'.format(
  481. self.name, version, e))
  482. else:
  483. if ver_str != version:
  484. warn('tool {} version {} is installed, but has reported version {}'.format(
  485. self.name, version, ver_str))
  486. else:
  487. self.versions_installed.append(version)
  488. def download(self, version):
  489. assert(version in self.versions)
  490. download_obj = self.versions[version].get_download_for_platform(self._platform)
  491. if not download_obj:
  492. fatal('No packages for tool {} platform {}!'.format(self.name, self._platform))
  493. raise DownloadError()
  494. url = download_obj.url
  495. archive_name = os.path.basename(url)
  496. local_path = os.path.join(global_idf_tools_path, 'dist', archive_name)
  497. mkdir_p(os.path.dirname(local_path))
  498. if os.path.isfile(local_path):
  499. if not self.check_download_file(download_obj, local_path):
  500. warn('removing downloaded file {0} and downloading again'.format(archive_name))
  501. os.unlink(local_path)
  502. else:
  503. info('file {0} is already downloaded'.format(archive_name))
  504. return
  505. downloaded = False
  506. for retry in range(DOWNLOAD_RETRY_COUNT):
  507. local_temp_path = local_path + '.tmp'
  508. info('Downloading {} to {}'.format(archive_name, local_temp_path))
  509. try:
  510. urlretrieve(url, local_temp_path, report_progress if not global_non_interactive else None)
  511. sys.stdout.write("\rDone\n")
  512. except Exception as e:
  513. # urlretrieve could throw different exceptions, e.g. IOError when the server is down
  514. # Errors are ignored because the downloaded file is checked a couple of lines later.
  515. warn('Download failure {}'.format(e))
  516. sys.stdout.flush()
  517. if not os.path.isfile(local_temp_path) or not self.check_download_file(download_obj, local_temp_path):
  518. warn('Failed to download {} to {}'.format(url, local_temp_path))
  519. continue
  520. rename_with_retry(local_temp_path, local_path)
  521. downloaded = True
  522. break
  523. if not downloaded:
  524. fatal('Failed to download, and retry count has expired')
  525. raise DownloadError()
  526. def install(self, version):
  527. # Currently this is called after calling 'download' method, so here are a few asserts
  528. # for the conditions which should be true once that method is done.
  529. assert (version in self.versions)
  530. download_obj = self.versions[version].get_download_for_platform(self._platform)
  531. assert (download_obj is not None)
  532. archive_name = os.path.basename(download_obj.url)
  533. archive_path = os.path.join(global_idf_tools_path, 'dist', archive_name)
  534. assert (os.path.isfile(archive_path))
  535. dest_dir = self.get_path_for_version(version)
  536. if os.path.exists(dest_dir):
  537. warn('destination path already exists, removing')
  538. shutil.rmtree(dest_dir)
  539. mkdir_p(dest_dir)
  540. unpack(archive_path, dest_dir)
  541. if self._current_options.strip_container_dirs:
  542. strip_container_dirs(dest_dir, self._current_options.strip_container_dirs)
  543. @staticmethod
  544. def check_download_file(download_obj, local_path):
  545. expected_sha256 = download_obj.sha256
  546. expected_size = download_obj.size
  547. file_size, file_sha256 = get_file_size_sha256(local_path)
  548. if file_size != expected_size:
  549. warn('file size mismatch for {}, expected {}, got {}'.format(local_path, expected_size, file_size))
  550. return False
  551. if file_sha256 != expected_sha256:
  552. warn('hash mismatch for {}, expected {}, got {}'.format(local_path, expected_sha256, file_sha256))
  553. return False
  554. return True
  555. @classmethod
  556. def from_json(cls, tool_dict):
  557. # json.load will return 'str' types in Python 3 and 'unicode' in Python 2
  558. expected_str_type = type(u'')
  559. # Validate json fields
  560. tool_name = tool_dict.get('name')
  561. if type(tool_name) is not expected_str_type:
  562. raise RuntimeError('tool_name is not a string')
  563. description = tool_dict.get('description')
  564. if type(description) is not expected_str_type:
  565. raise RuntimeError('description is not a string')
  566. version_cmd = tool_dict.get('version_cmd')
  567. if type(version_cmd) is not list:
  568. raise RuntimeError('version_cmd for tool %s is not a list of strings' % tool_name)
  569. version_regex = tool_dict.get('version_regex')
  570. if type(version_regex) is not expected_str_type or not version_regex:
  571. raise RuntimeError('version_regex for tool %s is not a non-empty string' % tool_name)
  572. version_regex_replace = tool_dict.get('version_regex_replace')
  573. if version_regex_replace and type(version_regex_replace) is not expected_str_type:
  574. raise RuntimeError('version_regex_replace for tool %s is not a string' % tool_name)
  575. export_paths = tool_dict.get('export_paths')
  576. if type(export_paths) is not list:
  577. raise RuntimeError('export_paths for tool %s is not a list' % tool_name)
  578. export_vars = tool_dict.get('export_vars', {})
  579. if type(export_vars) is not dict:
  580. raise RuntimeError('export_vars for tool %s is not a mapping' % tool_name)
  581. versions = tool_dict.get('versions')
  582. if type(versions) is not list:
  583. raise RuntimeError('versions for tool %s is not an array' % tool_name)
  584. install = tool_dict.get('install', False)
  585. if type(install) is not expected_str_type:
  586. raise RuntimeError('install for tool %s is not a string' % tool_name)
  587. info_url = tool_dict.get('info_url', False)
  588. if type(info_url) is not expected_str_type:
  589. raise RuntimeError('info_url for tool %s is not a string' % tool_name)
  590. license = tool_dict.get('license', False)
  591. if type(license) is not expected_str_type:
  592. raise RuntimeError('license for tool %s is not a string' % tool_name)
  593. strip_container_dirs = tool_dict.get('strip_container_dirs', 0)
  594. if strip_container_dirs and type(strip_container_dirs) is not int:
  595. raise RuntimeError('strip_container_dirs for tool %s is not an int' % tool_name)
  596. overrides_list = tool_dict.get('platform_overrides', [])
  597. if type(overrides_list) is not list:
  598. raise RuntimeError('platform_overrides for tool %s is not a list' % tool_name)
  599. supported_targets = tool_dict.get('supported_targets')
  600. if not isinstance(supported_targets, list):
  601. raise RuntimeError('supported_targets for tool %s is not a list of strings' % tool_name)
  602. # Create the object
  603. tool_obj = cls(tool_name, description, install, info_url, license, # type: ignore
  604. version_cmd, version_regex, supported_targets, version_regex_replace, # type: ignore
  605. strip_container_dirs) # type: ignore
  606. for path in export_paths:
  607. tool_obj.options.export_paths.append(path)
  608. for name, value in export_vars.items():
  609. tool_obj.options.export_vars[name] = value
  610. for index, override in enumerate(overrides_list):
  611. platforms_list = override.get('platforms')
  612. if type(platforms_list) is not list:
  613. raise RuntimeError('platforms for override %d of tool %s is not a list' % (index, tool_name))
  614. install = override.get('install')
  615. if install is not None and type(install) is not expected_str_type:
  616. raise RuntimeError('install for override %d of tool %s is not a string' % (index, tool_name))
  617. version_cmd = override.get('version_cmd')
  618. if version_cmd is not None and type(version_cmd) is not list:
  619. raise RuntimeError('version_cmd for override %d of tool %s is not a list of strings' %
  620. (index, tool_name))
  621. version_regex = override.get('version_regex')
  622. if version_regex is not None and (type(version_regex) is not expected_str_type or not version_regex):
  623. raise RuntimeError('version_regex for override %d of tool %s is not a non-empty string' %
  624. (index, tool_name))
  625. version_regex_replace = override.get('version_regex_replace')
  626. if version_regex_replace is not None and type(version_regex_replace) is not expected_str_type:
  627. raise RuntimeError('version_regex_replace for override %d of tool %s is not a string' %
  628. (index, tool_name))
  629. export_paths = override.get('export_paths')
  630. if export_paths is not None and type(export_paths) is not list:
  631. raise RuntimeError('export_paths for override %d of tool %s is not a list' % (index, tool_name))
  632. export_vars = override.get('export_vars')
  633. if export_vars is not None and type(export_vars) is not dict:
  634. raise RuntimeError('export_vars for override %d of tool %s is not a mapping' % (index, tool_name))
  635. tool_obj.platform_overrides.append(override)
  636. recommended_versions = {}
  637. for version_dict in versions:
  638. version = version_dict.get('name')
  639. if type(version) is not expected_str_type:
  640. raise RuntimeError('version name for tool {} is not a string'.format(tool_name))
  641. version_status = version_dict.get('status')
  642. if type(version_status) is not expected_str_type and version_status not in IDFToolVersion.STATUS_VALUES:
  643. raise RuntimeError('tool {} version {} status is not one of {}', tool_name, version,
  644. IDFToolVersion.STATUS_VALUES)
  645. version_obj = IDFToolVersion(version, version_status)
  646. for platform_id, platform_dict in version_dict.items():
  647. if platform_id in ['name', 'status']:
  648. continue
  649. if platform_id not in PLATFORM_FROM_NAME.keys():
  650. raise RuntimeError('invalid platform %s for tool %s version %s' %
  651. (platform_id, tool_name, version))
  652. version_obj.add_download(platform_id,
  653. platform_dict['url'], platform_dict['size'], platform_dict['sha256'])
  654. if version_status == IDFToolVersion.STATUS_RECOMMENDED:
  655. if platform_id not in recommended_versions:
  656. recommended_versions[platform_id] = []
  657. recommended_versions[platform_id].append(version)
  658. tool_obj.add_version(version_obj)
  659. for platform_id, version_list in recommended_versions.items():
  660. if len(version_list) > 1:
  661. raise RuntimeError('tool {} for platform {} has {} recommended versions'.format(
  662. tool_name, platform_id, len(recommended_versions)))
  663. if install != IDFTool.INSTALL_NEVER and len(recommended_versions) == 0:
  664. raise RuntimeError('required/optional tool {} for platform {} has no recommended versions'.format(
  665. tool_name, platform_id))
  666. tool_obj._update_current_options()
  667. return tool_obj
  668. def to_json(self):
  669. versions_array = []
  670. for version, version_obj in self.versions.items():
  671. version_json = {
  672. 'name': version,
  673. 'status': version_obj.status
  674. }
  675. for platform_id, download in version_obj.downloads.items():
  676. version_json[platform_id] = {
  677. 'url': download.url,
  678. 'size': download.size,
  679. 'sha256': download.sha256
  680. }
  681. versions_array.append(version_json)
  682. overrides_array = self.platform_overrides
  683. tool_json = {
  684. 'name': self.name,
  685. 'description': self.description,
  686. 'export_paths': self.options.export_paths,
  687. 'export_vars': self.options.export_vars,
  688. 'install': self.options.install,
  689. 'info_url': self.options.info_url,
  690. 'license': self.options.license,
  691. 'version_cmd': self.options.version_cmd,
  692. 'version_regex': self.options.version_regex,
  693. 'supported_targets': self.options.supported_targets,
  694. 'versions': versions_array,
  695. }
  696. if self.options.version_regex_replace != VERSION_REGEX_REPLACE_DEFAULT:
  697. tool_json['version_regex_replace'] = self.options.version_regex_replace
  698. if overrides_array:
  699. tool_json['platform_overrides'] = overrides_array
  700. if self.options.strip_container_dirs:
  701. tool_json['strip_container_dirs'] = self.options.strip_container_dirs
  702. return tool_json
  703. def load_tools_info(): # type: () -> typing.Dict[str, IDFTool]
  704. """
  705. Load tools metadata from tools.json, return a dictionary: tool name - tool info
  706. """
  707. tool_versions_file_name = global_tools_json
  708. with open(tool_versions_file_name, 'r') as f:
  709. tools_info = json.load(f)
  710. return parse_tools_info_json(tools_info)
  711. def parse_tools_info_json(tools_info):
  712. """
  713. Parse and validate the dictionary obtained by loading the tools.json file.
  714. Returns a dictionary of tools (key: tool name, value: IDFTool object).
  715. """
  716. if tools_info['version'] != TOOLS_FILE_VERSION:
  717. raise RuntimeError('Invalid version')
  718. tools_dict = OrderedDict()
  719. tools_array = tools_info.get('tools')
  720. if type(tools_array) is not list:
  721. raise RuntimeError('tools property is missing or not an array')
  722. for tool_dict in tools_array:
  723. tool = IDFTool.from_json(tool_dict)
  724. tools_dict[tool.name] = tool
  725. return tools_dict
  726. def dump_tools_json(tools_info):
  727. tools_array = []
  728. for tool_name, tool_obj in tools_info.items():
  729. tool_json = tool_obj.to_json()
  730. tools_array.append(tool_json)
  731. file_json = {'version': TOOLS_FILE_VERSION, 'tools': tools_array}
  732. return json.dumps(file_json, indent=2, separators=(',', ': '), sort_keys=True)
  733. def get_python_env_path():
  734. python_ver_major_minor = '{}.{}'.format(sys.version_info.major, sys.version_info.minor)
  735. version_file_path = os.path.join(global_idf_path, 'version.txt')
  736. if os.path.exists(version_file_path):
  737. with open(version_file_path, "r") as version_file:
  738. idf_version_str = version_file.read()
  739. else:
  740. idf_version_str = ''
  741. try:
  742. idf_version_str = subprocess.check_output(['git', 'describe'],
  743. cwd=global_idf_path, env=os.environ).decode()
  744. except OSError:
  745. # OSError should cover FileNotFoundError and WindowsError
  746. warn('Git was not found')
  747. except subprocess.CalledProcessError as e:
  748. warn('Git describe was unsuccessul: {}'.format(e.output))
  749. match = re.match(r'^v([0-9]+\.[0-9]+).*', idf_version_str)
  750. if match:
  751. idf_version = match.group(1)
  752. else:
  753. idf_version = None
  754. # fallback when IDF is a shallow clone
  755. try:
  756. with open(os.path.join(global_idf_path, 'components', 'esp_common', 'include', 'esp_idf_version.h')) as f:
  757. m = re.search(r'^#define\s+ESP_IDF_VERSION_MAJOR\s+(\d+).+?^#define\s+ESP_IDF_VERSION_MINOR\s+(\d+)',
  758. f.read(), re.DOTALL | re.MULTILINE)
  759. if m:
  760. idf_version = '.'.join((m.group(1), m.group(2)))
  761. else:
  762. warn('Reading IDF version from C header file failed!')
  763. except Exception as e:
  764. warn('Is it not possible to determine the IDF version: {}'.format(e))
  765. if idf_version is None:
  766. fatal('IDF version cannot be determined')
  767. raise SystemExit(1)
  768. idf_python_env_path = os.path.join(global_idf_tools_path, 'python_env',
  769. 'idf{}_py{}_env'.format(idf_version, python_ver_major_minor))
  770. if sys.platform == 'win32':
  771. subdir = 'Scripts'
  772. python_exe = 'python.exe'
  773. else:
  774. subdir = 'bin'
  775. python_exe = 'python'
  776. idf_python_export_path = os.path.join(idf_python_env_path, subdir)
  777. virtualenv_python = os.path.join(idf_python_export_path, python_exe)
  778. return idf_python_env_path, idf_python_export_path, virtualenv_python
  779. def get_idf_env(): # type: () -> Any
  780. try:
  781. idf_env_file_path = os.path.join(global_idf_tools_path, IDF_ENV_FILE) # type: ignore
  782. with open(idf_env_file_path, 'r') as idf_env_file:
  783. return json.load(idf_env_file)
  784. except (IOError, OSError):
  785. if not os.path.exists(idf_env_file_path):
  786. warn('File {} was not found. '.format(idf_env_file_path))
  787. else:
  788. filename, ending = os.path.splitext(os.path.basename(idf_env_file_path))
  789. warn('File {} can not be opened, renaming to {}'.format(idf_env_file_path,filename + '_failed' + ending))
  790. os.rename(idf_env_file_path, os.path.join(os.path.dirname(idf_env_file_path), (filename + '_failed' + ending)))
  791. info('Creating {}' .format(idf_env_file_path))
  792. return {'idfSelectedId': 'sha', 'idfInstalled': {'sha': {'targets': {}}}}
  793. def export_targets_to_idf_env_json(targets): # type: (list[str]) -> None
  794. idf_env_json = get_idf_env()
  795. targets = list(set(targets + get_user_defined_targets()))
  796. for env in idf_env_json['idfInstalled']:
  797. if env == idf_env_json['idfSelectedId']:
  798. idf_env_json['idfInstalled'][env]['targets'] = targets
  799. break
  800. try:
  801. if global_idf_tools_path: # mypy fix for Optional[str] in the next call
  802. # the directory doesn't exist if this is run on a clean system the first time
  803. mkdir_p(global_idf_tools_path)
  804. with open(os.path.join(global_idf_tools_path, IDF_ENV_FILE), 'w') as w: # type: ignore
  805. json.dump(idf_env_json, w, indent=4)
  806. except (IOError, OSError):
  807. warn('File {} can not be created. '.format(os.path.join(global_idf_tools_path, IDF_ENV_FILE))) # type: ignore
  808. def clean_targets(targets_str): # type: (str) -> list[str]
  809. targets_from_tools_json = get_all_targets_from_tools_json()
  810. invalid_targets = []
  811. targets_str = targets_str.lower()
  812. targets = targets_str.replace('-', '').split(',')
  813. if targets != ['all']:
  814. invalid_targets = [t for t in targets if t not in targets_from_tools_json]
  815. if invalid_targets:
  816. warn('Targets: "{}" are not supported. Only allowed options are: {}.'.format(', '.join(invalid_targets), ', '.join(targets_from_tools_json)))
  817. raise SystemExit(1)
  818. # removing duplicates
  819. targets = list(set(targets))
  820. export_targets_to_idf_env_json(targets)
  821. else:
  822. export_targets_to_idf_env_json(targets_from_tools_json)
  823. return targets
  824. def get_user_defined_targets(): # type: () -> list[str]
  825. try:
  826. with open(os.path.join(global_idf_tools_path, IDF_ENV_FILE), 'r') as idf_env_file: # type: ignore
  827. idf_env_json = json.load(idf_env_file)
  828. except (OSError, IOError):
  829. # warn('File {} was not found. Installing tools for all esp targets.'.format(os.path.join(global_idf_tools_path, IDF_ENV_FILE))) # type: ignore
  830. return []
  831. targets = []
  832. for env in idf_env_json['idfInstalled']:
  833. if env == idf_env_json['idfSelectedId']:
  834. targets = idf_env_json['idfInstalled'][env]['targets']
  835. break
  836. return targets
  837. def get_all_targets_from_tools_json(): # type: () -> list[str]
  838. tools_info = load_tools_info()
  839. targets_from_tools_json = [] # type: list[str]
  840. for _, v in tools_info.items():
  841. targets_from_tools_json.extend(v.get_supported_targets())
  842. # remove duplicates
  843. targets_from_tools_json = list(set(targets_from_tools_json))
  844. if 'all' in targets_from_tools_json:
  845. targets_from_tools_json.remove('all')
  846. return sorted(targets_from_tools_json)
  847. def filter_tools_info(tools_info): # type: (OrderedDict[str, IDFTool]) -> OrderedDict[str,IDFTool]
  848. targets = get_user_defined_targets()
  849. if not targets:
  850. return tools_info
  851. else:
  852. filtered_tools_spec = {k:v for k, v in tools_info.items() if
  853. (v.get_install_type() == IDFTool.INSTALL_ALWAYS or v.get_install_type() == IDFTool.INSTALL_ON_REQUEST) and
  854. (any(item in targets for item in v.get_supported_targets()) or v.get_supported_targets() == ['all'])}
  855. return OrderedDict(filtered_tools_spec)
  856. def action_list(args): # type: ignore
  857. tools_info = load_tools_info()
  858. for name, tool in tools_info.items():
  859. if tool.get_install_type() == IDFTool.INSTALL_NEVER:
  860. continue
  861. optional_str = ' (optional)' if tool.get_install_type() == IDFTool.INSTALL_ON_REQUEST else ''
  862. info('* {}: {}{}'.format(name, tool.description, optional_str))
  863. tool.find_installed_versions()
  864. versions_for_platform = {k: v for k, v in tool.versions.items() if v.compatible_with_platform()}
  865. if not versions_for_platform:
  866. info(' (no versions compatible with platform {})'.format(PYTHON_PLATFORM))
  867. continue
  868. versions_sorted = sorted(versions_for_platform.keys(), key=tool.versions.get, reverse=True)
  869. for version in versions_sorted:
  870. version_obj = tool.versions[version]
  871. info(' - {} ({}{})'.format(version, version_obj.status,
  872. ', installed' if version in tool.versions_installed else ''))
  873. def action_check(args):
  874. tools_info = load_tools_info()
  875. tools_info = filter_tools_info(tools_info)
  876. not_found_list = []
  877. info('Checking for installed tools...')
  878. for name, tool in tools_info.items():
  879. if tool.get_install_type() == IDFTool.INSTALL_NEVER:
  880. continue
  881. tool_found_somewhere = False
  882. info('Checking tool %s' % name)
  883. tool.find_installed_versions()
  884. if tool.version_in_path:
  885. info(' version found in PATH: %s' % tool.version_in_path)
  886. tool_found_somewhere = True
  887. else:
  888. info(' no version found in PATH')
  889. for version in tool.versions_installed:
  890. info(' version installed in tools directory: %s' % version)
  891. tool_found_somewhere = True
  892. if not tool_found_somewhere and tool.get_install_type() == IDFTool.INSTALL_ALWAYS:
  893. not_found_list.append(name)
  894. if not_found_list:
  895. fatal('The following required tools were not found: ' + ' '.join(not_found_list))
  896. raise SystemExit(1)
  897. def action_export(args):
  898. tools_info = load_tools_info()
  899. tools_info = filter_tools_info(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:
  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):
  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):
  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):
  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):
  1053. tools_info = load_tools_info()
  1054. tools_spec = args.tools
  1055. targets = [] # type: list[str]
  1056. # Installing only single tools, no targets are specified.
  1057. if 'required' in tools_spec:
  1058. targets = clean_targets(args.targets)
  1059. if args.platform not in PLATFORM_FROM_NAME:
  1060. fatal('unknown platform: {}' % args.platform)
  1061. raise SystemExit(1)
  1062. platform = PLATFORM_FROM_NAME[args.platform]
  1063. tools_info_for_platform = OrderedDict()
  1064. for name, tool_obj in tools_info.items():
  1065. tool_for_platform = tool_obj.copy_for_platform(platform)
  1066. tools_info_for_platform[name] = tool_for_platform
  1067. if not tools_spec or 'required' in tools_spec:
  1068. # Downloading tools for all ESP_targets required by the operating system.
  1069. tools_spec = [k for k, v in tools_info_for_platform.items() if v.get_install_type() == IDFTool.INSTALL_ALWAYS]
  1070. # Filtering tools user defined list of ESP_targets
  1071. if 'all' not in targets:
  1072. def is_tool_selected(tool): # type: (IDFTool) -> bool
  1073. supported_targets = tool.get_supported_targets()
  1074. return (any(item in targets for item in supported_targets) or supported_targets == ['all'])
  1075. tools_spec = [k for k in tools_spec if is_tool_selected(tools_info[k])]
  1076. info('Downloading tools for {}: {}'.format(platform, ', '.join(tools_spec)))
  1077. # Downloading tools for all ESP_targets (MacOS, Windows, Linux)
  1078. elif 'all' in tools_spec:
  1079. tools_spec = [k for k, v in tools_info_for_platform.items() if v.get_install_type() != IDFTool.INSTALL_NEVER]
  1080. info('Downloading tools for {}: {}'.format(platform, ', '.join(tools_spec)))
  1081. for tool_spec in tools_spec:
  1082. if '@' not in tool_spec:
  1083. tool_name = tool_spec
  1084. tool_version = None
  1085. else:
  1086. tool_name, tool_version = tool_spec.split('@', 1)
  1087. if tool_name not in tools_info_for_platform:
  1088. fatal('unknown tool name: {}'.format(tool_name))
  1089. raise SystemExit(1)
  1090. tool_obj = tools_info_for_platform[tool_name]
  1091. if tool_version is not None and tool_version not in tool_obj.versions:
  1092. fatal('unknown version for tool {}: {}'.format(tool_name, tool_version))
  1093. raise SystemExit(1)
  1094. if tool_version is None:
  1095. tool_version = tool_obj.get_recommended_version()
  1096. if tool_version is None:
  1097. fatal('tool {} not found for {} platform'.format(tool_name, platform))
  1098. raise SystemExit(1)
  1099. tool_spec = '{}@{}'.format(tool_name, tool_version)
  1100. info('Downloading {}'.format(tool_spec))
  1101. apply_url_mirrors(args, tool_obj.versions[tool_version].get_download_for_platform(platform))
  1102. tool_obj.download(tool_version)
  1103. def action_install(args):
  1104. tools_info = load_tools_info()
  1105. tools_spec = args.tools # type: ignore
  1106. targets = [] # type: list[str]
  1107. # Installing only single tools, no targets are specified.
  1108. if 'required' in tools_spec:
  1109. targets = clean_targets(args.targets)
  1110. info('Selected targets are: {}' .format(', '.join(get_user_defined_targets())))
  1111. if not tools_spec or 'required' in tools_spec:
  1112. # Installing tools for all ESP_targets required by the operating system.
  1113. tools_spec = [k for k, v in tools_info.items() if v.get_install_type() == IDFTool.INSTALL_ALWAYS]
  1114. # Filtering tools user defined list of ESP_targets
  1115. if 'all' not in targets:
  1116. def is_tool_selected(tool): # type: (IDFTool) -> bool
  1117. supported_targets = tool.get_supported_targets()
  1118. return (any(item in targets for item in supported_targets) or supported_targets == ['all'])
  1119. tools_spec = [k for k in tools_spec if is_tool_selected(tools_info[k])]
  1120. info('Installing tools: {}'.format(', '.join(tools_spec)))
  1121. # Installing tools for all ESP_targets (MacOS, Windows, Linux)
  1122. elif 'all' in tools_spec:
  1123. tools_spec = [k for k, v in tools_info.items() if v.get_install_type() != IDFTool.INSTALL_NEVER]
  1124. info('Installing tools: {}'.format(', '.join(tools_spec)))
  1125. for tool_spec in tools_spec:
  1126. if '@' not in tool_spec:
  1127. tool_name = tool_spec
  1128. tool_version = None
  1129. else:
  1130. tool_name, tool_version = tool_spec.split('@', 1)
  1131. if tool_name not in tools_info:
  1132. fatal('unknown tool name: {}'.format(tool_name))
  1133. raise SystemExit(1)
  1134. tool_obj = tools_info[tool_name]
  1135. if not tool_obj.compatible_with_platform():
  1136. fatal('tool {} does not have versions compatible with platform {}'.format(tool_name, CURRENT_PLATFORM))
  1137. raise SystemExit(1)
  1138. if tool_version is not None and tool_version not in tool_obj.versions:
  1139. fatal('unknown version for tool {}: {}'.format(tool_name, tool_version))
  1140. raise SystemExit(1)
  1141. if tool_version is None:
  1142. tool_version = tool_obj.get_recommended_version()
  1143. assert tool_version is not None
  1144. tool_obj.find_installed_versions()
  1145. tool_spec = '{}@{}'.format(tool_name, tool_version)
  1146. if tool_version in tool_obj.versions_installed:
  1147. info('Skipping {} (already installed)'.format(tool_spec))
  1148. continue
  1149. info('Installing {}'.format(tool_spec))
  1150. apply_url_mirrors(args, tool_obj.versions[tool_version].get_download_for_platform(PYTHON_PLATFORM))
  1151. tool_obj.download(tool_version)
  1152. tool_obj.install(tool_version)
  1153. def action_install_python_env(args): # type: ignore
  1154. reinstall = args.reinstall
  1155. idf_python_env_path, _, virtualenv_python = get_python_env_path()
  1156. is_virtualenv = hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)
  1157. if is_virtualenv and (not os.path.exists(idf_python_env_path) or reinstall):
  1158. fatal('This script was called from a virtual environment, can not create a virtual environment again')
  1159. raise SystemExit(1)
  1160. if os.path.exists(virtualenv_python):
  1161. try:
  1162. subprocess.check_call([virtualenv_python, '--version'], stdout=sys.stdout, stderr=sys.stderr)
  1163. except (OSError, subprocess.CalledProcessError):
  1164. # At this point we can reinstall the virtual environment if it is non-functional. This can happen at least
  1165. # when the Python interpreter was removed which was used to create the virtual environment.
  1166. reinstall = True
  1167. try:
  1168. subprocess.check_call([virtualenv_python, '-m', 'pip', '--version'], stdout=sys.stdout, stderr=sys.stderr)
  1169. except subprocess.CalledProcessError:
  1170. warn('PIP is not available in the virtual environment.')
  1171. # Reinstallation of the virtual environment could help if PIP was installed for the main Python
  1172. reinstall = True
  1173. if reinstall and os.path.exists(idf_python_env_path):
  1174. warn('Removing the existing Python environment in {}'.format(idf_python_env_path))
  1175. shutil.rmtree(idf_python_env_path)
  1176. if not os.path.exists(virtualenv_python):
  1177. info('Creating a new Python environment in {}'.format(idf_python_env_path))
  1178. try:
  1179. import virtualenv # noqa: F401
  1180. except ImportError:
  1181. info('Installing virtualenv')
  1182. subprocess.check_call([sys.executable, '-m', 'pip', 'install', '--user', 'virtualenv'],
  1183. stdout=sys.stdout, stderr=sys.stderr)
  1184. subprocess.check_call([sys.executable, '-m', 'virtualenv', '--seeder', 'pip', idf_python_env_path],
  1185. stdout=sys.stdout, stderr=sys.stderr)
  1186. env_copy = os.environ.copy()
  1187. if env_copy.get('PIP_USER') == 'yes':
  1188. warn('Found PIP_USER="yes" in the environment. Disabling PIP_USER in this shell to install packages into a virtual environment.')
  1189. env_copy['PIP_USER'] = 'no'
  1190. run_args = [virtualenv_python, '-m', 'pip', 'install', '--no-warn-script-location']
  1191. requirements_txt = os.path.join(global_idf_path, 'requirements.txt')
  1192. run_args += ['-r', requirements_txt]
  1193. if args.extra_wheels_dir:
  1194. run_args += ['--find-links', args.extra_wheels_dir]
  1195. info('Installing Python packages from {}'.format(requirements_txt))
  1196. subprocess.check_call(run_args, stdout=sys.stdout, stderr=sys.stderr, env=env_copy)
  1197. def action_add_version(args):
  1198. tools_info = load_tools_info()
  1199. tool_name = args.tool
  1200. tool_obj = tools_info.get(tool_name)
  1201. if not tool_obj:
  1202. info('Creating new tool entry for {}'.format(tool_name))
  1203. tool_obj = IDFTool(tool_name, TODO_MESSAGE, IDFTool.INSTALL_ALWAYS,
  1204. TODO_MESSAGE, TODO_MESSAGE, [TODO_MESSAGE], TODO_MESSAGE)
  1205. tools_info[tool_name] = tool_obj
  1206. version = args.version
  1207. version_obj = tool_obj.versions.get(version)
  1208. if version not in tool_obj.versions:
  1209. info('Creating new version {}'.format(version))
  1210. version_obj = IDFToolVersion(version, IDFToolVersion.STATUS_SUPPORTED)
  1211. tool_obj.versions[version] = version_obj
  1212. url_prefix = args.url_prefix or 'https://%s/' % TODO_MESSAGE
  1213. for file_path in args.files:
  1214. file_name = os.path.basename(file_path)
  1215. # Guess which platform this file is for
  1216. found_platform = None
  1217. for platform_alias, platform_id in PLATFORM_FROM_NAME.items():
  1218. if platform_alias in file_name:
  1219. found_platform = platform_id
  1220. break
  1221. if found_platform is None:
  1222. info('Could not guess platform for file {}'.format(file_name))
  1223. found_platform = TODO_MESSAGE
  1224. # Get file size and calculate the SHA256
  1225. file_size, file_sha256 = get_file_size_sha256(file_path)
  1226. url = url_prefix + file_name
  1227. info('Adding download for platform {}'.format(found_platform))
  1228. info(' size: {}'.format(file_size))
  1229. info(' SHA256: {}'.format(file_sha256))
  1230. info(' URL: {}'.format(url))
  1231. version_obj.add_download(found_platform, url, file_size, file_sha256)
  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_rewrite(args):
  1240. tools_info = load_tools_info()
  1241. json_str = dump_tools_json(tools_info)
  1242. if not args.output:
  1243. args.output = os.path.join(global_idf_path, TOOLS_FILE_NEW)
  1244. with open(args.output, 'w') as f:
  1245. f.write(json_str)
  1246. f.write('\n')
  1247. info('Wrote output to {}'.format(args.output))
  1248. def action_validate(args):
  1249. try:
  1250. import jsonschema
  1251. except ImportError:
  1252. fatal('You need to install jsonschema package to use validate command')
  1253. raise SystemExit(1)
  1254. with open(os.path.join(global_idf_path, TOOLS_FILE), 'r') as tools_file:
  1255. tools_json = json.load(tools_file)
  1256. with open(os.path.join(global_idf_path, TOOLS_SCHEMA_FILE), 'r') as schema_file:
  1257. schema_json = json.load(schema_file)
  1258. jsonschema.validate(tools_json, schema_json)
  1259. # on failure, this will raise an exception with a fairly verbose diagnostic message
  1260. def action_gen_doc(args):
  1261. f = args.output
  1262. tools_info = load_tools_info()
  1263. def print_out(text):
  1264. f.write(text + '\n')
  1265. print_out(".. |zwsp| unicode:: U+200B")
  1266. print_out(" :trim:")
  1267. print_out("")
  1268. idf_gh_url = "https://github.com/espressif/esp-idf"
  1269. for tool_name, tool_obj in tools_info.items():
  1270. info_url = tool_obj.options.info_url
  1271. if idf_gh_url + "/tree" in info_url:
  1272. info_url = re.sub(idf_gh_url + r"/tree/\w+/(.*)", r":idf:`\1`", info_url)
  1273. license_url = "https://spdx.org/licenses/" + tool_obj.options.license
  1274. print_out("""
  1275. .. _tool-{name}:
  1276. {name}
  1277. {underline}
  1278. {description}
  1279. .. include:: idf-tools-notes.inc
  1280. :start-after: tool-{name}-notes
  1281. :end-before: ---
  1282. License: `{license} <{license_url}>`_
  1283. More info: {info_url}
  1284. .. list-table::
  1285. :widths: 10 10 80
  1286. :header-rows: 1
  1287. * - Platform
  1288. - Required
  1289. - Download
  1290. """.rstrip().format(name=tool_name,
  1291. underline=args.heading_underline_char * len(tool_name),
  1292. description=tool_obj.description,
  1293. license=tool_obj.options.license,
  1294. license_url=license_url,
  1295. info_url=info_url))
  1296. for platform_name in sorted(tool_obj.get_supported_platforms()):
  1297. platform_tool = tool_obj.copy_for_platform(platform_name)
  1298. install_type = platform_tool.get_install_type()
  1299. if install_type == IDFTool.INSTALL_NEVER:
  1300. continue
  1301. elif install_type == IDFTool.INSTALL_ALWAYS:
  1302. install_type_str = "required"
  1303. elif install_type == IDFTool.INSTALL_ON_REQUEST:
  1304. install_type_str = "optional"
  1305. else:
  1306. raise NotImplementedError()
  1307. version = platform_tool.get_recommended_version()
  1308. version_obj = platform_tool.versions[version]
  1309. download_obj = version_obj.get_download_for_platform(platform_name)
  1310. # Note: keep the list entries indented to the same number of columns
  1311. # as the list header above.
  1312. print_out("""
  1313. * - {}
  1314. - {}
  1315. - {}
  1316. .. rst-class:: tool-sha256
  1317. SHA256: {}
  1318. """.strip('\n').format(platform_name, install_type_str, download_obj.url, download_obj.sha256))
  1319. print_out('')
  1320. print_out('')
  1321. def main(argv):
  1322. parser = argparse.ArgumentParser()
  1323. parser.add_argument('--quiet', help='Don\'t output diagnostic messages to stdout/stderr', action='store_true')
  1324. parser.add_argument('--non-interactive', help='Don\'t output interactive messages and questions', action='store_true')
  1325. parser.add_argument('--tools-json', help='Path to the tools.json file to use')
  1326. parser.add_argument('--idf-path', help='ESP-IDF path to use')
  1327. subparsers = parser.add_subparsers(dest='action')
  1328. subparsers.add_parser('list', help='List tools and versions available')
  1329. subparsers.add_parser('check', help='Print summary of tools installed or found in PATH')
  1330. export = subparsers.add_parser('export', help='Output command for setting tool paths, suitable for shell')
  1331. export.add_argument('--format', choices=[EXPORT_SHELL, EXPORT_KEY_VALUE], default=EXPORT_SHELL,
  1332. help='Format of the output: shell (suitable for printing into shell), ' +
  1333. 'or key-value (suitable for parsing by other tools')
  1334. export.add_argument('--prefer-system', help='Normally, if the tool is already present in PATH, ' +
  1335. 'but has an unsupported version, a version from the tools directory ' +
  1336. 'will be used instead. If this flag is given, the version in PATH ' +
  1337. 'will be used.', action='store_true')
  1338. install = subparsers.add_parser('install', help='Download and install tools into the tools directory')
  1339. install.add_argument('tools', metavar='TOOL', nargs='*', default=['required'],
  1340. help='Tools to install. ' +
  1341. 'To install a specific version use <tool_name>@<version> syntax. ' +
  1342. 'Use empty or \'required\' to install required tools, not optional ones. ' +
  1343. 'Use \'all\' to install all tools, including the optional ones.')
  1344. install.add_argument('--targets', default='all', help='A comma separated list of desired chip targets for installing.' +
  1345. ' It defaults to installing all supported targets.')
  1346. download = subparsers.add_parser('download', help='Download the tools into the dist directory')
  1347. download.add_argument('--platform', help='Platform to download the tools for')
  1348. download.add_argument('tools', metavar='TOOL', nargs='*', default=['required'],
  1349. help='Tools to download. ' +
  1350. 'To download a specific version use <tool_name>@<version> syntax. ' +
  1351. 'Use empty or \'required\' to download required tools, not optional ones. ' +
  1352. 'Use \'all\' to download all tools, including the optional ones.')
  1353. download.add_argument('--targets', default='all', help='A comma separated list of desired chip targets for installing.' +
  1354. ' It defaults to installing all supported targets.')
  1355. if IDF_MAINTAINER:
  1356. for subparser in [download, install]:
  1357. subparser.add_argument('--mirror-prefix-map', nargs='*',
  1358. help='Pattern to rewrite download URLs, with source and replacement separated by comma.' +
  1359. ' E.g. http://foo.com,http://test.foo.com')
  1360. install_python_env = subparsers.add_parser('install-python-env',
  1361. help='Create Python virtual environment and install the ' +
  1362. 'required Python packages')
  1363. install_python_env.add_argument('--reinstall', help='Discard the previously installed environment',
  1364. action='store_true')
  1365. install_python_env.add_argument('--extra-wheels-dir', help='Additional directories with wheels ' +
  1366. 'to use during installation')
  1367. if IDF_MAINTAINER:
  1368. add_version = subparsers.add_parser('add-version', help='Add or update download info for a version')
  1369. add_version.add_argument('--output', help='Save new tools.json into this file')
  1370. add_version.add_argument('--tool', help='Tool name to set add a version for', required=True)
  1371. add_version.add_argument('--version', help='Version identifier', required=True)
  1372. add_version.add_argument('--url-prefix', help='String to prepend to file names to obtain download URLs')
  1373. add_version.add_argument('files', help='File names of the download artifacts', nargs='*')
  1374. rewrite = subparsers.add_parser('rewrite', help='Load tools.json, validate, and save the result back into JSON')
  1375. rewrite.add_argument('--output', help='Save new tools.json into this file')
  1376. subparsers.add_parser('validate', help='Validate tools.json against schema file')
  1377. gen_doc = subparsers.add_parser('gen-doc', help='Write the list of tools as a documentation page')
  1378. gen_doc.add_argument('--output', type=argparse.FileType('w'), default=sys.stdout,
  1379. help='Output file name')
  1380. gen_doc.add_argument('--heading-underline-char', help='Character to use when generating RST sections', default='~')
  1381. args = parser.parse_args(argv)
  1382. if args.action is None:
  1383. parser.print_help()
  1384. parser.exit(1)
  1385. if args.quiet:
  1386. global global_quiet
  1387. global_quiet = True
  1388. if args.non_interactive:
  1389. global global_non_interactive
  1390. global_non_interactive = True
  1391. global global_idf_path
  1392. global_idf_path = os.environ.get('IDF_PATH')
  1393. if args.idf_path:
  1394. global_idf_path = args.idf_path
  1395. if not global_idf_path:
  1396. global_idf_path = os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))
  1397. os.environ['IDF_PATH'] = global_idf_path
  1398. global global_idf_tools_path
  1399. global_idf_tools_path = os.environ.get('IDF_TOOLS_PATH') or os.path.expanduser(IDF_TOOLS_PATH_DEFAULT)
  1400. # On macOS, unset __PYVENV_LAUNCHER__ variable if it is set.
  1401. # Otherwise sys.executable keeps pointing to the system Python, even when a python binary from a virtualenv is invoked.
  1402. # See https://bugs.python.org/issue22490#msg283859.
  1403. os.environ.pop('__PYVENV_LAUNCHER__', None)
  1404. if sys.version_info.major == 2:
  1405. try:
  1406. global_idf_tools_path.decode('ascii')
  1407. except UnicodeDecodeError:
  1408. fatal('IDF_TOOLS_PATH contains non-ASCII characters: {}'.format(global_idf_tools_path) +
  1409. '\nThis is not supported yet with Python 2. ' +
  1410. 'Please set IDF_TOOLS_PATH to a directory with an ASCII name, or switch to Python 3.')
  1411. raise SystemExit(1)
  1412. if CURRENT_PLATFORM == UNKNOWN_PLATFORM:
  1413. fatal('Platform {} appears to be unsupported'.format(PYTHON_PLATFORM))
  1414. raise SystemExit(1)
  1415. global global_tools_json
  1416. if args.tools_json:
  1417. global_tools_json = args.tools_json
  1418. else:
  1419. global_tools_json = os.path.join(global_idf_path, TOOLS_FILE)
  1420. action_func_name = 'action_' + args.action.replace('-', '_')
  1421. action_func = globals()[action_func_name]
  1422. action_func(args)
  1423. if __name__ == '__main__':
  1424. main(sys.argv[1:])