Skip to content
Open
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
Expand Up @@ -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@<branch-name>"
```
Replace `<branch-name>` 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`.

Expand Down
8 changes: 6 additions & 2 deletions src/reachy_mini/apps/sources/local_common_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
23 changes: 20 additions & 3 deletions src/reachy_mini/apps/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,31 @@

import asyncio
import logging
import os
from pathlib import Path


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,
cwd=Path.home(),
)

assert proc.stdout is not None # for mypy
Expand Down
26 changes: 10 additions & 16 deletions src/reachy_mini/utils/wireless_version/startup_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,10 @@ def check_and_sync_apps_venv_sdk() -> None:

"""
import json
import shutil
import os

from .update_available import get_install_source
from .utils import build_install_command

# Get daemon install info
try:
Expand Down Expand Up @@ -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:
Expand Down
54 changes: 16 additions & 38 deletions src/reachy_mini/utils/wireless_version/update.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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)
113 changes: 108 additions & 5 deletions src/reachy_mini/utils/wireless_version/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,124 @@

import asyncio
import logging
import os
import shlex
import shutil
from pathlib import Path
from typing import Callable

GITHUB_REPO = "pollen-robotics/reachy_mini"

async def call_logger_wrapper(command: list[str], logger: logging.Logger) -> None:
"""Run a command asynchronously, streaming stdout and stderr to logger in real time.

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: 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'}")
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: str,
logger: logging.Logger,
env: dict[str, str] | None = None,
) -> None:
"""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(
Expand Down