Files
lightweight-cobot/cobot/commands/rebuild.py
T

193 lines
6.2 KiB
Python

from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
from typing import List, Optional
from textual.app import App
from cobot.tui import SCREEN_CSS, InputScreen, LogScreen, PickScreen
from cobot.commands.local_setup import _ros2_env
_PROJECT_DIR = Path(__file__).parent.parent.parent
def _task_rebuild(screen: LogScreen, packages: List[str], symlink: bool) -> None:
"""Run colcon build for the selected packages (or all if packages is empty).
Streams output to the log and tracks per-package progress.
Запускает colcon build для выбранных пакетов (или всех если packages пуст).
Транслирует вывод в лог и отслеживает прогресс по каждому пакету.
"""
try:
env = _ros2_env()
# Count packages so we can show X / total progress.
# Считаем пакеты чтобы показывать X / всего в прогрессе.
list_cmd = ["colcon", "list", "--base-paths", "src"]
if packages:
list_cmd += ["--packages-select"] + packages
list_result = subprocess.run(
list_cmd, capture_output=True, text=True,
cwd=_PROJECT_DIR, env=env,
)
total = max(len([l for l in list_result.stdout.splitlines() if l.strip()]), 1)
pkg_label = " ".join(packages) if packages else "all packages"
symlink_label = " --symlink-install" if symlink else ""
screen.write(f"[bold]colcon build{symlink_label}{pkg_label}[/bold]\n")
screen.set_progress(0, f"0 / {total} packages done")
built = 0
cmd = ["colcon", "build", "--base-paths", "src"]
if symlink:
cmd.append("--symlink-install")
if packages:
cmd += ["--packages-select"] + packages
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, cwd=_PROJECT_DIR, env=env,
)
screen.set_proc(proc)
for line in proc.stdout:
s = line.rstrip()
if s:
screen.write(s)
if "Finished <<<" in line or "Failed <<<" in line:
built += 1
screen.set_progress(
built / total * 100,
f"{built} / {total} packages done",
)
proc.wait()
if screen.is_stopped():
return
if proc.returncode not in (0, -9):
screen.write("\n[red]Build failed.[/red]")
screen.finish(False)
return
screen.set_progress(100, "Done")
screen.write("\n[green]Build complete.[/green]")
screen.finish(True)
except Exception as exc:
if not screen.is_stopped():
screen.write(f"\n[red]Error:[/red] {exc}")
screen.finish(False)
class _RebuildApp(App[None]):
"""Rebuild wizard: optionally asks for packages and symlink flag, then runs colcon.
Мастер пересборки: опционально спрашивает пакеты и флаг symlink, затем запускает colcon.
"""
CSS = SCREEN_CSS
def __init__(self, packages: Optional[List[str]], symlink: Optional[bool]):
super().__init__()
# None means "ask the user interactively".
# None означает "спросить пользователя интерактивно".
self._packages = packages
self._symlink = symlink
def on_mount(self) -> None:
if self._packages is None:
self._ask_packages()
elif self._symlink is None:
self._ask_symlink()
else:
self._start()
def _ask_packages(self) -> None:
self.push_screen(
InputScreen(
"rebuild",
"Packages to rebuild (space-separated, leave empty for all):",
"",
note="Example: iiwa_controller iiwa_bringup",
),
self._got_packages,
)
def _got_packages(self, value: Optional[str]) -> None:
if value is None:
self.exit()
return
self._packages = value.split() if value.strip() else []
if self._symlink is None:
self._ask_symlink()
else:
self._start()
def _ask_symlink(self) -> None:
self.push_screen(
PickScreen(
"rebuild",
"Use --symlink-install?",
["Yes", "No"],
"Yes",
),
self._got_symlink,
)
def _got_symlink(self, value: Optional[str]) -> None:
if value is None:
self.exit()
return
self._symlink = value == "Yes"
self._start()
def _start(self) -> None:
self.push_screen(
LogScreen(
"Rebuilding packages",
lambda s: _task_rebuild(s, self._packages, self._symlink),
show_progress=True,
),
lambda _: self.exit(),
)
def register(subparsers: argparse._SubParsersAction) -> None:
p = subparsers.add_parser(
"rebuild",
help="Rebuild ROS2 packages in src/ with colcon",
)
p.add_argument(
"packages",
nargs="*",
metavar="PACKAGE",
help="Packages to rebuild (omit to be asked, or leave empty for all)",
)
p.set_defaults(symlink=None)
p.add_argument(
"--symlink-install",
dest="symlink",
action="store_true",
help="Pass --symlink-install to colcon build",
)
p.add_argument(
"--no-symlink-install",
dest="symlink",
action="store_false",
help="Do not pass --symlink-install to colcon build",
)
p.set_defaults(func=run)
def run(args: argparse.Namespace) -> None:
"""Entry point for the rebuild command.
Точка входа для команды rebuild.
"""
# Convert empty list to None so the TUI asks interactively.
# Преобразуем пустой список в None, чтобы TUI спросил интерактивно.
packages = args.packages if args.packages else None
_RebuildApp(packages, args.symlink).run()