feat: add installation scripts for ROS2 Jazzy Desktop and ros-base with progress reporting
This commit is contained in:
+58
-436
@@ -4,9 +4,6 @@ import argparse
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
|
||||||
import threading
|
|
||||||
import urllib.request
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List, Optional
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
@@ -23,42 +20,13 @@ _PROJECT_DIR = Path(__file__).parent.parent.parent
|
|||||||
# Название дистрибутива ROS2, который устанавливает этот скрипт.
|
# Название дистрибутива ROS2, который устанавливает этот скрипт.
|
||||||
_DISTRO = "jazzy"
|
_DISTRO = "jazzy"
|
||||||
|
|
||||||
# Path where apt expects the ROS2 GPG signing key to be stored.
|
# Webots simulator version targeted by this installer.
|
||||||
# Путь, по которому apt ожидает найти GPG-ключ подписи ROS2.
|
# Версия симулятора Webots, устанавливаемая этим скриптом.
|
||||||
_ROS_KEYRING = Path("/usr/share/keyrings/ros-archive-keyring.gpg")
|
|
||||||
|
|
||||||
# Path to the apt sources file that points to the ROS2 package repository.
|
|
||||||
# Путь к файлу источников apt, указывающему на репозиторий пакетов ROS2.
|
|
||||||
_ROS_SOURCES = Path("/etc/apt/sources.list.d/ros2.list")
|
|
||||||
|
|
||||||
# Path created by "rosdep init" to mark that rosdep has already been initialized.
|
|
||||||
# Путь, создаваемый "rosdep init" для отметки того, что rosdep уже инициализирован.
|
|
||||||
_ROSDEP_SOURCES = Path("/etc/ros/rosdep/sources.list.d/20-default.list")
|
|
||||||
|
|
||||||
# Webots simulator version and the direct .deb download URL for amd64.
|
|
||||||
# Версия симулятора Webots и прямая ссылка для скачивания .deb для amd64.
|
|
||||||
_WEBOTS_VERSION = "2025a"
|
_WEBOTS_VERSION = "2025a"
|
||||||
_WEBOTS_DEB_URL = (
|
|
||||||
f"https://github.com/cyberbotics/webots/releases/download/"
|
|
||||||
f"R{_WEBOTS_VERSION}/webots_{_WEBOTS_VERSION}_amd64.deb"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Environment variables passed to apt to suppress interactive prompts (e.g. "restart services?").
|
# Directory that contains the shell scripts used by this command.
|
||||||
# Переменные окружения для apt, подавляющие интерактивные запросы (например, "перезапустить сервисы?").
|
# Директория с shell-скриптами, используемыми этой командой.
|
||||||
_APT_ENV = {**os.environ, "DEBIAN_FRONTEND": "noninteractive"}
|
_SCRIPTS_DIR = _PROJECT_DIR / "scripts"
|
||||||
|
|
||||||
# Extra apt options: short per-connection and per-transfer timeouts plus retry count.
|
|
||||||
# Forces IPv4 because many VMs have broken IPv6 routing that causes silent hangs.
|
|
||||||
# Дополнительные опции apt: короткие таймауты на соединение и передачу данных, плюс число повторов.
|
|
||||||
# Принудительно используем IPv4, так как в VM часто сломана маршрутизация IPv6, что вызывает зависания.
|
|
||||||
_APT_OPTS = [
|
|
||||||
"-o", "Acquire::http::ConnectTimeout=15",
|
|
||||||
"-o", "Acquire::https::ConnectTimeout=15",
|
|
||||||
"-o", "Acquire::http::Timeout=30",
|
|
||||||
"-o", "Acquire::https::Timeout=30",
|
|
||||||
"-o", "Acquire::Retries=2",
|
|
||||||
"-o", "Acquire::ForceIPv4=true",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Type alias for the callable used to write a line to the TUI log screen.
|
# Type alias for the callable used to write a line to the TUI log screen.
|
||||||
# Псевдоним типа для функции записи строки в лог TUI.
|
# Псевдоним типа для функции записи строки в лог TUI.
|
||||||
@@ -165,353 +133,57 @@ def _run_logged(
|
|||||||
raise RuntimeError(f"Command failed: {cmd[0]}")
|
raise RuntimeError(f"Command failed: {cmd[0]}")
|
||||||
|
|
||||||
|
|
||||||
def _run_apt_with_progress(
|
|
||||||
cmd: List[str],
|
|
||||||
write: Write,
|
|
||||||
on_progress: Callable[[float], None],
|
|
||||||
env: dict | None = None,
|
|
||||||
register_proc: Callable | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Run an apt command, stream its stdout to the log, and report download progress.
|
|
||||||
|
|
||||||
Uses APT::Status-Fd to receive machine-readable progress lines on a private
|
|
||||||
pipe. A background thread reads the pipe and calls on_progress(0-100) for
|
|
||||||
each percentage update. When a new URI starts downloading it is printed to
|
|
||||||
the log so the user can see what is being fetched.
|
|
||||||
|
|
||||||
Запускает команду apt, передаёт stdout в лог и показывает прогресс скачивания.
|
|
||||||
Использует APT::Status-Fd для получения машинночитаемых строк прогресса через
|
|
||||||
приватный канал. Фоновый поток читает канал и вызывает on_progress(0-100) при
|
|
||||||
каждом обновлении процента. При начале скачивания нового файла его URI
|
|
||||||
выводится в лог, чтобы пользователь видел что именно загружается.
|
|
||||||
"""
|
|
||||||
r_fd, w_fd = os.pipe()
|
|
||||||
try:
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
cmd + ["-o", f"APT::Status-Fd={w_fd}"],
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
env=env or os.environ,
|
|
||||||
pass_fds=(w_fd,),
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
# Close the write end in the parent so the reader thread gets EOF when apt exits.
|
|
||||||
# Закрываем пишущий конец в родителе, чтобы читающий поток получил EOF при выходе apt.
|
|
||||||
os.close(w_fd)
|
|
||||||
|
|
||||||
if register_proc:
|
|
||||||
register_proc(proc)
|
|
||||||
|
|
||||||
def _read_status() -> None:
|
|
||||||
"""Parse APT::Status-Fd lines and forward percentage and URI info.
|
|
||||||
|
|
||||||
Status-Fd line format: dlstatus:index:pct:message
|
|
||||||
or for package installs: pmstatus:pkg:pct:message
|
|
||||||
|
|
||||||
Разбирает строки APT::Status-Fd и передаёт процент и URI.
|
|
||||||
Формат строки: dlstatus:index:pct:message
|
|
||||||
или для установки пакетов: pmstatus:pkg:pct:message
|
|
||||||
"""
|
|
||||||
last_uri: str = ""
|
|
||||||
fetched = 0
|
|
||||||
with os.fdopen(r_fd, "r") as f:
|
|
||||||
for line in f:
|
|
||||||
parts = line.strip().split(":", 3)
|
|
||||||
if len(parts) < 3:
|
|
||||||
continue
|
|
||||||
kind, _, pct_str = parts[0], parts[1], parts[2]
|
|
||||||
msg = parts[3] if len(parts) == 4 else ""
|
|
||||||
try:
|
|
||||||
on_progress(float(pct_str))
|
|
||||||
except ValueError:
|
|
||||||
continue
|
|
||||||
if kind == "dlstatus" and msg:
|
|
||||||
# Print each new URI once as it starts downloading.
|
|
||||||
# Выводим каждый новый URI один раз при начале загрузки.
|
|
||||||
uri = msg.split()[0]
|
|
||||||
if uri != last_uri:
|
|
||||||
last_uri = uri
|
|
||||||
fetched += 1
|
|
||||||
write(f"[dim][{fetched}] {msg}[/dim]")
|
|
||||||
|
|
||||||
t = threading.Thread(target=_read_status, daemon=True)
|
|
||||||
t.start()
|
|
||||||
for line in proc.stdout:
|
|
||||||
s = line.rstrip()
|
|
||||||
if s:
|
|
||||||
write(s)
|
|
||||||
proc.wait()
|
|
||||||
t.join()
|
|
||||||
|
|
||||||
if proc.returncode not in (0, -9):
|
|
||||||
raise RuntimeError(f"Command failed: {cmd[0]}")
|
|
||||||
|
|
||||||
|
|
||||||
# Installation steps - each step maps to one visible phase in the log screen
|
|
||||||
# Шаги установки - каждый шаг соответствует одной видимой фазе в экране лога
|
|
||||||
def _step_prereqs(
|
|
||||||
write: Write,
|
|
||||||
on_progress: Callable[[float], None],
|
|
||||||
register_proc: Callable | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Step 1 - Refresh apt cache and install packages required for the ROS2 setup.
|
|
||||||
|
|
||||||
Installs: software-properties-common, curl, gnupg2, lsb-release, build-essential.
|
|
||||||
Also enables the Ubuntu universe repository which some ROS2 dependencies live in.
|
|
||||||
|
|
||||||
Шаг 1 - Обновляет кеш apt и устанавливает пакеты, необходимые для настройки ROS2.
|
|
||||||
Устанавливает: software-properties-common, curl, gnupg2, lsb-release, build-essential.
|
|
||||||
Также включает репозиторий Ubuntu universe, в котором находятся некоторые зависимости ROS2.
|
|
||||||
"""
|
|
||||||
write("[bold]Step 1 / 5 - Prerequisites[/bold]")
|
|
||||||
write("[cyan][*][/cyan] Updating package lists...")
|
|
||||||
_run_apt_with_progress(
|
|
||||||
["sudo", "apt-get", "update"] + _APT_OPTS,
|
|
||||||
write, on_progress, _APT_ENV, register_proc,
|
|
||||||
)
|
|
||||||
write("[cyan][*][/cyan] Installing prerequisites...")
|
|
||||||
_run_apt_with_progress(
|
|
||||||
[
|
|
||||||
"sudo", "apt-get", "install", "-y", "--no-install-recommends",
|
|
||||||
"software-properties-common", "curl", "gnupg2",
|
|
||||||
"lsb-release", "build-essential",
|
|
||||||
] + _APT_OPTS,
|
|
||||||
write, on_progress, _APT_ENV, register_proc,
|
|
||||||
)
|
|
||||||
write("[cyan][*][/cyan] Adding universe repository...")
|
|
||||||
# --no-update prevents add-apt-repository from running its own apt-get update,
|
|
||||||
# which would ignore our timeout options and could hang indefinitely.
|
|
||||||
# --no-update запрещает add-apt-repository запускать собственный apt-get update,
|
|
||||||
# который игнорирует наши таймауты и может зависнуть.
|
|
||||||
_run_logged(["sudo", "add-apt-repository", "-y", "--no-update", "universe"], write,
|
|
||||||
register_proc=register_proc)
|
|
||||||
write("[green][ok][/green] Prerequisites ready")
|
|
||||||
|
|
||||||
|
|
||||||
def _step_ros2_repo(
|
|
||||||
write: Write,
|
|
||||||
on_progress: Callable[[float], None],
|
|
||||||
register_proc: Callable | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Step 2 - Download the ROS2 signing key and register the ROS2 apt repository.
|
|
||||||
|
|
||||||
Removes any previous key and sources file first so re-runs always start clean.
|
|
||||||
Downloads the key from the official ros/rosdistro GitHub repository, writes it
|
|
||||||
to the system keyring, then creates /etc/apt/sources.list.d/ros2.list and
|
|
||||||
refreshes only that source to avoid updating all Ubuntu mirrors.
|
|
||||||
|
|
||||||
Шаг 2 - Скачивает ключ подписи ROS2 и регистрирует репозиторий apt ROS2.
|
|
||||||
Сначала удаляет предыдущие ключ и файл источников, чтобы повторные запуски
|
|
||||||
всегда начинались с чистого состояния. Скачивает ключ с официального GitHub
|
|
||||||
репозитория ros/rosdistro, записывает его в системный кейринг, затем создаёт
|
|
||||||
/etc/apt/sources.list.d/ros2.list и обновляет только этот источник.
|
|
||||||
"""
|
|
||||||
write("\n[bold]Step 2 / 5 - ROS2 repository[/bold]")
|
|
||||||
|
|
||||||
# Remove previous key and sources file to avoid stale config on re-runs.
|
|
||||||
# Удаляем предыдущие ключ и файл источников для чистого состояния при повторных запусках.
|
|
||||||
for path in (_ROS_KEYRING, _ROS_SOURCES):
|
|
||||||
if path.exists():
|
|
||||||
subprocess.run(["sudo", "rm", "-f", str(path)], check=False)
|
|
||||||
write(f"[dim]Removed {path}[/dim]")
|
|
||||||
|
|
||||||
write("[cyan][*][/cyan] Downloading ROS2 signing key...")
|
|
||||||
key_url = "https://raw.githubusercontent.com/ros/rosdistro/master/ros.key"
|
|
||||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".gpg") as tmp:
|
|
||||||
tmp_path = tmp.name
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(key_url, timeout=30) as resp:
|
|
||||||
Path(tmp_path).write_bytes(resp.read())
|
|
||||||
subprocess.run(
|
|
||||||
["sudo", "install", "-m", "644", tmp_path, str(_ROS_KEYRING)],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
Path(tmp_path).unlink(missing_ok=True)
|
|
||||||
write("[green][ok][/green] Signing key installed")
|
|
||||||
|
|
||||||
# Detect machine architecture and Ubuntu codename to build the sources.list line.
|
|
||||||
# Определяем архитектуру машины и кодовое имя Ubuntu для строки sources.list.
|
|
||||||
arch = subprocess.check_output(["dpkg", "--print-architecture"], text=True).strip()
|
|
||||||
codename = subprocess.check_output(
|
|
||||||
["bash", "-c", ". /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}}"],
|
|
||||||
text=True,
|
|
||||||
).strip()
|
|
||||||
sources_line = (
|
|
||||||
f"deb [arch={arch} signed-by={_ROS_KEYRING}] "
|
|
||||||
f"http://packages.ros.org/ros2/ubuntu {codename} main\n"
|
|
||||||
)
|
|
||||||
result = subprocess.run(
|
|
||||||
["sudo", "tee", str(_ROS_SOURCES)],
|
|
||||||
input=sources_line, capture_output=True, text=True,
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
raise RuntimeError(f"Failed to write {_ROS_SOURCES}")
|
|
||||||
write(f"[dim]{_ROS_SOURCES}[/dim]")
|
|
||||||
|
|
||||||
# Update only the ROS2 source - avoids downloading all Ubuntu mirror metadata.
|
|
||||||
# Обновляем только источник ROS2 - избегаем скачивания метаданных всех зеркал Ubuntu.
|
|
||||||
write("[cyan][*][/cyan] Updating ROS2 package list...")
|
|
||||||
_run_apt_with_progress(
|
|
||||||
[
|
|
||||||
"sudo", "apt-get", "update",
|
|
||||||
"-o", f"Dir::Etc::sourcelist={_ROS_SOURCES}",
|
|
||||||
"-o", "Dir::Etc::sourceparts=-",
|
|
||||||
"-o", "APT::Get::List-Cleanup=0",
|
|
||||||
] + _APT_OPTS,
|
|
||||||
write, on_progress, _APT_ENV, register_proc,
|
|
||||||
)
|
|
||||||
write("[green][ok][/green] ROS2 repository ready")
|
|
||||||
|
|
||||||
|
|
||||||
def _step_install_ros2(
|
|
||||||
write: Write,
|
|
||||||
on_progress: Callable[[float], None],
|
|
||||||
pkg: str,
|
|
||||||
register_proc: Callable | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Step 3 - Install the chosen ROS2 Jazzy package via apt.
|
|
||||||
|
|
||||||
pkg is either "desktop" (full install with GUI tools) or "ros-base"
|
|
||||||
(minimal headless install). The full package name becomes ros-jazzy-{pkg}.
|
|
||||||
|
|
||||||
Шаг 3 - Устанавливает выбранный пакет ROS2 Jazzy через apt.
|
|
||||||
pkg - это либо "desktop" (полная установка с GUI), либо "ros-base"
|
|
||||||
(минимальная установка без GUI). Полное имя пакета: ros-jazzy-{pkg}.
|
|
||||||
"""
|
|
||||||
write(f"\n[bold]Step 3 / 5 - ros-{_DISTRO}-{pkg}[/bold]")
|
|
||||||
write(f"[cyan][*][/cyan] Installing ros-{_DISTRO}-{pkg}...")
|
|
||||||
_run_apt_with_progress(
|
|
||||||
["sudo", "apt-get", "install", "-y", f"ros-{_DISTRO}-{pkg}"] + _APT_OPTS,
|
|
||||||
write, on_progress, _APT_ENV, register_proc,
|
|
||||||
)
|
|
||||||
write(f"[green][ok][/green] ros-{_DISTRO}-{pkg} installed")
|
|
||||||
|
|
||||||
|
|
||||||
def _step_dev_tools(
|
|
||||||
write: Write,
|
|
||||||
on_progress: Callable[[float], None],
|
|
||||||
register_proc: Callable | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Step 4 - Install colcon, rosdep, vcstool and initialize rosdep.
|
|
||||||
|
|
||||||
Installs the Python packages needed to build and manage ROS2 workspaces.
|
|
||||||
Runs "rosdep init" only if it has not been run before, then always runs
|
|
||||||
"rosdep update" to fetch the latest package index.
|
|
||||||
|
|
||||||
Шаг 4 - Устанавливает colcon, rosdep, vcstool и инициализирует rosdep.
|
|
||||||
Устанавливает Python-пакеты, необходимые для сборки и управления рабочими
|
|
||||||
пространствами ROS2. Запускает "rosdep init" только если он ещё не запускался,
|
|
||||||
затем всегда запускает "rosdep update" для получения свежего индекса пакетов.
|
|
||||||
"""
|
|
||||||
write("\n[bold]Step 4 / 5 - Dev tools[/bold]")
|
|
||||||
write("[cyan][*][/cyan] Installing colcon, rosdep, vcstool...")
|
|
||||||
_run_apt_with_progress(
|
|
||||||
[
|
|
||||||
"sudo", "apt-get", "install", "-y",
|
|
||||||
"python3-argcomplete",
|
|
||||||
"python3-colcon-clean",
|
|
||||||
"python3-colcon-common-extensions",
|
|
||||||
"python3-rosdep",
|
|
||||||
"python3-vcstool",
|
|
||||||
] + _APT_OPTS,
|
|
||||||
write, on_progress, _APT_ENV, register_proc,
|
|
||||||
)
|
|
||||||
if not _ROSDEP_SOURCES.exists():
|
|
||||||
write("[cyan][*][/cyan] Initializing rosdep...")
|
|
||||||
_run_logged(["sudo", "rosdep", "init"], write, register_proc=register_proc)
|
|
||||||
else:
|
|
||||||
write("[green][ok][/green] rosdep already initialized")
|
|
||||||
write("[cyan][*][/cyan] Updating rosdep...")
|
|
||||||
_run_logged(["rosdep", "update", "--rosdistro", _DISTRO], write,
|
|
||||||
register_proc=register_proc)
|
|
||||||
write("[green][ok][/green] Dev tools ready")
|
|
||||||
|
|
||||||
|
|
||||||
def _step_shell_setup(write: Write) -> None:
|
|
||||||
"""Step 5 - Append ROS2 environment setup lines to the user shell rc file.
|
|
||||||
|
|
||||||
Adds "source /opt/ros/jazzy/setup.bash" so ROS2 commands are available in
|
|
||||||
every new terminal. Also adds a commented-out ROS_AUTOMATIC_DISCOVERY_RANGE
|
|
||||||
line as a reminder for multi-machine setups. Both lines are added only once.
|
|
||||||
|
|
||||||
Шаг 5 - Добавляет строки настройки окружения ROS2 в rc-файл оболочки пользователя.
|
|
||||||
Добавляет "source /opt/ros/jazzy/setup.bash" чтобы команды ROS2 были доступны
|
|
||||||
в каждом новом терминале. Также добавляет закомментированную строку
|
|
||||||
ROS_AUTOMATIC_DISCOVERY_RANGE как напоминание для многомашинных настроек.
|
|
||||||
Обе строки добавляются только один раз.
|
|
||||||
"""
|
|
||||||
write("\n[bold]Step 5 / 5 - Shell configuration[/bold]")
|
|
||||||
shell_name = Path(os.environ.get("SHELL", "/bin/bash")).name
|
|
||||||
rc = Path.home() / (".zshrc" if shell_name == "zsh" else ".bashrc")
|
|
||||||
rc_text = rc.read_text() if rc.exists() else ""
|
|
||||||
|
|
||||||
source_line = f"source /opt/ros/{_DISTRO}/setup.bash"
|
|
||||||
discovery_comment = "# export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST"
|
|
||||||
|
|
||||||
additions = []
|
|
||||||
if source_line not in rc_text:
|
|
||||||
additions.append(source_line)
|
|
||||||
if discovery_comment not in rc_text:
|
|
||||||
additions.append(discovery_comment)
|
|
||||||
|
|
||||||
if additions:
|
|
||||||
with rc.open("a") as f:
|
|
||||||
f.write(f"\n# ROS2 {_DISTRO}\n")
|
|
||||||
for line in additions:
|
|
||||||
f.write(line + "\n")
|
|
||||||
write(f"[green][ok][/green] Added ROS2 setup to ~/{rc.name}")
|
|
||||||
else:
|
|
||||||
write(f"[green][ok][/green] ROS2 setup already in ~/{rc.name}")
|
|
||||||
|
|
||||||
|
|
||||||
# Tasks - long-running functions executed inside a LogScreen background thread
|
# Tasks - long-running functions executed inside a LogScreen background thread
|
||||||
# Задачи - долгие функции, выполняемые в фоновом потоке внутри LogScreen
|
# Задачи - долгие функции, выполняемые в фоновом потоке внутри LogScreen
|
||||||
|
def _run_script(script: Path, screen: LogScreen) -> None:
|
||||||
|
"""Run a shell script, stream its output to the TUI log, and parse
|
||||||
|
PROGRESS:<pct>:<label> markers to update the progress bar.
|
||||||
|
Raises RuntimeError if the script exits with a non-zero code.
|
||||||
|
Запускает shell-скрипт, транслирует вывод в лог TUI и разбирает маркеры
|
||||||
|
PROGRESS:<pct>:<метка> для обновления прогресс-бара.
|
||||||
|
Выбрасывает RuntimeError если скрипт завершился с ненулевым кодом.
|
||||||
|
"""
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
["bash", str(script)],
|
||||||
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
|
text=True, cwd=_PROJECT_DIR,
|
||||||
|
)
|
||||||
|
screen.set_proc(proc)
|
||||||
|
for line in proc.stdout:
|
||||||
|
s = line.rstrip()
|
||||||
|
if s.startswith("PROGRESS:"):
|
||||||
|
# Format emitted by scripts: PROGRESS:<pct>:<label>
|
||||||
|
# Формат, выводимый скриптами: PROGRESS:<pct>:<метка>
|
||||||
|
parts = s.split(":", 2)
|
||||||
|
try:
|
||||||
|
screen.set_progress(float(parts[1]), parts[2] if len(parts) > 2 else "")
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
elif s:
|
||||||
|
screen.write(s)
|
||||||
|
proc.wait()
|
||||||
|
if proc.returncode not in (0, -9):
|
||||||
|
raise RuntimeError(f"Script failed (exit {proc.returncode}): {script.name}")
|
||||||
|
|
||||||
|
|
||||||
def _task_install(screen: LogScreen, pkg: str) -> None:
|
def _task_install(screen: LogScreen, pkg: str) -> None:
|
||||||
"""Full ROS2 Jazzy installation task, split into 5 sequential steps.
|
"""Run the ROS2 Jazzy installation shell script for the chosen variant (desktop / ros-base).
|
||||||
|
The script emits PROGRESS: markers so the bar advances during installation.
|
||||||
Maps each step to a sub-range of the 0-100% progress bar so the bar
|
Запускает shell-скрипт установки ROS2 Jazzy для выбранного варианта (desktop / ros-base).
|
||||||
advances smoothly through prerequisites, repo setup, ROS2 install,
|
Скрипт выводит маркеры PROGRESS:, чтобы прогресс-бар обновлялся во время установки.
|
||||||
dev tools and shell configuration.
|
|
||||||
|
|
||||||
Полная задача установки ROS2 Jazzy, разбитая на 5 последовательных шагов.
|
|
||||||
Каждый шаг отображается в своём диапазоне прогресс-бара 0-100%, так что
|
|
||||||
бар плавно движется через prerequisites, настройку репозитория, установку
|
|
||||||
ROS2, инструменты разработчика и настройку оболочки.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
def prog(lo: float, hi: float) -> Callable[[float], None]:
|
# "desktop" -> setup_ros2_desktop.sh, "ros-base" -> setup_ros2_ros_base.sh
|
||||||
"""Map a 0-100 apt percentage into the [lo, hi] sub-range of the progress bar.
|
script = _SCRIPTS_DIR / f"setup_ros2_{pkg.replace('-', '_')}.sh"
|
||||||
Отображает 0-100% apt в поддиапазон [lo, hi] прогресс-бара.
|
if not script.exists():
|
||||||
"""
|
screen.write(f"[red]Script not found:[/red] {script}")
|
||||||
return lambda p: screen.set_progress(lo + p / 100.0 * (hi - lo))
|
screen.finish(False)
|
||||||
|
|
||||||
screen.set_progress(0, "Preparing...")
|
|
||||||
_step_prereqs(screen.write, prog(0, 15), register_proc=screen.set_proc)
|
|
||||||
if screen.is_stopped():
|
|
||||||
return
|
return
|
||||||
|
screen.set_progress(0, "Starting installation...")
|
||||||
screen.set_progress(15, "Setting up ROS2 repository...")
|
_run_script(script, screen)
|
||||||
_step_ros2_repo(screen.write, prog(15, 30), register_proc=screen.set_proc)
|
if not screen.is_stopped():
|
||||||
if screen.is_stopped():
|
|
||||||
return
|
|
||||||
|
|
||||||
screen.set_progress(30, f"Installing ros-{_DISTRO}-{pkg}...")
|
|
||||||
_step_install_ros2(screen.write, prog(30, 75), pkg, register_proc=screen.set_proc)
|
|
||||||
if screen.is_stopped():
|
|
||||||
return
|
|
||||||
|
|
||||||
screen.set_progress(75, "Installing dev tools...")
|
|
||||||
_step_dev_tools(screen.write, prog(75, 95), register_proc=screen.set_proc)
|
|
||||||
if screen.is_stopped():
|
|
||||||
return
|
|
||||||
|
|
||||||
screen.set_progress(95, "Configuring shell...")
|
|
||||||
_step_shell_setup(screen.write)
|
|
||||||
screen.set_progress(100, "Done")
|
screen.set_progress(100, "Done")
|
||||||
|
screen.write(f"\n[green]ROS2 {_DISTRO} ({pkg}) installed successfully.[/green]")
|
||||||
screen.write(f"\n[green]ROS2 {_DISTRO} installed successfully.[/green]")
|
|
||||||
screen.finish(True)
|
screen.finish(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if not screen.is_stopped():
|
if not screen.is_stopped():
|
||||||
@@ -601,71 +273,21 @@ def _task_build(screen: LogScreen) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _task_install_webots(screen: LogScreen) -> None:
|
def _task_install_webots(screen: LogScreen) -> None:
|
||||||
"""Download the Webots .deb from GitHub and install it with apt.
|
"""Run the Webots installation shell script, streaming output and progress to the TUI.
|
||||||
|
|
||||||
Progress is split into two phases:
|
Запускает shell-скрипт установки Webots, транслируя вывод и прогресс в TUI.
|
||||||
- 0-65%: downloading the .deb file (streamed in 64 KB chunks).
|
|
||||||
- 65-100%: running apt-get install on the downloaded file.
|
|
||||||
|
|
||||||
Скачивает .deb Webots с GitHub и устанавливает его через apt.
|
|
||||||
Прогресс разделён на две фазы:
|
|
||||||
- 0-65%: скачивание .deb файла (потоковое, кусками по 64 КБ).
|
|
||||||
- 65-100%: запуск apt-get install для скачанного файла.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
screen.write(f"[bold]Installing Webots {_WEBOTS_VERSION}[/bold]\n")
|
script = _SCRIPTS_DIR / "install_webots.sh"
|
||||||
screen.write(f"[dim]{_WEBOTS_DEB_URL}[/dim]\n")
|
if not script.exists():
|
||||||
screen.set_progress(0, "Downloading Webots...")
|
screen.write(f"[red]Script not found:[/red] {script}")
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
deb_path = Path(tmp) / f"webots_{_WEBOTS_VERSION}_amd64.deb"
|
|
||||||
|
|
||||||
with urllib.request.urlopen(_WEBOTS_DEB_URL, timeout=30) as resp:
|
|
||||||
total = int(resp.headers.get("Content-Length", 0))
|
|
||||||
downloaded = 0
|
|
||||||
with open(deb_path, "wb") as f:
|
|
||||||
while True:
|
|
||||||
if screen.is_stopped():
|
|
||||||
return
|
|
||||||
chunk = resp.read(65536)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
f.write(chunk)
|
|
||||||
downloaded += len(chunk)
|
|
||||||
if total > 0:
|
|
||||||
pct = min(downloaded / total * 65, 65)
|
|
||||||
mb = downloaded / 1_048_576
|
|
||||||
total_mb = total / 1_048_576
|
|
||||||
screen.set_progress(pct, f"Downloading... {mb:.0f} / {total_mb:.0f} MB")
|
|
||||||
|
|
||||||
if screen.is_stopped():
|
|
||||||
return
|
|
||||||
|
|
||||||
screen.write("[green]Download complete.[/green]")
|
|
||||||
screen.set_progress(65, "Installing package...")
|
|
||||||
|
|
||||||
proc = subprocess.Popen(
|
|
||||||
["sudo", "apt-get", "install", "-y", str(deb_path)],
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
screen.set_proc(proc)
|
|
||||||
for line in proc.stdout:
|
|
||||||
s = line.rstrip()
|
|
||||||
if s:
|
|
||||||
screen.write(s)
|
|
||||||
proc.wait()
|
|
||||||
|
|
||||||
if proc.returncode not in (0, -9):
|
|
||||||
screen.write("\n[red]Webots installation failed.[/red]")
|
|
||||||
screen.finish(False)
|
screen.finish(False)
|
||||||
return
|
return
|
||||||
|
screen.set_progress(0, "Starting Webots installation...")
|
||||||
if screen.is_stopped():
|
_run_script(script, screen)
|
||||||
return
|
if not screen.is_stopped():
|
||||||
|
|
||||||
screen.set_progress(100, "Done")
|
screen.set_progress(100, "Done")
|
||||||
screen.write("\n[green]Webots installed successfully.[/green]")
|
screen.write(f"\n[green]Webots {_WEBOTS_VERSION} installed successfully.[/green]")
|
||||||
screen.finish(True)
|
screen.finish(True)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if not screen.is_stopped():
|
if not screen.is_stopped():
|
||||||
|
|||||||
Executable
+32
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Download and install the Webots simulator from the Cyberbotics GitHub release.
|
||||||
|
# Emits PROGRESS:<pct>:<label> lines so the Python caller can update its progress bar.
|
||||||
|
# Скачивает и устанавливает симулятор Webots из релизов GitHub Cyberbotics.
|
||||||
|
# Выводит строки PROGRESS:<pct>:<метка> для обновления прогресс-бара в Python.
|
||||||
|
set -euo pipefail
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
PROGRESS() { echo "PROGRESS:$1:$2"; }
|
||||||
|
|
||||||
|
WEBOTS_VERSION="2025a"
|
||||||
|
DEB_URL="https://github.com/cyberbotics/webots/releases/download/R${WEBOTS_VERSION}/webots_${WEBOTS_VERSION}_amd64.deb"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
DEB_PATH="${TMP_DIR}/webots_${WEBOTS_VERSION}_amd64.deb"
|
||||||
|
|
||||||
|
cleanup() { rm -rf "$TMP_DIR"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
PROGRESS 5 "Downloading Webots ${WEBOTS_VERSION}..."
|
||||||
|
echo "Downloading Webots ${WEBOTS_VERSION}..."
|
||||||
|
echo "URL: ${DEB_URL}"
|
||||||
|
|
||||||
|
# wget writes download progress to stderr; redirect to stdout so it appears in the TUI log.
|
||||||
|
wget --progress=dot:mega -O "$DEB_PATH" "$DEB_URL" 2>&1
|
||||||
|
|
||||||
|
PROGRESS 70 "Installing Webots package..."
|
||||||
|
echo "Download complete. Installing..."
|
||||||
|
|
||||||
|
sudo apt-get install -y "$DEB_PATH"
|
||||||
|
|
||||||
|
PROGRESS 100 "Done"
|
||||||
|
echo "Webots ${WEBOTS_VERSION} installed successfully."
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install ROS2 Jazzy Desktop on Ubuntu 24.04.
|
||||||
|
# Emits PROGRESS:<pct>:<label> lines so the Python caller can update its progress bar.
|
||||||
|
# Устанавливает ROS2 Jazzy Desktop на Ubuntu 24.04.
|
||||||
|
# Выводит строки PROGRESS:<pct>:<метка> для обновления прогресс-бара в Python.
|
||||||
|
set -euo pipefail
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
PROGRESS() { echo "PROGRESS:$1:$2"; }
|
||||||
|
|
||||||
|
PROGRESS 0 "Setting up locale..."
|
||||||
|
echo "Setting up locale..."
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get install -y -q locales
|
||||||
|
sudo locale-gen en_US en_US.UTF-8
|
||||||
|
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
PROGRESS 8 "Adding universe repository..."
|
||||||
|
echo "Adding universe repository..."
|
||||||
|
sudo apt-get install -y -q software-properties-common
|
||||||
|
sudo add-apt-repository -y universe
|
||||||
|
|
||||||
|
PROGRESS 16 "Adding ROS2 GPG key..."
|
||||||
|
echo "Adding ROS2 GPG key..."
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get install -y -q curl
|
||||||
|
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
|
||||||
|
-o /usr/share/keyrings/ros-archive-keyring.gpg
|
||||||
|
|
||||||
|
PROGRESS 22 "Configuring ROS2 apt repository..."
|
||||||
|
echo "Configuring ROS2 apt repository..."
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
|
||||||
|
http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo "$UBUNTU_CODENAME") main" \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
|
||||||
|
|
||||||
|
PROGRESS 28 "Updating package lists..."
|
||||||
|
echo "Updating package lists..."
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get upgrade -y -q
|
||||||
|
|
||||||
|
PROGRESS 35 "Installing ros-jazzy-desktop (this may take a while)..."
|
||||||
|
echo "Installing ros-jazzy-desktop..."
|
||||||
|
sudo apt-get install -y ros-jazzy-desktop
|
||||||
|
|
||||||
|
PROGRESS 75 "Installing ROS2 dev tools..."
|
||||||
|
echo "Installing ROS2 dev tools..."
|
||||||
|
sudo apt-get install -y ros-dev-tools
|
||||||
|
|
||||||
|
PROGRESS 88 "Initializing rosdep..."
|
||||||
|
echo "Initializing rosdep..."
|
||||||
|
sudo rosdep init 2>/dev/null || true
|
||||||
|
rosdep update
|
||||||
|
|
||||||
|
PROGRESS 100 "Done"
|
||||||
|
echo "ROS2 Jazzy Desktop installed successfully."
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install ROS2 Jazzy (ros-base) on Ubuntu 24.04.
|
||||||
|
# Emits PROGRESS:<pct>:<label> lines so the Python caller can update its progress bar.
|
||||||
|
# Устанавливает ROS2 Jazzy (ros-base) на Ubuntu 24.04.
|
||||||
|
# Выводит строки PROGRESS:<pct>:<метка> для обновления прогресс-бара в Python.
|
||||||
|
set -euo pipefail
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
PROGRESS() { echo "PROGRESS:$1:$2"; }
|
||||||
|
|
||||||
|
PROGRESS 0 "Setting up locale..."
|
||||||
|
echo "Setting up locale..."
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get install -y -q locales
|
||||||
|
sudo locale-gen en_US en_US.UTF-8
|
||||||
|
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
|
||||||
|
export LANG=en_US.UTF-8
|
||||||
|
|
||||||
|
PROGRESS 8 "Adding universe repository..."
|
||||||
|
echo "Adding universe repository..."
|
||||||
|
sudo apt-get install -y -q software-properties-common
|
||||||
|
sudo add-apt-repository -y universe
|
||||||
|
|
||||||
|
PROGRESS 16 "Adding ROS2 GPG key..."
|
||||||
|
echo "Adding ROS2 GPG key..."
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get install -y -q curl
|
||||||
|
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key \
|
||||||
|
-o /usr/share/keyrings/ros-archive-keyring.gpg
|
||||||
|
|
||||||
|
PROGRESS 22 "Configuring ROS2 apt repository..."
|
||||||
|
echo "Configuring ROS2 apt repository..."
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] \
|
||||||
|
http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo "$UBUNTU_CODENAME") main" \
|
||||||
|
| sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
|
||||||
|
|
||||||
|
PROGRESS 28 "Updating package lists..."
|
||||||
|
echo "Updating package lists..."
|
||||||
|
sudo apt-get update -q
|
||||||
|
sudo apt-get upgrade -y -q
|
||||||
|
|
||||||
|
PROGRESS 35 "Installing ros-jazzy-ros-base (this may take a while)..."
|
||||||
|
echo "Installing ros-jazzy-ros-base..."
|
||||||
|
sudo apt-get install -y ros-jazzy-ros-base
|
||||||
|
|
||||||
|
PROGRESS 75 "Installing ROS2 dev tools..."
|
||||||
|
echo "Installing ROS2 dev tools..."
|
||||||
|
sudo apt-get install -y ros-dev-tools
|
||||||
|
|
||||||
|
PROGRESS 88 "Initializing rosdep..."
|
||||||
|
echo "Initializing rosdep..."
|
||||||
|
sudo rosdep init 2>/dev/null || true
|
||||||
|
rosdep update
|
||||||
|
|
||||||
|
PROGRESS 100 "Done"
|
||||||
|
echo "ROS2 Jazzy (ros-base) installed successfully."
|
||||||
Reference in New Issue
Block a user