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
38+
39+ # Check Python version requirement
40+ if sys .version_info < (3 , 10 ):
41+ sys .stderr .write (
42+ f"Error: Python 3.10 or higher is required. "
43+ f"Current version: { sys .version_info .major } .{ sys .version_info .minor } .{ sys .version_info .micro } \n "
44+ f"Please update your Python installation.\n "
45+ )
46+ sys .exit (1 )
3747
3848# Python dependencies required for the build process
3949python_deps = {
5666# Framework directory path
5767FRAMEWORK_DIR = platform .get_package_dir ("framework-arduinoespressif32" )
5868
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 )
59131
60132def add_to_pythonpath (path ):
61133 """
@@ -80,14 +152,10 @@ def add_to_pythonpath(path):
80152 if normalized_path not in sys .path :
81153 sys .path .insert (0 , normalized_path )
82154
83-
84155def setup_python_paths ():
85156 """
86157 Setup Python paths based on the actual Python executable being used.
87- """
88- if not PYTHON_EXE or not os .path .isfile (PYTHON_EXE ):
89- return
90-
158+ """
91159 # Get the directory containing the Python executable
92160 python_dir = os .path .dirname (PYTHON_EXE )
93161 add_to_pythonpath (python_dir )
@@ -107,7 +175,6 @@ def setup_python_paths():
107175# Setup Python paths based on the actual Python executable
108176setup_python_paths ()
109177
110-
111178def _get_executable_path (python_exe , executable_name ):
112179 """
113180 Get the path to an executable binary (esptool, uv, etc.) based on the Python executable path.
@@ -119,14 +186,11 @@ def _get_executable_path(python_exe, executable_name):
119186 Returns:
120187 str: Path to executable or fallback to executable name
121188 """
122- if not python_exe or not os .path .isfile (python_exe ):
123- return executable_name # Fallback to command name
124189
125190 python_dir = os .path .dirname (python_exe )
126191
127- if sys .platform == "win32" :
128- scripts_dir = os .path .join (python_dir , "Scripts" )
129- 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" )
130194 else :
131195 # For Unix-like systems, executables are typically in the same directory as python
132196 # or in a bin subdirectory
@@ -228,7 +292,7 @@ def install_python_deps():
228292 uv_executable = _get_uv_executable_path (PYTHON_EXE )
229293
230294 # Add Scripts directory to PATH for Windows
231- if sys . platform == "win32" :
295+ if IS_WINDOWS :
232296 python_dir = os .path .dirname (PYTHON_EXE )
233297 scripts_dir = os .path .join (python_dir , "Scripts" )
234298 if os .path .isdir (scripts_dir ):
@@ -366,8 +430,10 @@ def install_esptool():
366430 return 'esptool' # Fallback
367431
368432
369- # Install Python dependencies and esptool
433+ # Install Python dependencies
370434install_python_deps ()
435+
436+ # Install esptool after dependencies
371437esptool_binary_path = install_esptool ()
372438
373439
@@ -756,7 +822,6 @@ def switch_off_ldf():
756822 if ' ' in esptool_binary_path
757823 else esptool_binary_path
758824)
759-
760825# Configure build tools and environment variables
761826env .Replace (
762827 __get_board_boot_mode = _get_board_boot_mode ,
0 commit comments