diff --git a/docs/en/api-guides/tools/idf-tools-notes.inc b/docs/en/api-guides/tools/idf-tools-notes.inc index 4d0c863923..7baaca01a9 100644 --- a/docs/en/api-guides/tools/idf-tools-notes.inc +++ b/docs/en/api-guides/tools/idf-tools-notes.inc @@ -43,6 +43,11 @@ On Linux and macOS, it is recommended to install CMake using the OS-specific pac .. tool-esp-clang-notes +--- + +.. tool-esp-clang-libs-notes + + --- .. tool-ninja-notes diff --git a/docs/zh_CN/api-guides/tools/idf-tools-notes.inc b/docs/zh_CN/api-guides/tools/idf-tools-notes.inc index 9097786aaf..73582b6f98 100644 --- a/docs/zh_CN/api-guides/tools/idf-tools-notes.inc +++ b/docs/zh_CN/api-guides/tools/idf-tools-notes.inc @@ -43,6 +43,11 @@ On Linux and macOS, it is recommended to install CMake using the OS-specific pac .. tool-esp-clang-notes +--- + +.. tool-esp-clang-libs-notes + + --- .. tool-ninja-notes diff --git a/tools/idf_tools.py b/tools/idf_tools.py index 0e839f8746..e7667b5e06 100755 --- a/tools/idf_tools.py +++ b/tools/idf_tools.py @@ -82,7 +82,7 @@ TOOLS_FILE = 'tools/tools.json' TOOLS_SCHEMA_FILE = 'tools/tools_schema.json' TOOLS_FILE_NEW = 'tools/tools.new.json' IDF_ENV_FILE = 'idf-env.json' -TOOLS_FILE_VERSION = 2 +TOOLS_FILE_VERSION = 3 IDF_TOOLS_PATH_DEFAULT = os.path.join('~', '.espressif') UNKNOWN_VERSION = 'unknown' SUBST_TOOL_PATH_REGEX = re.compile(r'\${TOOL_PATH}') @@ -789,6 +789,7 @@ IDFToolOptions = namedtuple('IDFToolOptions', [ 'version_regex', 'version_regex_replace', 'is_executable', + 'tool_info_file', 'export_paths', 'export_vars', 'install', @@ -818,7 +819,8 @@ class IDFTool(object): supported_targets: List[str], version_regex_replace: Optional[str] = None, strip_container_dirs: int = 0, - is_executable: bool = True) -> None: + is_executable: bool = True, + tool_info_file: str = '') -> None: self.name = name self.description = description self.drop_versions() @@ -826,12 +828,13 @@ class IDFTool(object): self.versions_installed: List[str] = [] if version_regex_replace is None: version_regex_replace = VERSION_REGEX_REPLACE_DEFAULT - self.options = IDFToolOptions(version_cmd, version_regex, version_regex_replace, is_executable, + self.options = IDFToolOptions(version_cmd, version_regex, version_regex_replace, is_executable, tool_info_file, [], OrderedDict(), install, info_url, license, strip_container_dirs, supported_targets) # type: ignore self.platform_overrides: List[Dict[str, str]] = [] self._platform = CURRENT_PLATFORM self._update_current_options() self.is_executable = is_executable + self.tool_info_file = tool_info_file def copy_for_platform(self, platform: str) -> 'IDFTool': """ @@ -905,6 +908,16 @@ class IDFTool(object): result[k] = v_repl return result + def parse_tool_version(self, ver_str: str) -> str: + """ + Extract the version string from the provided input and return it as a result. + Returns 'unknown' if version string can not be extracted.. + """ + match = re.search(self._current_options.version_regex, ver_str) # type: ignore + if not match: + return UNKNOWN_VERSION + return re.sub(self._current_options.version_regex, self._current_options.version_regex_replace, match.group(0)) # type: ignore + def get_version(self, extra_paths: Optional[List[str]] = None, executable_path: Optional[str] = None) -> str: """ Execute the tool, optionally prepending extra_paths to PATH, @@ -934,17 +947,39 @@ class IDFTool(object): except subprocess.CalledProcessError as e: raise ToolExecError(f'non-zero exit code ({e.returncode}) with message: {e.stderr.decode("utf-8",errors="ignore")}') # type: ignore - in_str = version_cmd_result.decode('utf-8') - match = re.search(self._current_options.version_regex, in_str) # type: ignore - if not match: - return UNKNOWN_VERSION - return re.sub(self._current_options.version_regex, self._current_options.version_regex_replace, match.group(0)) # type: ignore + return self.parse_tool_version(version_cmd_result.decode('utf-8')) + + def get_version_from_file(self, version: str) -> str: + """ + Extract the version string from tool info file and return it as a result. + Returns 'unknown' if version string can not be extracted. + """ + # this function can not be called for a different platform + assert self._platform == CURRENT_PLATFORM + # Replace '/' with OS specific path separator + info_file_path = os.path.join(*self.tool_info_file.split('/')) + info_file_path = os.path.join(self.get_path_for_version(version), info_file_path) + if not os.path.exists(info_file_path): + raise ToolNotFoundError(f'Tool {self.name} not found: No info file.') + with open(info_file_path, 'r', encoding='utf-8') as f: + try: + tool_info = json.load(f) + except (json.JSONDecodeError, UnicodeDecodeError): + raise ToolNotFoundError(f'Tool {self.name} not found: Bad info file.') + + if 'version' not in tool_info: + raise ToolNotFoundError(f'Tool {self.name} not found: No version in info file.') + + return self.parse_tool_version(tool_info['version']) def check_binary_valid(self, version: str) -> bool: if not self.is_executable: return True try: - ver_str = self.get_version(self.get_export_paths(version)) + if self.tool_info_file: + ver_str = self.get_version_from_file(version) + else: + ver_str = self.get_version(self.get_export_paths(version)) except (ToolNotFoundError, ToolExecError) as e: fatal(f'tool {self.name} version {version} is installed, but getting error: {e}') return False @@ -1028,17 +1063,18 @@ class IDFTool(object): # this function can not be called for a different platform assert self._platform == CURRENT_PLATFORM tool_error = False - # First check if the tool is in system PATH - try: - ver_str = self.get_version() - except ToolNotFoundError: - # not in PATH - pass - except ToolExecError as e: - fatal(f'tool {self.name} is found in PATH, but has failed: {e}') - tool_error = True - else: - self.version_in_path = ver_str + if not self.tool_info_file: + # First check if the tool is in system PATH + try: + ver_str = self.get_version() + except ToolNotFoundError: + # not in PATH + pass + except ToolExecError as e: + fatal(f'tool {self.name} is found in PATH, but has failed: {e}') + tool_error = True + else: + self.version_in_path = ver_str # Now check all the versions installed in GlobalVarsStore.idf_tools_path self.versions_installed = [] @@ -1053,7 +1089,10 @@ class IDFTool(object): self.versions_installed.append(version) continue try: - ver_str = self.get_version(self.get_export_paths(version)) + if self.tool_info_file: + ver_str = self.get_version_from_file(version) + else: + ver_str = self.get_version(self.get_export_paths(version)) except ToolNotFoundError as e: warn(f'directory for tool {self.name} version {version} is present, but the tool has not been found: {e}') except ToolExecError as e: @@ -1083,7 +1122,10 @@ class IDFTool(object): # export paths list directly here. paths = [os.path.join(tool_path, version, *p) for p in self._current_options.export_paths] try: - ver_str = self.get_version(paths) + if self.tool_info_file: + ver_str = self.get_version_from_file(version) + else: + ver_str = self.get_version(paths) except (ToolNotFoundError, ToolExecError): continue if ver_str != version: @@ -1191,6 +1233,10 @@ class IDFTool(object): if not isinstance(is_executable, bool): raise RuntimeError(f'is_executable for tool {tool_name} is not a bool') + tool_info_file = tool_dict.get('tool_info_file', '') + if not isinstance(tool_info_file, str): + raise RuntimeError(f'tool_info_file for tool {tool_name} is not a string') + version_cmd = tool_dict.get('version_cmd') if type(version_cmd) is not list: raise RuntimeError(f'version_cmd for tool {tool_name} is not a list of strings') @@ -1242,7 +1288,7 @@ class IDFTool(object): # Create the object tool_obj: 'IDFTool' = cls(tool_name, description, install, info_url, license, # type: ignore version_cmd, version_regex, supported_targets, version_regex_replace, # type: ignore - strip_container_dirs, is_executable) # type: ignore + strip_container_dirs, is_executable, tool_info_file) # type: ignore for path in export_paths: # type: ignore tool_obj.options.export_paths.append(path) # type: ignore @@ -1371,6 +1417,8 @@ class IDFTool(object): tool_json['strip_container_dirs'] = self.options.strip_container_dirs if self.options.is_executable is False: tool_json['is_executable'] = self.options.is_executable + if self.options.tool_info_file: + tool_json['tool_info_file'] = self.options.tool_info_file return tool_json diff --git a/tools/tools.json b/tools/tools.json index afffee87f8..066a90c9c3 100644 --- a/tools/tools.json +++ b/tools/tools.json @@ -300,6 +300,67 @@ } ] }, + { + "description": "Standalone Clang shared libraries distribution", + "export_paths": [], + "export_vars": {}, + "info_url": "https://github.com/espressif/llvm-project", + "install": "on_request", + "license": "Apache-2.0", + "name": "esp-clang-libs", + "supported_targets": [ + "esp32", + "esp32s2", + "esp32s3", + "esp32c3", + "esp32c2", + "esp32c6", + "esp32c5", + "esp32h2", + "esp32p4", + "esp32c61", + "esp32h21" + ], + "tool_info_file": "esp-clang/esp-clang-libs.info", + "version_cmd": [], + "version_regex": "([0-9a-zA-Z\\.\\-_]+)", + "versions": [ + { + "linux-amd64": { + "sha256": "c13b8a731beec0c2af56f94073ca17202c9e5047de4c6e61f5e3ed319d231962", + "size": 76164612, + "url": "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312/libs-clang-esp-19.1.2_20250312-x86_64-linux-gnu.tar.xz" + }, + "linux-arm64": { + "sha256": "dcf356d2a12eaf9660aa5e91cd70df14486d4bd45f47a8025327255b2fe3ece8", + "size": 69938392, + "url": "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312/libs-clang-esp-19.1.2_20250312-aarch64-linux-gnu.tar.xz" + }, + "linux-armhf": { + "sha256": "e9e110841ffdd43c7fcc57de24f501eec917a02e54f6dbf3a2069fed1017f8a8", + "size": 72698812, + "url": "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312/libs-clang-esp-19.1.2_20250312-arm-linux-gnueabihf.tar.xz" + }, + "macos": { + "sha256": "0ac4cf2340e766a240ae40519f404fceaea4262d5ab89292aa3a4118e6402492", + "size": 58104496, + "url": "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312/libs-clang-esp-19.1.2_20250312-x86_64-apple-darwin.tar.xz" + }, + "macos-arm64": { + "sha256": "d723ce5ae431b44305c888c989e7f405446a23c13f179edc1b6939f9ba437498", + "size": 48707440, + "url": "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312/libs-clang-esp-19.1.2_20250312-aarch64-apple-darwin.tar.xz" + }, + "name": "esp-19.1.2_20250312", + "status": "recommended", + "win64": { + "sha256": "29e5c945f601b85dd88c29bfabfd8c592d8064e3aba01d946e0ff475e5810c17", + "size": 59681772, + "url": "https://github.com/espressif/llvm-project/releases/download/esp-19.1.2_20250312/libs-clang-esp-19.1.2_20250312-x86_64-w64-mingw32.tar.xz" + } + } + ] + }, { "description": "Toolchain for 32-bit RISC-V based on GCC", "export_paths": [ @@ -985,5 +1046,5 @@ ] } ], - "version": 2 + "version": 3 } diff --git a/tools/tools_schema.json b/tools/tools_schema.json index 5c3efad012..c76eb6142d 100644 --- a/tools/tools_schema.json +++ b/tools/tools_schema.json @@ -72,6 +72,10 @@ "description": "If given, this will be used as substitute expression for the regex defined in version_regex, to obtain the version string. Not specifying this is equivalent to setting it to '\\1' (i.e. return the first capture group).", "type": "string" }, + "tool_info_file": { + "description": "Tools info file path. Relative to tool install folder. If present tool version will be read from it instead of invoking the binary.", + "type": "string" + }, "strip_container_dirs": { "type": "integer", "description": "If specified, this number of top directory levels will removed when extracting. E.g. if strip_container_dirs=2, archive path a/b/c/d.txt will be extracted as c/d.txt" @@ -286,5 +290,26 @@ } } } + }, + "$comment": "Condition verifying that version <=2 of tools.json must not contain 'tool_info_file' keyword in any of #/definitions/toolInfo properties.", + "if": { + "properties": { + "version": { + "maximum": 2 + } + } + }, + "then": { + "properties": { + "tools": { + "items": { + "not": { + "required": [ + "tool_info_file" + ] + } + } + } + } } }