Skip to content

IEP-1762: Eim gui cli launch fixes#1457

Open
alirana01 wants to merge 1 commit intomasterfrom
eim_gui_cli_launch_fixes
Open

IEP-1762: Eim gui cli launch fixes#1457
alirana01 wants to merge 1 commit intomasterfrom
eim_gui_cli_launch_fixes

Conversation

@alirana01
Copy link
Copy Markdown
Collaborator

@alirana01 alirana01 commented May 6, 2026

EIM GUI/CLI launch support

Description

Adds support for both GUI and CLI EIM installations, aligning the Eclipse plugin with the approach taken in vscode-esp-idf-extension PR #1822.

Previously, the plugin only checked the GUI install location (~/.espressif/eim_gui/) when resolving the EIM executable path. Users who install EIM via CLI (e.g., Homebrew on macOS, or the CLI-only binary) would not have their installation detected. Additionally, on macOS, attempting to launch a CLI-only binary through AppleScript's open -a would fail with an IllegalArgumentException since there is no .app bundle to derive.

This PR:

  • Adds CLI install path (~/.espressif/eim/) as the final fallback in resolveEimExecutablePath
  • Adds isEimGuiCapable(String eimPath) method that probes the binary with eim gui --help to detect GUI support
  • Fixes macOS launcher to handle CLI-only binaries by launching them directly (nohup + bash) instead of via AppleScript/open

Changes

File Change
EimConstants.java Added USER_EIM_CLI_DIR constant
ToolInitializer.java Added CLI fallback in resolver, getDefaultCliEimPath(), isEimGuiCapable()
MacOsEimLauncherStrategy.java Added isAppBundle() check + launchCliDirect() for non-app binaries

Resolution Priority (updated)

  1. System PATH (e.g., Homebrew-installed eim)
  2. eimPath from eim_idf.json (existence-checked)
  3. EIM_PATH environment variable (existence-checked)
  4. Default GUI install location (existence-checked)
  5. Default CLI install location (existence-checked)

Testing

  • macOS: Install EIM via Homebrew (eim in PATH) — verify detected and launched directly
  • macOS: Install EIM as .app in /Applications — verify launched via AppleScript as before
  • macOS: Place CLI-only binary at ~/.espressif/eim/eim — verify detected and launched
  • Linux: Place CLI binary at ~/.espressif/eim/eim — verify detected
  • Windows: Place eim.exe at ~/.espressif/eim/eim.exe — verify detected
  • Verify isEimGuiCapable returns true for GUI builds and false for CLI-only builds
  • Verify no regression when EIM is not installed (resolver returns empty, "EIM missing" flow triggers)

Dependencies

This PR builds on eim_path_discovery branch (PR #1446) which contains the base path resolution fixes.

Related

Summary by CodeRabbit

  • Improvements
    • Refined EIM executable detection to prioritize system PATH and configuration files
    • Added support for CLI-only EIM installations
    • Enhanced macOS EIM launcher to support both bundled applications and direct command-line execution
    • Added automatic detection of GUI capability in EIM installations

@alirana01 alirana01 self-assigned this May 6, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 808d43ec-9cf2-4613-b753-1cd102a8a711

📥 Commits

Reviewing files that changed from the base of the PR and between dc1e322 and 87c8ecf.

📒 Files selected for processing (3)
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java
✅ Files skipped from review due to trivial changes (1)
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java

📝 Walkthrough

Walkthrough

This PR extends EIM executable resolution to support CLI-only installations, adds platform-specific CLI path helpers, implements GUI capability detection, and updates the macOS launcher strategy to route non-bundle executables directly via CLI without AppleScript.

Changes

EIM CLI Executable Resolution and Direct Launch

Layer / File(s) Summary
Configuration Constants
bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java
New constant USER_EIM_CLI_DIR defines the CLI-only EIM directory (~/.espressif/eim/).
Executable Resolution and GUI Detection
bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
resolveEimExecutablePath() now follows a 5-tier priority chain (system PATH → JSON config → EIM_PATH env → GUI default → CLI default), with a final fallback to the new CLI directory. getDefaultCliEimPath() returns the platform-specific CLI binary path. isEimGuiCapable() validates GUI support by running eimPath gui --help with a 5-second timeout. TimeUnit import added for timeout support.
macOS Launch Routing and CLI Strategy
bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java
launch() now checks if eimPath is inside a .app bundle; bundle paths use the existing AppleScript flow, non-bundle paths route to launchCliDirect(). New launchCliDirect() launches via bash -lc with nohup, captures the echoed PID, and returns a LaunchResult. New isAppBundle() helper detects .app components by walking parent directories.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • espressif/idf-eclipse-plugin#1446: Modifies ToolInitializer EIM executable resolution and isEimInstalled() behavior—shares the same resolution priority logic updates.
  • espressif/idf-eclipse-plugin#1452: Updates EimConstants.java constants; this PR adds USER_EIM_CLI_DIR while that PR updates EIM_JSON_VALID_VERSION—both modify the same constants file.
  • espressif/idf-eclipse-plugin#1387: Modifies MacOsEimLauncherStrategy and EIM path resolution logic, including direct CLI launch behavior and GUI-vs-CLI handling.

Suggested reviewers

  • kolipakakondal
  • sigmaaa
  • AndriiFilippov

Poem

🐰 A constant path blooms in the shell,
CLI launch routes fare quite well,
No .app bundle? CLI direct,
GUI check? Logic perfected with respect,
macOS hops through bundles with care! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'IEP-1762: Eim gui cli launch fixes' directly describes the main change: adding support for both GUI and CLI EIM installations with launch fixes, which is the primary objective of this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch eim_gui_cli_launch_fixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java (2)

119-122: ⚡ Quick win

Avoid bash -l (login shell) for launching subprocesses.

bash -lc runs as a login shell and sources ~/.bash_profile/~/.profile. This pulls in arbitrary user dotfile state, slows the launcher, and can produce stderr noise that ends up captured into the launcher's combined output (and then parsed by parseFirstLongLine). For a programmatic spawn, bash -c (or even sh -c) is sufficient and more predictable.

♻️ Proposed change
-		Process launcher = new ProcessBuilder("bash", "-lc", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$
+		Process launcher = new ProcessBuilder("bash", "-c", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$
 				.redirectErrorStream(true).start();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java`
around lines 119 - 122, The ProcessBuilder in MacOsEimLauncherStrategy currently
uses "bash", "-lc" which invokes a login shell; update the launcher invocation
to avoid a login shell by replacing the "-lc" flag with "-c" (or use "sh", "-c")
when building the Process (the code around bashCmd and the
ProcessBuilder("bash", "-lc", bashCmd) call) so the spawned subprocess does not
source user login dotfiles or introduce extra stderr noise.

136-149: 💤 Low value

Deduplicate .app ancestor walk with deriveAppBundlePath.

isAppBundle and deriveAppBundlePath (lines 163–178) walk the same parent chain looking for a *.app segment. Extract a single helper (e.g. findAppBundleAncestor returning Optional<Path> or Path/null) and implement both in terms of it.

♻️ Suggested refactor
+	private Path findAppBundleAncestor(String eimPath)
+	{
+		Path p = Paths.get(eimPath).toAbsolutePath().normalize();
+		while (p != null)
+		{
+			Path name = p.getFileName();
+			if (name != null && name.toString().endsWith(".app")) //$NON-NLS-1$
+			{
+				return p;
+			}
+			p = p.getParent();
+		}
+		return null;
+	}
+
 	private boolean isAppBundle(String eimPath)
 	{
-		Path p = Paths.get(eimPath).toAbsolutePath().normalize();
-		while (p != null)
-		{
-			String name = p.getFileName() != null ? p.getFileName().toString() : ""; //$NON-NLS-1$
-			if (name.endsWith(".app")) //$NON-NLS-1$
-			{
-				return true;
-			}
-			p = p.getParent();
-		}
-		return false;
+		return findAppBundleAncestor(eimPath) != null;
 	}

(deriveAppBundlePath becomes the same call with a non-null assertion / IllegalArgumentException.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java`
around lines 136 - 149, isAppBundle and deriveAppBundlePath duplicate the same
parent-chain walk for finding a "*.app" ancestor; extract that logic into a
single helper (e.g. findAppBundleAncestor returning Optional<Path> or null) and
reimplement both to call it — isAppBundle should return true when the helper
finds a path, and deriveAppBundlePath should return the found Path or throw an
IllegalArgumentException if the helper yields empty/null; update references to
use the helper and remove the duplicated loop from both isAppBundle and
deriveAppBundlePath.
bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java (1)

51-54: 💤 Low value

isEimInstalled() spawns an expensive SystemExecutableFinder lookup on every call—but current callers are not in hot paths.

The method always invokes findEimOnSystemPath() first, which instantiates SystemExecutableFinder and scans the system PATH. However, the only two callers are:

  1. EimButtonLaunchListener.widgetSelected() — runs once per user button click
  2. EspressifToolStartup.earlyStartup() — runs once at startup

Neither is in a tight loop, UI refresh mechanism, or frequent listener. If these usage patterns change in the future, consider memoizing with invalidation on eim_idf.json modification to avoid repeated scans.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java`
around lines 51 - 54, isEimInstalled() currently calls
resolveEimExecutablePath(null) which always triggers findEimOnSystemPath()
(creating a SystemExecutableFinder and scanning PATH); add a simple memoization
in ToolInitializer by caching the last-resolved path and the last-modified
timestamp of the eim_idf.json that influences resolution, return the cached
value if the file timestamp hasn't changed, and invalidate/recompute the cache
when eim_idf.json modification time differs (update the cache inside
resolveEimExecutablePath / findEimOnSystemPath code paths so callers like
isEimInstalled(), EimButtonLaunchListener.widgetSelected(), and
EspressifToolStartup.earlyStartup() use the cached result).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@BUILD_FAILURE_ANALYSIS_24703204267.md`:
- Around line 1-168: The file BUILD_FAILURE_ANALYSIS_24703204267.md is
out-of-scope for this PR and should not be merged here; remove it from the
branch/PR and instead move the analysis into one of: a new issue, a PR comment,
a discussion thread, or a dedicated docs PR (e.g. under docs/ci/ with a stable
name), referencing the failing test class NewEspressifIDFProjectClangFilesTest
and CI run details as needed; update the commit to delete
BUILD_FAILURE_ANALYSIS_24703204267.md from this PR and add the analysis to the
chosen external location so the feature PR only contains the intended EIM
GUI/CLI changes.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java`:
- Around line 116-134: launchCliDirect currently starts the bash launcher
without waiting for or reliably reaping the Process and mixes stderr into stdout
so parseFirstLongLine can pick up shell warnings instead of the PID; change the
invocation so the shell prints a distinct marker (e.g. echo "PID:$!") into
stdout by modifying bashCmd, stop using redirectErrorStream(true) so stderr
stays separate, read stdout and parse the marked PID (update parsing
expectations in ProcessUtils.parseFirstLongLine or add a new parser), and after
reading output call launcher.waitFor with a short timeout and on timeout call
launcher.destroyForcibly() to ensure the process is reaped (also ensure the
error path destroys/waits the Process).

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java`:
- Around line 81-117: The resolveEimExecutablePath method currently calls
Files.exists(Paths.get(...)) on values from eimJson.getEimPath() and
idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH) which can
throw InvalidPathException for malformed strings; fix by adding a small helper
(e.g., safePathExists(String)) that wraps Paths.get(...)/Files.exists(...) in a
try/catch and returns false on InvalidPathException, then replace the direct
Files.exists(Paths.get(jsonPath)) and Files.exists(Paths.get(eimExePathEnv))
checks with calls to that helper so resolveEimExecutablePath (and callers like
earlyStartup/startExportOldConfig) won’t crash on bad external input.
- Around line 242-267: The method isEimGuiCapable currently redirects stderr to
stdout but never consumes the process output, which can deadlock; update
isEimGuiCapable to drain the combined stream from process.getInputStream() (e.g.
spawn a short-lived background reader thread or read fully into a buffer) while
waiting for the process, ensure you close the InputStream when done, and only
then check waitFor/exitValue; keep the existing timeout and error handling
around ProcessBuilder pb / process so the process output is always consumed and
streams are closed to avoid false negatives.

In `@docs/EIM_PATH_DISCOVERY.md`:
- Around line 102-217: Update the doc sections to match the refactor: replace
the old startup branching with EspressifToolStartup.earlyStartup calling
ToolInitializer.resolveEimExecutablePath(eimJson) as the single entry point for
EIM resolution, document the new five-step priority order (system PATH →
eim_idf.json eimPath → EIM_PATH env var → default GUI install → default CLI
install), remove references to the deleted findAndSetEimPath() and old
getDefaultEimPath-only behavior and instead describe getDefaultCliEimPath() and
USER_EIM_CLI_DIR, explain isEimGuiCapable(String) and the
MacOsEimLauncherStrategy.launchCliDirect path for non-.app binaries, and note
that isEimInstalled() now delegates to the resolver (so it scans PATH).

---

Nitpick comments:
In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java`:
- Around line 119-122: The ProcessBuilder in MacOsEimLauncherStrategy currently
uses "bash", "-lc" which invokes a login shell; update the launcher invocation
to avoid a login shell by replacing the "-lc" flag with "-c" (or use "sh", "-c")
when building the Process (the code around bashCmd and the
ProcessBuilder("bash", "-lc", bashCmd) call) so the spawned subprocess does not
source user login dotfiles or introduce extra stderr noise.
- Around line 136-149: isAppBundle and deriveAppBundlePath duplicate the same
parent-chain walk for finding a "*.app" ancestor; extract that logic into a
single helper (e.g. findAppBundleAncestor returning Optional<Path> or null) and
reimplement both to call it — isAppBundle should return true when the helper
finds a path, and deriveAppBundlePath should return the found Path or throw an
IllegalArgumentException if the helper yields empty/null; update references to
use the helper and remove the duplicated loop from both isAppBundle and
deriveAppBundlePath.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java`:
- Around line 51-54: isEimInstalled() currently calls
resolveEimExecutablePath(null) which always triggers findEimOnSystemPath()
(creating a SystemExecutableFinder and scanning PATH); add a simple memoization
in ToolInitializer by caching the last-resolved path and the last-modified
timestamp of the eim_idf.json that influences resolution, return the cached
value if the file timestamp hasn't changed, and invalidate/recompute the cache
when eim_idf.json modification time differs (update the cache inside
resolveEimExecutablePath / findEimOnSystemPath code paths so callers like
isEimInstalled(), EimButtonLaunchListener.widgetSelected(), and
EspressifToolStartup.earlyStartup() use the cached result).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3d1ac3f1-85d7-4059-8765-2ace704ecbd5

📥 Commits

Reviewing files that changed from the base of the PR and between 18a8fa0 and dc1e322.

📒 Files selected for processing (6)
  • BUILD_FAILURE_ANALYSIS_24703204267.md
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/EimConstants.java
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
  • bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java
  • bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java
  • docs/EIM_PATH_DISCOVERY.md

Comment thread BUILD_FAILURE_ANALYSIS_24703204267.md Outdated
Comment on lines +1 to +168
# Build failure analysis — CI run 24703204267

Investigation of the failing Linux CI run triggered by the merge of PR [#1438 "fix: add pr comment after the build with download links"](https://github.com/espressif/idf-eclipse-plugin/pull/1438) on `master` (`0acc4deaf720cc1b46af6be5608d921842756194`, 2026-04-21).

Failing job: [`build_linux` (ID 72250690470)](https://github.com/espressif/idf-eclipse-plugin/actions/runs/24703204267/job/72250690470). `build_macos` on the same run passed.

## TL;DR

The PR itself did not break the build. PR #1438 only touched `.github/workflows/pr-comment.yml` (a post-build commenter workflow). Nothing in the main `ci.yml` workflow, the plugin source, or the tests was modified.

The Linux job failed because **two UI tests in `NewEspressifIDFProjectClangFilesTest` failed on the self-hosted Linux runner**, and the `phoenix-actions/test-reporting@v12` step is configured with `fail-on-error: true`, which converts any failed JUnit result into a job failure (`Process completed with exit code 1.`).

The same test class passes on the macOS runner, so this is a Linux-runner-specific flakiness in a pair of cascading SWTBot test methods that depend on timing and UI state carried over between methods.

## Failing tests

Both failures come from `tests/com.espressif.idf.ui.test/target/surefire-reports/TEST-com.espressif.idf.ui.test.executable.cases.project.NewEspressifIDFProjectClangFilesTest.xml`, extracted from the check-run annotations on the `Linux Test Reports` job (ID 72279770001).

### 1. `shouldMatchExpectedClangdArgumentsAfterBuildingProjects` — `org.junit.ComparisonFailure`

```
org.junit.ComparisonFailure: expected:<...rk/workspace/Project[1]/build> but was:<...rk/workspace/Project[2]/build>
at org.junit.Assert.assertEquals(Assert.java:117)
at org.junit.Assert.assertEquals(Assert.java:146)
at ...NewEspressifIDFProjectClangFilesTest$Fixture.thenCompareActualClangdArgumentWithExpected(NewEspressifIDFProjectClangFilesTest.java:356)
at ...NewEspressifIDFProjectClangFilesTest$Fixture.thenCheckClangdArgumentAfterProjectBuilt(NewEspressifIDFProjectClangFilesTest.java:142)
at ...NewEspressifIDFProjectClangFilesTest.shouldMatchExpectedClangdArgumentsAfterBuildingProjects(NewEspressifIDFProjectClangFilesTest.java:111)
```

The expected vs actual paths correspond to `/opt/actions-runner/_work/workspace/Project1/build` and `.../Project2/build` respectively (the `...rk` prefix is JUnit's diff-trim of `/opt/actions-runner/_work`).

### 2. `shouldRecreateClangdFileAfterDeletionAndVerifyContent` — `WidgetNotFoundException`

```
org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException: Timed out waiting for tree item Project1
at org.eclipse.swtbot.swt.finder.widgets.SWTBotTree.getTreeItem(SWTBotTree.java:577)
at ...NewEspressifIDFProjectClangFilesTest$Fixture.whenClangdFileDeleted(NewEspressifIDFProjectClangFilesTest.java:168)
at ...NewEspressifIDFProjectClangFilesTest.shouldRecreateClangdFileAfterDeletionAndVerifyContent(NewEspressifIDFProjectClangFilesTest.java:81)
Caused by: TimeoutException: Timeout after: 5000 ms.: Could not find node with text Project1
```

## Root cause

### Failure #1 — stale clangd `--compile-commands-dir=` preference

Method execution order is `@FixMethodOrder(MethodSorters.NAME_ASCENDING)`, so the sequence is:

1. `shouldApplyClangFormatSettingsWhenAutoSaveIsEnabled` (uses `Project2`)
2. `shouldHaveClangFilesPresentAndContentCorrectForNewProject` (uses `Project1`)
3. `shouldMatchExpectedClangdArgumentsAfterBuildingProjects` &nbsp;← **fails here**
4. `shouldRecreateClangdFileAfterDeletionAndVerifyContent` &nbsp;← cascading failure

Inside test #3, the relevant lines are:

```105:113:tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/NewEspressifIDFProjectClangFilesTest.java
public void shouldMatchExpectedClangdArgumentsAfterBuildingProjects() throws Exception
{
Fixture.whenProjectIsBuiltUsingContextMenu(CLEAN_PROJECT2);
Fixture.thenCheckClangdArgumentAfterProjectBuilt(CLEAN_PROJECT2);
Fixture.whenSelectProjectInLaunchConfig();
Fixture.whenProjectIsBuiltUsingContextMenu(CLEAN_PROJECT1);
Fixture.thenCheckClangdArgumentAfterProjectBuilt(CLEAN_PROJECT1);
Fixture.thenClangdDriversUpdateOnSelectedTarget();
}
```

The assertion verifies the value displayed in the _clangd Additional arguments_ preference textbox under Window → Preferences → C/C++ → Editor (LSP) → clangd. That preference is a **workspace-scope** (`InstanceScope`) singleton, not per-project:

```79:93:bundles/com.espressif.idf.core/src/com/espressif/idf/core/util/LspService.java
public void updateCompileCommandsDir(String buildDir)
{
String qualifier = configuration.qualifier();
String identifier = ClangdMetadata.Predefined.additionalOptions.identifer();
IEclipsePreferences preferences = InstanceScope.INSTANCE.getNode(qualifier);

String existingOptions = preferences.get(identifier, StringUtil.EMPTY);
String compileCommandsDirString = "--compile-commands-dir="; //$NON-NLS-1$
String newCompuileCommandsDirString = compileCommandsDirString + buildDir;
String updatedOptions = existingOptions.contains(compileCommandsDirString)
? existingOptions.replaceAll(compileCommandsDirString + ".+", //$NON-NLS-1$
Matcher.quoteReplacement(newCompuileCommandsDirString))
: newCompuileCommandsDirString;
preferences.put(identifier, updatedOptions);
}
```

The only place that calls `updateCompileCommandsDir(...)` during a normal build is guarded by an error-count check:

```410:419:bundles/com.espressif.idf.core/src/com/espressif/idf/core/build/IDFBuildConfiguration.java
if (!monitor.isCanceled() && epm.getErrorCount() == 0)
{
project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
ParitionSizeHandler paritionSizeHandler = new ParitionSizeHandler(project, infoStream, console);
paritionSizeHandler.startCheckingSize();

LspService lspService = new LspService();
lspService.updateCompileCommandsDir(buildDir.toString());
lspService.restartLspServers();
}
```

So the preference is rewritten **only when**:
- the build is not cancelled, **and**
- `ErrorParserManager.getErrorCount() == 0`, **and**
- the build has actually progressed to the `Build complete` console line that `waitForProjectBuild` keys on.

On Linux this race is visible:
- After Project2 is built, the preference correctly reads `.../Project2/build` (assertion #1 passes).
- `whenSelectProjectInLaunchConfig()` switches the launch bar to `Project1` — this does **not** rewrite the clangd preference.
- Project1 is built. The test's `waitForProjectBuild` returns as soon as the CDT Build Console prints `Build complete`, but the preference write inside `IDFBuildConfiguration.build(...)` happens on the builder thread after the post-build refresh and `ParitionSizeHandler` start — any of these can lag, and if Project1's build reports a non-zero error count (e.g. from a transient parser error on the self-hosted runner) the `updateCompileCommandsDir` call is skipped entirely.
- `whenOpenClangdPreferences()` opens the dialog and reads the stale `.../Project2/build` value, so `assertEquals` fails at line 356.

The fix committed on 2026-01-13 (`4d9b9fe7` "fix: 'shouldMatchExpectedClangdArgumentsAfterBuildingProjects' test") already added `whenSelectProjectInLaunchConfig()` to address this same stale-value class of bug. On the Brno self-hosted Ubuntu runner (`BrnoUBU0004`) the fix is still not sufficient because Linux runs the full suite in ~31 m vs macOS's ~12 m — timing is ~2.5x slower.

### Failure #2 — cascading UI state

`shouldRecreateClangdFileAfterDeletionAndVerifyContent` immediately calls:

```80:82:tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/NewEspressifIDFProjectClangFilesTest.java
{
Fixture.whenClangdFileDeleted(CLEAN_PROJECT1);
Fixture.thenClangdFileIsAbsent(CLEAN_PROJECT1);
```

…which calls `bot.tree().getTreeItem("Project1")`. When the previous test aborted with a `ComparisonFailure`, the Preferences dialog was still modal (or had just been closed), the Project Explorer no longer had focus, and the background LSP-server restart triggered after Project2's build was still running. SWTBot's default 5 s wait expires before `Project1` is re-rendered in the tree, producing the `WidgetNotFoundException`.

This is a **knock-on failure**, not an independent bug. Fixing Failure #1 is expected to fix Failure #2 too.

## Why macOS passed but Linux failed

- `build_macos` completed in **11 m 47 s**; `build_linux` took **31 m 3 s**. The Linux runner is ~2.5x slower and far more likely to lose the race described above.
- Both jobs run on self-hosted runners (`runs-on: ide eclipse` group) but the Linux runner additionally ran the eim-based ESP-IDF installer, which emitted `Failed to copy OpenOCD rules: ... Permission denied (os error 13)` (benign) and a burst of Pydantic serializer warnings while resolving IDF component manager dependencies. Neither of these causes test failures directly, but they lengthen setup and push timing margins tighter.
- PR #1438 touched no product or test code, so there is no new regression — the failure is an existing flaky-test problem exposed by the slow Linux runner.

## Other noise in the log (informational, not failure-causing)

- `ERROR - Failed to copy OpenOCD rules: Failed to copy /tmp/esp/openocd-esp32/.../60-openocd.rules to /etc/udev/rules.d/60-openocd.rules . Make sure you have the necessary permissions.` — the runner user isn't `root`; udev rules copy fails. Safe to ignore for CI.
- Many `PydanticSerializationUnexpectedValue` warnings from `/tmp/esp/python/v5.4/venv/lib/python3.10/site-packages/pydantic/main.py` during ESP-IDF component dependency resolution.
- `WARN - Mirror ping failed for https://mirrors.aliyun.com: 403` / `https://pypi.tuna.tsinghua.edu.cn: TimedOut` — expected from the Brno network reaching Chinese mirrors.
- `tm4e` grammar registry warnings (`No grammar source for scope [source.scala]` etc.) and `joni.exception.SyntaxException: invalid pattern in look-behind` from the TextMate tokenizer — known upstream noise, not test failures.
- `Node.js 20 actions are deprecated` — warning only; won't break until September 2026.

## Suggested remediation

Short term (stabilise CI, same day):

1. **Re-run the failing job.** The two tests are flaky on the Linux self-hosted runner and frequently pass on retry. The run has an explicit "fix: add pr comment after the build with download links" merge commit that didn't change product code, so a re-run is safe.
2. If re-run still fails, temporarily set `fail-on-error: false` on `phoenix-actions/test-reporting@v12` in `ci.yml` for the Linux job, or mark the two tests `@Ignore` until the real fix lands.

Medium term (fix the flake):

3. In `IDFBuildConfiguration.build(...)`, update the clangd `--compile-commands-dir=` preference **unconditionally** when `compile_commands.json` exists in `buildDir`, rather than gating it on `epm.getErrorCount() == 0`. A compile commands database is produced by CMake before compilation and is still meaningful for LSP even if later compile steps report errors.
4. After writing the preference, call `preferences.flush()` to force the `InstanceScope` write to the backing store before `restartLspServers()` returns.
5. In the test, replace the direct `whenOpenClangdPreferences() + assertEquals(...)` sequence with a polling wait on the `InstanceScope` preference value (`TestWidgetWaitUtility.waitUntil(...)`), then open the dialog once the value is correct.
6. Split `shouldMatchExpectedClangdArgumentsAfterBuildingProjects` into two `@Test` methods — one per project — with a `@Before` hook that re-focuses the Project Explorer, so a Project2-phase failure can't cascade into a Project1-phase failure.
7. Add a recovery step at the top of `shouldRecreateClangdFileAfterDeletionAndVerifyContent` that re-opens/focuses the Project Explorer and re-selects `Project1` (same pattern as `whenNewProjectIsSelected`) before assuming the tree item is visible.

Long term:

8. Move the self-hosted Linux runner to hardware with comparable speed to the macOS runners, or lower the SWTBot default timeout only for this test class rather than globally.

## References

- Failing run: <https://github.com/espressif/idf-eclipse-plugin/actions/runs/24703204267>
- Failing job: <https://github.com/espressif/idf-eclipse-plugin/actions/runs/24703204267/job/72250690470>
- Test source: [`tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/NewEspressifIDFProjectClangFilesTest.java`](tests/com.espressif.idf.ui.test/src/com/espressif/idf/ui/test/executable/cases/project/NewEspressifIDFProjectClangFilesTest.java)
- Related previous fix: commit `4d9b9fe7` — "fix: 'shouldMatchExpectedClangdArgumentsAfterBuildingProjects' test" (2026-01-13)
- PR that triggered this run: [#1438](https://github.com/espressif/idf-eclipse-plugin/pull/1438) — documentation/CI-only change to `pr-comment.yml`.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Out-of-scope file: this CI-failure analysis doesn't belong in this PR.

This document analyzes a Linux CI failure attributed to PR #1438 and the flaky NewEspressifIDFProjectClangFilesTest. None of the content relates to the EIM GUI/CLI launch fixes that this PR delivers (path resolver, isEimGuiCapable, macOS CLI launcher). Mixing unrelated investigation notes into a feature PR makes the history harder to read and may end up shipped as a top-level repo artifact (BUILD_FAILURE_ANALYSIS_24703204267.md) that future readers won't be able to associate with anything.

Suggest moving this analysis to an issue/PR comment, a discussion, or — if it must live in the repo — to a dedicated docs PR (e.g. under docs/ci/ with a stable filename, not a run-id‑suffixed one). Also worth deciding whether such generated forensic notes belong in version control at all.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~5-~5: The operating system from Apple is written “macOS”.
Context: ...ions/runs/24703204267/job/72250690470). build_macos on the same run passed. ## TL;DR The...

(MAC_OS)


[uncategorized] ~9-~9: The official name of this software platform is spelled with a capital “H”.
Context: ... break the build. PR #1438 only touched .github/workflows/pr-comment.yml (a post-build...

(GITHUB)


[grammar] ~110-~110: Ensure spelling is correct
Context: ...thread after the post-build refresh and ParitionSizeHandler start — any of these can lag, and if Project1's bui...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.22.1)

[warning] 21-21: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 34-34: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@BUILD_FAILURE_ANALYSIS_24703204267.md` around lines 1 - 168, The file
BUILD_FAILURE_ANALYSIS_24703204267.md is out-of-scope for this PR and should not
be merged here; remove it from the branch/PR and instead move the analysis into
one of: a new issue, a PR comment, a discussion thread, or a dedicated docs PR
(e.g. under docs/ci/ with a stable name), referencing the failing test class
NewEspressifIDFProjectClangFilesTest and CI run details as needed; update the
commit to delete BUILD_FAILURE_ANALYSIS_24703204267.md from this PR and add the
analysis to the chosen external location so the feature PR only contains the
intended EIM GUI/CLI changes.

Comment on lines +116 to +134
private LaunchResult launchCliDirect(String eimPath) throws IOException
{
String quotedPath = ProcessUtils.bashSingleQuote(eimPath);
String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$

Process launcher = new ProcessBuilder("bash", "-lc", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$
.redirectErrorStream(true).start();

String out = ProcessUtils.readAll(launcher.getInputStream());
Long pid = ProcessUtils.parseFirstLongLine(out);

if (pid == null)
{
Logger.log("macOS CLI launcher output was:\n" + out); //$NON-NLS-1$
throw new IOException("No PID found in launcher output. Output was:\n" + out); //$NON-NLS-1$
}

return LaunchResult.ofPid(pid.longValue(), eimPath, out);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reap the launcher process and harden parseFirstLongLine against shell stderr.

Two related concerns in launchCliDirect:

  1. The launcher Process is never waitFor()-ed and never explicitly destroyed on the error path. Reading to EOF usually implies bash has finished, but there's no guarantee the kernel has reaped it before this method returns; calling waitFor() (with a small timeout) makes lifecycle deterministic.
  2. With redirectErrorStream(true), any shell-init message printed to stderr (warnings from .bash_profile, locale errors, etc.) is interleaved with echo $!. parseFirstLongLine will then pick whichever line appears first — e.g. a bash: warning: ... line — and fail to find the PID. Combined with the bash -lc issue, this is a realistic failure mode on user machines.

Consider separating stderr (don't redirect), or print the PID with a marker (e.g. echo "PID:$!") and parse that.

♻️ Suggested hardening
-		String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo $!"; //$NON-NLS-1$ //$NON-NLS-2$
-
-		Process launcher = new ProcessBuilder("bash", "-lc", bashCmd) //$NON-NLS-1$ //$NON-NLS-2$
-				.redirectErrorStream(true).start();
-
-		String out = ProcessUtils.readAll(launcher.getInputStream());
+		String bashCmd = "nohup " + quotedPath + " > /dev/null 2>&1 & echo EIM_PID:$!"; //$NON-NLS-1$ //$NON-NLS-2$
+
+		ProcessBuilder pb = new ProcessBuilder("bash", "-c", bashCmd); //$NON-NLS-1$ //$NON-NLS-2$
+		Process launcher = pb.start();
+
+		String out = ProcessUtils.readAll(launcher.getInputStream());
+		try
+		{
+			launcher.waitFor();
+		}
+		catch (InterruptedException e)
+		{
+			Thread.currentThread().interrupt();
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/launch/strategies/MacOsEimLauncherStrategy.java`
around lines 116 - 134, launchCliDirect currently starts the bash launcher
without waiting for or reliably reaping the Process and mixes stderr into stdout
so parseFirstLongLine can pick up shell warnings instead of the PID; change the
invocation so the shell prints a distinct marker (e.g. echo "PID:$!") into
stdout by modifying bashCmd, stop using redirectErrorStream(true) so stderr
stays separate, read stdout and parse the marked PID (update parsing
expectations in ProcessUtils.parseFirstLongLine or add a new parser), and after
reading output call launcher.waitFor with a short timeout and on timeout call
launcher.destroyForcibly() to ensure the process is reaped (also ensure the
error path destroys/waits the Process).

Comment on lines +81 to 117
public String resolveEimExecutablePath(EimJson eimJson)
{
String fromPath = findEimOnSystemPath();
if (!StringUtil.isEmpty(fromPath))
{
return fromPath;
}

if (eimJson != null && !StringUtil.isEmpty(eimJson.getEimPath()))
{
String jsonPath = eimJson.getEimPath();
if (Files.exists(Paths.get(jsonPath)))
{
return jsonPath;
}
}

String eimExePathEnv = idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH);
boolean exists = !StringUtil.isEmpty(eimExePathEnv) && Files.exists(Paths.get(eimExePathEnv));
if (!exists)
if (!StringUtil.isEmpty(eimExePathEnv) && Files.exists(Paths.get(eimExePathEnv)))
{
return eimExePathEnv;
}

Path defaultEimPath = getDefaultEimPath();
if (defaultEimPath != null && Files.exists(defaultEimPath))
{
return defaultEimPath.toString();
}

Path cliEimPath = getDefaultCliEimPath();
if (cliEimPath != null && Files.exists(cliEimPath))
{
// Fallback: check in user home .espressif/eim_gui folder
Path defaultEimPath = getDefaultEimPath();
if (defaultEimPath != null)
exists = Files.exists(defaultEimPath);
return cliEimPath.toString();
}
return exists;

return StringUtil.EMPTY;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard Files.exists against malformed path strings.

Paths.get(jsonPath) and Paths.get(eimExePathEnv) can throw InvalidPathException (a RuntimeException) if the JSON config or the user's EIM_PATH env var contains an illegal path (e.g. NUL byte, malformed Windows path). Today that would bubble out of resolveEimExecutablePath and crash startup paths in EspressifToolStartup.earlyStartup/startExportOldConfig.

A simple try/catch around each existence check (or a small helper that returns false on InvalidPathException) keeps resolution robust against bad external input, and the call site can then continue to the next priority level.

🛡️ Proposed helper
+	private static boolean fileExistsSafe(String path)
+	{
+		try
+		{
+			return !StringUtil.isEmpty(path) && Files.exists(Paths.get(path));
+		}
+		catch (java.nio.file.InvalidPathException e)
+		{
+			Logger.log("Invalid path encountered during EIM resolution: " + path); //$NON-NLS-1$
+			return false;
+		}
+	}

…and use it in the JSON/env-var checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java`
around lines 81 - 117, The resolveEimExecutablePath method currently calls
Files.exists(Paths.get(...)) on values from eimJson.getEimPath() and
idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH) which can
throw InvalidPathException for malformed strings; fix by adding a small helper
(e.g., safePathExists(String)) that wraps Paths.get(...)/Files.exists(...) in a
try/catch and returns false on InvalidPathException, then replace the direct
Files.exists(Paths.get(jsonPath)) and Files.exists(Paths.get(eimExePathEnv))
checks with calls to that helper so resolveEimExecutablePath (and callers like
earlyStartup/startExportOldConfig) won’t crash on bad external input.

Comment on lines +242 to 267
public boolean isEimGuiCapable(String eimPath)
{
idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, eimPath);
if (StringUtil.isEmpty(eimPath))
{
return false;
}

try
{
ProcessBuilder pb = new ProcessBuilder(eimPath, "gui", "--help"); //$NON-NLS-1$ //$NON-NLS-2$
pb.redirectErrorStream(true);
Process process = pb.start();
boolean finished = process.waitFor(5, TimeUnit.SECONDS);
if (!finished)
{
process.destroyForcibly();
return false;
}
return process.exitValue() == 0;
}
catch (IOException | InterruptedException e)
{
Logger.log("EIM does not support the gui subcommand, falling back to CLI mode."); //$NON-NLS-1$
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isEimGuiCapable can deadlock and report false negatives — drain the process output.

redirectErrorStream(true) is set, but the combined stdout/stderr stream is never read. If eim gui --help ever writes more than the OS pipe buffer (typically 16–64 KB on Linux/macOS), the child process blocks on write(), waitFor(5, SECONDS) returns false, the process is force-killed, and the method returns false — incorrectly reporting a GUI‑capable binary as CLI‑only. Even if today's help text is small, this is a fragile contract for an external CLI whose output you don't control.

Drain the stream while waiting (or before exitValue()), and close it explicitly:

🛡️ Proposed fix
 		try
 		{
 			ProcessBuilder pb = new ProcessBuilder(eimPath, "gui", "--help"); //$NON-NLS-1$ //$NON-NLS-2$
 			pb.redirectErrorStream(true);
 			Process process = pb.start();
+			// Drain output on a background thread so the child doesn't block on a full pipe buffer.
+			Thread drainer = new Thread(() -> {
+				try (var in = process.getInputStream())
+				{
+					in.transferTo(OutputStream.nullOutputStream());
+				}
+				catch (IOException ignored)
+				{
+					// process exit will be observed via waitFor
+				}
+			}, "eim-gui-help-drain"); //$NON-NLS-1$
+			drainer.setDaemon(true);
+			drainer.start();
 			boolean finished = process.waitFor(5, TimeUnit.SECONDS);
 			if (!finished)
 			{
 				process.destroyForcibly();
 				return false;
 			}
 			return process.exitValue() == 0;
 		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java`
around lines 242 - 267, The method isEimGuiCapable currently redirects stderr to
stdout but never consumes the process output, which can deadlock; update
isEimGuiCapable to drain the combined stream from process.getInputStream() (e.g.
spawn a short-lived background reader thread or read fully into a buffer) while
waiting for the process, ensure you close the InputStream when done, and only
then check waitFor/exitValue; keep the existing timeout and error handling
around ProcessBuilder pb / process so the process output is always consumed and
streams are closed to avoid false negatives.

Comment thread docs/EIM_PATH_DISCOVERY.md Outdated
Comment on lines +102 to +217
## 3. Startup: how `EIM_PATH` gets set (`EspressifToolStartup`)

On IDE startup (`IStartup.earlyStartup`), logic is:

1. If **EIM is not considered installed** and **`eim_idf.json` is missing**, the flow stops early (user notified).
2. Otherwise **`eimJson`** is loaded via `ToolInitializer.loadEimJson()` (parser above).
3. **`EIM_PATH`** is then set:
- If **`eimJson != null`** → use **`eimJson.getEimPath()`** (authoritative path from EIM’s JSON).
- Else → **fallback** `toolInitializer.findAndSetEimPath()` (see section 4).

```84:127:bundles/com.espressif.idf.ui/src/com/espressif/idf/ui/tools/EspressifToolStartup.java
if (!toolInitializer.isEimInstalled() && !toolInitializer.isEimIdfJsonPresent())
{
Logger.log("EIM not installed");
notifyMissingTools();
return;
}

try
{
eimJson = toolInitializer.loadEimJson();
}
catch (EimVersionMismatchException e)
{
Logger.log(e);
return;
}
// ...
// Set EimPath on every startup to ensure proper path in configurations
if (eimJson != null)
{
idfEnvironmentVariables.addEnvVariable(IDFEnvironmentVariables.EIM_PATH, eimJson.getEimPath());
}
else
{
// Fail-safe call to ensure if the eim is in Applications or user.home it is setup in env vars
toolInitializer.findAndSetEimPath();
}
```

**Important:** If `loadEimJson()` fails or returns null (e.g. JSON version mismatch exception causes early `return` before lines 118–126), **`EIM_PATH` is never set** in that startup path—the code exits at line 96–98.

---

## 4. Fallback “default locations” (`ToolInitializer`)

When there is **no usable `eimJson`**, `findAndSetEimPath()` assigns **`EIM_PATH`** to a **fixed default path per OS** (whether or not the file exists is **not** checked inside `findAndSetEimPath` itself):

```163:169:bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
public void findAndSetEimPath()
{
Path defaultEimPath = getDefaultEimPath();

if (defaultEimPath != null)
setEimPathInEnvVar(defaultEimPath.toString());
}
```

`getDefaultEimPath()`:

```138:161:bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
public Path getDefaultEimPath()
{
String userHome = System.getProperty("user.home"); //$NON-NLS-1$
Path defaultEimPath;
String os = Platform.getOS();
if (os.equals(Platform.OS_WIN32))
{
defaultEimPath = Paths.get(userHome, ".espressif", "eim_gui", //$NON-NLS-1$//$NON-NLS-2$
"eim.exe"); //$NON-NLS-1$
}
else if (os.equals(Platform.OS_MACOSX))
{
defaultEimPath = Paths.get("/Applications", //$NON-NLS-1$
"eim.app", "Contents", //$NON-NLS-1$//$NON-NLS-2$
"MacOS", "eim"); //$NON-NLS-1$ //$NON-NLS-2$
}
else
{
defaultEimPath = Paths.get(userHome, ".espressif", //$NON-NLS-1$
"eim_gui", "eim"); //$NON-NLS-1$//$NON-NLS-2$
}

return defaultEimPath;
}
```

**Default executable paths encoded here:**

| OS | Default `EIM_PATH` string |
|----|---------------------------|
| **Windows** | `%USERPROFILE%\.espressif\eim_gui\eim.exe` |
| **macOS** | `/Applications/eim.app/Contents/MacOS/eim` |
| **Linux** | `~/.espressif/eim_gui/eim` |

### 4.1 “Is EIM installed?” check

`isEimInstalled()` first checks whether **`EIM_PATH`** points to an **existing file**. If not, it falls back to checking **`getDefaultEimPath()`** on disk:

```49:61:bundles/com.espressif.idf.core/src/com/espressif/idf/core/tools/ToolInitializer.java
public boolean isEimInstalled()
{
String eimExePathEnv = idfEnvironmentVariables.getEnvValue(IDFEnvironmentVariables.EIM_PATH);
boolean exists = !StringUtil.isEmpty(eimExePathEnv) && Files.exists(Paths.get(eimExePathEnv));
if (!exists)
{
// Fallback: check in user home .espressif/eim_gui folder
Path defaultEimPath = getDefaultEimPath();
if (defaultEimPath != null)
exists = Files.exists(defaultEimPath);
}
return exists;
}
```

So detection **does not** scan `PATH`; it uses **saved `EIM_PATH`** and **default well-known paths**.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Documentation is out of sync with the code in this PR — it describes the removed behavior.

This file still documents the pre-refactor flow:

  • Section 3 (Startup, lines 108–139) shows the old if (eimJson != null) addEnvVariable(EIM_PATH, eimJson.getEimPath()) else findAndSetEimPath() branching. In this PR, EspressifToolStartup.earlyStartup was changed to call toolInitializer.resolveEimExecutablePath(eimJson) and the findAndSetEimPath fallback was removed.
  • Section 4 (lines 146–158) documents findAndSetEimPath(), which has been deleted from ToolInitializer in this PR (replaced by resolveEimExecutablePath).
  • Section 4.1 (lines 197–217) shows the old isEimInstalled() body that reads EIM_PATH and falls back to getDefaultEimPath(). The new implementation simply delegates to !StringUtil.isEmpty(resolveEimExecutablePath(null)), which also scans system PATH, eim_idf.json, env var, GUI default, and CLI default — a meaningfully different contract (e.g. the doc explicitly says "detection does not scan PATH", which is now false).
  • Section 7 summary table does not mention the new resolver, getDefaultCliEimPath, isEimGuiCapable, or USER_EIM_CLI_DIR.

Net effect: a reader following this doc will have completely wrong expectations about how EIM_PATH is resolved — and the doc even contradicts itself versus the very PR that introduces it.

Please refresh sections 3, 4, 4.1, and 7 to reflect:

  1. The five-step priority order: system PATHeimPath from eim_idf.jsonEIM_PATH env var → default GUI install (~/.espressif/eim_gui/...) → default CLI install (~/.espressif/eim/...).
  2. The single entry point ToolInitializer.resolveEimExecutablePath(EimJson).
  3. The new getDefaultCliEimPath() and USER_EIM_CLI_DIR.
  4. isEimGuiCapable(String) and how it gates GUI vs CLI launch on macOS (and the new MacOsEimLauncherStrategy.launchCliDirect path for non-.app binaries).
  5. That isEimInstalled() is now resolver-backed and does scan PATH.

Happy to draft the rewrite if useful.

🧰 Tools
🪛 LanguageTool

[style] ~148-~148: Consider shortening this phrase to just ‘whether’, unless you mean ‘regardless of whether’.
Context: ...`** to a fixed default path per OS (whether or not the file exists is not checked insi...

(WHETHER)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/EIM_PATH_DISCOVERY.md` around lines 102 - 217, Update the doc sections
to match the refactor: replace the old startup branching with
EspressifToolStartup.earlyStartup calling
ToolInitializer.resolveEimExecutablePath(eimJson) as the single entry point for
EIM resolution, document the new five-step priority order (system PATH →
eim_idf.json eimPath → EIM_PATH env var → default GUI install → default CLI
install), remove references to the deleted findAndSetEimPath() and old
getDefaultEimPath-only behavior and instead describe getDefaultCliEimPath() and
USER_EIM_CLI_DIR, explain isEimGuiCapable(String) and the
MacOsEimLauncherStrategy.launchCliDirect path for non-.app binaries, and note
that isEimInstalled() now delegates to the resolver (so it scans PATH).

@kolipakakondal kolipakakondal added this to the v4.3.0 milestone May 7, 2026
- Add CLI install path (~/.espressif/eim/) as final fallback in resolver
- Add getDefaultCliEimPath() for platform-specific CLI binary locations
- Add isEimGuiCapable() to detect GUI support via `eim gui --help`
- Handle CLI-only EIM binaries on macOS by launching directly instead
  of attempting AppleScript open on non-.app paths
- Add USER_EIM_CLI_DIR constant in EimConstants

Aligns with vscode-esp-idf-extension PR #1822 approach of checking
both GUI and CLI managed install locations and detecting GUI capability.
@alirana01 alirana01 force-pushed the eim_gui_cli_launch_fixes branch from dc1e322 to 87c8ecf Compare May 7, 2026 08:59
@AndriiFilippov
Copy link
Copy Markdown
Collaborator

@alirana01 hi !

Tested under:
OS: Windows 11 / Mac OS / Linux Ubuntu

I think we should fix this issue if we really want to apply vscode feature here. the way it works in vscode -> if EIM-CLI was installed via homebrew or winget then extension executes EIM-CLI wizard in VSCode Terminal. we should copy this behavior. Because for now, IDE JUST executes EIM-CLI.

Launched EIM application: C:\Users\andri\AppData\Local\Microsoft\WinGet\Packages\Espressif.EIM-CLI_Microsoft.Winget.Source_8wekyb3d8bbwe\eim.EXE (pid=73172)
EIM has been closed.
Refreshing UI after EIM closed...
EIM-CLI.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants