diff --git a/builder/main.py b/builder/main.py index b71d6f757..1c46018a1 100644 --- a/builder/main.py +++ b/builder/main.py @@ -51,6 +51,7 @@ platform = env.PioPlatform() projectconfig = env.GetProjectConfig() terminal_cp = locale.getpreferredencoding().lower() +PYTHON_EXE = env.subst("$PYTHONEXE") # Global Python executable path # Framework directory path FRAMEWORK_DIR = platform.get_package_dir("framework-arduinoespressif32") @@ -80,24 +81,20 @@ def add_to_pythonpath(path): sys.path.insert(0, normalized_path) -def setup_python_paths(env): +def setup_python_paths(): """ Setup Python paths based on the actual Python executable being used. - - Args: - env: SCons environment object """ - python_exe = env.subst('$PYTHONEXE') - if not python_exe or not os.path.isfile(python_exe): + if not PYTHON_EXE or not os.path.isfile(PYTHON_EXE): return # Get the directory containing the Python executable - python_dir = os.path.dirname(python_exe) + python_dir = os.path.dirname(PYTHON_EXE) add_to_pythonpath(python_dir) # Try to find site-packages directory using the actual Python executable result = subprocess.run( - [python_exe, "-c", "import site; print(site.getsitepackages()[0])"], + [PYTHON_EXE, "-c", "import site; print(site.getsitepackages()[0])"], capture_output=True, text=True, timeout=5 @@ -108,7 +105,68 @@ def setup_python_paths(env): add_to_pythonpath(site_packages) # Setup Python paths based on the actual Python executable -setup_python_paths(env) +setup_python_paths() + + +def _get_executable_path(python_exe, executable_name): + """ + Get the path to an executable binary (esptool, uv, etc.) based on the Python executable path. + + Args: + python_exe (str): Path to Python executable + executable_name (str): Name of the executable to find (e.g., 'esptool', 'uv') + + Returns: + str: Path to executable or fallback to executable name + """ + if not python_exe or not os.path.isfile(python_exe): + return executable_name # Fallback to command name + + python_dir = os.path.dirname(python_exe) + + if sys.platform == "win32": + scripts_dir = os.path.join(python_dir, "Scripts") + executable_path = os.path.join(scripts_dir, f"{executable_name}.exe") + else: + # For Unix-like systems, executables are typically in the same directory as python + # or in a bin subdirectory + executable_path = os.path.join(python_dir, executable_name) + + # If not found in python directory, try bin subdirectory + if not os.path.isfile(executable_path): + bin_dir = os.path.join(python_dir, "bin") + executable_path = os.path.join(bin_dir, executable_name) + + if os.path.isfile(executable_path): + return executable_path + + return executable_name # Fallback to command name + + +def _get_esptool_executable_path(python_exe): + """ + Get the path to the esptool executable binary. + + Args: + python_exe (str): Path to Python executable + + Returns: + str: Path to esptool executable + """ + return _get_executable_path(python_exe, "esptool") + + +def _get_uv_executable_path(python_exe): + """ + Get the path to the uv executable binary. + + Args: + python_exe (str): Path to Python executable + + Returns: + str: Path to uv executable + """ + return _get_executable_path(python_exe, "uv") def get_packages_to_install(deps, installed_packages): @@ -138,9 +196,12 @@ def install_python_deps(): Returns: bool: True if successful, False otherwise """ + # Get uv executable path + uv_executable = _get_uv_executable_path(PYTHON_EXE) + try: result = subprocess.run( - ["uv", "--version"], + [uv_executable, "--version"], capture_output=True, text=True, timeout=3 @@ -152,7 +213,7 @@ def install_python_deps(): if not uv_available: try: result = subprocess.run( - [env.subst("$PYTHONEXE"), "-m", "pip", "install", "uv>=0.1.0", "-q", "-q", "-q"], + [PYTHON_EXE, "-m", "pip", "install", "uv>=0.1.0", "-q", "-q", "-q"], capture_output=True, text=True, timeout=30, # 30 second timeout @@ -162,6 +223,17 @@ def install_python_deps(): if result.stderr: print(f"Error output: {result.stderr.strip()}") return False + + # Update uv executable path after installation + uv_executable = _get_uv_executable_path(PYTHON_EXE) + + # Add Scripts directory to PATH for Windows + if sys.platform == "win32": + python_dir = os.path.dirname(PYTHON_EXE) + scripts_dir = os.path.join(python_dir, "Scripts") + if os.path.isdir(scripts_dir): + os.environ["PATH"] = scripts_dir + os.pathsep + os.environ.get("PATH", "") + except subprocess.TimeoutExpired: print("Error: uv installation timed out") return False @@ -182,7 +254,7 @@ def _get_installed_uv_packages(): """ result = {} try: - cmd = ["uv", "pip", "list", "--format=json"] + cmd = [uv_executable, "pip", "list", "--format=json"] result_obj = subprocess.run( cmd, capture_output=True, @@ -221,8 +293,8 @@ def _get_installed_uv_packages(): packages_list = [f"{p}{python_deps[p]}" for p in packages_to_install] cmd = [ - "uv", "pip", "install", - f"--python={env.subst('$PYTHONEXE')}", + uv_executable, "pip", "install", + f"--python={PYTHON_EXE}", "--quiet", "--upgrade" ] + packages_list @@ -254,68 +326,37 @@ def _get_installed_uv_packages(): return True -def install_esptool(env): +def install_esptool(): """ Install esptool from package folder "tool-esptoolpy" using uv package manager. Also determines the path to the esptool executable binary. - Args: - env: SCons environment object - Returns: str: Path to esptool executable, or 'esptool' as fallback """ - def _get_esptool_executable_path(python_exe): - """ - Get the path to the esptool executable binary. - - Args: - python_exe (str): Path to Python executable - - Returns: - str: Path to esptool executable - """ - if not python_exe or not os.path.isfile(python_exe): - return 'esptool' # Fallback - - python_dir = os.path.dirname(python_exe) - - if sys.platform == "win32": - scripts_dir = os.path.join(python_dir, "Scripts") - esptool_exe = os.path.join(scripts_dir, "esptool.exe") - else: - scripts_dir = os.path.join(python_dir) - esptool_exe = os.path.join(scripts_dir, "esptool") - - if os.path.isfile(esptool_exe): - return esptool_exe - - return 'esptool' - try: subprocess.check_call( - [env.subst("$PYTHONEXE"), "-c", "import esptool"], + [PYTHON_EXE, "-c", "import esptool"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=os.environ ) - python_exe = env.subst("$PYTHONEXE") - esptool_binary_path = _get_esptool_executable_path(python_exe) + esptool_binary_path = _get_esptool_executable_path(PYTHON_EXE) return esptool_binary_path except (subprocess.CalledProcessError, FileNotFoundError): pass esptool_repo_path = env.subst(platform.get_package_dir("tool-esptoolpy") or "") if esptool_repo_path and os.path.isdir(esptool_repo_path): + uv_executable = _get_uv_executable_path(PYTHON_EXE) try: subprocess.check_call([ - "uv", "pip", "install", "--quiet", - f"--python={env.subst('$PYTHONEXE')}", + uv_executable, "pip", "install", "--quiet", + f"--python={PYTHON_EXE}", "-e", esptool_repo_path ], env=os.environ) - python_exe = env.subst("$PYTHONEXE") - esptool_binary_path = _get_esptool_executable_path(python_exe) + esptool_binary_path = _get_esptool_executable_path(PYTHON_EXE) return esptool_binary_path except subprocess.CalledProcessError as e: @@ -327,7 +368,7 @@ def _get_esptool_executable_path(python_exe): # Install Python dependencies and esptool install_python_deps() -esptool_binary_path = install_esptool(env) +esptool_binary_path = install_esptool() def BeforeUpload(target, source, env): @@ -874,7 +915,7 @@ def firmware_metrics(target, source, env): return try: - cmd = [env.subst("$PYTHONEXE"), "-m", "esp_idf_size", "--ng"] + cmd = [PYTHON_EXE, "-m", "esp_idf_size", "--ng"] # Parameters from platformio.ini extra_args = env.GetProjectOption("custom_esp_idf_size_args", "") @@ -1002,7 +1043,7 @@ def firmware_metrics(target, source, env): env.Replace( UPLOADER=join(FRAMEWORK_DIR, "tools", "espota.py"), UPLOADERFLAGS=["--debug", "--progress", "-i", "$UPLOAD_PORT"], - UPLOADCMD='"$PYTHONEXE" "$UPLOADER" $UPLOADERFLAGS -f $SOURCE', + UPLOADCMD=f'"{PYTHON_EXE}" "$UPLOADER" $UPLOADERFLAGS -f $SOURCE', ) if set(["uploadfs", "uploadfsota"]) & set(COMMAND_LINE_TARGETS): env.Append(UPLOADERFLAGS=["--spiffs"])