Skip to content

fix: recreate penv on Python version mismatch, resolve site-packages dynamically#459

Merged
Jason2866 merged 2 commits into
developfrom
fix/penv-python-version-mismatch
Apr 10, 2026
Merged

fix: recreate penv on Python version mismatch, resolve site-packages dynamically#459
Jason2866 merged 2 commits into
developfrom
fix/penv-python-version-mismatch

Conversation

@Jason2866
Copy link
Copy Markdown

@Jason2866 Jason2866 commented Apr 10, 2026

Problem

When the host Python interpreter is upgraded (e.g. Homebrew 3.13→3.14, or ESPHome ships with a different Python than the one that created the penv), the existing ~/.platformio/penv still contains packages compiled for the old ABI.

setup_python_paths() constructed the site-packages path from sys.version_info of the running process:

python_ver = f"python{sys.version_info.major}.{sys.version_info.minor}"
# → "python3.14" but penv has lib/python3.13/site-packages

This pointed to a directory that did not exist inside the penv, causing:

Solution

This PR implements a two-layer defense:

1. Auto-recreate penv on Python version mismatch

Both setup_pipenv_in_package() and _setup_pipenv_minimal() now detect when the existing penv was created with a different Python major.minor version and automatically recreate it:

Python version mismatch: penv has 3.13, current interpreter is 3.14. Recreating penv...

2. Dynamically discover site-packages (defense in depth)

setup_python_paths() no longer hardcodes the path from sys.version_info. Instead, _get_penv_site_packages() inspects the actual penv directory structure to find the real site-packages path. Additionally, conflicting system site-packages are filtered from sys.path and the penv site-packages is inserted at index 0.

New helper functions

Function Purpose
_get_penv_python_version() Detect Python version inside existing penv (POSIX: inspect lib/pythonX.Y/, Windows: query penv Python executable)
_penv_version_matches() Compare penv Python vs running interpreter
_get_penv_site_packages() Dynamically locate site-packages directory in penv

Testing

  • Verified syntax: python3 -c "import py_compile; py_compile.compile('builder/penv_setup.py', doraise=True)"
  • Covers macOS/Linux (POSIX lib/pythonX.Y/) and Windows (Lib/) layouts
  • Gracefully handles missing/corrupted penv (returns None, no crash)

Fixes: esphome/esphome#15619
Fixes: esphome/esphome#15519
Supersedes: #458

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced Python environment setup to automatically detect and handle version mismatches between the build environment and project requirements.
    • Improved site-packages path detection for more accurate dependency resolution during builds.
    • Optimized Python path handling to ensure correct package loading.

…dynamically

When the host Python interpreter is upgraded (e.g. Homebrew 3.13→3.14)
the existing penv still contains packages compiled for the old ABI.
setup_python_paths() constructed the site-packages path from
sys.version_info of the *running* process, which pointed to a
directory that did not exist inside the penv, causing
ModuleNotFoundError for littlefs, fatfs, and other C-extension
packages.

This commit:

1. Adds _get_penv_python_version() to detect the actual Python
   version inside an existing penv by inspecting lib/pythonX.Y/
   directories (POSIX) or querying the penv Python executable
   (Windows).

2. Adds _penv_version_matches() to compare penv vs running Python.

3. Modifies setup_pipenv_in_package() and _setup_pipenv_minimal()
   to detect version mismatches and automatically recreate the penv
   with the correct Python version.

4. Replaces the hardcoded sys.version_info path construction in
   setup_python_paths() with _get_penv_site_packages() that
   dynamically discovers the actual site-packages directory.

5. Filters conflicting system site-packages from sys.path and
   inserts the penv's site-packages at index 0 to ensure penv
   packages always take precedence.

Fixes: esphome/esphome#15619
Fixes: esphome/esphome#15519
Amp-Thread-ID: https://ampcode.com/threads/T-019d7790-e061-71cf-9dba-d9da022b203b
Co-authored-by: Amp <amp@ampcode.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

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.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5d0481a1-023e-4dad-8799-b4214f90a8de

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Modified builder/penv_setup.py to add helper functions for inspecting an existing penv's Python version and locating its site-packages directory. Updated penv creation logic to delete and recreate penvs when version mismatches are detected. Restructured path management to insert the penv's site-packages at the front of sys.path and remove unrelated entries.

Changes

Cohort / File(s) Summary
Python Environment Setup
builder/penv_setup.py
Added helper functions to introspect penv Python version and detect the real site-packages directory from the filesystem layout. Modified setup_pipenv_in_package and _setup_pipenv_minimal to delete and recreate penv when version mismatches occur. Reworked setup_python_paths to properly prioritize the detected penv site-packages in sys.path, remove unrelated entries, and invoke site.addsitedir.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 The penv paths were winding and confused,
So I hopped through and set them straight—no fused!
Version checks now guard the virtualenv throne,
And site-packages rest where they've always known. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the two main changes: recreating penv on Python version mismatch and dynamically resolving site-packages, which directly address the ModuleNotFoundError and ImportError issues.
Linked Issues check ✅ Passed The PR implements both required solutions: auto-recreating penv on version mismatch and dynamically discovering site-packages, directly addressing the root causes of both esphome issues #15619 and #15519.
Out of Scope Changes check ✅ Passed All changes in builder/penv_setup.py are focused on resolving penv Python version mismatches and dynamically locating site-packages; no unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/penv-python-version-mismatch

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.

@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai full review and analyze if the PR solves the esphome issues: esphome/esphome#15619
esphome/esphome#15519

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

🧩 Analysis chain

Now let me look at the actual PR code to do a thorough review.


🏁 Script executed:

#!/bin/bash
# Get the PR diff and key files
cat builder/penv_setup.py | head -300

Length of output: 10975


🏁 Script executed:

#!/bin/bash
# Look at the new helper functions added in this PR
grep -n "_get_penv_python_version\|_penv_version_matches\|_get_penv_site_packages\|setup_python_paths\|setup_pipenv_in_package\|_setup_pipenv_minimal" builder/penv_setup.py

Length of output: 826


🏁 Script executed:

#!/bin/bash
# Get the setup_python_paths and _setup_pipenv_minimal functions
sed -n '295,380p' builder/penv_setup.py
echo "---"
sed -n '620,760p' builder/penv_setup.py

Length of output: 8644


Now let me look at the esphome issues referenced.

@Jason2866: Here is my full review of PR #459 and an analysis of whether it solves the two ESPHome issues.


Analysis: Does This PR Fix the ESPHome Issues?

ESPHome #15619ModuleNotFoundError: No module named 'littlefs'

Root cause confirmed: The error appeared at /Users/jharris/.platformio/packages/tool-scons/scons-local-4.8.1/SCons/Script/SConscript.py inside builder/main.py line 25: from littlefs import LittleFS, triggered because ESPHome installed on Apple Silicon Mac via Homebrew hit a compilation error complaining about littlefs missing on all projects. The old setup_python_paths() constructed the path using sys.version_info of the running process (e.g., Python 3.14), but the penv was created under Python 3.13 — so lib/python3.14/site-packages simply didn't exist.

How this PR fixes it — Layer 1 (penv recreation): Both setup_pipenv_in_package() and _setup_pipenv_minimal() now call _penv_version_matches() before reusing an existing penv. On mismatch, shutil.rmtree(penv_dir) wipes the stale environment and recreates it with the current interpreter, causing littlefs-python to be reinstalled with the correct ABI. ✅

How this PR fixes it — Layer 2 (dynamic path discovery): Even before the penv is recreated, _get_penv_site_packages() now inspects the actual lib/pythonX.Y/site-packages directories present on disk rather than constructing a path from sys.version_info. This means lib/python3.13/site-packages (the real existing path) is found and used, preventing the ModuleNotFoundError even in degenerate flows. ✅

Verdict: ✅ FIXED


ESPHome #15519ImportError: cannot import name 'lfs' from partially initialized module 'littlefs'

Root cause confirmed: The error ImportError: cannot import name 'lfs' from partially initialized module 'littlefs' (most likely due to a circular import) appeared in builder/main.py at from littlefs import LittleFS. The "partially initialized module" symptom typically indicates that a compiled native extension (.so/.pyd) was built for a different Python ABI — the old penv's littlefs C extension (built for Python 3.13) was being loaded by Python 3.14.

How this PR fixes it: The Layer 1 penv recreation wipes the old environment entirely and reinstalls littlefs-python, downloading or building a wheel for the correct Python version/ABI. This eliminates the stale binary extension that causes the load failure. ✅

Verdict: ✅ FIXED


Full Code Review

✅ Strengths

  1. Two-layer defense is robust. Recreating the penv is the correct long-term fix; the dynamic path discovery is an excellent defense-in-depth measure.
  2. Cross-platform coverage. POSIX and Windows layouts are handled differently and correctly — Windows uses Lib/site-packages (versionless) while POSIX inspects lib/pythonX.Y/.
  3. Graceful degradation. All three new helper functions return None on error, and setup_python_paths() early-returns if _get_penv_site_packages() returns None, so a broken/missing penv doesn't crash.
  4. Informative log message on version mismatch helps users understand why penv is being rebuilt.
  5. sys.path filtering removes conflicting system site-packages entries and inserts penv's path at index 0 — ensures penv packages always take precedence.

⚠️ Issues Found

1. Lexicographic sort is not the same as version sort (minor, cosmetic)

In both _get_penv_python_version() and _get_penv_site_packages(), the POSIX path is found via:

for entry in sorted(lib_dir.iterdir(), reverse=True):

The comment says "Prefer the newest python version directory." However, sorted() on strings is lexicographic, so python3.9 sorts after python3.10 (since '9' > '1'). In practice only one Python version directory exists in a typical penv, so this is not a real-world bug — but the comment is misleading. A more correct sort key would be:

for entry in sorted(lib_dir.iterdir(), key=lambda e: tuple(int(x) for x in e.name[6:].split('.') if x.isdigit()), reverse=True):

2. import shutil inside function body

In both setup_pipenv_in_package() and _setup_pipenv_minimal():

import shutil
shutil.rmtree(penv_dir, ignore_errors=True)

shutil is a stdlib module and should be at the top of the file with the other imports. This is a minor style issue with no functional impact.

3. site.addsitedir() may create duplicate sys.path entries

In setup_python_paths():

if site_packages not in sys.path:
    sys.path.insert(0, site_packages)
site.addsitedir(site_packages)

site.addsitedir() processes .pth files and may re-add site_packages to sys.path. The if ... not in sys.path guard prevents a duplicate before addsitedir, but addsitedir itself may add it again. This is unlikely to cause real problems since Python deduplicates module lookups, but consider:

if site_packages not in sys.path:
    sys.path.insert(0, site_packages)
site.addsitedir(site_packages)
# Re-ensure penv is still first after addsitedir may have appended it
if sys.path[0] != site_packages:
    sys.path.remove(site_packages)
    sys.path.insert(0, site_packages)

4. Windows: subprocess call in _get_penv_python_version() can be slow/fragile

On Windows, the function invokes the penv Python executable as a subprocess to determine version:

out = subprocess.check_output(
    [penv_python, "-c", "import sys; print(sys.version_info.major, sys.version_info.minor)"],
    text=True, timeout=5
)

If the penv Python is corrupted or the timeout is insufficient, the function returns None (treated as "no penv") and the mismatch is silently skipped. A fallback of parsing the pyvenv.cfg file (which is always present and contains version = 3.13.x) would be more reliable and faster. Example:

cfg = penv_path / "pyvenv.cfg"
if cfg.is_file():
    for line in cfg.read_text().splitlines():
        if line.startswith("version"):
            ver_str = line.split("=")[1].strip()  # e.g. "3.13.2"
            parts = ver_str.split(".")
            return (int(parts[0]), int(parts[1]))

5. Redundant call to _get_penv_python_version() in mismatch blocks

In both setup_pipenv_in_package() and _setup_pipenv_minimal():

if os.path.isfile(penv_python_path) and not _penv_version_matches(penv_dir):
    penv_ver = _get_penv_python_version(penv_dir)  # called again!

_penv_version_matches() already calls _get_penv_python_version() internally. This means _get_penv_python_version() is called twice on a mismatch (once for the check, once for the log message). On Windows this means two subprocess invocations. Consider refactoring to avoid the double call.


Summary

Issue Status
esphome #15619 ModuleNotFoundError: littlefs ✅ Fixed by Layer 1 (penv recreate) + Layer 2 (dynamic path)
esphome #15519 ImportError: lfs partially initialized ✅ Fixed by Layer 1 (ABI-correct penv recreate)
Lexicographic vs version sort ⚠️ Minor/cosmetic
import shutil inside function ⚠️ Style only
site.addsitedir duplicate path ⚠️ Low impact
Windows: subprocess vs pyvenv.cfg ⚠️ Robustness improvement opportunity
Double call to _get_penv_python_version ⚠️ Minor performance

The PR correctly addresses both reported ESPHome issues. The core logic is sound. The issues above are minor quality/robustness improvements.


🧠 Learnings used
Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2025-09-23T14:15:00.476Z
Learning: In the pioarduino/platform-espressif32 project, espidf.py creates an extra/separate virtual environment from the penv setup, so cryptography version differences between penv_setup.py and espidf.py do not cause conflicts since they operate in isolated environments.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 448
File: builder/relinker/configuration.py:145-150
Timestamp: 2026-03-29T17:40:06.024Z
Learning: In pioarduino/platform-espressif32, when the relinker (builder/relinker/configuration.py, builder/relinker/relinker.py) runs inside the PlatformIO/SCons build system, both BUILD_DIR and IDF_PATH are always set in the process environment by PlatformIO. There is no need to defensively guard against missing BUILD_DIR or IDF_PATH in the relinker scripts when used in the PlatformIO context. The existing `build_dir or os.environ.get('BUILD_DIR')` fallback in paths_c.__init__ is sufficient.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 296
File: builder/penv_setup.py:612-647
Timestamp: 2025-09-23T16:55:39.788Z
Learning: In the pioarduino/platform-espressif32 project, Jason2866 prefers no fallback mechanisms in penv setup functions. The penv virtual environment setup must work properly and should crash immediately rather than falling back to host Python environments, to avoid using "unknown env" configurations.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2025-09-23T14:15:00.476Z
Learning: In the pioarduino/platform-espressif32 project, espidf.py creates and manages its own separate IDF virtual environment (accessed via get_python_exe() from get_idf_venv_dir()). This IDF venv is completely isolated from the penv setup, so different cryptography versions between penv_setup.py and espidf.py don't cause conflicts as they exist in separate virtual environments.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2025-09-22T10:59:14.425Z
Learning: In the pioarduino/platform-espressif32 project, the penv setup ensures that uv gets installed in the venv (penv), so additional safeguards for uv availability are not needed.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 255
File: builder/penv_setup.py:26-40
Timestamp: 2025-08-10T19:13:25.599Z
Learning: In the pioarduino/platform-espressif32 repository's builder/penv_setup.py file, semantic_version is imported but intentionally not listed in python_deps because it's a transitive dependency of platformio-core (version 2.10.*). When platformio is installed from the GitHub URL, semantic_version is automatically installed as one of its dependencies.
✅ Actions performed

Full review triggered.

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.

🧹 Nitpick comments (3)
builder/penv_setup.py (3)

230-241: Redundant _get_penv_python_version() calls on Windows.

_penv_version_matches() (line 232) already calls _get_penv_python_version() internally. The subsequent call at line 233 duplicates this work—on Windows, this spawns a second subprocess. Since this only runs on version mismatch (uncommon), the impact is low.

Potential optimization to avoid redundant calls
     # Recreate penv when Python version changed (e.g. Homebrew upgraded 3.13→3.14)
     penv_python_path = get_executable_path(penv_dir, "python")
-    if os.path.isfile(penv_python_path) and not _penv_version_matches(penv_dir):
-        penv_ver = _get_penv_python_version(penv_dir)
+    penv_ver = _get_penv_python_version(penv_dir) if os.path.isfile(penv_python_path) else None
+    current_ver = (sys.version_info.major, sys.version_info.minor)
+    if penv_ver is not None and penv_ver != current_ver:
-        current_ver = (sys.version_info.major, sys.version_info.minor)
         print(
             f"Python version mismatch: penv has {penv_ver[0]}.{penv_ver[1]}, "
             f"current interpreter is {current_ver[0]}.{current_ver[1]}. "
             f"Recreating penv..."
         )
         import shutil
         shutil.rmtree(penv_dir, ignore_errors=True)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/penv_setup.py` around lines 230 - 241, Avoid the duplicate subprocess
call to _get_penv_python_version by calling it once and using its result for the
mismatch check and print: fetch penv_ver = _get_penv_python_version(penv_dir)
(using get_executable_path/penv_python_path as needed), compute current_ver from
sys.version_info, compare penv_ver to current_ver instead of calling
_penv_version_matches again, and only then call shutil.rmtree(penv_dir,
ignore_errors=True) when they differ; alternatively, change
_penv_version_matches to return the detected penv version so the caller can
reuse it instead of invoking _get_penv_python_version twice.

319-323: site.addsitedir() may alter path ordering.

site.addsitedir() processes .pth files and can append additional paths to sys.path, potentially placing them after the penv site-packages that was inserted at index 0. If any .pth files in the penv add paths that should take precedence, they might end up in the wrong order.

In practice, this should be fine since conflicting system site-packages are already filtered out (lines 313-317). Consider re-ensuring penv remains first if issues arise:

Optional: Re-ensure penv is first after addsitedir
     site.addsitedir(site_packages)
+
+    # Ensure penv site-packages remains first after addsitedir processing
+    if site_packages in sys.path:
+        sys.path.remove(site_packages)
+        sys.path.insert(0, site_packages)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/penv_setup.py` around lines 319 - 323, The current sequence
(inserting site_packages at sys.path[0] then calling
site.addsitedir(site_packages)) can let .pth processing append new paths after
your penv; to fix, ensure the penv stays first by either calling
site.addsitedir(site_packages) first and then inserting site_packages at
sys.path[0], or by re-checking and moving site_packages back to index 0 after
site.addsitedir(site_packages) so that sys.path ordering guarantees penv
site-packages precedence (referencing site.addsitedir, sys.path, and
site_packages).

162-169: Lexicographic sorting may mis-order version directories.

sorted(..., reverse=True) orders strings lexicographically, causing python3.9 to sort before python3.10 (since '1' < '9'). If a penv ever contains multiple pythonX.Y directories, this would select the wrong one.

In practice, a typical penv has only one pythonX.Y directory, so this is unlikely to cause issues. Consider using numeric sorting if robustness is desired:

Suggested numeric sort
-    for entry in sorted(lib_dir.iterdir(), reverse=True):
+    def _version_key(e):
+        if e.name.startswith("python"):
+            try:
+                parts = e.name[len("python"):].split(".")
+                return (int(parts[0]), int(parts[1]))
+            except (ValueError, IndexError):
+                return (0, 0)
+        return (0, 0)
+
+    for entry in sorted(lib_dir.iterdir(), key=_version_key, reverse=True):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@builder/penv_setup.py` around lines 162 - 169, The loop relies on
lexicographic sorting of lib_dir.iterdir(), which can mis-order names like
"python3.9" vs "python3.10"; instead, collect candidate entries that match the
"pythonX.Y" pattern (use entry.name and ver_str), parse major and minor into
ints (as you already do into major, minor), sort candidates numerically by
(major, minor) in descending order, then pick the first candidate and return
(int(major), int(minor)); update the code around entry.name, ver_str, major,
minor to build and sort a list of (major, minor, entry) tuples rather than
relying on sorted(..., reverse=True).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@builder/penv_setup.py`:
- Around line 230-241: Avoid the duplicate subprocess call to
_get_penv_python_version by calling it once and using its result for the
mismatch check and print: fetch penv_ver = _get_penv_python_version(penv_dir)
(using get_executable_path/penv_python_path as needed), compute current_ver from
sys.version_info, compare penv_ver to current_ver instead of calling
_penv_version_matches again, and only then call shutil.rmtree(penv_dir,
ignore_errors=True) when they differ; alternatively, change
_penv_version_matches to return the detected penv version so the caller can
reuse it instead of invoking _get_penv_python_version twice.
- Around line 319-323: The current sequence (inserting site_packages at
sys.path[0] then calling site.addsitedir(site_packages)) can let .pth processing
append new paths after your penv; to fix, ensure the penv stays first by either
calling site.addsitedir(site_packages) first and then inserting site_packages at
sys.path[0], or by re-checking and moving site_packages back to index 0 after
site.addsitedir(site_packages) so that sys.path ordering guarantees penv
site-packages precedence (referencing site.addsitedir, sys.path, and
site_packages).
- Around line 162-169: The loop relies on lexicographic sorting of
lib_dir.iterdir(), which can mis-order names like "python3.9" vs "python3.10";
instead, collect candidate entries that match the "pythonX.Y" pattern (use
entry.name and ver_str), parse major and minor into ints (as you already do into
major, minor), sort candidates numerically by (major, minor) in descending
order, then pick the first candidate and return (int(major), int(minor)); update
the code around entry.name, ver_str, major, minor to build and sort a list of
(major, minor, entry) tuples rather than relying on sorted(..., reverse=True).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f071d449-77d7-4064-af56-9fda0e761ef8

📥 Commits

Reviewing files that changed from the base of the PR and between 8a1cce9 and dcf7a02.

📒 Files selected for processing (1)
  • builder/penv_setup.py

…process

Parse the 'version' key from pyvenv.cfg which is always written by
both 'python -m venv' and 'uv venv'. This is faster, more reliable,
and works even when the penv Python executable is corrupted or
unresponsive.

Falls back to inspecting lib/pythonX.Y/ directories on POSIX if
pyvenv.cfg is missing.

Amp-Thread-ID: https://ampcode.com/threads/T-019d7790-e061-71cf-9dba-d9da022b203b
Co-authored-by: Amp <amp@ampcode.com>
@Jason2866
Copy link
Copy Markdown
Author

@coderabbitai analyze the changes in commit ef3fb76 if the fix the minor issues you listed

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the commit details from GitHub
gh api repos/pioarduino/platform-espressif32/commits/ef3fb76c6ac4fac5b48476da63a746e8a69741f7 --jq '.commit.message'
echo "---DIFF---"
gh api repos/pioarduino/platform-espressif32/commits/ef3fb76c6ac4fac5b48476da63a746e8a69741f7 | jq -r '.files[] | "FILE: \(.filename)\nPATCH:\n\(.patch)\n---"'

Length of output: 5784


@Jason2866: Here is the analysis of commit ef3fb76 against each of the five minor issues from my previous review:


# Issue Status in ef3fb76
1 Lexicographic vs version sort ⚠️ Partially fixed
2 import shutil inside function body ✅ Fixed
3 site.addsitedir() duplicate path ✅ Fixed
4 Windows: subprocess vs pyvenv.cfg ✅ Fixed
5 Double call to _get_penv_python_version() 🟡 Mitigated (not removed)

Detailed breakdown

Issue 1 — Lexicographic sort (partially fixed)

  • _get_penv_site_packages() now uses a proper numeric sort key:
    key=lambda e: tuple(int(x) for x in e.name[6:].split('.') if x.isdigit())
  • ⚠️ The POSIX fallback inside _get_penv_python_version() still uses plain sorted(lib_dir.iterdir(), reverse=True) without the numeric key. That said, this path is now only reached when pyvenv.cfg is missing or unparseable, making it an extremely rare edge case. Real-world impact is negligible.

Issue 2 — import shutil inside function (fixed)
import shutil was moved to the top-level import block, and both inline occurrences in setup_pipenv_in_package() and _setup_pipenv_minimal() were removed. ✅

Issue 3 — site.addsitedir() duplicate (fixed)
The re-ensure block was added immediately after site.addsitedir():

if sys.path[0] != site_packages:
    sys.path.remove(site_packages)
    sys.path.insert(0, site_packages)

This guarantees penv stays first even if addsitedir appended it. ✅

Issue 4 — Windows subprocess (fixed)
_get_penv_python_version() was completely refactored. It now primarily reads pyvenv.cfg (cross-platform, no subprocess), with the POSIX lib/pythonX.Y/ directory scan as a fallback. The Windows subprocess invocation is entirely gone. ✅

Issue 5 — Double call to _get_penv_python_version() (mitigated)
The double call still exists structurally — _penv_version_matches() calls _get_penv_python_version() once, and the log line calls it again to format the version string. However, since _get_penv_python_version() now just reads pyvenv.cfg (a cheap file I/O operation) instead of spawning a subprocess, the performance impact is entirely negligible. This is acceptable as-is.


Summary: 4 out of 5 issues are fully resolved. The one remaining gap (lexicographic sort in the POSIX fallback of _get_penv_python_version()) is a very minor cosmetic concern that only applies when pyvenv.cfg is absent — an extreme edge case. The commit is a solid improvement overall.


🧠 Learnings used
Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 296
File: builder/penv_setup.py:612-647
Timestamp: 2025-09-23T16:55:39.788Z
Learning: In the pioarduino/platform-espressif32 project, Jason2866 prefers no fallback mechanisms in penv setup functions. The penv virtual environment setup must work properly and should crash immediately rather than falling back to host Python environments, to avoid using "unknown env" configurations.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2026-03-07T22:12:49.994Z
Learning: In monitor/filter_exception_decoder.py for pioarduino/platform-espressif32: the `is_return_addr` / `-1` decrement in `_resolve_address` and `_prefetch_addresses` was intentionally removed in PR 419. ESP-IDF's panic handler already provides call-site adjusted addresses; decrementing again produces wrong function/line results. On RISC-V with compressed instructions, subtracting 1 byte can land mid-instruction. Stack trace words should never be blindly decremented — `PcAddressMatcher` ELF section filtering is the correct mechanism. Cache keys must use the original address string (not the decremented lookup) for prefetch/resolve consistency.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 448
File: builder/relinker/configuration.py:145-150
Timestamp: 2026-03-29T17:40:06.024Z
Learning: In pioarduino/platform-espressif32, when the relinker (builder/relinker/configuration.py, builder/relinker/relinker.py) runs inside the PlatformIO/SCons build system, both BUILD_DIR and IDF_PATH are always set in the process environment by PlatformIO. There is no need to defensively guard against missing BUILD_DIR or IDF_PATH in the relinker scripts when used in the PlatformIO context. The existing `build_dir or os.environ.get('BUILD_DIR')` fallback in paths_c.__init__ is sufficient.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 371
File: builder/spiffsgen.py:620-629
Timestamp: 2025-12-27T16:33:38.710Z
Learning: In the pioarduino/platform-espressif32 project, when extracting SPIFFS files in builder/spiffsgen.py, Jason2866 prefers to check if rel_path is empty after lstrip('/') and skip such files with a warning message, rather than attempting to handle empty parent directories in os.makedirs calls. This treats empty paths as invalid/corrupted data.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 268
File: builder/penv_setup.py:87-96
Timestamp: 2025-08-26T14:52:29.044Z
Learning: For the pioarduino/platform-espressif32 project, Jason2866 prefers to trust system executables when falling back to PATH-based command resolution (like "uv"), rather than implementing additional validation checks for security concerns.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 296
File: builder/main.py:49-50
Timestamp: 2025-09-22T14:47:08.855Z
Learning: For the pioarduino/platform-espressif32 project, Jason2866 prefers that critical setup operations like platform.setup_python_env should crash immediately if they fail rather than having fallback mechanisms, as this ensures problems are detected early and clearly rather than being masked.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2025-09-23T12:35:35.508Z
Learning: For the pioarduino/platform-espressif32 project, Jason2866 has explicitly instructed to NEVER create issues or PRs outside of his repositories (pioarduino/*). All issue and PR creation must be strictly limited to his own repositories only, never in upstream or other repositories.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2025-09-23T12:35:35.508Z
Learning: For the pioarduino/platform-espressif32 project, Jason2866 has explicitly instructed to NEVER create issues or PRs outside of his repositories (pioarduino/*). All issue and PR creation must be strictly limited to his own repositories only, never in upstream or other repositories.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 281
File: builder/frameworks/component_manager.py:1047-1054
Timestamp: 2025-09-04T15:27:18.112Z
Learning: In the pioarduino/platform-espressif32 project, Jason2866 prefers not to add guard checks for missing arduino_libs_mcu in backup operations, preferring to let the operation proceed rather than silently skip when the directory might be missing.

Learnt from: Jason2866
Repo: pioarduino/platform-espressif32 PR: 0
File: :0-0
Timestamp: 2025-09-23T14:15:00.476Z
Learning: In the pioarduino/platform-espressif32 project, espidf.py creates an extra/separate virtual environment from the penv setup, so cryptography version differences between penv_setup.py and espidf.py do not cause conflicts since they operate in isolated environments.

@Jason2866 Jason2866 merged commit 348a00a into develop Apr 10, 2026
1 check passed
@Jason2866 Jason2866 deleted the fix/penv-python-version-mismatch branch April 10, 2026 14:50
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.

1 participant