제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.reposURL을 실행 확인했다 — 각 절의 실행 확인 블록을 보라.
4.7.2 언제 소스 빌드가 필요한가
| 상황 | 접근 |
|---|---|
| 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 바이너리 | 거의 없음 |
| 공용 오버레이 | 패치한 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
안전한 절차:
- apt 버전을 확인한다.
- 상류 저장소에서 그 릴리스 태그를 기준으로 포크한다.
- 패치를 그 위에 적용한다.
- 패치한 패키지와 그것에 의존하는 패키지를 함께 소스로 빌드한다(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.yaml | colcon 옵션 |
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를 다룬다. 로봇의 임베디드 보드에서 빌드하는 것이 왜 고통스럽고, 어떤 대안이 있는가.