3434from platformio .project .helpers import get_project_dir
3535from platformio .package .version import pepver_to_semver
3636from platformio .util import get_serial_ports
37+ from platformio .compat import IS_WINDOWS
3738
3839# Check Python version requirement
3940if sys .version_info < (3 , 10 ):
6566# Framework directory path
6667FRAMEWORK_DIR = platform .get_package_dir ("framework-arduinoespressif32" )
6768
69+ platformio_dir = projectconfig .get ("platformio" , "core_dir" )
70+ penv_dir = os .path .join (platformio_dir , "penv" )
71+
72+ pip_path = os .path .join (
73+ penv_dir ,
74+ "Scripts" if IS_WINDOWS else "bin" ,
75+ "pip" + (".exe" if IS_WINDOWS else "" ),
76+ )
77+
78+ def setup_pipenv_in_package ():
79+ """
80+ Checks if 'penv' folder exists in platformio dir and creates virtual environment if not.
81+ """
82+ if not os .path .exists (penv_dir ):
83+ env .Execute (
84+ env .VerboseAction (
85+ '"$PYTHONEXE" -m venv --clear "%s"' % penv_dir ,
86+ "Creating a new virtual environment for Python dependencies" ,
87+ )
88+ )
89+
90+ assert os .path .isfile (
91+ pip_path
92+ ), "Error: Failed to create a proper virtual environment. Missing the `pip` binary!"
93+
94+ penv_python = os .path .join (penv_dir , "Scripts" , "python.exe" ) if IS_WINDOWS else os .path .join (penv_dir , "bin" , "python" )
95+ env .Replace (PYTHONEXE = penv_python )
96+ print (f"PYTHONEXE updated to penv environment: { penv_python } " )
97+
98+ setup_pipenv_in_package ()
99+ # Update global PYTHON_EXE variable after potential pipenv setup
100+ PYTHON_EXE = env .subst ("$PYTHONEXE" )
101+ python_exe = PYTHON_EXE
102+
103+ # Ensure penv Python directory is in PATH for subprocess calls
104+ python_dir = os .path .dirname (PYTHON_EXE )
105+ current_path = os .environ .get ("PATH" , "" )
106+ if python_dir not in current_path :
107+ os .environ ["PATH" ] = python_dir + os .pathsep + current_path
108+
109+ # Verify the Python executable exists
110+ assert os .path .isfile (PYTHON_EXE ), f"Python executable not found: { PYTHON_EXE } "
111+
112+ if os .path .isfile (python_exe ):
113+ # Update sys.path to include penv site-packages
114+ if IS_WINDOWS :
115+ penv_site_packages = os .path .join (penv_dir , "Lib" , "site-packages" )
116+ else :
117+ # Find the actual site-packages directory in the venv
118+ penv_lib_dir = os .path .join (penv_dir , "lib" )
119+ if os .path .isdir (penv_lib_dir ):
120+ for python_dir in os .listdir (penv_lib_dir ):
121+ if python_dir .startswith ("python" ):
122+ penv_site_packages = os .path .join (penv_lib_dir , python_dir , "site-packages" )
123+ break
124+ else :
125+ penv_site_packages = None
126+ else :
127+ penv_site_packages = None
128+
129+ if penv_site_packages and os .path .isdir (penv_site_packages ) and penv_site_packages not in sys .path :
130+ sys .path .insert (0 , penv_site_packages )
68131
69132def add_to_pythonpath (path ):
70133 """
@@ -89,14 +152,10 @@ def add_to_pythonpath(path):
89152 if normalized_path not in sys .path :
90153 sys .path .insert (0 , normalized_path )
91154
92-
93155def setup_python_paths ():
94156 """
95157 Setup Python paths based on the actual Python executable being used.
96- """
97- if not PYTHON_EXE or not os .path .isfile (PYTHON_EXE ):
98- return
99-
158+ """
100159 # Get the directory containing the Python executable
101160 python_dir = os .path .dirname (PYTHON_EXE )
102161 add_to_pythonpath (python_dir )
@@ -116,7 +175,6 @@ def setup_python_paths():
116175# Setup Python paths based on the actual Python executable
117176setup_python_paths ()
118177
119-
120178def _get_executable_path (python_exe , executable_name ):
121179 """
122180 Get the path to an executable binary (esptool, uv, etc.) based on the Python executable path.
@@ -128,14 +186,11 @@ def _get_executable_path(python_exe, executable_name):
128186 Returns:
129187 str: Path to executable or fallback to executable name
130188 """
131- if not python_exe or not os .path .isfile (python_exe ):
132- return executable_name # Fallback to command name
133189
134190 python_dir = os .path .dirname (python_exe )
135191
136- if sys .platform == "win32" :
137- scripts_dir = os .path .join (python_dir , "Scripts" )
138- executable_path = os .path .join (scripts_dir , f"{ executable_name } .exe" )
192+ if IS_WINDOWS :
193+ executable_path = os .path .join (python_dir , f"{ executable_name } .exe" )
139194 else :
140195 # For Unix-like systems, executables are typically in the same directory as python
141196 # or in a bin subdirectory
@@ -237,7 +292,7 @@ def install_python_deps():
237292 uv_executable = _get_uv_executable_path (PYTHON_EXE )
238293
239294 # Add Scripts directory to PATH for Windows
240- if sys . platform == "win32" :
295+ if IS_WINDOWS :
241296 python_dir = os .path .dirname (PYTHON_EXE )
242297 scripts_dir = os .path .join (python_dir , "Scripts" )
243298 if os .path .isdir (scripts_dir ):
@@ -375,8 +430,10 @@ def install_esptool():
375430 return 'esptool' # Fallback
376431
377432
378- # Install Python dependencies and esptool
433+ # Install Python dependencies
379434install_python_deps ()
435+
436+ # Install esptool after dependencies
380437esptool_binary_path = install_esptool ()
381438
382439
@@ -765,7 +822,6 @@ def switch_off_ldf():
765822 if ' ' in esptool_binary_path
766823 else esptool_binary_path
767824)
768-
769825# Configure build tools and environment variables
770826env .Replace (
771827 __get_board_boot_mode = _get_board_boot_mode ,
0 commit comments