#!/bin/bash
check_lerobot_python_compat() {
local toml_path="${WORKSPACE}/libs/lerobot/pyproject.toml"
local required_python=">=3.10"
if [[ -f "${toml_path}" ]]; then
local extracted_req
extracted_req=$(grep -oP '^requires-python\s*=\s*"\K[^"]+' "${toml_path}" || true)
if [[ -n "${extracted_req}" ]]; then
required_python="${extracted_req}"
fi
fi
if ! "${VENV_PYTHON}" -c "
import sys
from packaging.specifiers import SpecifierSet
version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
req = '${required_python}'
if not req:
sys.exit(0)
spec = SpecifierSet(req)
if version not in spec:
print(f'ERROR: Python {version} does not satisfy lerobot requirement {req}')
sys.exit(1)
" 2>/dev/null; then
local err_msg
err_msg=$("${VENV_PYTHON}" -c "
import sys
from packaging.specifiers import SpecifierSet
version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
req = '${required_python}'
if not req:
sys.exit(0)
spec = SpecifierSet(req)
if version not in spec:
print(f'ERROR: Python {version} does not satisfy lerobot requirement {req}')
sys.exit(1)
" 2>&1)
log_error "${err_msg}"
return 1
fi
return 0
}
check_lerobot_ros_numpy_compat() {
if ! "${VENV_PYTHON}" -c "
import sys
from packaging.specifiers import SpecifierSet
version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
spec = SpecifierSet('<3.12')
if version not in spec:
print(f'WARN: Python {version} detected. ROS 2 Humble binary packages (cv_bridge, image_transport)')
print('WARN: are compiled against NumPy 1.x. Python 3.12+ typically requires NumPy 2.x, which')
print('WARN: breaks ABI compatibility. C++ extensions may crash at runtime.')
" 2>/dev/null; then
local warn_msg
warn_msg=$("${VENV_PYTHON}" -c "
import sys
from packaging.specifiers import SpecifierSet
version = f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}'
spec = SpecifierSet('<3.12')
if version not in spec:
print(f'WARN: Python {version} detected. ROS 2 Humble binary packages (cv_bridge, image_transport)')
print('WARN: are compiled against NumPy 1.x. Python 3.12+ typically requires NumPy 2.x, which')
print('WARN: breaks ABI compatibility. C++ extensions may crash at runtime.')
" 2>&1)
while IFS= read -r line; do
log_warn "${line}"
done <<< "${warn_msg}"
fi
return 0
}
install_lerobot_editable() {
local pip_runner=("$@")
if ! check_lerobot_python_compat; then
log_error "Cannot install lerobot: Python version is incompatible."
log_error "Ensure patches 0001/0002 from scripts/setup/lerobot_patches.sh are applied"
log_error "to downgrade the requirement to >=3.10 if you are on an older system."
exit 1
fi
check_lerobot_ros_numpy_compat
"${pip_runner[@]}" install -e "${WORKSPACE}/libs/lerobot"
}
setup_python_venv() {
if ! platform_supports_local_workspace_build; then
log_info "Skipping workspace venv setup on ${SETUP_PLATFORM_ID}."
log_info "Use the board ROS runtime directly after sourcing $(platform_ros_setup_path)."
log_info "Cross-build IB_Robot artifacts on the host with scripts/openharmony/build_ibrobot_oh_custom.sh."
PYTHON_ENV_STATUS="skipped"
log_skipped "Workspace Python virtual environment"
return 0
fi
local venv_path="${WORKSPACE}/venv"
local lerobot_dir="${WORKSPACE}/libs/lerobot"
local host_python_path host_python_version host_py_major host_py_minor
host_python_path="$(command -v python3 || true)"
if [[ -z "${host_python_path}" ]]; then
log_error "python3 not found on PATH. Install python3 (>=3.10) before running setup.sh."
exit 1
fi
host_python_version="$(python3 -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])' 2>/dev/null || echo "unknown")"
log_info "Using host python3: ${host_python_path} (version ${host_python_version})"
host_py_major="$(python3 -c 'import sys; print(sys.version_info[0])' 2>/dev/null || echo 0)"
host_py_minor="$(python3 -c 'import sys; print(sys.version_info[1])' 2>/dev/null || echo 0)"
if (( host_py_major < 3 )) || { (( host_py_major == 3 )) && (( host_py_minor < 10 )); }; then
log_error "Python ${host_python_version} is too old. setup.sh requires Python >= 3.10."
log_error "On openEuler: 'sudo dnf install -y python3.10 python3.10-devel' and re-run."
exit 1
fi
if [[ ! -d "${venv_path}" ]]; then
run_with_live_output "Creating virtual environment at ${venv_path} with --system-site-packages..." "${SETUP_BOOTSTRAP_PYTHON_BIN:-python3}" -m venv --system-site-packages "${venv_path}"
else
log_info "Virtual environment already exists at ${venv_path}."
fi
log_info "Configuring Python environment and dependencies..."
source "${venv_path}/bin/activate"
if [[ -n "${PYTHONPATH:-}" ]]; then
log_warn "Clearing inherited PYTHONPATH for isolated package installation inside ${venv_path}."
unset PYTHONPATH
fi
if [[ -n "${PYTHONHOME:-}" ]]; then
log_warn "Clearing inherited PYTHONHOME for isolated package installation inside ${venv_path}."
unset PYTHONHOME
fi
if [[ -z "${VENV_PYTHON}" ]]; then
log_error "No working virtual environment python was found under ${venv_path}/bin."
exit 1
fi
local pip_install=("${VENV_PYTHON}" -m pip install)
run_cmd "${VENV_PYTHON}" -m pip install --upgrade pip --quiet
run_cmd "${VENV_PYTHON}" -m pip install --force-reinstall "setuptools==75.8.2" --quiet
log_info "Installing PyYAML for the lerobot patch dispatcher..."
run_cmd "${VENV_PYTHON}" -m pip install pyyaml --quiet
if [[ -d "${lerobot_dir}" ]]; then
ensure_lerobot_patch_stack_applied
fi
if [[ -d "${lerobot_dir}" ]]; then
log_info "Installing LeRobot in editable mode..."
install_lerobot_editable "${VENV_PYTHON}" -m pip
fi
log_info "Installing base Python dependencies..."
run_cmd "${pip_install[@]}" -r "${WORKSPACE}/requirements/base.txt" --quiet
log_info "Installing hardware dependencies..."
run_cmd "${pip_install[@]}" -r "${WORKSPACE}/requirements/hardware.txt" --quiet
log_info "Installing optional phone teleoperation dependencies..."
if [[ "${AUTO_YES}" == true ]]; then
log_info "Auto-yes mode: installing both phone backends (hebi-py + teleop)..."
run_cmd "${pip_install[@]}" hebi-py teleop --quiet
log_done "Phone teleoperation dependencies installed (hebi-py + teleop)"
else
echo ""
echo " Phone teleoperation backends (optional):"
echo " 1) iOS only — hebi-py (HEBI Mobile I/O + ARKit)"
echo " 2) Android only — teleop (WebXR WebSocket)"
echo " 3) Both (iOS + Android)"
echo " 0) Skip phone backends"
echo ""
while true; do
read -r -p " Enter your choice [0-3]: " PHONE_CHOICE
case "${PHONE_CHOICE}" in
1)
run_cmd "${pip_install[@]}" hebi-py --quiet
log_done "Phone dependencies installed: hebi-py (iOS)"
break
;;
2)
run_cmd "${pip_install[@]}" teleop --quiet
log_done "Phone dependencies installed: teleop (Android)"
break
;;
3)
run_cmd "${pip_install[@]}" hebi-py teleop --quiet
log_done "Phone dependencies installed: hebi-py + teleop (iOS + Android)"
break
;;
0)
log_info "Skipping phone teleoperation dependencies."
break
;;
*)
echo " Invalid choice. Please enter 0-3."
;;
esac
done
fi
log_info "Installing dev-tools (tensorboard, rerun, gitlint, ruff, pre-commit)..."
run_cmd "${pip_install[@]}" -r "${WORKSPACE}/requirements/dev-tools.txt" --quiet
case "${SETUP_PLATFORM_ID}" in
ubuntu-22.04)
run_cmd "${pip_install[@]}" -r "${WORKSPACE}/requirements/ubuntu-22.04.txt" --quiet
;;
openeuler-embedded-24.03)
run_cmd "${pip_install[@]}" -r "${WORKSPACE}/requirements/openeuler-24.03.txt" --quiet
;;
esac
if [[ -f "${WORKSPACE}/.pre-commit-config.yaml" ]]; then
"${VENV_PYTHON}" -m pre_commit install
fi
log_info "Installing colcon-common-extensions + colcon-mixin into the workspace venv..."
run_cmd "${pip_install[@]}" --ignore-installed --upgrade colcon-common-extensions colcon-mixin --quiet
if ! PYTHONNOUSERSITE=1 "${VENV_PYTHON}" - <<'PY'
import setuptools.command.develop as develop
raise SystemExit(
0 if hasattr(develop, "develop") and hasattr(develop.develop, "install_for_development")
else 1
)
PY
then
log_warn "colcon symlink installation will fail because setuptools>=71 removed setup.py develop."
log_warn "Please ensure setuptools is downgraded or wait for colcon-core updates."
fi
log_info "Pinning Empy 3.3.4 for ROS 2 Humble rosidl compatibility..."
run_cmd "${pip_install[@]}" --force-reinstall "empy==3.3.4" --quiet
log_info "Ensuring rosdep is present in the workspace venv..."
run_cmd "${pip_install[@]}" rosdep --quiet
log_info "Pinning NumPy 1.26.4 + opencv-python-headless<4.12 (ROS 2 Humble ABI)..."
run_cmd "${pip_install[@]}" --force-reinstall "numpy==1.26.4" "opencv-python-headless<4.12" --quiet
local commit_msg_hook
commit_msg_hook="$(git rev-parse --git-path hooks/commit-msg 2>/dev/null || true)"
if [[ -f "${commit_msg_hook}" ]] && grep -qi "gitlint" "${commit_msg_hook}"; then
log_warn "gitlint commit-msg hook already exists at ${commit_msg_hook}; keeping it."
else
log_info "Installing gitlint commit-msg hook..."
printf 'y\n' | gitlint install-hook || log_warn "Failed to install gitlint hook"
fi
local venv_numpy_ver="unknown" venv_colcon_path="missing" venv_py_ver="unknown"
venv_py_ver="$(PYTHONNOUSERSITE=1 "${VENV_PYTHON}" -c 'import sys; print("%d.%d.%d" % sys.version_info[:3])' 2>/dev/null || echo unknown)"
venv_numpy_ver="$(PYTHONNOUSERSITE=1 "${VENV_PYTHON}" -c 'import numpy; print(numpy.__version__)' 2>/dev/null || echo unknown)"
venv_colcon_path="$(PYTHONNOUSERSITE=1 "${VENV_PYTHON}" -c 'import colcon, os; print(os.path.dirname(colcon.__file__))' 2>/dev/null || echo missing)"
local user_site user_site_status="not-present" user_site_colcon=""
user_site="$("${VENV_PYTHON}" -m site --user-site 2>/dev/null || true)"
if [[ -n "${user_site}" && -d "${user_site}" ]]; then
local user_pkg_count
user_pkg_count="$(find "${user_site}" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
user_site_status="active (${user_pkg_count} packages)"
if find "${user_site}" -maxdepth 1 -name "colcon*" | grep -q .; then
user_site_colcon="DETECTED"
fi
fi
echo -e "\n${YELLOW}Python Environment Summary:${NC}"
echo " Python Version: ${venv_py_ver} (${VENV_PYTHON})"
echo " NumPy Version: ${venv_numpy_ver} (Target: 1.26.x for ROS 2 ABI)"
echo " Colcon Path: ${venv_colcon_path}"
echo " User Site-Packages: ${user_site_status} (${user_site})"
if [[ -n "${user_site_colcon}" ]]; then
log_warn "User site-packages contain a colcon installation that may shadow the workspace venv."
log_warn "If 'colcon build' fails outside of build.sh, consider clearing the user site colcon:"
log_warn " rm -rf ${user_site}/colcon* ${user_site%/lib/*}/bin/colcon*"
fi
PYTHON_ENV_STATUS="done"
}