Repository: Jason2866/platform-espressif32
Pull request #118: Add tl-install to install tools not in pio registry. Using for install of newer OpenOCD
Author: login: Jason2866
Created: 2025-04-27T11:29:41Z | Merged: 2025-04-27T11:56:54Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Improved management of tool installation and activation, providing more robust and conditional handling for certain development tools.

- **Chores**
	- Updated package ownership and attributes for several tools.
	- Added a new tool package and updated version references for existing tools.
	- Refined logic for enabling debug and upload tools based on build configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/platform.json b/platform.json
index a0fd8e6b6..466330722 100644
--- a/platform.json
+++ b/platform.json
@@ -38,7 +38,7 @@
     "framework-espidf": {
       "type": "framework",
       "optional": true,
-      "owner": "Jason2866",
+      "owner": "tasmota",
       "version": "https://github.com/tasmota/esp-idf/releases/download/v5.5.0.250419/esp-idf-v5.5.0.zip"
     },
     "toolchain-xtensa-esp-elf": {
@@ -73,9 +73,16 @@
     },
     "tool-esptoolpy": {
       "type": "uploader",
+      "optional": false,
       "owner": "tasmota",
       "version": "https://github.com/tasmota/esptool/releases/download/v4.8.9/esptool.zip"
     },
+    "tl-install": {
+      "type": "tool",
+      "optional": false,
+      "owner": "pioarduino",
+      "version": "https://github.com/pioarduino/esp_install/releases/download/v5.0.0/esp_install-v5.0.0.zip"
+    },
     "tool-dfuutil-arduino": {
       "type": "uploader",
       "optional": true,
@@ -85,16 +92,18 @@
     "tool-openocd-esp32": {
       "type": "debugger",
       "optional": true,
-      "owner": "platformio",
-      "version": "~2.1100.0"
+      "owner": "pioarduino",
+      "version": "https://github.com/pioarduino/registry/releases/download/0.0.1/openocd-v0.12.0-esp32-20250226.zip"
     },
     "tool-esp-rom-elfs": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "0.0.1+20241011"
     },
     "tool-mklittlefs": {
       "type": "uploader",
+      "optional": true,
       "owner": "tasmota",
       "version": "^3.2.0"
     },
@@ -105,31 +114,37 @@
       "version": "~2.0.0"
     },
     "tool-cppcheck": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "~1.21100"
     },
     "tool-clangtidy": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "^1.190100.0"
     },
     "tool-pvs-studio": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "^7.18.59866"
     },
     "tool-cmake": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "~3.30.2"
     },
     "tool-ninja": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "^1.7.0"
     },
     "tool-scons": {
+      "type": "tool",
       "optional": true,
       "owner": "platformio",
       "version": "~4.40801.0"
diff --git a/platform.py b/platform.py
index e47d61faf..5ca534d28 100644
--- a/platform.py
+++ b/platform.py
@@ -13,21 +13,26 @@
 # limitations under the License.
 
 import os
-import urllib
+import subprocess
 import sys
-import json
-import re
+import shutil
+from os.path import join
 
 from platformio.public import PlatformBase, to_unix_path
+from platformio.proc import get_pythonexe_path
+from platformio.project.config import ProjectConfig
+from platformio.package.manager.tool import ToolPackageManager
 
 
 IS_WINDOWS = sys.platform.startswith("win")
 # Set Platformio env var to use windows_amd64 for all windows architectures
 # only windows_amd64 native espressif toolchains are available
-# needs platformio core >= 6.1.16b2 or pioarduino core 6.1.16+test
+# needs platformio/pioarduino core >= 6.1.17
 if IS_WINDOWS:
     os.environ["PLATFORMIO_SYSTEM_TYPE"] = "windows_amd64"
 
+python_exe = get_pythonexe_path()
+pm = ToolPackageManager()
 
 class Espressif32Platform(PlatformBase):
     def configure_default_packages(self, variables, targets):
@@ -39,6 +44,53 @@ def configure_default_packages(self, variables, targets):
         board_sdkconfig = variables.get("board_espidf.custom_sdkconfig", board_config.get("espidf.custom_sdkconfig", ""))
         frameworks = variables.get("pioframework", [])
 
+        def install_tool(TOOL):
+            self.packages[TOOL]["optional"] = False
+            TOOL_PATH = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), TOOL)
+            TOOL_PACKAGE_PATH = os.path.join(TOOL_PATH, "package.json")
+            TOOLS_PATH_DEFAULT = os.path.join(os.path.expanduser("~"), ".platformio")
+            IDF_TOOLS = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), "tl-install", "tools", "idf_tools.py")
+            TOOLS_JSON_PATH = os.path.join(TOOL_PATH, "tools.json")
+            TOOLS_PIO_PATH = os.path.join(TOOL_PATH, ".piopm")
+            IDF_TOOLS_CMD = (
+                python_exe,
+                IDF_TOOLS,
+                "--quiet",
+                "--non-interactive",
+                "--tools-json",
+                TOOLS_JSON_PATH,
+                "install"
+            )
+
+            tl_flag = bool(os.path.exists(IDF_TOOLS))
+            json_flag = bool(os.path.exists(TOOLS_JSON_PATH))
+            pio_flag = bool(os.path.exists(TOOLS_PIO_PATH))
+            if tl_flag and json_flag:
+                rc = subprocess.run(IDF_TOOLS_CMD).returncode
+                if rc != 0:
+                    sys.stderr.write("Error: Couldn't execute 'idf_tools.py install'\n")
+                else:
+                    tl_path = "file://" + join(TOOLS_PATH_DEFAULT, "tools", TOOL)
+                    if not os.path.exists(join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json")):
+                        shutil.copyfile(TOOL_PACKAGE_PATH, join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json"))
+                    self.packages.pop(TOOL, None)
+                    if os.path.exists(TOOL_PATH) and os.path.isdir(TOOL_PATH):
+                        try:
+                            shutil.rmtree(TOOL_PATH)
+                        except Exception as e:
+                            print(f"Error while removing the tool folder: {e}")                   
+                    pm.install(tl_path)
+            # tool is already installed, just activate it
+            if tl_flag and pio_flag and not json_flag:
+                self.packages[TOOL]["version"] = TOOL_PATH
+                self.packages[TOOL]["optional"] = False
+            
+            return
+
+        # Installer only needed for setup, deactivate when installed
+        if bool(os.path.exists(os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), "tl-install", "tools", "idf_tools.py"))):
+            self.packages["tl-install"]["optional"] = True
+
         if "arduino" in frameworks and variables.get("custom_sdkconfig") is None and len(str(board_sdkconfig)) < 3:
             self.packages["framework-arduinoespressif32"]["optional"] = False
 
@@ -47,7 +99,7 @@ def configure_default_packages(self, variables, targets):
             self.packages["framework-espidf"]["optional"] = False
             self.packages["framework-arduinoespressif32"]["optional"] = False
 
-        # Enable check tools only when "check_tool" is enabled
+        # Enable check tools only when "check_tool" is active
         for p in self.packages:
             if p in ("tool-cppcheck", "tool-clangtidy", "tool-pvs-studio"):
                 self.packages[p]["optional"] = False if str(variables.get("check_tool")).strip("['']") in p else True
@@ -58,8 +110,6 @@ def configure_default_packages(self, variables, targets):
                 self.packages["tool-mklittlefs"]["optional"] = False
             elif filesystem == "fatfs":
                 self.packages["tool-mkfatfs"]["optional"] = False
-        if variables.get("upload_protocol"):
-            self.packages["tool-openocd-esp32"]["optional"] = False
         if os.path.isdir("ulp"):
             self.packages["toolchain-esp32ulp"]["optional"] = False
 
@@ -75,15 +125,11 @@ def configure_default_packages(self, variables, targets):
         else:
             del self.packages["tool-dfuutil-arduino"]
 
-        # Starting from v12, Espressif's toolchains are shipped without
-        # bundled GDB. Instead, it's distributed as separate packages for Xtensa
-        # and RISC-V targets.
-        for gdb_package in ("tool-xtensa-esp-elf-gdb", "tool-riscv32-esp-elf-gdb"):
-            self.packages[gdb_package]["optional"] = False
-            # if IS_WINDOWS:
-                # Note: On Windows GDB v12 is not able to
-                # launch a GDB server in pipe mode while v11 works fine
-                # self.packages[gdb_package]["version"] = "~11.2.0"
+        # install GDB and OpenOCD when debug mode or upload_protocol is set
+        if (variables.get("build_type") or "debug" in "".join(targets)) or variables.get("upload_protocol"):
+            for gdb_package in ("tool-xtensa-esp-elf-gdb", "tool-riscv32-esp-elf-gdb"):
+                self.packages[gdb_package]["optional"] = False
+            install_tool("tool-openocd-esp32")
 
         # Common packages for IDF and mixed Arduino+IDF projects
         if "espidf" in frameworks:
@@ -96,8 +142,6 @@ def configure_default_packages(self, variables, targets):
                     "tool-esp-rom-elfs",
                  ):
                     self.packages[p]["optional"] = False
-                elif p in ("tool-mconf", "tool-idf") and IS_WINDOWS:
-                    self.packages[p]["optional"] = False
 
         if mcu in ("esp32", "esp32s2", "esp32s3"):
             self.packages["toolchain-xtensa-esp-elf"]["optional"] = False


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>platform.json (1)</summary><blockquote>

`80-85`: **`tl-install` should probably start as *optional* to avoid an initial no-op download**

`configure_default_packages` flips the flag back to `true` once `idf_tools.py` is detected, but the first run will still download the 30 MB ZIP even for users who already have the ESP-IDF tool-chain installed system-wide.  
A lighter flow:

```diff
-      "optional": false,
+      "optional": true,
```

and let `install_tool()` mark it non-optional when a download is genuinely required.

</blockquote></details>
<details>
<summary>platform.py (1)</summary><blockquote>

`78-81`: **Trailing comma in `os.path.exists(TOOL_PATH,)` is harmless but noisy**

```python
if os.path.exists(TOOL_PATH,) and os.path.isdir(TOOL_PATH):
```

The dangling comma turns the argument into a tuple in some linters’ AST, producing false positives.  
Drop it for readability:

```diff
- if os.path.exists(TOOL_PATH,) and os.path.isdir(TOOL_PATH):
+ if os.path.exists(TOOL_PATH) and os.path.isdir(TOOL_PATH):
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 79a6a9f68c4aec94a31c6f97a9781426c1873688 and 720d79f38c2f4ac0843540c959b86497c0bd3bab.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `platform.json` (4 hunks)
* `platform.py` (4 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build (ubuntu-latest, examples/arduino-blink)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>platform.json (1)</summary>

`75-79`: **`tool-esptoolpy` is now mandatory – verify that every board really needs it**

Setting `"optional": false` forces the package to be fetched even for boards that use alternative upload protocols (e.g. USB-DFU, J-Link, OTA-only devices).  
If any board in `boards/` relies on a different uploader, this will pull an unused package and lengthen installation time.

Consider reverting to `true` and enabling it selectively in `configure_default_packages`, similar to the logic you already apply for GDB/OpenOCD.

  
Would you like me to draft a quick search script that lists boards whose `upload.protocol` ≠ `esptool` so we can double-check?

</details>
<details>
<summary>platform.py (1)</summary>

`128-133`: **Enabling GDB/OpenOCD on `upload_protocol` is great – but remember `erase_flash` target**

Users often invoke `pio run -t erase_flash` before a clean upload; that path doesn’t include `"debug"` nor sets `build_type`.  
They would still need OpenOCD if their protocol is JTAG-based.

Consider expanding the condition:

```python
if any(k in targets for k in ("debug", "erase_flash")) or variables.get("upload_protocol"):
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-27T11:33:22Z

Review comments:
diff_hunk: @@ -85,16 +92,18 @@
     "tool-openocd-esp32": {
       "type": "debugger",
       "optional": true,
-      "owner": "platformio",
-      "version": "~2.1100.0"
+      "owner": "pioarduino",
+      "version": "https://github.com/pioarduino/registry/releases/download/0.0.1/openocd-v0.12.0-esp32-20250226.zip"
     },
path: platform.json
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Hard-coding an owner-hosted ZIP URL removes checksum / signature validation**

Changing the owner to `pioarduino` and pointing directly to a GitHub asset bypasses PlatformIO’s registry integrity checks.  
If the file gets replaced, users may get a different binary without version bumping.

Mitigations:

* Pin the asset by SHA256 in `packages/package.json` (PlatformIO honours `"checksum"`).
* Host the package in PlatformIO registry so normal semantic-version resolution & signatures apply.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T11:33:20Z
author_association: NONE
diff_hunk: @@ -39,6 +44,53 @@ def configure_default_packages(self, variables, targets):
         board_sdkconfig = variables.get("board_espidf.custom_sdkconfig", board_config.get("espidf.custom_sdkconfig", ""))
         frameworks = variables.get("pioframework", [])
 
+        def install_tool(TOOL):
+            self.packages[TOOL]["optional"] = False
+            TOOL_PATH = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), TOOL)
+            TOOL_PACKAGE_PATH = os.path.join(TOOL_PATH, "package.json")
+            TOOLS_PATH_DEFAULT = os.path.join(os.path.expanduser("~"), ".platformio")
+            IDF_TOOLS = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), "tl-install", "tools", "idf_tools.py")
+            TOOLS_JSON_PATH = os.path.join(TOOL_PATH, "tools.json")
+            TOOLS_PIO_PATH = os.path.join(TOOL_PATH, ".piopm")
+            IDF_TOOLS_CMD = (
+                python_exe,
+                IDF_TOOLS,
+                "--quiet",
+                "--non-interactive",
+                "--tools-json",
+                TOOLS_JSON_PATH,
+                "install"
+            )
+
+            tl_flag = bool(os.path.exists(IDF_TOOLS))
+            json_flag = bool(os.path.exists(TOOLS_JSON_PATH))
+            pio_flag = bool(os.path.exists(TOOLS_PIO_PATH))
+            if tl_flag and json_flag:
+                rc = subprocess.run(IDF_TOOLS_CMD).returncode
+                if rc != 0:
+                    sys.stderr.write("Error: Couldn't execute 'idf_tools.py install'\n")
+                else:
+                    tl_path = "file://" + join(TOOLS_PATH_DEFAULT, "tools", TOOL)
+                    if not os.path.exists(join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json")):
+                        shutil.copyfile(TOOL_PACKAGE_PATH, join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json"))
+                    self.packages.pop(TOOL, None)
+                    if os.path.exists(TOOL_PATH,) and os.path.isdir(TOOL_PATH):
+                        try:
+                            shutil.rmtree(TOOL_PATH)
+                        except Exception as e:
+                            print(f"Error while removing the tool folder: {e}")                   
+                    pm.install(tl_path)
+            # tool is already installed, just activate it
+            if tl_flag and pio_flag and not json_flag:
+                self.packages[TOOL]["version"] = TOOL_PATH
+                self.packages[TOOL]["optional"] = False
+            
+            return
+
path: platform.py
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`install_tool()` mixes several concerns and is brittle – consider refactoring**

The helper currently:

1. Mutates `self.packages` twice, and even `pop`s the entry while later code (debug server) still references the package id.
2. Performs filesystem cleanup, subprocess execution, package-manager calls and state toggling in one ~40-line block.
3. Uses a mix of `os.path.join` and the imported `join`, plus magic constants (`"tl-install"`, `"tools"`) scattered through the body.

This makes future maintenance and unit testing difficult.  A cleaner approach would be:

* Split into:
  * `_run_idf_tools_install(tool_name) -> None | Err`
  * `_register_existing_tool(tool_name) -> None`
  * small shim that decides which branch to run.
* Return a boolean so the caller (“debug section”) can act on failure.
* Replace string literals with `Pathlib` operations or module-level constants for clarity.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T11:33:20Z
author_association: NONE
diff_hunk: @@ -39,6 +44,53 @@ def configure_default_packages(self, variables, targets):
         board_sdkconfig = variables.get("board_espidf.custom_sdkconfig", board_config.get("espidf.custom_sdkconfig", ""))
         frameworks = variables.get("pioframework", [])
 
+        def install_tool(TOOL):
+            self.packages[TOOL]["optional"] = False
+            TOOL_PATH = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), TOOL)
+            TOOL_PACKAGE_PATH = os.path.join(TOOL_PATH, "package.json")
+            TOOLS_PATH_DEFAULT = os.path.join(os.path.expanduser("~"), ".platformio")
+            IDF_TOOLS = os.path.join(ProjectConfig.get_instance().get("platformio", "packages_dir"), "tl-install", "tools", "idf_tools.py")
+            TOOLS_JSON_PATH = os.path.join(TOOL_PATH, "tools.json")
+            TOOLS_PIO_PATH = os.path.join(TOOL_PATH, ".piopm")
+            IDF_TOOLS_CMD = (
+                python_exe,
+                IDF_TOOLS,
+                "--quiet",
+                "--non-interactive",
+                "--tools-json",
+                TOOLS_JSON_PATH,
+                "install"
+            )
+
+            tl_flag = bool(os.path.exists(IDF_TOOLS))
+            json_flag = bool(os.path.exists(TOOLS_JSON_PATH))
+            pio_flag = bool(os.path.exists(TOOLS_PIO_PATH))
+            if tl_flag and json_flag:
+                rc = subprocess.run(IDF_TOOLS_CMD).returncode
+                if rc != 0:
+                    sys.stderr.write("Error: Couldn't execute 'idf_tools.py install'\n")
+                else:
+                    tl_path = "file://" + join(TOOLS_PATH_DEFAULT, "tools", TOOL)
+                    if not os.path.exists(join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json")):
+                        shutil.copyfile(TOOL_PACKAGE_PATH, join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json"))
+                    self.packages.pop(TOOL, None)
path: platform.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing directory creation can raise `FileNotFoundError` when copying `package.json`**

`shutil.copyfile()` assumes the destination directory (`~/.platformio/tools/<TOOL>/`) already exists.  
On a fresh machine it doesn’t, causing the whole installation to abort.

```diff
-    if not os.path.exists(join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json")):
-        shutil.copyfile(TOOL_PACKAGE_PATH, join(TOOLS_PATH_DEFAULT, "tools", TOOL, "package.json"))
+    dst_dir = join(TOOLS_PATH_DEFAULT, "tools", TOOL)
+    os.makedirs(dst_dir, exist_ok=True)
+    dst_pkg = join(dst_dir, "package.json")
+    if not os.path.exists(dst_pkg):
+        shutil.copyfile(TOOL_PACKAGE_PATH, dst_pkg)
```

  


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                rc = subprocess.run(IDF_TOOLS_CMD).returncode
                if rc != 0:
                    sys.stderr.write("Error: Couldn't execute 'idf_tools.py install'\n")
                else:
                    tl_path = "file://" + join(TOOLS_PATH_DEFAULT, "tools", TOOL)
                    dst_dir = join(TOOLS_PATH_DEFAULT, "tools", TOOL)
                    os.makedirs(dst_dir, exist_ok=True)
                    dst_pkg = join(dst_dir, "package.json")
                    if not os.path.exists(dst_pkg):
                        shutil.copyfile(TOOL_PACKAGE_PATH, dst_pkg)
                    self.packages.pop(TOOL, None)
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-27T11:33:20Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-27T11:29:47Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change updates `platform.json` to adjust tool package ownerships, explicitly set optional and type attributes, add a new `tl-install` tool package, and update version references for certain tools to use direct URLs. In `platform.py`, a new internal helper function `install_tool` is introduced within the `Espressif32Platform.configure_default_packages` method to manage the installation and activation of tools, particularly OpenOCD, based on build context and configuration. The logic for enabling debug and upload tools is refined to be conditional, and some legacy or redundant code is removed.

## Changes

| File(s)                                      | Change Summary                                                                                                                                                                                                                                   |
|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| platform.json                                | Changed ownership for several packages, added explicit `"optional"` and `"type"` attributes, introduced new `tl-install` tool package, updated some package versions to direct URLs, and clarified tool package metadata.                         |
| platform.py                                  | Added internal `install_tool` helper in `Espressif32Platform.configure_default_packages`, refactored logic for conditional tool installation (especially OpenOCD), refined debug tool activation, and removed redundant/legacy code.             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant PlatformIO
    participant Espressif32Platform
    participant install_tool
    participant tl-install
    participant OpenOCD

    User->>PlatformIO: Start build/upload/debug
    PlatformIO->>Espressif32Platform: configure_default_packages()
    Espressif32Platform->>install_tool: (if OpenOCD or tl-install needed)
    install_tool->>tl-install: Check/install tl-install tool
    install_tool->>OpenOCD: Install/activate OpenOCD via tl-install
    install_tool-->>Espressif32Platform: Return control
    Espressif32Platform-->>PlatformIO: Packages configured
    PlatformIO-->>User: Proceed with build/upload/debug
```

## Possibly related PRs

- **Jason2866/platform-espressif32#114**: Both PRs modify the internal `install_tool` function, focusing on package metadata handling.
- **Jason2866/platform-espressif32#117**: Both PRs optimize and simplify the `install_tool` logic and package handling for OpenOCD and tl-install.
- **Jason2866/platform-espressif32#113**: Both PRs refactor tool installation logic, particularly for OpenOCD, using the Espressif installer.

## Poem

> A rabbit hops through fields of code,  
> With tools and scripts in tidy load.  
> Ownerships shift, new helpers bloom,  
> OpenOCD finds extra room.  
> Debug and upload, now smart and keen—  
> The burrow’s build is crisp and clean!  
> 🐇✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Jason2866/platform-espressif32&utm_content=118):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: Jason2866
created_at: 2025-04-27T11:30:33Z
body: @coderabbitai full review

author_association: OWNER
user: login: coderabbitai[bot]
created_at: 2025-04-27T11:30:37Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Full review triggered.

</details>
author_association: NONE