fix: update ROS2 apt repository setup to use ros2-apt-source package and improve installation process

This commit is contained in:
Даниил Грабарь
2026-05-22 13:31:30 +10:00
parent 4202421d5b
commit 6360023e6f
+73 -46
View File
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import glob
import json
import os import os
import shutil import shutil
import subprocess import subprocess
@@ -17,11 +19,15 @@ from cobot.commands.docker_setup import run as _docker_setup
_PROJECT_DIR = Path(__file__).parent.parent.parent _PROJECT_DIR = Path(__file__).parent.parent.parent
# Paths used for the ROS2 apt repository signing key and sources list. # GitHub repository that publishes the official ROS2 apt source package.
# Пути для ключа подписи apt-репозитория ROS2 и файла sources list. # Репозиторий GitHub, публикующий официальный пакет источника apt для ROS2.
_ROS_KEYRING = Path("/usr/share/keyrings/ros-archive-keyring.gpg") _ROS_APT_SOURCE_API = (
_ROS_SOURCES = Path("/etc/apt/sources.list.d/ros2.list") "https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest"
_ROS_KEY_URL = "https://raw.githubusercontent.com/ros/rosdistro/master/ros.key" )
_ROS_APT_SOURCE_DEB = (
"https://github.com/ros-infrastructure/ros-apt-source/releases/download"
"/{version}/ros2-apt-source_{version}.{codename}_all.deb"
)
# Suppress apt interactive prompts such as "restart services?". # Suppress apt interactive prompts such as "restart services?".
# Подавляем интерактивные запросы apt, например "перезапустить службы?". # Подавляем интерактивные запросы apt, например "перезапустить службы?".
@@ -174,8 +180,8 @@ def _setup_locale(write: Write) -> None:
write("[green][ok][/green] Locale configured") write("[green][ok][/green] Locale configured")
# Add the official ROS2 apt repository and its signing key so we can install ROS2 packages. # Add the official ROS2 apt repository using the ros2-apt-source package.
# Добавляем официальный apt-репозиторий ROS2 и его ключ подписи, чтобы можно было установить пакеты ROS2. # Добавляем официальный репозиторий ROS2 через пакет ros2-apt-source.
def _add_ros2_repo( def _add_ros2_repo(
write: Write, write: Write,
on_progress: Optional[Callable[[float], None]] = None, on_progress: Optional[Callable[[float], None]] = None,
@@ -185,61 +191,82 @@ def _add_ros2_repo(
if on_progress: if on_progress:
on_progress(p) on_progress(p)
write("[cyan][*][/cyan] Adding ROS2 apt repository...") write("[cyan][*][/cyan] Configuring ROS2 apt repository...")
# Best-effort update - 60 second timeout so a bad mirror doesn't hang forever. # Update apt cache so we can install curl and software-properties-common.
# Фоновое обновление с таймаутом 60 секунд, чтобы зависший зеркальный сервер не блокировал процесс. # Обновляем кеш apt перед установкой curl и software-properties-common.
subprocess.run(["sudo", "apt-get", "update"] + _APT_TIMEOUTS, capture_output=True, timeout=120) subprocess.run(["sudo", "apt-get", "update"] + _APT_TIMEOUTS, capture_output=True, timeout=120)
_prog(15) _prog(10)
_run_quiet( _run_quiet(
["sudo", "apt-get", "install", "-y", "--no-install-recommends", ["sudo", "apt-get", "install", "-y", "--no-install-recommends",
"software-properties-common", "curl", "gnupg"], "software-properties-common", "curl"],
write, _APT_ENV, write, _APT_ENV,
) )
_prog(35) _prog(20)
_run_quiet(["sudo", "add-apt-repository", "-y", "universe"], write) _run_quiet(["sudo", "add-apt-repository", "-y", "universe"], write)
_prog(45) _prog(30)
write("[cyan][*][/cyan] Downloading ROS2 signing key...") # Fetch the latest ros2-apt-source release version from the GitHub API.
with tempfile.NamedTemporaryFile(delete=False, suffix=".key") as tmp: # The User-Agent header is required by the GitHub API.
# Получаем последнюю версию ros2-apt-source через GitHub API.
# Заголовок User-Agent обязателен для GitHub API.
write("[cyan][*][/cyan] Fetching ros2-apt-source package...")
req = urllib.request.Request(
_ROS_APT_SOURCE_API,
headers={"User-Agent": "cobot-installer"},
)
with urllib.request.urlopen(req, timeout=30) as resp:
version = json.loads(resp.read())["tag_name"]
codename = subprocess.check_output(
["bash", "-c", ". /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}}"],
text=True,
).strip()
deb_url = _ROS_APT_SOURCE_DEB.format(version=version, codename=codename)
_prog(40)
# Download the .deb and install it. The package sets up the GPG key and sources list automatically.
# Скачиваем .deb и устанавливаем его. Пакет сам настраивает GPG-ключ и список источников.
with tempfile.NamedTemporaryFile(suffix=".deb", delete=False) as tmp:
tmp_path = tmp.name tmp_path = tmp.name
try: try:
# 30 second timeout - if GitHub is unreachable we fail fast instead of hanging forever. with urllib.request.urlopen(deb_url, timeout=60) as resp:
# Таймаут 30 секунд - если GitHub недоступен, падаем быстро вместо бесконечного зависания. Path(tmp_path).write_bytes(resp.read())
with urllib.request.urlopen(_ROS_KEY_URL, timeout=30) as resp: _prog(55)
with open(tmp_path, "wb") as f:
f.write(resp.read()) # Remember which files exist before dpkg so we can find the new sources file it creates.
_prog(60) # Запоминаем файлы до dpkg, чтобы найти новый файл источников, который он создаст.
# Convert the ASCII-armored key to binary GPG format that apt understands. before = set(glob.glob("/etc/apt/sources.list.d/*"))
# Конвертируем ключ из ASCII-armor формата в бинарный GPG, который понимает apt. _run_quiet(["sudo", "dpkg", "-i", tmp_path], write)
_run_quiet(["sudo", "gpg", "--dearmor", "--yes", "-o", str(_ROS_KEYRING), tmp_path]) after = set(glob.glob("/etc/apt/sources.list.d/*"))
finally: finally:
os.unlink(tmp_path) os.unlink(tmp_path)
write("[green][ok][/green] Signing key installed")
_prog(65)
arch = subprocess.check_output(["dpkg", "--print-architecture"], text=True).strip() write("[green][ok][/green] ROS2 apt source installed")
codename = subprocess.check_output( _prog(60)
["bash", "-c", ". /etc/os-release && echo $UBUNTU_CODENAME"], text=True
).strip() # Update ONLY the newly added ROS2 sources file, not all Ubuntu repos.
sources_line = ( # This avoids re-downloading hundreds of MB of ubuntu package lists that are already cached.
f"deb [arch={arch} signed-by={_ROS_KEYRING}] " # Обновляем ТОЛЬКО новый файл источников ROS2, а не все репозитории Ubuntu.
f"http://packages.ros.org/ros2/ubuntu {codename} main\n" # Это позволяет не перекачивать сотни МБ списков пакетов Ubuntu, которые уже закешированы.
) write("[cyan][*][/cyan] Updating ROS2 package list...")
proc = subprocess.run( new_files = [f for f in (after - before) if f.endswith((".list", ".sources"))]
["sudo", "tee", str(_ROS_SOURCES)], if new_files:
input=sources_line, capture_output=True, text=True, update_cmd = [
) "sudo", "apt-get", "update", "-q",
if proc.returncode != 0: "-o", f"Dir::Etc::sourcelist={new_files[0]}",
raise RuntimeError(f"Failed to write {_ROS_SOURCES}") "-o", "Dir::Etc::sourceparts=-",
_prog(70) "-o", "APT::Get::List-Cleanup=0",
] + _APT_TIMEOUTS
else:
# Fallback when the deb updates an existing file rather than creating a new one.
# Запасной вариант когда deb обновляет существующий файл, а не создаёт новый.
update_cmd = ["sudo", "apt-get", "update", "-q"] + _APT_TIMEOUTS
write("[cyan][*][/cyan] Updating apt cache...")
_run_apt_with_progress( _run_apt_with_progress(
["sudo", "apt-get", "update", "-q"] + _APT_TIMEOUTS, update_cmd, write,
write, lambda p: _prog(60 + p * 0.40),
lambda p: _prog(70 + p * 0.30),
_APT_ENV, _APT_ENV,
register_proc=register_proc, register_proc=register_proc,
) )