Skip to content

Full uv path #227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 15, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 96 additions & 55 deletions builder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -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", "")
Expand Down Expand Up @@ -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"])
Expand Down