idf_tools.py 122 KB

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