AEGLOSLAB ROS 2 Jazzy 대전

제4부 빌드·패키징·배포

4.7 소스 빌드·vcstool·워크스페이스 전략

4.7.1 이 장에서 배우는 것 / 선수 지식

apt로 설치한 바이너리로 충분한 경우가 많다. 그런데 언젠가 이 상황들을 만난다 — 생태계 패키지에 버그가 있어 패치해야 하고, Jazzy에 릴리스되지 않은 기능이 필요하고, 소스를 읽어야 원인을 알 수 있고, 릴리스 전 브랜치를 시험해야 한다.

이 장은 소스 빌드를 관리 가능한 상태로 유지하는 전략을 다룬다. 계획 없이 소스를 받아 빌드하면 워크스페이스가 수백 패키지로 부풀고, 빌드에 한 시간이 걸리고, 무엇이 왜 그 버전인지 아무도 모르는 상태가 된다.

배우는 것:

  • 언제 소스 빌드가 필요한가 — 그리고 언제 필요 없는가.
  • vcstool — .repos 파일로 여러 저장소를 다루는 법.
  • 워크스페이스 계층 전략 — 무엇을 어느 층에 둘 것인가.
  • 부분 오버레이 — 패치한 패키지만 소스로.
  • 포크 관리 — 상류 변경을 따라가는 방법.
  • 재현성 — 커밋 고정과 검증.
  • ROS 2 코어 소스 빌드 — 정말 필요할 때.

선수 지식: 0.5(오버레이·언더레이), 4.5(colcon), 4.6(rosdep), 4.4(package.xml).

정직: .repos 파일 URL·브랜치 이름은 배포판·시점에 따라 다르다. 아래 예시는 형식을 보이기 위한 것이며 실제 URL은 해당 프로젝트 문서에서 확인해야 한다(확인 필요). 이 환경에서 실행 검증하지 않았다(목차 3). 144차가 vcstool 명령 전체와 4.7.7의 ros2.repos URL을 실행 확인했다 — 각 절의 실행 확인 블록을 보라.

4.7.2 언제 소스 빌드가 필요한가

아니오 예 소스 빌드가 필요한가? 버그를 고쳐야 하나? ✅ 해당 패키지만 소스로 릴리스 안 된 기능이 필요한가?
아니오 아니오 예 예 예 아니오 릴리스 안 된 기능이 필요한가? ✅ 해당 패키지만 소스로 소스를 읽어야 하는가? 읽기 전용 클론 빌드 불필요 플랫폼에 바이너리가 없는가? ✅ 전체 소스 빌드 ❌ apt 사용
(계속 — 릴리스 안 된 기능 분기)
상황접근
Nav2 플래너에 버그그 패키지만 소스로(4.7.5)
Rolling의 기능이 필요해당 패키지만, 호환 확인 필요
왜 이렇게 동작하는지 알고 싶다클론만 하고 빌드는 안 함(2.1.6)
지원되지 않는 플랫폼·아키텍처전체 소스 빌드 또는 크로스 컴파일(4.8)
커스텀 rmw·미들웨어 개발관련 패키지 소스
단순히 최신을 쓰고 싶다하지 말 것

견해: "소스로 빌드하면 최신이라 좋다"는 함정이다. 소스 빌드는 빌드 시간·의존 관리·업데이트 추적 비용을 모두 부담하는 것이고, 그 대가로 얻는 것이 명확하지 않으면 손해다. 필요한 패키지만 최소로 소스에 두는 것이 원칙이다.

읽기 전용 클론이 과소평가된다(2.1.6). 소스를 이해하기 위해서라면 빌드할 필요가 없다 — 클론해서 grep하고 읽으면 된다. 이것만으로 디버깅 속도가 크게 달라진다.

mkdir -p ~/ros2_src_read && cd ~/ros2_src_read
git clone -b jazzy https://github.com/ros2/rclcpp.git
grep -rn "spin_until_future_complete" rclcpp/rclcpp/src/

4.7.3 vcstool

여러 git 저장소를 한 파일로 관리하는 도구다.

sudo apt install python3-vcstool

.repos 파일 형식:

repositories:
  navigation2:
    type: git
    url: https://github.com/ros-navigation/navigation2.git
    version: jazzy
  my_robot_driver:
    type: git
    url: [email protected]:myorg/my_robot_driver.git
    version: main
  patched_pkg:
    type: git
    url: https://github.com/myorg/some_pkg_fork.git
    version: 3f2a1b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a    # 커밋 고정

주요 명령:

# 내려받기
vcs import src < robot.repos

# 상태 확인 (전체 저장소의 git status)
vcs status src

# 전부 pull
vcs pull src

# 현재 상태를 .repos로 내보내기
vcs export src > current.repos

# 정확한 커밋으로 내보내기 (재현성)
vcs export --exact src > locked.repos

# 임의 명령 실행
vcs custom src --args log --oneline -1
vcs custom src --args checkout jazzy

# 브랜치 확인
vcs branch src

실행 확인(144차, Jazzy). 이 절의 명령 전체를 로컬 git 저장소 두 개(repo_a·repo_b, file:// URL)로 재현했다 — vcs import(정확히 이 절의 .repos 형식으로 클론 성공)·vcs status(git status 요약을 저장소별로 출력)·vcs branch(현재 브랜치를 저장소별로 표시)·vcs custom --args log --oneline -1(임의 git 명령을 그대로 실행)·vcs pull(상류에 새 커밋을 만든 뒤 정확히 fast-forward)·vcs export(원래 .repos와 형식·내용이 일치) 전부 문서 그대로 동작했다. 목차 3, tools/gap_harness/gap144.

vcs export --exact가 재현성의 핵심이다. 브랜치 이름 대신 커밋 해시로 고정한 파일을 만든다.

# vcs export --exact 결과
repositories:
  navigation2:
    type: git
    url: https://github.com/ros-navigation/navigation2.git
    version: a1b2c3d4e5f6789012345678901234567890abcd

실행 확인(144차). vcs export --exact src가 실제로 version: 필드를 jazzy·main 같은 브랜치 이름 대신 40자 SHA-1 전체(b43a8b69086904c2af737613f5a7d71fdc76c76e 형태)로 바꾼 것을 확인했다 — 이 절의 서술 그대로다.

견해: 개발용 .repos는 브랜치로, 릴리스용 .repos는 커밋 해시로 두는 이중 구조가 실용적이다. 개발 중에는 상류를 따라가고, 배포 시점에 vcs export --exact로 고정해 그 파일을 태그와 함께 보관한다. "그때 무엇으로 빌드했나"를 6개월 뒤에 답할 수 있게 된다.

# 릴리스 시점
vcs export --exact src > release/v1.2.0.repos
git add release/v1.2.0.repos && git commit -m "release v1.2.0 lock"

4.7.4 워크스페이스 계층 전략

0.5에서 오버레이 메커니즘을 봤다. 여기서는 무엇을 어느 층에 둘 것인가의 전략이다.

/opt/ros/jazzy (apt 설치 — 변경 안 함) 공용 워크스페이스 패치된 생태계 패키지 프로젝트 워크스페이스 우리 코드 개발 워크스페이스 현재 작업 중인 패키지
층내용재빌드 빈도
시스템(/opt/ros/jazzy)apt 바이너리거의 없음
공용 오버레이패치한 Nav2·MoveIt 2 등드묾
프로젝트우리 로봇 패키지자주
개발지금 고치는 패키지매우 자주

층을 나누는 이유는 빌드 시간이다. 전부 한 워크스페이스에 두면, 한 줄 고쳐도 colcon이 전체 그래프를 확인해야 한다. 층을 나누면 자주 바뀌는 것만 재빌드한다.

# 층별 소싱
source /opt/ros/jazzy/setup.bash
source ~/common_ws/install/setup.bash
source ~/project_ws/install/setup.bash
cd ~/dev_ws && colcon build --symlink-install
source ~/dev_ws/install/setup.bash

대가와 함정:

함정설명
소싱 순서 오류하위 계층을 먼저 소싱해야 한다(0.5)
층 간 버전 불일치하위 계층 재빌드 후 상위 계층도 재빌드 필요할 수 있다
디버깅 혼란어느 층의 패키지가 쓰이는지 혼동
배포 복잡도여러 워크스페이스를 함께 배포해야
# 실제로 어느 것이 쓰이는지 확인
ros2 pkg prefix nav2_bt_navigator
# /home/user/common_ws/install/nav2_bt_navigator  ← 오버레이가 이김

견해: 층은 두세 개까지가 관리 가능하다. 네 개를 넘어가면 소싱 순서와 재빌드 규칙을 사람이 기억하지 못한다. 개인 개발 환경에서는 "시스템 + 프로젝트" 두 층으로 충분한 경우가 많고, 생태계 패키지를 패치하게 될 때만 중간층을 추가한다.

함정: 상위 층에서 하위 층의 패키지를 같은 이름으로 빌드하면 조용히 덮어쓴다. 의도한 것이면 괜찮지만, 실수로 같은 이름을 쓰면 디버깅이 매우 어렵다(0.5). ros2 pkg prefix로 확인하는 습관이 필요하다.

4.7.5 부분 오버레이 — 패치한 패키지만

Nav2 전체(수십 패키지)를 소스로 빌드할 필요는 없다. 고칠 패키지 하나만 소스에 두고 나머지는 apt 바이너리를 쓴다.

# patched.repos — 필요한 것만
repositories:
  navigation2:
    type: git
    url: https://github.com/myorg/navigation2.git    # 우리 포크
    version: jazzy-patched
vcs import src < patched.repos

# 필요한 패키지만 빌드
colcon build --packages-select nav2_smac_planner

문제: 그 패키지의 의존이 소스에도 있으면 함께 빌드된다. --packages-select로 하나만 지정하면 되지만, ABI 호환성을 확인해야 한다.

위험설명
ABI 불일치소스 빌드한 패키지와 apt 바이너리의 헤더·구조체가 다르면 런타임 크래시
버전 불일치포크가 apt 버전보다 오래됐거나 새로우면 API 불일치
조용한 실패링크는 되는데 동작이 이상

함정: ABI 문제는 진단이 매우 어렵다. 컴파일·링크가 성공하고, 실행하면 이유 없이 크래시하거나 값이 비정상이다. 소스로 빌드하는 패키지의 상류 버전을 apt 버전과 맞추는 것이 가장 안전하다.

# apt 버전 확인
apt-cache policy ros-jazzy-nav2-smac-planner
dpkg -l | grep nav2

# 포크의 기반 커밋이 그 버전과 맞는지 확인
cd src/navigation2 && git log --oneline -5

안전한 절차:

  1. apt 버전을 확인한다.
  2. 상류 저장소에서 그 릴리스 태그를 기준으로 포크한다.
  3. 패치를 그 위에 적용한다.
  4. 패치한 패키지와 그것에 의존하는 패키지를 함께 소스로 빌드한다(ABI 안전).

견해: 가장 안전한 것은 "패치한 패키지가 속한 저장소 전체를 소스로" 두는 것이다. Nav2를 패치하면 Nav2 전체를 소스로 빌드한다. 빌드 시간은 늘지만 ABI 문제가 사라진다. 빌드 시간과 안전성의 트레이드오프이며, 불확실하면 안전을 택하라 — ABI 디버깅에 쓰는 시간이 빌드 시간보다 훨씬 비용이 크다.

4.7.6 포크 관리

상류 패키지를 패치하면 그 패치를 계속 유지해야 한다.

# 포크 클론 후 상류 등록
git clone [email protected]:myorg/navigation2.git
cd navigation2
git remote add upstream https://github.com/ros-navigation/navigation2.git
git fetch upstream

# 패치 브랜치를 상류 위에 리베이스
git checkout jazzy-patched
git rebase upstream/jazzy

패치 관리 전략:

전략설명적합
리베이스 브랜치패치를 상류 위에 계속 적용함패치가 적고 오래 유지
머지 브랜치상류를 주기적으로 머지패치가 많고 충돌 잦음
패치 파일.patch를 보관하고 빌드 시 적용패치가 아주 작을 때
상류 기여PR을 보내 병합가장 좋은 해법

견해: 패치를 오래 들고 있는 것은 부채다. 상류가 바뀔 때마다 리베이스·검증 비용이 들고, 배포판 업그레이드(15.1) 때 큰 문제로 드러난다. 가능하면 상류에 PR을 보내라(15.5) — 병합되면 다음 릴리스에서 패치가 필요 없어진다. 병합까지 시간이 걸리더라도, 그동안 포크를 유지하는 비용과 비교하면 대개 이득이다.

패치를 문서화하라:

# PATCHES.md
## navigation2 (jazzy-patched)
- `nav2_smac_planner`: 대각 이동 비용 계산 수정
  - 이유: 좁은 통로에서 경로가 벽에 붙음
  - 상류 PR: https://github.com/ros-navigation/navigation2/pull/1234 (검토 중)
  - 제거 조건: PR 병합 후 릴리스되면

"제거 조건"을 적어 두는 것이 중요하다. 이것이 없으면 6개월 뒤 아무도 그 패치가 왜 있는지, 아직 필요한지 모른다.

4.7.7 ROS 2 코어 소스 빌드

정말 필요한 경우가 아니면 하지 말 것. 수백 패키지, 수십 분~수 시간의 빌드, 그리고 지속적 유지 부담이다.

필요한 경우:

  • 지원되지 않는 OS·아키텍처.
  • rclcpp·rcl 자체를 수정해야 함.
  • 커스텀 rmw 개발(15.4).
  • 릴리스 전 배포판(Rolling) 시험.
mkdir -p ~/ros2_jazzy/src && cd ~/ros2_jazzy

# 저장소 목록 내려받기
wget https://raw.githubusercontent.com/ros2/ros2/jazzy/ros2.repos
# (확인 필요: 파일명·브랜치는 배포판마다 다르다)

vcs import src < ros2.repos

# 의존 설치
sudo apt update
rosdep install --from-paths src --ignore-src -y \
  --skip-keys "fastcdr rti-connext-dds-6.0.1 urdfdom_headers"
# (확인 필요: skip-keys 목록은 상황에 따라 다르다)

실행 확인(144차). wget https://raw.githubusercontent.com/ros2/ros2/jazzy/ros2.repos가 실제로 433줄의 유효한 .repos 파일을 내려받았다 — ament/ament_cmake부터 시작하는 저장소 목록이 이 절의 형식과 일치한다. URL 패턴 자체(.../ros2/ros2/<배포판 이름>/ros2.repos)는 예상보다 안정적이다 — 같은 URL 패턴에서 jazzy를 humble로만 바꿔도 똑같이 유효한 파일을 내려받았다(파일명은 고정, 브랜치 이름=배포판 이름이라는 규칙도 두 배포판에서 동일했다). "파일명·브랜치는 배포판마다 다르다"는 경고는 여전히 유효하지만(향후 배포판에서 이 규칙이 계속된다는 보장은 없다), 적어도 Jazzy·Humble 두 배포판 사이에서는 패턴이 예측 가능했다. rosdep install --skip-keys 이하는 전체 ROS 2 코어 소스 빌드가 필요해(수백 패키지·긴 시간) 이 절 자신의 조언대로("정말 필요한 경우가 아니면 하지 말 것") 이번 회차에 실행하지 않았다. 목차 3, tools/gap_harness/gap144.

# 빌드 (시간·메모리 많이 필요 — 4.5.5)
colcon build --symlink-install --parallel-workers 4 \
  --cmake-args -DCMAKE_BUILD_TYPE=Release

함정: 시스템에 apt로 설치된 ROS 2가 있으면 충돌한다. 소스 빌드 워크스페이스를 쓸 때는 /opt/ros/jazzy/setup.bash를 소싱하지 않아야 한다. 두 개를 섞으면 어느 버전이 쓰이는지 예측할 수 없다.

# 소스 빌드 환경 (시스템 ROS 소싱 안 함)
source ~/ros2_jazzy/install/setup.bash

부분 소스 빌드가 대개 더 낫다:

# rclcpp만 고치고 싶다면 — 그것과 의존 패키지만
mkdir -p ~/rclcpp_ws/src && cd ~/rclcpp_ws
git clone -b jazzy https://github.com/ros2/rclcpp.git src/rclcpp
source /opt/ros/jazzy/setup.bash
colcon build --packages-select rclcpp
source install/setup.bash    # 오버레이로 시스템 rclcpp를 덮어씀

이것이 훨씬 실용적이다. 다만 ABI 주의사항(4.7.5)이 그대로 적용된다 — rclcpp를 다른 버전으로 바꾸면 그것에 링크된 모든 패키지에 영향이 갈 수 있다.

4.7.8 재현성

"6개월 뒤에 같은 것을 빌드할 수 있는가."

요소고정 방법
소스 버전vcs export --exact(4.7.3)
apt 패키지 버전버전 고정 또는 이미지 태그
베이스 이미지날짜·해시 태그
rosdep 데이터베이스이미지에 캐시 포함(4.6.6)
빌드 옵션mixin·defaults.yaml을 버전 관리(4.5)
컴파일러 버전이미지로 고정
# 설치된 ROS 패키지 버전 스냅숏
dpkg -l | grep '^ii  ros-jazzy' | awk '{print $2"="$3}' > apt-lock.txt

# 복원
sudo apt install $(cat apt-lock.txt | tr '\n' ' ')

견해: 가장 확실한 재현성 수단은 컨테이너 이미지다. 소스·의존·툴체인이 하나의 아티팩트로 고정된다. .repos 잠금 파일과 이미지 태그를 릴리스 태그와 함께 보관하면, "v1.2.0을 빌드한 환경"을 그대로 되살릴 수 있다(4.9·14.8).

릴리스 시 함께 보관할 것:

파일고정하는 것
release/v1.2.0/locked.repos소스 커밋 — vcs export --exact
release/v1.2.0/apt-lock.txt설치된 apt 패키지 버전
release/v1.2.0/Dockerfile빌드 환경·툴체인
release/v1.2.0/build-options.yamlcolcon 옵션

4.7.9 함정 카탈로그

#함정증상대처
1이유 없이 전체 소스 빌드빌드 시간·유지 부담필요한 것만(4.7.2)
2소스 패키지와 apt 버전 불일치ABI 크래시상류 릴리스 태그 기준 포크
3저장소 일부만 소스로ABI 위험저장소 단위로 소스화
4브랜치만 기록재현 불가vcs export --exact
5패치 이유·제거 조건 미기록6개월 뒤 알 수 없음PATCHES.md
6패치를 상류에 안 보냄영구 부채PR 제출(15.5)
7워크스페이스 층 과다소싱 순서 혼란두세 층까지
8소스 빌드 + 시스템 ROS 동시 소싱버전 예측 불가하나만 소싱
9오버레이가 조용히 덮어씀어느 버전인지 모름ros2 pkg prefix 확인
10상류 pull 후 검증 없이 사용예상 못 한 변경변경 로그 확인·테스트
11재현 정보 미보관과거 빌드 복원 불가잠금 파일 세트
12읽기용인데 빌드까지 함시간 낭비클론만(4.7.2)

4.7.10 체크포인트

  • 소스 빌드가 필요한 경우와 필요 없는 경우를 각각 세 가지 들 수 있는가. "최신이라 좋다"가 왜 함정인가.
  • 읽기 전용 클론이 유용한 이유는 무엇인가.
  • vcstool의 주요 명령 다섯 가지를 말할 수 있는가. vcs export --exact가 왜 중요한가.
  • 개발용과 릴리스용 .repos를 다르게 관리하는 방식과 그 이유를 설명할 수 있는가.
  • 워크스페이스를 층으로 나누는 이유는 무엇인가. 층이 많아질 때의 함정 네 가지는?
  • 생태계 패키지 하나만 패치할 때의 ABI 위험을 설명할 수 있는가. 가장 안전한 접근은?
  • 포크 패치 관리 전략 네 가지 중 근본적 해법은 무엇인가. PATCHES.md에 반드시 적어야 할 것은?
  • ROS 2 코어를 소스로 빌드할 때 시스템 ROS와의 관계에서 주의할 점은?
  • 부분 소스 빌드가 전체 소스 빌드보다 실용적인 이유와, 그때의 주의사항은?
  • 6개월 뒤 같은 빌드를 재현하려면 무엇을 보관해야 하는가. 가장 확실한 수단은?

다음 장 4.8은 빌드 대상 아키텍처가 개발 머신과 다를 때 — 크로스 컴파일과 arm64를 다룬다. 로봇의 임베디드 보드에서 빌드하는 것이 왜 고통스럽고, 어떤 대안이 있는가.