From c02938544f70af54d4f558bf97639165c07d851f Mon Sep 17 00:00:00 2001 From: CarolinePascal Date: Thu, 12 Feb 2026 16:07:05 +0100 Subject: [PATCH 1/5] feat(env): adding support for passing environment variables when running a command with subprocess --- src/reachy_mini/apps/utils.py | 21 ++++++++++++++++--- .../utils/wireless_version/utils.py | 9 +++++++- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/reachy_mini/apps/utils.py b/src/reachy_mini/apps/utils.py index f039ff996..b6fc9ca69 100644 --- a/src/reachy_mini/apps/utils.py +++ b/src/reachy_mini/apps/utils.py @@ -2,14 +2,29 @@ import asyncio import logging +import os -async def running_command(command: list[str], logger: logging.Logger) -> int: - """Run a shell command and stream its output to the provided logger.""" +async def running_command( + command: list[str], + logger: logging.Logger, + env: dict[str, str] | None = None, +) -> int: + """Run a shell command and stream its output to the provided logger. + + Args: + command: The command to run as a list of strings. + logger: Logger instance for output streaming. + env: Optional environment variables dict. If None, inherits current environment. + + """ logger.info(f"Running command: {' '.join(command)}") proc = await asyncio.create_subprocess_exec( - *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE + *command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env={**os.environ, **env} if env else None, ) assert proc.stdout is not None # for mypy diff --git a/src/reachy_mini/utils/wireless_version/utils.py b/src/reachy_mini/utils/wireless_version/utils.py index dff1b3650..8184788d7 100644 --- a/src/reachy_mini/utils/wireless_version/utils.py +++ b/src/reachy_mini/utils/wireless_version/utils.py @@ -2,21 +2,28 @@ import asyncio import logging +import os from typing import Callable -async def call_logger_wrapper(command: list[str], logger: logging.Logger) -> None: +async def call_logger_wrapper( + command: list[str], + logger: logging.Logger, + env: dict[str, str] | None = None, +) -> None: """Run a command asynchronously, streaming stdout and stderr to logger in real time. Args: command: list or tuple of command arguments (not a string) logger: logger object with .info and .error methods + env: Optional environment variables dict. If None, inherits current environment. """ process = await asyncio.create_subprocess_exec( *command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + env={**os.environ, **env} if env else None, ) async def stream_output( From 4aca7c259040417e8f1d0a680d5f9bfed85c4ce1 Mon Sep 17 00:00:00 2001 From: CarolinePascal Date: Wed, 11 Feb 2026 21:00:55 +0100 Subject: [PATCH 2/5] feat(clean install): cleaning, unifying and optimizing installation processes on the Wireless version * Unique function build_command() to generate the installation commands * Improved installation speed with git refs with a 3 steps approach * Improved robustness by calling installation commands from HOME instead of current dir --- src/reachy_mini/apps/utils.py | 2 + .../utils/wireless_version/startup_check.py | 26 ++--- .../utils/wireless_version/update.py | 54 +++------ .../utils/wireless_version/utils.py | 105 +++++++++++++++++- 4 files changed, 128 insertions(+), 59 deletions(-) diff --git a/src/reachy_mini/apps/utils.py b/src/reachy_mini/apps/utils.py index b6fc9ca69..e36aba6d7 100644 --- a/src/reachy_mini/apps/utils.py +++ b/src/reachy_mini/apps/utils.py @@ -3,6 +3,7 @@ import asyncio import logging import os +from pathlib import Path async def running_command( @@ -25,6 +26,7 @@ async def running_command( stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env={**os.environ, **env} if env else None, + cwd=Path.home(), ) assert proc.stdout is not None # for mypy diff --git a/src/reachy_mini/utils/wireless_version/startup_check.py b/src/reachy_mini/utils/wireless_version/startup_check.py index d4af91e22..0611f068a 100644 --- a/src/reachy_mini/utils/wireless_version/startup_check.py +++ b/src/reachy_mini/utils/wireless_version/startup_check.py @@ -260,9 +260,10 @@ def check_and_sync_apps_venv_sdk() -> None: """ import json - import shutil from .update_available import get_install_source + from .utils import build_install_command + import os # Get daemon install info try: @@ -324,24 +325,17 @@ def check_and_sync_apps_venv_sdk() -> None: return # Build install command - use_uv = shutil.which("uv") is not None - if daemon_info["source"] == "git" and daemon_info.get("git_ref"): - git_url = f"git+https://github.com/pollen-robotics/reachy_mini.git@{daemon_info['git_ref']}" - pkg = f"reachy-mini[gstreamer] @ {git_url}" - extra = ["--force-reinstall"] - print(f"Syncing apps_venv to git ref: {daemon_info['git_ref']}") - else: - pkg = f"reachy-mini[gstreamer]=={daemon_info['version']}" - extra = [] - print(f"Syncing apps_venv to version: {daemon_info['version']}") + cmd, extra_env = build_install_command( + "gstreamer", + git_ref=daemon_info.get("git_ref") if daemon_info["source"] == "git" else None, + version=daemon_info["version"] if daemon_info["source"] != "git" else None, + python=apps_venv_python, + ) - if use_uv: - cmd = ["uv", "pip", "install", "--python", str(apps_venv_python), pkg] + extra - else: - cmd = [str(Path("/venvs/apps_venv/bin/pip")), "install", pkg] + extra + resolved_env = {**os.environ, **extra_env} if extra_env else None try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300, env=resolved_env, cwd=Path.home()) if result.returncode == 0: print("Successfully synced apps_venv SDK") else: diff --git a/src/reachy_mini/utils/wireless_version/update.py b/src/reachy_mini/utils/wireless_version/update.py index 6a66b07f9..7524fd523 100644 --- a/src/reachy_mini/utils/wireless_version/update.py +++ b/src/reachy_mini/utils/wireless_version/update.py @@ -1,12 +1,9 @@ """Module to handle software updates for the Reachy Mini wireless.""" import logging -import shutil from pathlib import Path -from .utils import call_logger_wrapper - -GITHUB_REPO = "pollen-robotics/reachy_mini" +from .utils import build_install_command, call_logger_wrapper async def update_reachy_mini( @@ -22,50 +19,31 @@ async def update_reachy_mini( git_ref: If set, install from this GitHub tag/branch instead of PyPI. """ - # Build install command based on mode - if git_ref: - # Install from GitHub ref - logger.info(f"Installing from GitHub ref: {git_ref}") - git_url = f"git+https://github.com/{GITHUB_REPO}.git@{git_ref}" - daemon_pkg = f"reachy_mini[wireless-version, gstreamer] @ {git_url}" - apps_pkg = f"reachy-mini[gstreamer] @ {git_url}" - extra_args = ["--force-reinstall"] - else: - # Install from PyPI - logger.info("Installing from PyPI...") - daemon_pkg = "reachy_mini[wireless-version, gstreamer]" - apps_pkg = "reachy-mini[gstreamer]" - extra_args = ["--pre"] if pre_release else [] - # Update daemon venv logger.info("Updating daemon venv...") - await call_logger_wrapper( - ["pip", "install", "--upgrade", daemon_pkg] + extra_args, - logger, + cmd, extra_env = build_install_command( + "wireless-version,gstreamer", + git_ref=git_ref, + pre_release=pre_release, + upgrade=True, ) + await call_logger_wrapper(cmd, logger, env=extra_env or None) # Update apps_venv if it exists apps_venv_python = Path("/venvs/apps_venv/bin/python") if apps_venv_python.exists(): logger.info("Updating apps_venv SDK...") - - if shutil.which("uv"): - install_cmd = [ - "uv", "pip", "install", "--python", str(apps_venv_python), - "--upgrade", apps_pkg, - ] + extra_args - else: - install_cmd = [ - str(Path("/venvs/apps_venv/bin/pip")), - "install", "--upgrade", apps_pkg, - ] + extra_args - - await call_logger_wrapper(install_cmd, logger) + cmd, extra_env = build_install_command( + "gstreamer", + git_ref=git_ref, + pre_release=pre_release, + python=apps_venv_python, + upgrade=True, + ) + await call_logger_wrapper(cmd, logger, env=extra_env or None) logger.info("Apps venv SDK updated successfully") else: logger.info("apps_venv not found, skipping") # Restart daemon to apply updates - await call_logger_wrapper( - ["sudo", "systemctl", "restart", "reachy-mini-daemon"], logger - ) + await call_logger_wrapper("sudo systemctl restart reachy-mini-daemon", logger) diff --git a/src/reachy_mini/utils/wireless_version/utils.py b/src/reachy_mini/utils/wireless_version/utils.py index 8184788d7..2bf702afc 100644 --- a/src/reachy_mini/utils/wireless_version/utils.py +++ b/src/reachy_mini/utils/wireless_version/utils.py @@ -3,27 +3,122 @@ import asyncio import logging import os +import shlex +import shutil +from pathlib import Path from typing import Callable +GITHUB_REPO = "pollen-robotics/reachy_mini" + + +def _check_uv_available() -> bool: + """Check if uv is available on the system.""" + return shutil.which("uv") is not None + + +def build_install_command( + extras: str, + *, + git_ref: str | None = None, + version: str | None = None, + pre_release: bool = False, + python: Path | None = None, + upgrade: bool = False, + verbose: bool = False, +) -> tuple[str, dict[str, str]]: + """Build a pip/uv install shell command for reachy-mini. + + For a *git_ref* install the command chains three steps: + + 1. ``--force-reinstall --no-deps --no-cache-dir`` - reinstall the package + itself without the dependencies. + 2. ``check`` - check if dependencies need to be updated. + 3. ``--upgrade --upgrade-strategy only-if-needed`` - if dependencies need to be updated, upgrade them. + + Args: + extras: Pip extras string, e.g. ``"gstreamer"`` or + ``"wireless-version, gstreamer"``. + git_ref: If set, install from this GitHub tag/branch. + version: If set (and *git_ref* is ``None``), pin to this PyPI version. + pre_release: If ``True`` (and neither *git_ref* nor *version* is set), + add ``--pre`` to allow pre-release versions. + python: Target Python interpreter for an external venv. + Uses ``uv`` when available, otherwise the venv's own ``pip``. + If ``None``, uses the current environment's ``pip``. + upgrade: If ``True``, add the ``--upgrade`` flag. + verbose: If ``True``, add ``-vvv`` flag (only on step 2 for git ref). + Returns: + A tuple of the form ``(command, extra_env)`` where *command* is a shell string and *extra_env* contains any additional environment variables needed for the install. + + """ + # --- Base command (pip or uv) --- + if python is not None: + if _check_uv_available(): + base = ["uv", "pip", "install", "--python", str(python)] + print(f"Using uv with python: {python}") + else: + base = [str(python.parent / "pip"), "install"] + print(f"Using pip from venv: {python.parent / 'pip'}") + else: + base = ["pip", "install"] + print("Using current environment pip") + + if verbose: + base.append("-vvv") + + # --- Package, extra args & env --- + if git_ref: + print(f"Installing from git ref: {git_ref}") + git_url = f"git+https://github.com/{GITHUB_REPO}.git@{git_ref}" + git_package = f"reachy-mini[{extras}] @ {git_url}" + # Step 1: force reinstall the package without the dependencies + step1 = shlex.join(base + [git_package, "--force-reinstall", "--no-deps", "--no-cache-dir"]) + # Step 2: check if dependencies need to be updated + check_base = [arg if arg != "install" else "check" for arg in base] + step2 = shlex.join(check_base) + # Step 3: update dependencies if needed + step3 = shlex.join(base + [f"reachy-mini[{extras}]", "--upgrade", "--upgrade-strategy", "only-if-needed"]) + cmd = f"{step1} && {step2} || {step3}" + print(f"Git ref install: {cmd}") + extra_env = {} + return cmd, extra_env + + print(f"Installing from PyPI: {version if version else 'latest pre-release' if pre_release else 'latest stable'}") + package = f"reachy-mini[{extras}]" + if version: + package = f"{package}=={version}" + extra_args = [] + if pre_release: + extra_args.append("--pre") + if upgrade: + extra_args.append("--upgrade") + extra_env = {} + + cmd = shlex.join(base + [package] + extra_args) + print(f"Install command: {cmd}") + return cmd, extra_env + async def call_logger_wrapper( - command: list[str], + command: str, logger: logging.Logger, env: dict[str, str] | None = None, ) -> None: - """Run a command asynchronously, streaming stdout and stderr to logger in real time. + """Run a shell command asynchronously, streaming stdout and stderr to logger in real time. Args: - command: list or tuple of command arguments (not a string) + command: Shell command string. logger: logger object with .info and .error methods env: Optional environment variables dict. If None, inherits current environment. """ - process = await asyncio.create_subprocess_exec( - *command, + logger.info(f"Running: {command}") + process = await asyncio.create_subprocess_shell( + command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env={**os.environ, **env} if env else None, + cwd=Path.home(), ) async def stream_output( From 468cfa19521809bc88af5e83ef686dd5f0e7abb6 Mon Sep 17 00:00:00 2001 From: CarolinePascal Date: Thu, 12 Feb 2026 17:04:13 +0100 Subject: [PATCH 3/5] chore(format): formatting code --- src/reachy_mini/utils/wireless_version/startup_check.py | 2 +- src/reachy_mini/utils/wireless_version/utils.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/reachy_mini/utils/wireless_version/startup_check.py b/src/reachy_mini/utils/wireless_version/startup_check.py index 0611f068a..d5415b23c 100644 --- a/src/reachy_mini/utils/wireless_version/startup_check.py +++ b/src/reachy_mini/utils/wireless_version/startup_check.py @@ -260,10 +260,10 @@ def check_and_sync_apps_venv_sdk() -> None: """ import json + import os from .update_available import get_install_source from .utils import build_install_command - import os # Get daemon install info try: diff --git a/src/reachy_mini/utils/wireless_version/utils.py b/src/reachy_mini/utils/wireless_version/utils.py index 2bf702afc..6b88d0a62 100644 --- a/src/reachy_mini/utils/wireless_version/utils.py +++ b/src/reachy_mini/utils/wireless_version/utils.py @@ -47,6 +47,7 @@ def build_install_command( If ``None``, uses the current environment's ``pip``. upgrade: If ``True``, add the ``--upgrade`` flag. verbose: If ``True``, add ``-vvv`` flag (only on step 2 for git ref). + Returns: A tuple of the form ``(command, extra_env)`` where *command* is a shell string and *extra_env* contains any additional environment variables needed for the install. @@ -80,7 +81,7 @@ def build_install_command( step3 = shlex.join(base + [f"reachy-mini[{extras}]", "--upgrade", "--upgrade-strategy", "only-if-needed"]) cmd = f"{step1} && {step2} || {step3}" print(f"Git ref install: {cmd}") - extra_env = {} + extra_env: dict[str, str] = {} return cmd, extra_env print(f"Installing from PyPI: {version if version else 'latest pre-release' if pre_release else 'latest stable'}") From 3a8723be5121b51a6dd8958386171c4cebffdf43 Mon Sep 17 00:00:00 2001 From: CarolinePascal Date: Thu, 5 Feb 2026 21:29:54 +0100 Subject: [PATCH 4/5] fix(git lfs): fixing uv's git lfs issue by setting the env variable UV_GIT_LFS to 1 when installing an app. --- .../platforms/reachy_mini/install_daemon_from_branch.md | 4 ++-- src/reachy_mini/apps/sources/local_common_venv.py | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/source/platforms/reachy_mini/install_daemon_from_branch.md b/docs/source/platforms/reachy_mini/install_daemon_from_branch.md index 50531c26c..310cabc5b 100644 --- a/docs/source/platforms/reachy_mini/install_daemon_from_branch.md +++ b/docs/source/platforms/reachy_mini/install_daemon_from_branch.md @@ -76,13 +76,13 @@ Now you can modify the code in `~/reachy_mini` and test your changes without aff 3. **Install the specific branch:** ```bash - pip install --no-cache-dir --force-reinstall \ + UV_GIT_LFS=1 uv pip install --no-cache-dir --force-reinstall \ "reachy_mini[gstreamer,wireless-version] @ git+https://github.com/pollen-robotics/reachy_mini.git@" ``` Replace `` with the branch you want to test (e.g., `develop`, `feature/my-feature`, `bugfix/issue-123`). > [!NOTE] - > We have to use `pip` here and not `uv` because `uv pip install` [does not work correctly with `git lfs`](https://github.com/astral-sh/uv/issues/3312). + > We have to specify `UV_GIT_LFS=1` to make sure `uv` uses `git lfs` correctly. 4. **(Only for versions ≤ 1.2.13)** Repeat steps 2 and 3 using `/venvs/apps_venv`. diff --git a/src/reachy_mini/apps/sources/local_common_venv.py b/src/reachy_mini/apps/sources/local_common_venv.py index f74c719e7..f840c84d3 100644 --- a/src/reachy_mini/apps/sources/local_common_venv.py +++ b/src/reachy_mini/apps/sources/local_common_venv.py @@ -653,10 +653,12 @@ async def install_package( str(python_path), target, ] + extra_env = {"UV_GIT_LFS": "1"} else: install_cmd = [str(python_path), "-m", "pip", "install", target] + extra_env = {} - ret = await running_command(install_cmd, logger=logger) + ret = await running_command(install_cmd, logger=logger, env=extra_env) if ret != 0: return ret @@ -683,10 +685,12 @@ async def install_package( # Original behavior: install into current environment if use_uv: install_cmd = ["uv", "pip", "install", "--python", sys.executable, target] + extra_env = {"UV_GIT_LFS": "1"} else: install_cmd = [sys.executable, "-m", "pip", "install", target] + extra_env = {} - ret = await running_command(install_cmd, logger=logger) + ret = await running_command(install_cmd, logger=logger, env=extra_env) if ret == 0 and app.extra: # Save app metadata so we can match by extra.id later From ba843a06d704c27acf22ea9bf251ef1182cea1c6 Mon Sep 17 00:00:00 2001 From: CarolinePascal Date: Wed, 11 Feb 2026 12:05:53 +0100 Subject: [PATCH 5/5] fix(git based install): adding UV_GIT_LFS=1 for git based reachy_mini installations --- src/reachy_mini/utils/wireless_version/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reachy_mini/utils/wireless_version/utils.py b/src/reachy_mini/utils/wireless_version/utils.py index 6b88d0a62..d6577320f 100644 --- a/src/reachy_mini/utils/wireless_version/utils.py +++ b/src/reachy_mini/utils/wireless_version/utils.py @@ -81,7 +81,7 @@ def build_install_command( step3 = shlex.join(base + [f"reachy-mini[{extras}]", "--upgrade", "--upgrade-strategy", "only-if-needed"]) cmd = f"{step1} && {step2} || {step3}" print(f"Git ref install: {cmd}") - extra_env: dict[str, str] = {} + extra_env: dict[str, str] = {"UV_GIT_LFS": "1"} return cmd, extra_env print(f"Installing from PyPI: {version if version else 'latest pre-release' if pre_release else 'latest stable'}")