idf_tools.py 109 KB

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